Embed clips in admin review
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { CheckCircle2, Film, Layers3, PlayCircle } from '@lucide/vue'
|
import { CheckCircle2, Film, Layers3, PlayCircle } from '@lucide/vue'
|
||||||
|
|
||||||
|
import { buildClipEmbed, type ClipEmbed } from '../../lib/clipEmbeds'
|
||||||
import { useAwardsStore } from '../../stores/awards'
|
import { useAwardsStore } from '../../stores/awards'
|
||||||
import type { AdminClipSubmissionItem } from '../../types/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)),
|
(!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 duplicateUrls = computed(() => {
|
||||||
const counts = new Map<string, number>()
|
const counts = new Map<string, number>()
|
||||||
for (const clip of submissions.value) {
|
for (const clip of submissions.value) {
|
||||||
@@ -150,6 +154,7 @@ export function useAdminClipManager() {
|
|||||||
submissions,
|
submissions,
|
||||||
categoryName,
|
categoryName,
|
||||||
clips,
|
clips,
|
||||||
|
clipEmbeds,
|
||||||
stats,
|
stats,
|
||||||
statusFilters,
|
statusFilters,
|
||||||
platformFilters,
|
platformFilters,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const {
|
|||||||
submissions,
|
submissions,
|
||||||
categoryName,
|
categoryName,
|
||||||
clips,
|
clips,
|
||||||
|
clipEmbeds,
|
||||||
stats,
|
stats,
|
||||||
statusFilters,
|
statusFilters,
|
||||||
platformFilters,
|
platformFilters,
|
||||||
@@ -95,7 +96,8 @@ 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>
|
<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 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 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">
|
<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" />
|
<Film class="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +108,20 @@ const {
|
|||||||
<span v-if="clip.categoryId"> · {{ categoryName[clip.categoryId] }}</span>
|
<span v-if="clip.categoryId"> · {{ categoryName[clip.categoryId] }}</span>
|
||||||
· von {{ clip.submittedByTwitchId }}
|
· von {{ clip.submittedByTwitchId }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="duplicateUrlCount(clip.clipUrl) > 1" class="mt-1 text-xs font-semibold text-amber-700">
|
<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.
|
Mögliches Duplikat: diese URL wurde {{ duplicateUrlCount(clip.clipUrl) }}x eingereicht.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="creatorClipCount(clip) > 1" class="mt-1 text-xs font-semibold text-violet-700">
|
<p v-if="creatorClipCount(clip) > 1" class="mt-1 text-xs font-semibold text-violet-700">
|
||||||
@@ -116,18 +131,24 @@ const {
|
|||||||
Zuletzt geprüft von {{ clip.reviewedByTwitchId || 'Admin' }} · {{ new Date(clip.reviewedAt).toLocaleString('de-DE') }}
|
Zuletzt geprüft von {{ clip.reviewedByTwitchId || 'Admin' }} · {{ new Date(clip.reviewedAt).toLocaleString('de-DE') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span :class="['shrink-0 rounded-full border px-3 py-1 text-xs font-semibold', platformClass(clip.platform)]">{{ clip.platform }}</span>
|
</div>
|
||||||
<span :class="['shrink-0 rounded-full border px-3 py-1 text-xs font-semibold', statusClass(clip.status)]">{{ statusLabel(clip.status) }}</span>
|
<div class="min-w-0">
|
||||||
<a
|
<div v-if="clipEmbeds[clip.id]" class="overflow-hidden rounded-2xl border border-violet-100 bg-slate-950 shadow-sm">
|
||||||
:href="clip.clipUrl"
|
<iframe
|
||||||
target="_blank"
|
class="aspect-video w-full"
|
||||||
rel="noopener"
|
:src="clipEmbeds[clip.id]?.src"
|
||||||
referrerpolicy="no-referrer"
|
:title="clip.title || `${clip.platform} Clip ${clip.id}`"
|
||||||
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"
|
loading="lazy"
|
||||||
>
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture; web-share"
|
||||||
<ExternalLink class="h-3.5 w-3.5" /> Clip öffnen
|
allowfullscreen
|
||||||
</a>
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
<div class="w-full lg:w-[320px]">
|
/>
|
||||||
|
</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>
|
||||||
|
<div class="w-full">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="reviewNotes[clip.id]"
|
v-model="reviewNotes[clip.id]"
|
||||||
rows="2"
|
rows="2"
|
||||||
|
|||||||
Reference in New Issue
Block a user