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>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
<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()
|
||||
@@ -31,6 +33,12 @@ 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
|
||||
@@ -57,9 +65,9 @@ async function submitNomination() {
|
||||
nominees: nominees.value,
|
||||
})
|
||||
|
||||
submitMessage.value = `${response.saved} Nominierungen fuer ${response.category} gespeichert.`
|
||||
submitMessage.value = `Stark! ${response.saved} Nominierung(en) für ${response.category} sind eingetragen.`
|
||||
} catch (error) {
|
||||
submitError.value = error instanceof Error ? error.message : 'Nominierung konnte nicht gespeichert werden.'
|
||||
submitError.value = error instanceof Error ? error.message : 'Ups – das hat nicht geklappt. Versuch es gleich nochmal.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -67,34 +75,33 @@ async function submitNomination() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-10 pb-14">
|
||||
<div class="space-y-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.35em] text-amber-500">Nominierungs-Flow</p>
|
||||
<h1 class="max-w-[12ch] font-[Cormorant_Garamond] text-6xl leading-[0.92] text-violet-800">Kategorien waehlen, Regeln live pruefen</h1>
|
||||
<p class="max-w-3xl text-lg leading-8 text-slate-600">
|
||||
Nur Twitch Login, kein separates Konto. Das Team pflegt Kategorien pro Jahr, waehrend die UI sofort Limits, Dubletten und editierbare Entwuerfe abbildet.
|
||||
</p>
|
||||
</div>
|
||||
<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"
|
||||
/>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[0.68fr_1.32fr]">
|
||||
<Card class="p-7">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">Regeln</p>
|
||||
<ul class="mt-5 space-y-4 text-slate-600">
|
||||
<li>Pro Kategorie nur eine Nominierung derselben Person.</li>
|
||||
<li>Insgesamt maximal drei Nominierungen in diesem Draft.</li>
|
||||
<li>Freitext-Ideen landen spaeter in der Review-Liste.</li>
|
||||
<li>Bereits gespeicherte Entwuerfe koennen bis zur Deadline bearbeitet werden.</li>
|
||||
</ul>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
<Card class="p-7">
|
||||
<div class="grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
|
||||
<div class="space-y-5">
|
||||
<p v-if="!authStore.isLoggedIn" class="rounded-[26px] border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-700">
|
||||
Bitte zuerst ueber den Kopfbereich mit einem Twitch-Account einloggen, damit die Nominierung gespeichert werden kann.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<label class="text-sm font-semibold text-slate-600">Kategorie</label>
|
||||
<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"
|
||||
@@ -102,53 +109,101 @@ async function submitNomination() {
|
||||
option-value="value"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<div v-if="selectedCategory" class="rounded-[28px] bg-violet-50/70 p-6">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">{{ selectedCategory.groupName }}</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">{{ selectedCategory.name }}</h2>
|
||||
<p class="mt-2 text-slate-600">{{ selectedCategory.description }}</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-[28px] border border-violet-100 bg-white/70 p-5">
|
||||
<label class="text-sm font-semibold text-slate-600">Neuen Namen hinzufuegen</label>
|
||||
<input
|
||||
v-model="nomineeName"
|
||||
type="text"
|
||||
class="w-full rounded-2xl border border-violet-200 bg-white px-4 py-3"
|
||||
placeholder="z. B. Shiro Ch."
|
||||
/>
|
||||
<Button @click="addNominee">Nominierung hinzufuegen</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Dein Entwurf</h3>
|
||||
<div
|
||||
v-for="name in nominees"
|
||||
:key="name"
|
||||
class="flex items-center justify-between rounded-[26px] border border-violet-100 bg-white/85 px-5 py-5"
|
||||
>
|
||||
<div>
|
||||
<p class="font-semibold text-slate-800">{{ name }}</p>
|
||||
<p class="text-sm text-slate-500">@{{ name.toLowerCase().replace(/\s+/g, '') }}</p>
|
||||
</div>
|
||||
<button class="text-sm font-semibold text-rose-500" @click="removeNominee(name)">Entfernen</button>
|
||||
</div>
|
||||
<p class="rounded-[26px] border border-dashed border-violet-200 bg-violet-50/60 px-5 py-5 text-sm text-slate-600">
|
||||
Live-Status: {{ nominees.length }}/3 Slots belegt.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<p v-if="submitMessage" class="rounded-[26px] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm text-emerald-700">
|
||||
{{ submitMessage }}
|
||||
</p>
|
||||
<p v-if="submitError" class="rounded-[26px] border border-rose-200 bg-rose-50 px-5 py-4 text-sm text-rose-700">
|
||||
{{ submitError }}
|
||||
</p>
|
||||
<Button :disabled="submitting || !authStore.isLoggedIn || !selectedCategoryId || nominees.length === 0" @click="submitNomination">
|
||||
{{ submitting ? 'Speichert ...' : 'Nominierung speichern' }}
|
||||
<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>
|
||||
</Card>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user