Align backend with frontend, make clips end-to-end, polish admin

Backend
- Add ClipSubmission entity + table (runtime bootstrapper) and
  POST /api/public/clips (server-derives platform from the link)
- Surface clip submissions in the admin season detail
- Add DELETE candidate/category/clip endpoints with audit entries

Frontend
- Clips admin: real moderation view (list, open link, delete) instead
  of placeholder; wired clipSubmissions through types/api/store
- Categories admin: add delete with confirm modal (matches Candidates)
- Voting ranking bar uses the unified purple gradient
- Fix ASCII transliterations to proper German umlauts across admin
- Remove orphan AdminView 2.vue

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-18 07:53:19 +02:00
parent cca297a4eb
commit 24f9a69022
22 changed files with 504 additions and 145 deletions
+10 -10
View File
@@ -176,11 +176,11 @@ async function createCategory() {
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Jahresstatus</p>
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Award-Jahr steuern</h2>
<p class="mt-2 max-w-xl text-sm leading-6 text-slate-500">
Hier legst du fest, in welcher Phase das Jahr ist und ob genau dieses Jahr oeffentlich fuer Community, Voting und Archiv sichtbar ist.
Hier legst du fest, in welcher Phase das Jahr ist und ob genau dieses Jahr öffentlich für Community, Voting und Archiv sichtbar ist.
</p>
</div>
<div class="rounded-2xl border px-4 py-3 text-sm font-semibold" :class="seasonForm.isCurrent ? 'border-emerald-100 bg-emerald-50 text-emerald-700' : 'border-slate-100 bg-slate-50 text-slate-500'">
{{ seasonForm.isCurrent ? 'Oeffentlich aktiv' : 'Intern vorbereitet' }}
{{ seasonForm.isCurrent ? 'Öffentlich aktiv' : 'Intern vorbereitet' }}
</div>
</div>
</div>
@@ -195,16 +195,16 @@ async function createCategory() {
placeholder="z.B. Community Voting, Nominierung, Archiviert"
/>
<span class="block text-xs leading-5 text-slate-500">
Diese Phase wird als Orientierung fuer Team und spaeter auch fuer Public-Kommunikation genutzt.
Diese Phase wird als Orientierung für Team und später auch für Public-Kommunikation genutzt.
</span>
</label>
<label class="flex cursor-pointer gap-4 rounded-[24px] border border-violet-100 bg-violet-50/50 p-4 transition hover:bg-violet-50">
<input v-model="seasonForm.isCurrent" type="checkbox" class="mt-1 h-4 w-4 shrink-0 accent-violet-600" />
<span>
<span class="block font-semibold text-slate-800">Dieses Award-Jahr oeffentlich schalten</span>
<span class="block font-semibold text-slate-800">Dieses Award-Jahr öffentlich schalten</span>
<span class="mt-1 block text-sm leading-6 text-slate-500">
Wenn aktiv, gilt dieses Jahr als aktueller Public-Kontext. Nur ein Award-Jahr sollte gleichzeitig oeffentlich sein.
Wenn aktiv, gilt dieses Jahr als aktueller Public-Kontext. Nur ein Award-Jahr sollte gleichzeitig öffentlich sein.
</span>
</span>
</label>
@@ -226,7 +226,7 @@ async function createCategory() {
<div class="flex flex-col gap-3 border-t border-violet-100 pt-5 sm:flex-row sm:items-center sm:justify-between">
<p class="text-sm leading-6 text-slate-500">
Speichert Phase und Public-Status fuer <strong class="text-slate-700">{{ seasonDetail.name }}</strong>.
Speichert Phase und Public-Status für <strong class="text-slate-700">{{ seasonDetail.name }}</strong>.
</p>
<Button :disabled="seasonSaving || !selectedSeasonId" @click="saveSeason">
{{ seasonSaving ? 'Speichert ...' : 'Jahresstatus speichern' }}
@@ -248,7 +248,7 @@ async function createCategory() {
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Neue Kategorie</p>
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Kategorie planen</h2>
<p class="mt-2 text-sm leading-6 text-slate-500">
Lege zuerst Gruppe, Namen und Limit fest. Slug und Sortierung bestimmen spaeter URL, Anzeige und Reihenfolge im Voting.
Lege zuerst Gruppe, Namen und Limit fest. Slug und Sortierung bestimmen später URL, Anzeige und Reihenfolge im Voting.
</p>
</div>
</div>
@@ -268,7 +268,7 @@ async function createCategory() {
<label class="space-y-2">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Beschreibung</span>
<textarea v-model="newCategoryForm.description" class="min-h-28 w-full rounded-2xl border border-violet-200 px-4 py-3 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="Kurz erklaeren, wofuer diese Kategorie steht." />
<textarea v-model="newCategoryForm.description" class="min-h-28 w-full rounded-2xl border border-violet-200 px-4 py-3 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="Kurz erklären, wofür diese Kategorie steht." />
</label>
<div class="grid gap-4 sm:grid-cols-3">
@@ -288,7 +288,7 @@ async function createCategory() {
<div class="flex flex-col gap-3 border-t border-violet-100 pt-5 sm:flex-row sm:items-center sm:justify-between">
<p class="text-sm leading-6 text-slate-500">
Neue Kategorien sind sofort Teil des gewaehlten Award-Jahres und koennen danach unten weiter bearbeitet werden.
Neue Kategorien sind sofort Teil des gewählten Award-Jahres und können danach unten weiter bearbeitet werden.
</p>
<Button :disabled="categorySaving === 'new' || !selectedSeasonId" @click="createCategory">
{{ categorySaving === 'new' ? 'Erstellt ...' : 'Kategorie anlegen' }}
@@ -305,7 +305,7 @@ async function createCategory() {
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Kategorien</p>
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Kategorien dieses Jahres</h2>
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
Pruefe Struktur, Slug, Limit und Kandidatenzahl pro Kategorie. Erst filtern, dann gezielt bearbeiten.
Prüfe Struktur, Slug, Limit und Kandidatenzahl pro Kategorie. Erst filtern, dann gezielt bearbeiten.
</p>
</div>
<div class="grid gap-2 sm:grid-cols-3 xl:min-w-[380px]">