Refactor admin panel into categorized navigation

This commit is contained in:
AzuTear
2026-06-17 12:37:17 +02:00
parent 92dd6f7432
commit 953257bcef
11 changed files with 901 additions and 660 deletions
@@ -0,0 +1,164 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import Select from 'primevue/select'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
import Button from '../../components/ui/Button.vue'
import Card from '../../components/ui/Card.vue'
import { useAwardsStore } from '../../stores/awards'
const store = useAwardsStore()
const candidateSaving = ref<number | 'new' | null>(null)
const adminMessage = ref('')
const adminError = ref('')
const newCandidateForm = reactive({
categoryId: 0,
displayName: '',
channelSlug: '',
platform: 'Twitch',
})
const candidateForms = reactive<Record<number, {
categoryId: number
displayName: string
channelSlug: string
platform: string
}>>({})
const seasonDetail = computed(() => store.adminSeasonDetail)
const selectedSeasonId = computed(() => store.adminSelectedSeasonId)
const categoryOptions = computed(() =>
seasonDetail.value.categories.map((category) => ({
label: `${category.groupName} · ${category.name}`,
value: category.id,
})),
)
watch(
seasonDetail,
(detail) => {
for (const candidate of detail.candidates) {
candidateForms[candidate.id] = {
categoryId: candidate.categoryId,
displayName: candidate.displayName,
channelSlug: candidate.channelSlug,
platform: candidate.platform,
}
}
newCandidateForm.categoryId = detail.categories[0]?.id ?? 0
},
{ immediate: true },
)
async function saveCandidate(candidateId: number) {
if (!selectedSeasonId.value) return
candidateSaving.value = candidateId
adminMessage.value = ''
adminError.value = ''
try {
await store.updateAdminCandidate(candidateId, selectedSeasonId.value, candidateForms[candidateId])
adminMessage.value = 'Kandidat gespeichert.'
} catch (error) {
adminError.value = error instanceof Error ? error.message : 'Kandidat konnte nicht gespeichert werden.'
} finally {
candidateSaving.value = null
}
}
async function createCandidate() {
if (!selectedSeasonId.value || !newCandidateForm.categoryId) return
candidateSaving.value = 'new'
adminMessage.value = ''
adminError.value = ''
try {
await store.createAdminCandidate(selectedSeasonId.value, newCandidateForm)
adminMessage.value = 'Kandidat angelegt.'
newCandidateForm.displayName = ''
newCandidateForm.channelSlug = ''
newCandidateForm.platform = 'Twitch'
} catch (error) {
adminError.value = error instanceof Error ? error.message : 'Kandidat konnte nicht angelegt werden.'
} finally {
candidateSaving.value = null
}
}
</script>
<template>
<div class="space-y-6">
<AdminSeasonToolbar />
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<Card class="p-7">
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Kandidatenpflege</h2>
<p class="mt-2 text-sm text-slate-500">Bekannte Kandidaten koennen pro Kategorie gepflegt und fuer Voting und Archiv genutzt werden.</p>
</div>
<span class="text-sm uppercase tracking-[0.2em] text-slate-500">
{{ seasonDetail.candidates.length }} Kandidaten
</span>
</div>
<div class="mt-6 space-y-4">
<div
v-for="candidate in seasonDetail.candidates"
:key="candidate.id"
class="rounded-[26px] border border-violet-100 bg-white/90 p-5"
>
<div class="grid gap-4 md:grid-cols-2">
<Select
v-model="candidateForms[candidate.id].categoryId"
:options="categoryOptions"
option-label="label"
option-value="value"
class="w-full"
/>
<input v-model="candidateForms[candidate.id].displayName" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Display Name" />
<input v-model="candidateForms[candidate.id].channelSlug" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="@channel" />
<input v-model="candidateForms[candidate.id].platform" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Platform" />
</div>
<div class="mt-4 flex justify-end">
<Button :disabled="candidateSaving === candidate.id" @click="saveCandidate(candidate.id)">
{{ candidateSaving === candidate.id ? 'Speichert ...' : 'Kandidat speichern' }}
</Button>
</div>
</div>
</div>
</Card>
<Card class="p-7">
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Neuer Kandidat</h2>
<div class="mt-6 space-y-4">
<Select
v-model="newCandidateForm.categoryId"
:options="categoryOptions"
option-label="label"
option-value="value"
class="w-full"
/>
<input v-model="newCandidateForm.displayName" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Display Name" />
<input v-model="newCandidateForm.channelSlug" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="@channel" />
<input v-model="newCandidateForm.platform" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Platform" />
<Button :disabled="candidateSaving === 'new' || !selectedSeasonId || !newCandidateForm.categoryId" @click="createCandidate">
{{ candidateSaving === 'new' ? 'Erstellt ...' : 'Kandidat anlegen' }}
</Button>
</div>
<p v-if="adminMessage" class="mt-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
{{ adminMessage }}
</p>
<p v-if="adminError" class="mt-6 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
{{ adminError }}
</p>
</Card>
</div>
</div>
</template>