Files
vtuber-awards/frontend/src/components/AppShell.vue
T
2026-06-25 19:52:46 +02:00

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>