Initial VTuber Awards implementation
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user