Improve admin navigation and local API reachability

This commit is contained in:
AzuTear
2026-06-17 13:24:41 +02:00
parent 953257bcef
commit 4a211189f0
11 changed files with 330 additions and 23 deletions
@@ -2,6 +2,7 @@
import { computed, reactive, ref, watch } from 'vue'
import Select from 'primevue/select'
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
import Button from '../../components/ui/Button.vue'
import Card from '../../components/ui/Card.vue'
@@ -28,12 +29,26 @@ const candidateForms = reactive<Record<number, {
const seasonDetail = computed(() => store.adminSeasonDetail)
const selectedSeasonId = computed(() => store.adminSelectedSeasonId)
const candidateFilter = ref('')
const categoryOptions = computed(() =>
seasonDetail.value.categories.map((category) => ({
label: `${category.groupName} · ${category.name}`,
value: category.id,
})),
)
const categoryLabelMap = computed(() =>
Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, `${category.groupName} · ${category.name}`])),
)
const filteredCandidates = computed(() => {
const query = candidateFilter.value.trim().toLowerCase()
if (!query) return seasonDetail.value.candidates
return seasonDetail.value.candidates.filter((candidate) =>
[candidate.displayName, candidate.channelSlug, candidate.platform, categoryLabelMap.value[candidate.categoryId] ?? '']
.join(' ')
.toLowerCase()
.includes(query),
)
})
watch(
seasonDetail,
@@ -92,6 +107,12 @@ async function createCandidate() {
<template>
<div class="space-y-6">
<AdminPageHeader
eyebrow="Candidates"
title="Kandidatenbasis pflegen"
description="Hier findest du alle Kandidaten der gewaehlten Season an einem Ort. Filtere zuerst und bearbeite dann nur die relevanten Eintraege."
/>
<AdminSeasonToolbar />
<div class="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
@@ -102,13 +123,25 @@ async function createCandidate() {
<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
{{ filteredCandidates.length }} / {{ seasonDetail.candidates.length }} Kandidaten
</span>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]">
<input
v-model="candidateFilter"
type="text"
class="rounded-2xl border border-violet-200 px-4 py-3"
placeholder="Nach Name, Handle, Plattform oder Kategorie filtern"
/>
<div class="rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-3 text-sm text-slate-600">
Tipp: Nutze erst den Filter und aendere danach nur den Zielkandidaten.
</div>
</div>
<div class="mt-6 space-y-4">
<div
v-for="candidate in seasonDetail.candidates"
v-for="candidate in filteredCandidates"
:key="candidate.id"
class="rounded-[26px] border border-violet-100 bg-white/90 p-5"
>
@@ -131,6 +164,10 @@ async function createCandidate() {
</Button>
</div>
</div>
<p v-if="filteredCandidates.length === 0" class="rounded-[26px] border border-dashed border-violet-100 px-5 py-6 text-sm text-slate-500">
Keine Kandidaten passen zum aktuellen Filter.
</p>
</div>
</Card>