Files
vtuber-awards/frontend/src/views/NominationsView.vue
T
AzuTear cca297a4eb Redesign public site, add phase gating and clip submission
- Rebuild landing page and Nominations/Voting/Winners views in the
  Jayuhime brand style (purple/gold/pastel-rainbow, stars, PageHero banner)
- Gate participation by season phase (nominate/clip share the nomination
  window, vote in the voting window); hide closed/locked pages from nav
- Add login-gated clip submission flow (link-based) + voting clip links
- Make candidate admin scalable: searchable, filterable, paginated table
  with modal create/edit and confirm-delete; add delete API/store actions
- New reusable Modal and PageHero components, usePhases composable
- Segmented top nav and full-size header on admin routes
- vite: honor PORT env for preview

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 06:46:41 +02:00

210 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import Select from 'primevue/select'
import { Plus, Sparkles, Star, Trophy, X } from '@lucide/vue'
import { useAwardsStore } from '../stores/awards'
import { useAuthStore } from '../stores/auth'
import Button from '../components/ui/Button.vue'
import Card from '../components/ui/Card.vue'
import PageHero from '../components/ui/PageHero.vue'
const store = useAwardsStore()
const authStore = useAuthStore()
const selectedCategoryId = ref<number | null>(null)
const nomineeName = ref('')
const nominees = ref<string[]>(['Hoshimi Miyu', 'Kurainu'])
const submitting = ref(false)
const submitMessage = ref('')
const submitError = ref('')
onMounted(async () => {
await store.loadHomeData()
selectedCategoryId.value = store.categories.categories[0]?.id ?? null
})
const categories = computed(() =>
store.categories.categories.map((category) => ({
label: category.name,
value: category.id,
})),
)
const selectedCategory = computed(() =>
store.categories.categories.find((category) => category.id === selectedCategoryId.value),
)
const slotsLeft = computed(() => 3 - nominees.value.length)
function slugFor(name: string) {
return `@${name.toLowerCase().replace(/\s+/g, '')}`
}
function addNominee() {
const value = nomineeName.value.trim()
if (!value || nominees.value.includes(value) || nominees.value.length >= 3) return
nominees.value = [...nominees.value, value]
nomineeName.value = ''
}
function removeNominee(name: string) {
nominees.value = nominees.value.filter((entry) => entry !== name)
}
async function submitNomination() {
if (!selectedCategoryId.value || nominees.value.length === 0) return
submitting.value = true
submitMessage.value = ''
submitError.value = ''
try {
const response = await store.submitNomination({
year: store.categories.year,
categoryId: selectedCategoryId.value,
twitchUserId: authStore.session?.twitchUserId ?? '',
nominees: nominees.value,
})
submitMessage.value = `Stark! ${response.saved} Nominierung(en) für ${response.category} sind eingetragen.`
} catch (error) {
submitError.value = error instanceof Error ? error.message : 'Ups das hat nicht geklappt. Versuch es gleich nochmal.'
} finally {
submitting.value = false
}
}
</script>
<template>
<div class="space-y-12 pb-16">
<PageHero
eyebrow="Schritt 1 · Nominieren"
title="Wen feiern wir dieses Jahr?"
:description="`Trag deine Lieblings-Creator ein und bring sie auf die große Bühne. Pro Kategorie hast du ${ 3 } Plätze nutz sie für die VTuber, die dein Jahr gemacht haben.`"
:icon="Star"
/>
<Card class="overflow-hidden p-0">
<!-- Stepper -->
<div class="flex items-center gap-4 border-b border-violet-100 px-7 py-5 text-xs font-semibold text-slate-400 sm:px-9">
<span class="text-violet-600">1 · Kategorie</span>
<span class="h-px flex-1 bg-slate-200" />
<span class="text-violet-600">2 · Favoriten eintragen</span>
<span class="h-px flex-1 bg-slate-200" />
<span>3 · Abschicken</span>
</div>
<div class="grid gap-8 p-7 sm:p-9 lg:grid-cols-[0.82fr_1.18fr]">
<!-- Left: category + add -->
<div class="space-y-5">
<p v-if="!authStore.isLoggedIn" class="rounded-2xl border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-800">
Logg dich kurz oben mit Twitch ein dann zählt deine Nominierung. 💜
</p>
<div class="space-y-2">
<label class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-400">In welcher Kategorie?</label>
<Select
v-model="selectedCategoryId"
:options="categories"
option-label="label"
option-value="value"
class="w-full"
/>
</div>
<div v-if="selectedCategory" class="relative overflow-hidden rounded-2xl bg-[linear-gradient(135deg,#ece2ff,#f6ecff_55%,#fff2dd)] p-6">
<div class="absolute right-3 top-3 h-1 w-24 rounded-full bg-[linear-gradient(90deg,#c4b5fd,#f5d0fe,#fecdd3,#fde68a,#bbf7d0,#bae6fd)]" />
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-violet-500">{{ selectedCategory.groupName }}</p>
<h2 class="mt-2 font-[Cormorant_Garamond] text-3xl text-violet-800">{{ selectedCategory.name }}</h2>
<p class="mt-2 text-sm leading-7 text-slate-600">{{ selectedCategory.description }}</p>
</div>
<div class="space-y-3 rounded-2xl border border-violet-100 bg-white/70 p-5">
<label class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-400">Wen möchtest du nominieren?</label>
<input
v-model="nomineeName"
type="text"
class="w-full rounded-2xl border border-violet-200 bg-white px-4 py-3 text-sm"
placeholder="Name oder @handle, z. B. Shiro Ch."
@keyup.enter="addNominee"
/>
<Button class="w-full gap-2" :disabled="slotsLeft <= 0" @click="addNominee">
<Plus class="h-4 w-4" /> {{ slotsLeft > 0 ? 'Zur Liste hinzufügen' : 'Alle Plätze belegt' }}
</Button>
</div>
</div>
<!-- Right: draft -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h3 class="font-[Cormorant_Garamond] text-3xl text-violet-800">Deine Favoriten</h3>
<p class="text-sm text-slate-500">
{{ slotsLeft > 0 ? `Noch ${slotsLeft} Platz${slotsLeft === 1 ? '' : 'e'} frei` : 'Volle Liste richtig so!' }}
</p>
</div>
<span class="rounded-full bg-violet-50 px-3 py-1 text-xs font-semibold text-violet-600">{{ nominees.length }}/3</span>
</div>
<transition-group name="list" tag="div" class="space-y-2">
<div
v-for="(name, index) in nominees"
:key="name"
class="flex items-center justify-between rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-3"
>
<div class="flex items-center gap-3">
<span class="grid h-10 w-10 place-items-center rounded-full bg-[linear-gradient(135deg,#c4b5fd,#f5a9d6)] text-sm font-semibold text-white">
{{ name.charAt(0) }}
</span>
<div>
<p class="font-semibold text-slate-800">{{ name }}</p>
<p class="text-xs text-slate-500">{{ slugFor(name) }}</p>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs font-semibold text-violet-400">#{{ index + 1 }}</span>
<button class="grid h-8 w-8 place-items-center rounded-full text-rose-400 transition hover:bg-rose-50" @click="removeNominee(name)">
<X class="h-4 w-4" />
</button>
</div>
</div>
</transition-group>
<div v-if="nominees.length === 0" class="rounded-2xl border border-dashed border-violet-200 bg-violet-50/40 px-5 py-8 text-center">
<Sparkles class="mx-auto h-6 w-6 text-amber-400" />
<p class="mt-2 text-sm text-slate-500">Noch leer wer hat dieses Jahr deinen Bildschirm zum Leuchten gebracht?</p>
</div>
<div class="rounded-2xl bg-violet-50/50 px-5 py-4 text-xs leading-6 text-slate-500">
<p class="flex items-center gap-2"><Star class="h-3.5 w-3.5 text-amber-400" /> Bis zu 3 Favoriten pro Kategorie keine Person doppelt.</p>
<p class="flex items-center gap-2"><Star class="h-3.5 w-3.5 text-amber-400" /> Alles bleibt bis zum Ende der Phase änderbar.</p>
</div>
<p v-if="submitMessage" class="rounded-2xl border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm text-emerald-700">{{ submitMessage }}</p>
<p v-if="submitError" class="rounded-2xl border border-rose-200 bg-rose-50 px-5 py-4 text-sm text-rose-700">{{ submitError }}</p>
<Button
class="w-full gap-2"
:disabled="submitting || !authStore.isLoggedIn || !selectedCategoryId || nominees.length === 0"
@click="submitNomination"
>
<Trophy class="h-4 w-4" />
{{ submitting ? 'Speichert ...' : 'Nominierungen abschicken' }}
</Button>
</div>
</div>
</Card>
</div>
</template>
<style scoped>
.list-enter-active,
.list-leave-active {
transition: all 0.25s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(-6px);
}
</style>