Refactor app architecture and clean local artifacts

This commit is contained in:
AzuTear
2026-06-24 23:43:14 +02:00
parent 17134b3b82
commit fef1d36fe8
274 changed files with 37724 additions and 6065 deletions
@@ -0,0 +1,223 @@
import { computed, reactive, ref, watch } from 'vue'
import { SOCIAL_ICON_OPTIONS, socialIconOptionForValue } from '../../lib/socialIcons'
import { useAwardsStore } from '../../stores/awards'
import type { AdminCandidateItem } from '../../types/awards'
interface CandidateForm {
categoryId: number
displayName: string
channelSlug: string
platform: string
}
const pageSize = 10
export function useAdminCandidateManager() {
const store = useAwardsStore()
const saving = ref(false)
const deleting = ref(false)
const adminMessage = ref('')
const adminError = ref('')
const search = ref('')
const categoryFilter = ref<number | null>(null)
const page = ref(1)
const modalOpen = ref(false)
const editingId = ref<number | 'new' | null>(null)
const candidateToDelete = ref<AdminCandidateItem | null>(null)
const form = reactive<CandidateForm>({ categoryId: 0, displayName: '', channelSlug: '', platform: 'Twitch' })
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 })),
)
const categoryFilterOptions = computed(() => [{ label: 'Alle Kategorien', value: null }, ...categoryOptions.value])
const categoryLabelMap = computed<Record<number, string>>(() =>
Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, `${category.groupName} · ${category.name}`])),
)
const duplicateCandidateKeys = computed(() => {
const counts = new Map<string, number>()
for (const candidate of seasonDetail.value.candidates) {
const nameKey = createCandidateDuplicateKey(candidate, 'name')
const slugKey = createCandidateDuplicateKey(candidate, 'slug')
counts.set(nameKey, (counts.get(nameKey) ?? 0) + 1)
if (candidate.channelSlug.trim()) {
counts.set(slugKey, (counts.get(slugKey) ?? 0) + 1)
}
}
return counts
})
const duplicateCandidateCount = computed(() =>
seasonDetail.value.candidates.filter((candidate) => hasDuplicateCandidateKey(candidate, duplicateCandidateKeys.value)).length,
)
const filteredCandidates = computed(() => {
const query = search.value.trim().toLowerCase()
let list = seasonDetail.value.candidates
if (categoryFilter.value) {
list = list.filter((candidate) => candidate.categoryId === categoryFilter.value)
}
if (query) {
list = list.filter((candidate) =>
[candidate.displayName, candidate.channelSlug, candidate.platform, categoryLabelMap.value[candidate.categoryId] ?? '']
.join(' ')
.toLowerCase()
.includes(query),
)
}
return list
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredCandidates.value.length / pageSize)))
const pagedCandidates = computed(() => {
const start = (page.value - 1) * pageSize
return filteredCandidates.value.slice(start, start + pageSize)
})
const rangeStart = computed(() => (filteredCandidates.value.length === 0 ? 0 : (page.value - 1) * pageSize + 1))
const rangeEnd = computed(() => Math.min(page.value * pageSize, filteredCandidates.value.length))
const modalTitle = computed(() => (editingId.value === 'new' ? 'Kandidat anlegen' : 'Kandidat bearbeiten'))
const canSave = computed(() =>
Boolean(selectedSeasonId.value && form.categoryId && form.displayName.trim() && form.channelSlug.trim() && form.platform.trim()),
)
const candidatePlatformOptions = computed(() => SOCIAL_ICON_OPTIONS.filter((option) => option.key !== 'website'))
const selectedPlatformValue = computed(() => socialIconOptionForValue(form.platform)?.key ?? 'custom')
watch([search, categoryFilter, () => seasonDetail.value.candidates.length], () => {
page.value = 1
})
watch(totalPages, (max) => {
if (page.value > max) {
page.value = max
}
})
function clearFilters() {
search.value = ''
categoryFilter.value = null
}
function openCreate() {
adminMessage.value = ''
adminError.value = ''
editingId.value = 'new'
form.categoryId = categoryFilter.value ?? seasonDetail.value.categories[0]?.id ?? 0
form.displayName = ''
form.channelSlug = ''
form.platform = 'Twitch'
modalOpen.value = true
}
function openEdit(candidate: AdminCandidateItem) {
adminMessage.value = ''
adminError.value = ''
editingId.value = candidate.id
form.categoryId = candidate.categoryId
form.displayName = candidate.displayName
form.channelSlug = candidate.channelSlug
form.platform = candidate.platform
modalOpen.value = true
}
function handlePlatformSelection(event: Event) {
const value = (event.target as HTMLSelectElement).value
if (value === 'custom') {
if (socialIconOptionForValue(form.platform)) {
form.platform = ''
}
return
}
const option = socialIconOptionForValue(value)
form.platform = option?.label ?? value
}
async function saveModal() {
if (!canSave.value || !selectedSeasonId.value) {
return
}
saving.value = true
adminError.value = ''
try {
if (editingId.value === 'new') {
await store.createAdminCandidate(selectedSeasonId.value, { ...form })
adminMessage.value = `${form.displayName}" wurde angelegt.`
} else if (typeof editingId.value === 'number') {
await store.updateAdminCandidate(editingId.value, selectedSeasonId.value, { ...form })
adminMessage.value = `${form.displayName}" wurde gespeichert.`
}
modalOpen.value = false
} catch (error) {
adminError.value = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'
} finally {
saving.value = false
}
}
async function confirmDelete() {
if (!candidateToDelete.value || !selectedSeasonId.value) {
return
}
deleting.value = true
adminError.value = ''
try {
await store.deleteAdminCandidate(candidateToDelete.value.id, selectedSeasonId.value)
adminMessage.value = `${candidateToDelete.value.displayName}" wurde gelöscht.`
candidateToDelete.value = null
} catch (error) {
adminError.value = error instanceof Error ? error.message : 'Löschen fehlgeschlagen.'
} finally {
deleting.value = false
}
}
return {
seasonDetail,
saving,
deleting,
adminMessage,
adminError,
search,
categoryFilter,
page,
categoryOptions,
categoryFilterOptions,
categoryLabelMap,
duplicateCandidateKeys,
duplicateCandidateCount,
filteredCandidates,
pagedCandidates,
totalPages,
rangeStart,
rangeEnd,
modalOpen,
form,
modalTitle,
canSave,
candidatePlatformOptions,
selectedPlatformValue,
candidateToDelete,
clearFilters,
openCreate,
openEdit,
handlePlatformSelection,
saveModal,
confirmDelete,
}
}
function createCandidateDuplicateKey(candidate: AdminCandidateItem, field: 'name' | 'slug') {
const value = field === 'name' ? candidate.displayName : candidate.channelSlug
return `${candidate.categoryId}:${field}:${value.trim().toLowerCase()}`
}
function hasDuplicateCandidateKey(candidate: AdminCandidateItem, candidateKeys: Map<string, number>) {
return (candidateKeys.get(createCandidateDuplicateKey(candidate, 'name')) ?? 0) > 1
|| (candidateKeys.get(createCandidateDuplicateKey(candidate, 'slug')) ?? 0) > 1
}