Files
vtuber-awards/frontend/src/components/admin/AdminNominationReviewModal.vue
T
2026-06-26 18:09:29 +02:00

188 lines
8.1 KiB
Vue

<script setup lang="ts">
import { onBeforeUnmount, onMounted, watch } from 'vue'
import { Sparkles, X } from '@lucide/vue'
import AdminReviewDecisionPanel from './AdminReviewDecisionPanel.vue'
import AdminReviewsHistorySection from './AdminReviewsHistorySection.vue'
import AdminReviewsQueueHeader from './AdminReviewsQueueHeader.vue'
import AdminReviewsQueueList from './AdminReviewsQueueList.vue'
import { useAdminReviewsManager } from './useAdminReviewsManager'
import { watchAdminToast } from '../../composables/useAdminToast'
const props = defineProps<{
open: boolean
}>()
const emit = defineEmits<{
close: []
}>()
const {
reviewSaving,
adminMessage,
adminError,
reviewForms,
seasonDetail,
reviewFilter,
categoryFilter,
selectedNominationId,
candidatePlatformOptions,
filteredNominations,
selectedNomination,
reviewStats,
reviewedNominations,
categoryOptions,
selectedCandidateCollision,
selectedRelatedPendingNominations,
selectedNominationSignalSummary,
canApproveSelected,
approveNomination,
rejectNomination,
selectedPlatformValue,
handlePlatformSelection,
extractNominationStreamUrl,
} = useAdminReviewsManager()
watchAdminToast(adminMessage, adminError)
function closeModal() {
emit('close')
}
function onKey(event: KeyboardEvent) {
if (event.key === 'Escape' && props.open) {
closeModal()
}
}
watch(
() => props.open,
(open) => {
if (typeof document !== 'undefined') {
document.body.style.overflow = open ? 'hidden' : ''
}
},
{ immediate: true },
)
onMounted(() => window.addEventListener('keydown', onKey))
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey)
if (typeof document !== 'undefined') document.body.style.overflow = ''
})
</script>
<template>
<Teleport to="body">
<div
v-if="open"
class="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-6"
@click.self="closeModal"
>
<div class="absolute inset-0 bg-violet-950/35 backdrop-blur-sm" @click="closeModal" />
<section class="relative z-10 flex max-h-[calc(100vh-2rem)] w-full max-w-6xl flex-col overflow-hidden rounded-[28px] border border-violet-200/80 bg-white shadow-[0_40px_90px_rgba(76,40,160,0.28)]">
<header class="flex items-start justify-between gap-4 border-b border-violet-100 bg-[linear-gradient(135deg,#f7f2ff,#fff7ed)] px-5 py-4 sm:px-6">
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="grid h-9 w-9 shrink-0 place-items-center rounded-2xl bg-violet-100 text-violet-700">
<Sparkles class="h-4.5 w-4.5" />
</span>
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-500">Review-Fokus</p>
</div>
<h2 class="mt-3 text-2xl font-bold text-slate-950">Nominierung entscheiden</h2>
<p class="mt-1 max-w-2xl text-sm leading-6 text-slate-500">
Die Nominierungsliste bleibt scanbar; Entscheidungen laufen hier konzentriert mit Queue, Prüfung und Kandidatenübernahme.
</p>
</div>
<button
type="button"
class="grid h-10 w-10 shrink-0 place-items-center rounded-full text-slate-400 transition hover:bg-white/80 hover:text-violet-700"
aria-label="Review-Dialog schließen"
@click="closeModal"
>
<X class="h-5 w-5" />
</button>
</header>
<div class="min-h-0 overflow-y-auto bg-white">
<AdminReviewsQueueHeader
v-model:review-filter="reviewFilter"
v-model:category-filter="categoryFilter"
:total-pending="seasonDetail.pendingNominations.length"
:visible-count="filteredNominations.length"
:review-stats="reviewStats"
:category-options="categoryOptions"
/>
<div class="space-y-4 p-4 sm:p-6">
<div class="grid gap-5 xl:grid-cols-[minmax(300px,0.8fr)_minmax(0,1.2fr)]">
<div class="space-y-2">
<!-- Keyboard shortcut legend fixed above scrollable list -->
<div v-if="filteredNominations.length > 0" class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-2xl border border-violet-100 bg-violet-50/60 px-3 py-2">
<span class="text-[10px] font-bold uppercase tracking-[0.16em] text-violet-500">Tastaturkürzel</span>
<span class="flex items-center gap-1.5">
<span class="inline-flex gap-0.5">
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm"></kbd>
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm"></kbd>
</span>
<span class="text-[11px] text-slate-500">/ </span>
<span class="inline-flex gap-0.5">
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm">J</kbd>
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm">K</kbd>
</span>
<span class="text-[11px] text-slate-500">Navigieren</span>
</span>
<span class="flex items-center gap-1.5">
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-emerald-200 bg-emerald-50 px-1.5 text-[10px] font-bold text-emerald-700 shadow-sm">A</kbd>
<span class="text-[11px] text-slate-500">Annehmen</span>
</span>
<span class="flex items-center gap-1.5">
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-rose-200 bg-rose-50 px-1.5 text-[10px] font-bold text-rose-700 shadow-sm">R</kbd>
<span class="text-[11px] text-slate-500">Ablehnen</span>
</span>
</div>
<AdminReviewsQueueList
:nominations="filteredNominations"
:total-pending="seasonDetail.pendingNominations.length"
:selected-nomination-id="selectedNominationId"
@select="selectedNominationId = $event"
/>
</div>
<AdminReviewDecisionPanel
:nomination="selectedNomination"
:review-saving="reviewSaving"
:review-form="selectedNomination ? reviewForms[selectedNomination.id] : null"
:candidate-platform-options="candidatePlatformOptions"
:selected-candidate-collision="selectedCandidateCollision"
:selected-related-pending-nominations="selectedRelatedPendingNominations"
:signal-summary="selectedNominationSignalSummary"
:stream-url="selectedNomination ? extractNominationStreamUrl(selectedNomination) : ''"
:can-approve-selected="canApproveSelected"
:selected-platform-value="selectedPlatformValue"
@platform-change="handlePlatformSelection"
@approve="approveNomination"
@reject="rejectNomination"
/>
</div>
<details class="rounded-[24px] border border-violet-100 bg-violet-50/40">
<summary class="cursor-pointer px-5 py-4 text-sm font-semibold text-violet-900">
Letzte Entscheidungen anzeigen
</summary>
<div class="border-t border-violet-100 p-5">
<AdminReviewsHistorySection
:reviewed-nominations="reviewedNominations"
:reviewed-total="seasonDetail.reviewedNominations.length"
/>
</div>
</details>
</div>
</div>
</section>
</div>
</Teleport>
</template>