Initial VTuber Awards implementation

This commit is contained in:
AzuTear
2026-06-17 11:35:45 +02:00
commit 670259a983
74 changed files with 15797 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import { Star } from '@lucide/vue'
import Button from './ui/Button.vue'
import { useAuthStore } from '../stores/auth'
const route = useRoute()
const authStore = useAuthStore()
const loginOpen = ref(false)
const loginError = ref('')
const loginForm = reactive({
twitchUserId: 'jayuhime_demo',
displayName: 'Jayuhime',
role: 'viewer' as 'viewer' | 'admin',
})
const navItems = [
{ label: 'Home', to: '/' },
{ label: 'Nominierung', to: '/nominations' },
{ label: 'Voting', to: '/voting' },
{ label: 'Gewinner', to: '/winners' },
{ label: 'Admin', to: '/admin' },
]
const currentLabel = computed(
() => navItems.find((item) => item.to === route.path)?.label ?? 'Awards',
)
const visibleNavItems = computed(() =>
navItems.filter((item) => item.to !== '/admin' || authStore.isAdmin),
)
async function login(role: 'viewer' | 'admin') {
loginError.value = ''
loginForm.role = role
try {
await authStore.login(loginForm)
loginOpen.value = false
} catch (error) {
loginError.value = error instanceof Error ? error.message : 'Login fehlgeschlagen.'
}
}
</script>
<template>
<div class="min-h-screen overflow-x-hidden bg-[linear-gradient(180deg,#fffdf9_0%,#fff6ee_38%,#fff9f4_100%)] text-slate-800">
<div class="pointer-events-none absolute inset-x-0 top-0 h-[520px] bg-[radial-gradient(circle_at_top_left,_rgba(237,214,167,0.38),transparent_34%),radial-gradient(circle_at_top_right,_rgba(210,195,255,0.32),transparent_28%)]" />
<div class="pointer-events-none absolute left-1/2 top-36 h-[560px] w-[560px] -translate-x-1/2 rounded-full border border-white/70 opacity-70 blur-3xl" />
<div class="relative mx-auto w-full max-w-[1460px] px-4 py-4 sm:px-6 lg:px-10">
<div class="mb-5 flex items-center justify-between border-b border-black/8 px-2 pb-3 text-[11px] uppercase tracking-[0.34em] text-slate-500">
<span>VTuber Star Awards 2026</span>
<span>{{ currentLabel }}</span>
</div>
<header class="mb-10 flex flex-col gap-6 rounded-[34px] border border-white/70 bg-white/72 px-5 py-5 shadow-[0_24px_80px_rgba(93,63,135,0.08)] backdrop-blur lg:px-7">
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
<RouterLink to="/" class="flex items-center gap-4 text-slate-800 no-underline">
<div class="grid h-12 w-12 place-items-center rounded-[1.4rem] bg-[linear-gradient(135deg,#f6e3b2,#f5c877)] text-amber-950 shadow-[0_16px_28px_rgba(245,200,119,0.35)]">
<Star class="h-5 w-5" />
</div>
<div>
<strong class="block text-sm tracking-[0.35em]">VTUBER</strong>
<span class="block text-[11px] tracking-[0.45em] text-slate-500">STAR AWARDS</span>
</div>
</RouterLink>
<div class="flex flex-wrap items-center gap-3">
<template v-if="authStore.session">
<div class="rounded-full border border-violet-100 bg-violet-50/70 px-4 py-2 text-sm text-violet-800">
{{ authStore.session.displayName }} · {{ authStore.session.role }}
</div>
<Button variant="ghost" @click="authStore.logout()">Logout</Button>
</template>
<template v-else>
<Button variant="ghost" @click="loginOpen = !loginOpen">Sign in</Button>
<Button @click="login('viewer')">Mit Twitch Login</Button>
</template>
</div>
</div>
<div v-if="loginOpen && !authStore.session" class="rounded-[28px] border border-violet-100 bg-white/80 p-5">
<div class="grid gap-4 md:grid-cols-3">
<input v-model="loginForm.displayName" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Display Name" />
<input v-model="loginForm.twitchUserId" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Twitch User ID" />
<div class="flex flex-wrap gap-3">
<Button :disabled="authStore.loading" @click="login('viewer')">
{{ authStore.loading ? 'Loggt ein ...' : 'Viewer Login' }}
</Button>
<Button variant="secondary" :disabled="authStore.loading" @click="login('admin')">Admin Login</Button>
</div>
</div>
<p v-if="loginError" class="mt-3 text-sm text-rose-700">{{ loginError }}</p>
</div>
<div class="flex flex-col gap-4 border-t border-black/6 pt-4 lg:flex-row lg:items-center lg:justify-between">
<nav class="flex flex-wrap items-center gap-2 text-sm text-slate-600">
<RouterLink
v-for="item in visibleNavItems"
:key="item.to"
:to="item.to"
class="rounded-full px-4 py-2 transition hover:bg-violet-50 hover:text-violet-700"
:class="route.path === item.to ? 'bg-violet-100 text-violet-800' : ''"
>
{{ item.label }}
</RouterLink>
</nav>
<div class="flex items-center gap-3 text-xs uppercase tracking-[0.24em] text-slate-500">
<span class="inline-flex h-2 w-2 rounded-full bg-emerald-500" />
Community Voting live
</div>
</div>
</header>
<slot />
</div>
</div>
</template>
+44
View File
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { computed } from 'vue'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-xl text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-violet-600 text-white shadow-lg shadow-violet-500/20 hover:bg-violet-500',
secondary: 'border border-amber-300/60 bg-white text-amber-600 hover:bg-amber-50',
ghost: 'bg-white/70 text-slate-700 hover:bg-white',
},
size: {
default: 'h-11 px-5',
lg: 'h-12 px-6',
sm: 'h-9 px-4',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
const props = defineProps<{
variant?: VariantProps<typeof buttonVariants>['variant']
size?: VariantProps<typeof buttonVariants>['size']
class?: string
}>()
const classes = computed(() =>
cn(buttonVariants({ variant: props.variant, size: props.size }), props.class),
)
</script>
<template>
<button :class="classes">
<slot />
</button>
</template>
+20
View File
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { cn } from '../../lib/utils'
defineProps<{
class?: string
}>()
</script>
<template>
<div
:class="
cn(
'rounded-[28px] border border-violet-200/70 bg-white/80 shadow-[0_24px_60px_rgba(168,145,214,0.12)] backdrop-blur',
$props.class,
)
"
>
<slot />
</div>
</template>