Embed clips in admin review

This commit is contained in:
AzuTear
2026-06-25 09:59:47 +02:00
parent fd81c968f6
commit 54293f4c45
2 changed files with 57 additions and 31 deletions
@@ -1,6 +1,7 @@
import { computed, reactive, ref, watch } from 'vue'
import { CheckCircle2, Film, Layers3, PlayCircle } from '@lucide/vue'
import { buildClipEmbed, type ClipEmbed } from '../../lib/clipEmbeds'
import { useAwardsStore } from '../../stores/awards'
import type { AdminClipSubmissionItem } from '../../types/awards'
@@ -37,6 +38,9 @@ export function useAdminClipManager() {
(!search || [clip.title, clip.creator, clip.platform, clip.submittedByTwitchId, clip.clipUrl].join(' ').toLowerCase().includes(search)),
)
})
const clipEmbeds = computed<Record<number, ClipEmbed | null>>(() =>
Object.fromEntries(submissions.value.map((clip) => [clip.id, buildClipEmbed(clip.clipUrl)])),
)
const duplicateUrls = computed(() => {
const counts = new Map<string, number>()
for (const clip of submissions.value) {
@@ -150,6 +154,7 @@ export function useAdminClipManager() {
submissions,
categoryName,
clips,
clipEmbeds,
stats,
statusFilters,
platformFilters,
+52 -31
View File
@@ -22,6 +22,7 @@ const {
submissions,
categoryName,
clips,
clipEmbeds,
stats,
statusFilters,
platformFilters,
@@ -95,39 +96,59 @@ const {
<p v-if="adminError" class="border-b border-rose-100 bg-rose-50 px-5 py-3 text-sm text-rose-700">{{ adminError }}</p>
<div class="divide-y divide-violet-50">
<div v-for="clip in clips" :key="clip.id" class="flex flex-col gap-3 px-5 py-4 lg:flex-row lg:items-center lg:gap-4">
<div class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[linear-gradient(135deg,#ece2ff,#fff2dd)] text-violet-600">
<Film class="h-5 w-5" />
<div v-for="clip in clips" :key="clip.id" class="grid gap-4 px-5 py-5 xl:grid-cols-[minmax(0,1fr)_minmax(320px,420px)_320px] xl:items-start">
<div class="flex min-w-0 gap-4">
<div class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[linear-gradient(135deg,#ece2ff,#fff2dd)] text-violet-600">
<Film class="h-5 w-5" />
</div>
<div class="min-w-0 flex-1">
<p class="truncate font-semibold text-slate-900">{{ clip.title || 'Ohne Titel' }}</p>
<p class="truncate text-sm text-slate-500">
{{ clip.creator || 'Unbekannt' }}
<span v-if="clip.categoryId"> · {{ categoryName[clip.categoryId] }}</span>
· von {{ clip.submittedByTwitchId }}
</p>
<div class="mt-3 flex flex-wrap gap-2">
<span :class="['rounded-full border px-3 py-1 text-xs font-semibold', platformClass(clip.platform)]">{{ clip.platform }}</span>
<span :class="['rounded-full border px-3 py-1 text-xs font-semibold', statusClass(clip.status)]">{{ statusLabel(clip.status) }}</span>
<a
:href="clip.clipUrl"
target="_blank"
rel="noopener"
referrerpolicy="no-referrer"
class="inline-flex items-center gap-1.5 rounded-full border border-violet-200 px-3 py-1 text-xs font-semibold text-violet-700 transition hover:bg-violet-50"
>
<ExternalLink class="h-3.5 w-3.5" /> Clip öffnen
</a>
</div>
<p v-if="duplicateUrlCount(clip.clipUrl) > 1" class="mt-3 text-xs font-semibold text-amber-700">
Mögliches Duplikat: diese URL wurde {{ duplicateUrlCount(clip.clipUrl) }}x eingereicht.
</p>
<p v-if="creatorClipCount(clip) > 1" class="mt-1 text-xs font-semibold text-violet-700">
Sammelpunkt: {{ creatorClipCount(clip) }} Clips für diese Person oder diesen Kandidaten.
</p>
<p v-if="clip.reviewedAt" class="mt-1 text-xs text-slate-500">
Zuletzt geprüft von {{ clip.reviewedByTwitchId || 'Admin' }} · {{ new Date(clip.reviewedAt).toLocaleString('de-DE') }}
</p>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="truncate font-semibold text-slate-900">{{ clip.title || 'Ohne Titel' }}</p>
<p class="truncate text-sm text-slate-500">
{{ clip.creator || 'Unbekannt' }}
<span v-if="clip.categoryId"> · {{ categoryName[clip.categoryId] }}</span>
· von {{ clip.submittedByTwitchId }}
</p>
<p v-if="duplicateUrlCount(clip.clipUrl) > 1" class="mt-1 text-xs font-semibold text-amber-700">
Mögliches Duplikat: diese URL wurde {{ duplicateUrlCount(clip.clipUrl) }}x eingereicht.
</p>
<p v-if="creatorClipCount(clip) > 1" class="mt-1 text-xs font-semibold text-violet-700">
Sammelpunkt: {{ creatorClipCount(clip) }} Clips für diese Person oder diesen Kandidaten.
</p>
<p v-if="clip.reviewedAt" class="mt-1 text-xs text-slate-500">
Zuletzt geprüft von {{ clip.reviewedByTwitchId || 'Admin' }} · {{ new Date(clip.reviewedAt).toLocaleString('de-DE') }}
</p>
<div class="min-w-0">
<div v-if="clipEmbeds[clip.id]" class="overflow-hidden rounded-2xl border border-violet-100 bg-slate-950 shadow-sm">
<iframe
class="aspect-video w-full"
:src="clipEmbeds[clip.id]?.src"
:title="clip.title || `${clip.platform} Clip ${clip.id}`"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin"
/>
</div>
<div v-else class="rounded-2xl border border-dashed border-amber-200 bg-amber-50 px-4 py-5 text-sm font-medium leading-6 text-amber-800">
Dieser Clip kann nicht eingebettet werden. Öffne ihn über den Clip-Link zur manuellen Prüfung.
</div>
</div>
<span :class="['shrink-0 rounded-full border px-3 py-1 text-xs font-semibold', platformClass(clip.platform)]">{{ clip.platform }}</span>
<span :class="['shrink-0 rounded-full border px-3 py-1 text-xs font-semibold', statusClass(clip.status)]">{{ statusLabel(clip.status) }}</span>
<a
:href="clip.clipUrl"
target="_blank"
rel="noopener"
referrerpolicy="no-referrer"
class="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-violet-200 px-3 py-1.5 text-xs font-semibold text-violet-700 transition hover:bg-violet-50"
>
<ExternalLink class="h-3.5 w-3.5" /> Clip öffnen
</a>
<div class="w-full lg:w-[320px]">
<div class="w-full">
<textarea
v-model="reviewNotes[clip.id]"
rows="2"