239 lines
8.1 KiB
Vue
239 lines
8.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed, reactive, ref } from 'vue'
|
|
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
|
|
|
import AppShellAccountModals from './AppShellAccountModals.vue'
|
|
import { privacyContentToHtml } from '../lib/privacyContent'
|
|
import { useAuthStore } from '../stores/auth'
|
|
import { useAwardsStore } from '../stores/awards'
|
|
import type { AuthRole } from '../types/awards'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
const awardsStore = useAwardsStore()
|
|
|
|
const loginForm = reactive({
|
|
twitchUserId: 'jayuhime_viewer',
|
|
displayName: 'Jayuhime',
|
|
role: 'viewer' as AuthRole,
|
|
})
|
|
|
|
const accountOpen = ref(false)
|
|
const deleteConfirm = ref(false)
|
|
const privacyOpen = ref(false)
|
|
const accountActionError = ref('')
|
|
const accountActionSuccess = ref('')
|
|
|
|
defineExpose({ privacyOpen })
|
|
|
|
const navItems = [
|
|
{ label: 'Landingpage', to: '/' },
|
|
]
|
|
|
|
const visibleNavItems = computed(() => navItems)
|
|
const privacyContent = computed(
|
|
() => awardsStore.overview.siteContent.privacyPolicyContent || awardsStore.adminSiteSettings.privacyPolicyContent,
|
|
)
|
|
const privacyEmail = computed(
|
|
() => awardsStore.overview.siteContent.privacyEmail || awardsStore.adminSiteSettings.privacyEmail,
|
|
)
|
|
const privacyContentHtml = computed(() => privacyContentToHtml(privacyContent.value))
|
|
|
|
async function doLogin() {
|
|
try {
|
|
await authStore.login(loginForm)
|
|
} catch {}
|
|
}
|
|
|
|
async function doLogout() {
|
|
closeAccountModal()
|
|
accountActionError.value = ''
|
|
await authStore.logout()
|
|
await router.replace({ name: 'login' })
|
|
}
|
|
|
|
async function bindTeamTwitch() {
|
|
accountActionError.value = ''
|
|
accountActionSuccess.value = ''
|
|
try {
|
|
await authStore.startTwitchAuthorization({
|
|
purpose: 'team-binding',
|
|
returnUrl: route.fullPath,
|
|
})
|
|
} catch (error) {
|
|
accountActionError.value = error instanceof Error
|
|
? error.message
|
|
: 'Twitch-Login konnte nicht gestartet werden.'
|
|
}
|
|
}
|
|
|
|
async function deleteMyData() {
|
|
accountActionError.value = ''
|
|
try {
|
|
await authStore.deleteMyData()
|
|
accountOpen.value = false
|
|
deleteConfirm.value = false
|
|
await awardsStore.loadHomeData()
|
|
await router.replace({ name: 'login' })
|
|
} catch (error) {
|
|
accountActionError.value = error instanceof Error
|
|
? error.message
|
|
: 'Deine Daten konnten gerade nicht gelöscht werden.'
|
|
}
|
|
}
|
|
|
|
function closeAccountModal() {
|
|
accountOpen.value = false
|
|
deleteConfirm.value = false
|
|
accountActionError.value = ''
|
|
accountActionSuccess.value = ''
|
|
}
|
|
|
|
function openPrivacyModal() {
|
|
privacyOpen.value = true
|
|
}
|
|
|
|
function closePrivacyModal() {
|
|
privacyOpen.value = false
|
|
}
|
|
|
|
function requestAccountDeletion() {
|
|
deleteConfirm.value = true
|
|
}
|
|
|
|
function cancelAccountDeletion() {
|
|
deleteConfirm.value = false
|
|
accountActionError.value = ''
|
|
}
|
|
|
|
function isActive(to: string) {
|
|
if (to === '/') return route.path === '/'
|
|
return route.path === to || route.path.startsWith(to + '/')
|
|
}
|
|
|
|
const linkBase = 'padding:9px 14px;border-radius:9px;font-family:\'Outfit\',sans-serif;font-size:14px;text-decoration:none;display:inline-block;transition:all .15s;'
|
|
const linkActive = linkBase + 'background:rgba(139,108,219,.1);color:#5f44ad;font-weight:600;'
|
|
const linkInactive = linkBase + 'background:transparent;color:#6f6685;font-weight:500;'
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div style="min-height:100vh;overflow-x:hidden;font-family:'Outfit',sans-serif;line-height:1.5;background:#f4eefb;color:#3f3556;">
|
|
<template v-if="route.name === 'home' || route.meta.bareShell">
|
|
<slot />
|
|
</template>
|
|
<template v-else>
|
|
<!-- Nav -->
|
|
<nav class="app-shell-topbar" style="position:sticky;top:0;z-index:50;background:rgba(248,243,254,.86);border-bottom:1px solid #ece2fa;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);">
|
|
<div class="app-shell-topbar__inner" style="width:100%;padding:0 24px;height:64px;display:flex;align-items:center;gap:16px;">
|
|
|
|
<!-- Logo -->
|
|
<RouterLink to="/" class="app-shell-brand" style="display:flex;align-items:center;gap:10px;text-decoration:none;flex:none;">
|
|
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#8b6cdb,#e7b13e);display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;box-shadow:0 6px 14px rgba(139,108,219,.3);flex:none;">✦</div>
|
|
<span class="app-shell-brand__text" style="font-family:'Fredoka',sans-serif;font-size:17px;font-weight:600;color:#3f3556;">VTuber Star Award</span>
|
|
</RouterLink>
|
|
|
|
<!-- Links -->
|
|
<nav class="app-shell-links" style="display:flex;align-items:center;gap:4px;margin-left:auto;justify-content:flex-end;">
|
|
<RouterLink
|
|
v-for="item in visibleNavItems"
|
|
:key="item.to"
|
|
:to="item.to"
|
|
class="app-shell-link app-shell-link--secondary"
|
|
:style="isActive(item.to) ? linkActive : linkInactive"
|
|
>{{ item.label }}</RouterLink>
|
|
<RouterLink
|
|
v-if="authStore.isAdmin"
|
|
to="/admin"
|
|
class="app-shell-link"
|
|
:style="route.path.startsWith('/admin') ? linkActive : linkInactive"
|
|
>Admin</RouterLink>
|
|
</nav>
|
|
|
|
<!-- Auth -->
|
|
<div class="app-shell-auth" style="display:flex;align-items:center;gap:8px;">
|
|
<template v-if="authStore.session">
|
|
<button
|
|
class="app-shell-account-button"
|
|
@click="accountOpen = true"
|
|
style="display:inline-flex;align-items:center;gap:8px;padding:9px 14px;border-radius:999px;border:none;background:rgba(139,108,219,.08);color:#6a4fb8;font-family:'Outfit',sans-serif;font-weight:600;font-size:14px;cursor:pointer;"
|
|
>
|
|
<span style="width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex:none;">
|
|
{{ authStore.session.displayName.charAt(0).toUpperCase() }}
|
|
</span>
|
|
<span class="app-shell-account-label">@{{ authStore.session.twitchUserId }}</span>
|
|
</button>
|
|
</template>
|
|
<template v-else>
|
|
<button
|
|
@click="doLogin"
|
|
style="display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:999px;border:none;background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;font-family:'Outfit',sans-serif;font-weight:600;font-size:14px;cursor:pointer;box-shadow:0 6px 18px rgba(124,86,196,.3);"
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
|
|
<path d="M4 2L2.4 6v14.5h5V23h2.7l2.5-2.5h4L21.6 16V2H4zm15.3 13.1l-2.9 2.9h-4.4l-2.5 2.5v-2.5H6.7V3.7h12.6v11.4z"/>
|
|
<path d="M11.1 7.1h1.7v5h-1.7zM15.7 7.1h1.7v5h-1.7z"/>
|
|
</svg>
|
|
Mit Twitch anmelden
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div style="max-width:1460px;margin:0 auto;padding:16px 16px 0;box-sizing:border-box;">
|
|
<slot />
|
|
</div>
|
|
|
|
<AppShellAccountModals
|
|
:account-open="accountOpen"
|
|
:privacy-open="privacyOpen"
|
|
:delete-confirm="deleteConfirm"
|
|
:session="authStore.session"
|
|
:privacy-content-html="privacyContentHtml"
|
|
:privacy-email="privacyEmail"
|
|
:account-action-error="accountActionError"
|
|
:account-action-success="accountActionSuccess"
|
|
:auth-loading="authStore.loading"
|
|
@close-account="closeAccountModal"
|
|
@close-privacy="closePrivacyModal"
|
|
@open-privacy="openPrivacyModal"
|
|
@request-delete="requestAccountDeletion"
|
|
@cancel-delete="cancelAccountDeletion"
|
|
@logout="doLogout"
|
|
@bind-team-twitch="bindTeamTwitch"
|
|
@confirm-delete="deleteMyData"
|
|
/>
|
|
</template>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@media (max-width: 720px) {
|
|
.app-shell-topbar__inner {
|
|
gap: 8px !important;
|
|
padding: 0 12px !important;
|
|
}
|
|
|
|
.app-shell-brand__text,
|
|
.app-shell-account-label {
|
|
display: none !important;
|
|
}
|
|
|
|
.app-shell-links {
|
|
gap: 2px !important;
|
|
}
|
|
|
|
.app-shell-account-button {
|
|
padding: 8px !important;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.app-shell-link--secondary {
|
|
display: none !important;
|
|
}
|
|
}
|
|
</style>
|