Clean up repository hygiene

- Expand .gitignore with editor state, secrets, logs, and archive patterns
- Remove AdminCategoriesViewV2.vue (replaced, no remaining references)
- Exclude accidental AdminContentView duplicate from tsconfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-28 23:31:52 +02:00
parent fc5c13a4fd
commit 4b2c5fa15d
3 changed files with 33 additions and 463 deletions
@@ -1,462 +0,0 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
ChevronRight,
ExternalLink,
Layers3,
PlusCircle,
Search,
Tags,
Trash2,
TriangleAlert,
Users,
X,
} from '@lucide/vue'
import { RouterLink } from 'vue-router'
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
import { useAdminCategoryManager } from '../../components/admin/useAdminCategoryManager'
import { watchAdminToast } from '../../composables/useAdminToast'
import Button from '../../components/ui/Button.vue'
import Card from '../../components/ui/Card.vue'
import Modal from '../../components/ui/Modal.vue'
const {
selectedSeasonId,
query,
statusFilter,
selectedCategoryId,
saving,
adminMessage,
adminError,
editForms,
newCategoryForm,
filteredCategories,
selectedCategory,
categoryStats,
statusFilters,
categoryToDelete,
deleting,
saveCategory,
createCategory,
fillNewSlug,
confirmDeleteCategory,
} = useAdminCategoryManager()
watchAdminToast(adminMessage, adminError)
// ---------- "Neu" panel toggle ----------
const showCreatePanel = ref(false)
// ---------- Grouped list ----------
const groupedCategories = computed(() => {
const groups = new Map<string, typeof filteredCategories.value>()
for (const cat of filteredCategories.value) {
const key = cat.groupName || 'Ohne Gruppe'
if (!groups.has(key)) groups.set(key, [])
groups.get(key)!.push(cat)
}
return [...groups.entries()].map(([name, cats]) => ({ name, cats }))
})
// ---------- Stats für aktuell gewählte Kategorie ----------
const selectedCandidateCount = computed(() =>
selectedCategory.value
? filteredCategories.value.find((c) => c.id === selectedCategory.value!.id)?.candidates ?? 0
: 0,
)
const selectedPendingCount = computed(() =>
selectedCategory.value
? filteredCategories.value.find((c) => c.id === selectedCategory.value!.id)?.pending ?? 0
: 0,
)
// ---------- Top-level stats ----------
const topStats = computed(() => [
{
label: 'Kategorien',
value: categoryStats.value.find((s) => s.label === 'Kategorien')?.value ?? 0,
tone: 'text-violet-700 bg-violet-50 border-violet-100',
},
{
label: 'Kandidaten',
value: categoryStats.value.find((s) => s.label === 'Kandidaten')?.value ?? 0,
tone: 'text-cyan-700 bg-cyan-50 border-cyan-100',
},
{
label: 'Offene Reviews',
value: categoryStats.value.find((s) => s.label === 'Reviews')?.value ?? 0,
tone: 'text-amber-700 bg-amber-50 border-amber-100',
},
{
label: 'Leer',
value: statusFilters.value.find((f) => f.key === 'empty')?.count ?? 0,
tone: 'text-rose-700 bg-rose-50 border-rose-100',
},
])
</script>
<template>
<div class="space-y-6">
<AdminPageHeader
eyebrow="Kategorien"
description="Kategorien verwalten, Gruppen pflegen und Kandidatenbasis prüfen."
:icon="Tags"
/>
<AdminSeasonToolbar />
<!-- Top stats -->
<section class="grid grid-cols-2 gap-4 xl:grid-cols-4">
<Card v-for="stat in topStats" :key="stat.label" class="flex items-center gap-4 p-5">
<span class="grid h-11 w-11 shrink-0 place-items-center rounded-2xl border text-sm" :class="stat.tone">
<span class="text-lg font-black">{{ stat.value }}</span>
</span>
<span class="text-sm font-semibold text-slate-600">{{ stat.label }}</span>
</Card>
</section>
<section class="grid gap-6 xl:grid-cols-[minmax(340px,0.82fr)_minmax(0,1.18fr)]">
<!-- Linke Spalte: Gruppierte Liste -->
<div class="flex flex-col gap-3">
<Card class="overflow-hidden">
<!-- Search + Filter -->
<div class="space-y-3 border-b border-violet-100 p-4">
<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-11 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="Kategorie, Gruppe oder Slug"
/>
</label>
<div class="flex flex-wrap gap-1.5">
<button
v-for="filter in statusFilters"
:key="filter.key"
type="button"
class="rounded-full border px-3 py-1 text-xs font-semibold transition"
:class="statusFilter === filter.key
? 'border-violet-200 bg-violet-100 text-violet-800'
: 'border-violet-100 bg-white text-slate-600 hover:bg-violet-50'"
@click="statusFilter = filter.key"
>
{{ filter.label }} · {{ filter.count }}
</button>
</div>
</div>
<!-- Grouped category list -->
<div class="max-h-[640px] overflow-y-auto">
<template v-if="groupedCategories.length > 0">
<div v-for="group in groupedCategories" :key="group.name">
<!-- Group header -->
<div class="sticky top-0 z-10 flex items-center gap-2 border-b border-violet-50 bg-violet-50/90 px-4 py-2 backdrop-blur-sm">
<Layers3 class="h-3.5 w-3.5 text-violet-400" />
<span class="text-[11px] font-bold uppercase tracking-[0.18em] text-violet-500">{{ group.name }}</span>
<span class="ml-auto rounded-full border border-violet-100 bg-white px-2 py-0.5 text-[10px] font-semibold text-violet-600">{{ group.cats.length }}</span>
</div>
<!-- Categories in group -->
<div class="divide-y divide-violet-50/80">
<button
v-for="cat in group.cats"
:key="cat.id"
type="button"
class="group w-full px-4 py-3 text-left transition"
:class="selectedCategory?.id === cat.id
? 'bg-[linear-gradient(135deg,#ece2ff30,#fff2dd20)] outline-none ring-1 ring-inset ring-violet-200'
: 'hover:bg-violet-50/50'"
@click="selectedCategoryId = cat.id; showCreatePanel = false"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<p class="truncate text-sm font-semibold text-slate-900">{{ cat.name }}</p>
<ChevronRight
class="h-3.5 w-3.5 shrink-0 text-violet-400 opacity-0 transition group-hover:opacity-100"
:class="selectedCategory?.id === cat.id ? 'opacity-100' : ''"
/>
</div>
<p class="mt-0.5 truncate text-xs text-slate-400">/{{ cat.slug }}</p>
<!-- Badges -->
<div class="mt-2 flex flex-wrap gap-1">
<span
class="rounded-full px-2 py-0.5 text-[10px] font-semibold"
:class="cat.candidates > 0 ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-600'"
>
{{ cat.candidates }} Kandidaten
</span>
<span v-if="cat.pending > 0" class="rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold text-amber-700">
{{ cat.pending }} Reviews
</span>
<span class="rounded-full bg-slate-50 px-2 py-0.5 text-[10px] font-semibold text-slate-500">
Limit {{ cat.maxNomineesPerUser }}
</span>
</div>
</div>
<span class="mt-0.5 shrink-0 rounded-xl border border-violet-100 bg-white px-2 py-0.5 text-[11px] font-bold text-violet-600">
#{{ cat.sortOrder }}
</span>
</div>
</button>
</div>
</div>
</template>
<p v-else class="px-5 py-10 text-center text-sm text-slate-500">
Keine Kategorien passen zu diesem Filter.
</p>
</div>
<!-- Add button -->
<div class="border-t border-violet-100 p-3">
<button
type="button"
class="flex w-full items-center justify-center gap-2 rounded-2xl border py-2.5 text-sm font-semibold transition"
:class="showCreatePanel
? 'border-violet-200 bg-violet-100 text-violet-800'
: 'border-violet-100 bg-white text-violet-700 hover:bg-violet-50'"
@click="showCreatePanel = !showCreatePanel; selectedCategoryId = null"
>
<component :is="showCreatePanel ? X : PlusCircle" class="h-4 w-4" />
{{ showCreatePanel ? 'Abbrechen' : 'Kategorie anlegen' }}
</button>
</div>
</Card>
</div>
<!-- Rechte Spalte: Edit / Create -->
<div class="space-y-0">
<!-- Create panel -->
<Card v-if="showCreatePanel" class="p-6">
<div class="flex items-start gap-4">
<span class="grid h-11 w-11 shrink-0 place-items-center rounded-2xl bg-violet-100 text-violet-700">
<PlusCircle class="h-5 w-5" />
</span>
<div>
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Neue Kategorie</p>
<h2 class="mt-1 text-xl font-bold text-slate-900">Kategorie anlegen</h2>
</div>
</div>
<div class="mt-5 grid gap-4 sm:grid-cols-2">
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Gruppe</span>
<input
v-model="newCategoryForm.groupName"
placeholder="z. B. Streaming"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Name</span>
<input
v-model="newCategoryForm.name"
placeholder="Bester Streamer"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Slug</span>
<div class="flex gap-2">
<input
v-model="newCategoryForm.slug"
placeholder="bester-streamer"
class="h-12 min-w-0 flex-1 rounded-2xl border border-violet-200 px-4 text-sm font-mono outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
<button
type="button"
class="h-12 rounded-2xl border border-violet-100 bg-violet-50 px-4 text-xs font-semibold text-violet-700 transition hover:bg-violet-100"
@click="fillNewSlug"
>
Auto
</button>
</div>
</label>
<div class="grid grid-cols-2 gap-3">
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Reihenfolge</span>
<input
v-model.number="newCategoryForm.sortOrder"
type="number"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Limit</span>
<input
v-model.number="newCategoryForm.maxNomineesPerUser"
type="number"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
</div>
</div>
<label class="mt-4 block space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Beschreibung</span>
<textarea
v-model="newCategoryForm.description"
rows="3"
placeholder="Kurze Beschreibung für Team und Kandidaten-Seite"
class="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"
/>
</label>
<div class="mt-5 flex justify-end gap-3">
<Button variant="ghost" @click="showCreatePanel = false">Abbrechen</Button>
<Button :disabled="saving === 'new' || !selectedSeasonId" @click="createCategory">
{{ saving === 'new' ? 'Erstellt ' : 'Kategorie anlegen' }}
</Button>
</div>
</Card>
<!-- Edit panel -->
<Card v-else-if="selectedCategory && editForms[selectedCategory.id]" class="overflow-hidden">
<!-- Header -->
<div class="border-b border-violet-100 bg-gradient-to-br from-white via-violet-50/40 to-[#f7eef8] px-6 py-5">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">
{{ editForms[selectedCategory.id].groupName || 'Ohne Gruppe' }}
</p>
<h2 class="mt-1 truncate text-xl font-bold text-slate-900">{{ selectedCategory.name }}</h2>
<p class="mt-1 font-mono text-sm text-slate-400">/{{ selectedCategory.slug }}</p>
</div>
<span class="shrink-0 rounded-2xl border border-violet-200 bg-white px-3 py-2 text-sm font-bold text-violet-700">
#{{ selectedCategory.sortOrder }}
</span>
</div>
<!-- Quick stats row -->
<div class="mt-4 flex flex-wrap gap-3">
<RouterLink
to="/admin/candidates"
class="flex items-center gap-1.5 rounded-full border border-violet-100 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 transition hover:border-violet-200 hover:bg-violet-50"
>
<Users class="h-3.5 w-3.5 text-violet-500" />
{{ selectedCandidateCount }} Kandidaten
<ExternalLink class="h-3 w-3 text-slate-400" />
</RouterLink>
<span
v-if="selectedPendingCount > 0"
class="flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-3 py-1.5 text-xs font-semibold text-amber-700"
>
{{ selectedPendingCount }} Reviews offen
</span>
<span
v-if="selectedCandidateCount === 0"
class="flex items-center gap-1.5 rounded-full border border-rose-200 bg-rose-50 px-3 py-1.5 text-xs font-semibold text-rose-600"
>
Keine Kandidaten
</span>
</div>
</div>
<!-- Form body -->
<div class="p-6">
<div class="grid gap-4 sm:grid-cols-2">
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Gruppe</span>
<input
v-model="editForms[selectedCategory.id].groupName"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Name</span>
<input
v-model="editForms[selectedCategory.id].name"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Slug</span>
<input
v-model="editForms[selectedCategory.id].slug"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 font-mono text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
<div class="grid grid-cols-2 gap-3">
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Reihenfolge</span>
<input
v-model.number="editForms[selectedCategory.id].sortOrder"
type="number"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
<label class="space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Limit</span>
<input
v-model.number="editForms[selectedCategory.id].maxNomineesPerUser"
type="number"
class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
/>
</label>
</div>
</div>
<label class="mt-4 block space-y-1.5">
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Beschreibung</span>
<textarea
v-model="editForms[selectedCategory.id].description"
rows="3"
class="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"
/>
</label>
</div>
<!-- Actions -->
<div class="flex items-center justify-between gap-3 border-t border-violet-100 px-6 py-4">
<Button
variant="ghost"
class="gap-2 border border-rose-100 bg-rose-50 text-rose-600 hover:bg-rose-100"
@click="categoryToDelete = selectedCategory"
>
<Trash2 class="h-4 w-4" /> Löschen
</Button>
<Button :disabled="saving === selectedCategory.id" @click="saveCategory(selectedCategory.id)">
{{ saving === selectedCategory.id ? 'Speichert ' : 'Speichern' }}
</Button>
</div>
</Card>
<!-- Empty state when nothing selected -->
<Card v-else class="flex flex-col items-center justify-center gap-4 py-20 text-center">
<span class="grid h-14 w-14 place-items-center rounded-full bg-violet-100 text-violet-400">
<Tags class="h-7 w-7" />
</span>
<div>
<p class="font-semibold text-slate-700">Kategorie auswählen</p>
<p class="mt-1 text-sm text-slate-500">Links eine Kategorie anklicken oder eine neue anlegen.</p>
</div>
<Button class="gap-2 mt-2" @click="showCreatePanel = true; selectedCategoryId = null">
<PlusCircle class="h-4 w-4" /> Neue Kategorie
</Button>
</Card>
</div>
</section>
<!-- Delete modal -->
<Modal :open="!!categoryToDelete" title="Kategorie löschen?" @close="categoryToDelete = null">
<div class="flex items-start gap-4">
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-full bg-rose-50 text-rose-500">
<TriangleAlert class="h-6 w-6" />
</span>
<p class="text-sm leading-7 text-slate-600">
<strong class="text-slate-800">{{ categoryToDelete?.name }}</strong>" und alle zugehörigen Kandidaten werden
aus diesem Award-Jahr entfernt. Das lässt sich nicht rückgängig machen.
</p>
</div>
<template #footer>
<Button variant="ghost" @click="categoryToDelete = null">Abbrechen</Button>
<Button class="!bg-rose-600 hover:!bg-rose-500" :disabled="deleting" @click="confirmDeleteCategory">
{{ deleting ? 'Löscht …' : 'Endgültig löschen' }}
</Button>
</template>
</Modal>
</div>
</template>
+2 -1
View File
@@ -10,5 +10,6 @@
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["src/views/admin/AdminContentView 2.vue"]
}