Expand admin navigation and pages

This commit is contained in:
AzuTear
2026-06-18 00:21:40 +02:00
parent 178f014a4a
commit 0fa763667d
9 changed files with 1097 additions and 35 deletions
+101
View File
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ExternalLink, Film, Search, Tags, Users } from '@lucide/vue'
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
import Card from '../../components/ui/Card.vue'
import { useAwardsStore } from '../../stores/awards'
const store = useAwardsStore()
const query = ref('')
const seasonDetail = computed(() => store.adminSeasonDetail)
const clipCategories = computed(() =>
seasonDetail.value.categories.filter((category) => `${category.groupName} ${category.name}`.toLowerCase().includes('clip')),
)
const clipCategoryIds = computed(() => new Set(clipCategories.value.map((category) => category.id)))
const clipCandidates = computed(() => {
const search = query.value.trim().toLowerCase()
return seasonDetail.value.candidates
.filter((candidate) => clipCategoryIds.value.has(candidate.categoryId))
.filter((candidate) => !search || [candidate.displayName, candidate.channelSlug, candidate.platform].join(' ').toLowerCase().includes(search))
})
const candidateCategory = computed(() => Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, category.name])))
const stats = computed(() => [
{ label: 'Clip-Kategorien', value: clipCategories.value.length, icon: Tags },
{ label: 'Clip-Kandidaten', value: clipCandidates.value.length, icon: Users },
{ label: 'Offene Reviews', value: seasonDetail.value.pendingNominations.filter((nomination) => clipCategoryIds.value.has(nomination.categoryId)).length, icon: Film },
])
</script>
<template>
<div class="space-y-6">
<AdminPageHeader
eyebrow="Clips"
title="Clip-Kategorien im Blick behalten"
description="Bis ein eigener Clip-Review-Endpunkt existiert, zeigt diese Seite die operativen Clip-Kategorien, Kandidaten und offenen Review-Faelle kompakt an."
:icon="Film"
/>
<AdminSeasonToolbar />
<section class="grid gap-4 lg:grid-cols-3">
<Card v-for="stat in stats" :key="stat.label" class="p-5">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-violet-500">{{ stat.label }}</p>
<strong class="mt-3 block text-3xl text-violet-900">{{ stat.value }}</strong>
</div>
<div class="grid h-10 w-10 place-items-center rounded-2xl bg-violet-100 text-violet-700">
<component :is="stat.icon" class="h-5 w-5" />
</div>
</div>
</Card>
</section>
<section class="grid gap-6 xl:grid-cols-[0.86fr_1.14fr]">
<Card class="p-6">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Workflow</p>
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Was hier geprueft wird</h2>
<div class="mt-5 space-y-3">
<div class="rounded-2xl border border-violet-100 bg-violet-50/50 p-4">
<p class="font-semibold text-slate-900">1. Clip-Kategorien vorhanden?</p>
<p class="mt-1 text-sm leading-6 text-slate-500">Kategorien mit Clip im Namen werden hier automatisch gebuendelt.</p>
</div>
<div class="rounded-2xl border border-violet-100 bg-white/90 p-4">
<p class="font-semibold text-slate-900">2. Kandidaten gepflegt?</p>
<p class="mt-1 text-sm leading-6 text-slate-500">Ohne Kandidaten kann die Community spaeter keine saubere Clip-Auswahl treffen.</p>
</div>
<div class="rounded-2xl border border-amber-100 bg-amber-50/70 p-4">
<p class="font-semibold text-amber-800">3. Backend-Ausbau offen</p>
<p class="mt-1 text-sm leading-6 text-amber-700">Fuer echte Clip-Moderation brauchen wir spaeter eine ClipSubmission-Admin-API mit Status, Duplikaten und Entscheidung.</p>
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-violet-100 p-5">
<label class="relative block">
<Search class="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-400" />
<input v-model="query" class="h-12 w-full rounded-2xl border border-violet-200 bg-white pl-11 pr-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="Clip-Kandidat, Handle oder Plattform suchen" />
</label>
</div>
<div class="divide-y divide-violet-50">
<div v-for="candidate in clipCandidates" :key="candidate.id" class="grid gap-3 px-5 py-4 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center">
<div class="min-w-0">
<p class="font-semibold text-slate-900">{{ candidate.displayName }}</p>
<p class="mt-1 text-sm text-slate-500">{{ candidate.channelSlug }} · {{ candidate.platform }} · {{ candidateCategory[candidate.categoryId] }}</p>
</div>
<span class="inline-flex items-center gap-2 rounded-full border border-violet-100 bg-violet-50 px-3 py-1 text-xs font-semibold text-violet-700">
<ExternalLink class="h-3.5 w-3.5" />
Clip-Link spaeter
</span>
</div>
<p v-if="clipCandidates.length === 0" class="px-5 py-10 text-center text-sm text-slate-500">
Keine Clip-Kandidaten gefunden. Lege zuerst eine Clip-Kategorie und passende Kandidaten an.
</p>
</div>
</Card>
</section>
</div>
</template>