Add winner archive, host image upload and live demo data
Deliver the demo-ready feature set and seed data so the live site can be presented end to end: - Winner archive: ArchivedWinner domain, admin CRUD endpoints/view/manager and public archive surface, backed by AddArchivedWinners migration. - Host presentation: host image upload and artist name on SiteSettings with public image endpoint and supporting migrations. - Clip submissions: idempotent table-ensure migration plus current-season demo clips for review workflows. - Demo seed data: sponsors, share links and 2025 archived winners, with a guarded RemoveDemoSeasons cleanup; all seeds guard against real data. - EnsureRuntimeSchemaParity migration to align runtime schema defensively. - Admin/home UI refinements; remove unused team role permissions modal and dead share-quick-links code. All seed and schema migrations are idempotent (IF NOT EXISTS / ON CONFLICT) and skip when real season data is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { Archive, Link as LinkIcon, Plus, Save, Trash2 } from '@lucide/vue'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
import Button from '../../components/ui/Button.vue'
|
||||
import Card from '../../components/ui/Card.vue'
|
||||
import { watchAdminToast } from '../../composables/useAdminToast'
|
||||
import { useAdminArchiveManager } from '../../components/admin/useAdminArchiveManager'
|
||||
|
||||
const {
|
||||
loading,
|
||||
adminError,
|
||||
adminMessage,
|
||||
groupedDrafts,
|
||||
stats,
|
||||
loadArchivedWinners,
|
||||
addDraft,
|
||||
saveDraft,
|
||||
deleteDraft,
|
||||
} = useAdminArchiveManager()
|
||||
|
||||
watchAdminToast(adminMessage, adminError)
|
||||
|
||||
onMounted(() => {
|
||||
void loadArchivedWinners()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<AdminPageHeader
|
||||
eyebrow="Archiv"
|
||||
description="Historische Gewinner für die Landingpage unabhängig vom aktiven Award-Jahr pflegen. Änderungen landen direkt in der öffentlichen Archivquelle."
|
||||
:icon="Archive"
|
||||
/>
|
||||
|
||||
<section class="grid gap-3 sm:grid-cols-3">
|
||||
<Card v-for="card in stats" :key="card.label" class="p-4">
|
||||
<p class="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">{{ card.label }}</p>
|
||||
<strong class="mt-2 block text-2xl text-slate-950">{{ card.value }}</strong>
|
||||
<p class="mt-1 text-sm leading-5 text-slate-500">{{ card.note }}</p>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Card class="p-5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-slate-950">Archiv-Gewinner verwalten</h2>
|
||||
<p class="mt-1 text-sm leading-6 text-slate-500">
|
||||
Pro Zeile kannst du Jahr, Gewinner, Link, Kategorie und Unterkategorie anlegen, anpassen oder löschen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button class="gap-2" @click="addDraft">
|
||||
<Plus class="h-4 w-4" />
|
||||
Neue Zeile
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div v-if="loading" class="rounded-[26px] border border-dashed border-violet-200 bg-white/70 px-5 py-10 text-center text-sm text-slate-500">
|
||||
Archiv wird geladen ...
|
||||
</div>
|
||||
|
||||
<div v-else-if="groupedDrafts.length === 0" class="rounded-[26px] border border-dashed border-violet-200 bg-white/70 px-5 py-10 text-center text-sm text-slate-500">
|
||||
Noch keine Archiv-Gewinner vorhanden.
|
||||
</div>
|
||||
|
||||
<section v-else class="space-y-5">
|
||||
<div
|
||||
v-for="group in groupedDrafts"
|
||||
:key="group.key"
|
||||
class="space-y-3"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-[11px] font-bold uppercase tracking-[0.18em] text-violet-600">Archiv</p>
|
||||
<h2 class="text-2xl font-bold text-slate-950">{{ group.label }}</h2>
|
||||
</div>
|
||||
<span class="rounded-full border border-violet-100 bg-violet-50 px-3 py-1.5 text-xs font-semibold text-violet-700">
|
||||
{{ group.items.length }} Einträge
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-2">
|
||||
<Card
|
||||
v-for="draft in group.items"
|
||||
:key="draft.localKey"
|
||||
class="p-5"
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-4 sm:grid-cols-[120px_minmax(0,1fr)]">
|
||||
<label class="grid min-w-0 gap-1.5">
|
||||
<span class="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Jahr</span>
|
||||
<input
|
||||
v-model="draft.year"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxlength="4"
|
||||
class="h-11 rounded-2xl border border-violet-200 bg-white px-4 text-sm text-slate-900 outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
|
||||
placeholder="2025"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Gewinner</span>
|
||||
<input
|
||||
v-model="draft.winnerName"
|
||||
type="text"
|
||||
class="h-11 rounded-2xl border border-violet-200 bg-white px-4 text-sm text-slate-900 outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
|
||||
placeholder="Name des Gewinners"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Link</span>
|
||||
<div class="relative">
|
||||
<LinkIcon class="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-400" />
|
||||
<input
|
||||
v-model="draft.winnerUrl"
|
||||
type="url"
|
||||
class="h-11 w-full rounded-2xl border border-violet-200 bg-white pl-11 pr-4 text-sm text-slate-900 outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
|
||||
placeholder="https://twitch.tv/..."
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Kategorie</span>
|
||||
<input
|
||||
v-model="draft.category"
|
||||
type="text"
|
||||
class="h-11 rounded-2xl border border-violet-200 bg-white px-4 text-sm text-slate-900 outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
|
||||
placeholder="z. B. Rising Stars"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="grid gap-1.5">
|
||||
<span class="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Unterkategorie</span>
|
||||
<input
|
||||
v-model="draft.subcategory"
|
||||
type="text"
|
||||
class="h-11 rounded-2xl border border-violet-200 bg-white px-4 text-sm text-slate-900 outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100"
|
||||
placeholder="z. B. Aufbau"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-violet-100 pt-4">
|
||||
<p class="text-xs text-slate-400">
|
||||
{{ draft.updatedAt ? `Zuletzt aktualisiert: ${new Date(draft.updatedAt).toLocaleString('de-DE')}` : draft.isNew ? 'Neue Archivzeile' : 'Noch nicht aktualisiert' }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="gap-2"
|
||||
:disabled="draft.isDeleting || draft.isSaving"
|
||||
@click="deleteDraft(draft.localKey)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{{ draft.isDeleting ? 'Löscht ...' : 'Löschen' }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
class="gap-2"
|
||||
:disabled="draft.isDeleting || draft.isSaving"
|
||||
@click="saveDraft(draft.localKey)"
|
||||
>
|
||||
<Save class="h-4 w-4" />
|
||||
{{ draft.isSaving ? 'Speichert ...' : 'Speichern' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,7 +25,6 @@ const {
|
||||
categoryFilterOptions,
|
||||
categoryLabelMap,
|
||||
duplicateCandidateKeys,
|
||||
candidateIdentitySummaries,
|
||||
candidateWorkflowNotices,
|
||||
duplicateCandidateCount,
|
||||
acceptedCandidateCount,
|
||||
@@ -48,9 +47,13 @@ const {
|
||||
clipEmbedStatusOptions,
|
||||
readinessFilterOptions,
|
||||
candidateToDelete,
|
||||
deletePreviewLoading,
|
||||
candidateDeletePreview,
|
||||
clearFilters,
|
||||
openCreate,
|
||||
openEdit,
|
||||
openDelete,
|
||||
closeDeleteModal,
|
||||
handlePlatformSelection,
|
||||
saveModal,
|
||||
confirmDelete,
|
||||
@@ -149,11 +152,10 @@ watchAdminToast(adminMessage, adminError)
|
||||
:range-end="rangeEnd"
|
||||
:category-label-map="categoryLabelMap"
|
||||
:duplicate-candidate-keys="duplicateCandidateKeys"
|
||||
:candidate-identity-summaries="candidateIdentitySummaries"
|
||||
:candidate-workflow-notices="candidateWorkflowNotices"
|
||||
:acceptance-status-options="acceptanceStatusOptions"
|
||||
@edit="openEdit"
|
||||
@delete="candidateToDelete = $event"
|
||||
@delete="openDelete"
|
||||
@update:page="page = $event"
|
||||
@open-create="openCreate"
|
||||
/>
|
||||
@@ -189,8 +191,10 @@ watchAdminToast(adminMessage, adminError)
|
||||
|
||||
<AdminCandidateDeleteModal
|
||||
:candidate="candidateToDelete"
|
||||
:preview="candidateDeletePreview"
|
||||
:loading="deletePreviewLoading"
|
||||
:deleting="deleting"
|
||||
@close="candidateToDelete = null"
|
||||
@close="closeDeleteModal"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,8 @@ const {
|
||||
saveError,
|
||||
privacyPreviewOpen,
|
||||
iconUploadError,
|
||||
hostImageUploadError,
|
||||
hostImageUploading,
|
||||
privacyPreviewHtml,
|
||||
privacyUpdatedLabel,
|
||||
addSocialLink,
|
||||
@@ -47,6 +49,7 @@ const {
|
||||
socialSimpleIconColor,
|
||||
handleSocialIconUpload,
|
||||
clearSocialIcon,
|
||||
handleHostImageUpload,
|
||||
saveSiteSettings,
|
||||
adminSiteSettings,
|
||||
} = useAdminContentManager()
|
||||
@@ -164,7 +167,14 @@ function closeContentEditor() {
|
||||
:icon="FileText"
|
||||
/>
|
||||
|
||||
<AdminContentBasicsSection :form="form" :saving="saving" :save-site-settings="saveSiteSettings" />
|
||||
<AdminContentBasicsSection
|
||||
:form="form"
|
||||
:saving="saving"
|
||||
:host-image-uploading="hostImageUploading"
|
||||
:host-image-upload-error="hostImageUploadError"
|
||||
:handle-host-image-upload="handleHostImageUpload"
|
||||
:save-site-settings="saveSiteSettings"
|
||||
/>
|
||||
|
||||
<section aria-labelledby="content-editor-title" class="space-y-4">
|
||||
<div>
|
||||
|
||||
@@ -66,6 +66,7 @@ const fullNavGroups = computed(() => [
|
||||
{ label: 'Jahre', to: '/admin/years', description: 'Jahresstatus und Setup', icon: CalendarCog, permission: 'years', badge: () => `${store.adminSeasons.length}` },
|
||||
{ label: 'Kategorien', to: '/admin/categories', description: 'Struktur und Limits', icon: Tags, permission: 'categories', badge: () => `${store.adminSeasonDetail.categories.length}` },
|
||||
{ label: 'Kandidaten', to: '/admin/candidates', description: 'Kandidatenbasis pflegen', icon: Users, permission: 'candidates', badge: () => `${store.adminSeasonDetail.candidates.length}` },
|
||||
{ label: 'Archiv', to: '/admin/archive', description: 'Historische Gewinner pflegen', icon: Trophy, permission: 'winners', badge: () => store.adminArchivedWinners.length > 0 ? `${store.adminArchivedWinners.length}` : null },
|
||||
...(clipAdminMenuVisible.value ? [{
|
||||
label: 'Clips',
|
||||
to: '/admin/clips',
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, KeyRound, LockKeyhole, Pencil, RefreshCw, RotateCcw, Save, ShieldCheck, Trash2, UserPlus, Users } from '@lucide/vue'
|
||||
import { Check, KeyRound, LockKeyhole, Minus, Pencil, RefreshCw, RotateCcw, Save, ShieldCheck, Trash2, UserPlus, Users } from '@lucide/vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
import AdminTeamRolePermissionsModal from '../../components/admin/AdminTeamRolePermissionsModal.vue'
|
||||
import { useAdminTeamManager } from '../../components/admin/useAdminTeamManager'
|
||||
import { useBodyScrollLock } from '../../composables/useBodyScrollLock'
|
||||
import { watchAdminToast } from '../../composables/useAdminToast'
|
||||
@@ -22,7 +21,6 @@ const canChangeOwnPassword = computed(() => authStore.isOwnerOrCreator && authSt
|
||||
const passwordModalOpen = ref(false)
|
||||
const createMemberModalOpen = ref(false)
|
||||
const editMemberModalOpen = ref(false)
|
||||
const rolePermissionsModalOpen = ref(false)
|
||||
const profileError = ref('')
|
||||
const profileSuccess = ref('')
|
||||
const ownPasswordForm = reactive({
|
||||
@@ -132,6 +130,10 @@ function canDeleteMember(member: AdminTeamMember) {
|
||||
return true
|
||||
}
|
||||
|
||||
function handlePermissionChange(roleKey: string, permissionKey: string, event: Event) {
|
||||
setRolePermission(roleKey, permissionKey, event.target instanceof HTMLInputElement && event.target.checked)
|
||||
}
|
||||
|
||||
function openCreateMemberModal() {
|
||||
startCreateMember()
|
||||
createMemberModalOpen.value = true
|
||||
@@ -281,16 +283,6 @@ async function changeOwnPassword() {
|
||||
<RefreshCw class="mr-2 h-4 w-4" :class="refreshingPresence ? 'animate-spin' : ''" />
|
||||
Aktualisieren
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="border border-violet-200 bg-violet-50 text-violet-700 shadow-sm shadow-violet-200/40 hover:-translate-y-0.5 hover:border-violet-300 hover:bg-violet-100 hover:text-violet-800 hover:shadow-lg hover:shadow-violet-200/60"
|
||||
@click="rolePermissionsModalOpen = true"
|
||||
>
|
||||
<ShieldCheck class="mr-2 h-4 w-4" />
|
||||
Rollenrechte
|
||||
</Button>
|
||||
<Button size="sm" type="button" @click="openCreateMemberModal">
|
||||
<UserPlus class="mr-2 h-4 w-4" />
|
||||
Neuer Login
|
||||
@@ -377,18 +369,84 @@ async function changeOwnPassword() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<AdminTeamRolePermissionsModal
|
||||
:open="rolePermissionsModalOpen"
|
||||
:saving="saving"
|
||||
:has-role-changes="hasRoleChanges"
|
||||
:roles="roles"
|
||||
:permissions="permissions"
|
||||
:permission-groups="permissionGroups"
|
||||
:role-has-permission="roleHasPermission"
|
||||
:set-role-permission="setRolePermission"
|
||||
:save-role-permissions="saveRolePermissions"
|
||||
@close="rolePermissionsModalOpen = false"
|
||||
/>
|
||||
<Card class="p-4 md:p-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="grid h-9 w-9 place-items-center rounded-xl bg-violet-50 text-violet-600">
|
||||
<ShieldCheck class="h-4 w-4" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-[10px] font-bold uppercase tracking-[0.18em] text-violet-500">Rollenrechte</p>
|
||||
<h2 class="mt-1 text-lg font-bold text-slate-900">Berechtigungen einstellen</h2>
|
||||
<p class="mt-1 text-xs font-semibold text-slate-500">
|
||||
Alle Rollen bleiben direkt vergleichbar, ohne zwischen Einzelansichten zu wechseln.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="button" :disabled="saving || !hasRoleChanges" @click="saveRolePermissions">
|
||||
<Save class="mr-2 h-4 w-4" />
|
||||
{{ saving ? 'Speichert...' : 'Speichern' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-x-auto rounded-xl border border-violet-100">
|
||||
<table class="min-w-[920px] divide-y divide-violet-100 text-sm">
|
||||
<thead class="bg-violet-50/70 text-left text-[10px] font-bold uppercase tracking-[0.16em] text-violet-500">
|
||||
<tr>
|
||||
<th class="sticky left-0 z-10 bg-violet-50/95 px-3 py-2">Menüpunkt</th>
|
||||
<th v-for="role in roles" :key="role.key" class="px-3 py-2 text-center">
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<span>{{ role.label }}</span>
|
||||
<span
|
||||
v-if="role.key === 'owner' || role.key === 'creator'"
|
||||
class="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.12em] text-slate-500"
|
||||
>
|
||||
fixiert
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-violet-100 bg-white/70">
|
||||
<tr v-for="permission in permissions" :key="permission.key">
|
||||
<td class="sticky left-0 z-10 max-w-[280px] bg-white px-3 py-3 align-top">
|
||||
<span class="block font-bold text-slate-900">{{ permission.label }}</span>
|
||||
<span class="mt-1 block text-xs leading-5 text-slate-500">{{ permission.description }}</span>
|
||||
<span class="mt-2 inline-flex rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-[10px] font-bold text-slate-500">
|
||||
{{ permission.menuPath }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-for="role in roles" :key="`${role.key}-${permission.key}`" class="px-3 py-3 text-center">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center" :class="role.key === 'owner' || role.key === 'creator' ? 'cursor-not-allowed' : ''">
|
||||
<input
|
||||
class="sr-only"
|
||||
type="checkbox"
|
||||
:checked="roleHasPermission(role.key, permission.key)"
|
||||
:disabled="role.key === 'owner' || role.key === 'creator'"
|
||||
:aria-label="`${role.label}: ${permission.label}`"
|
||||
@change="handlePermissionChange(role.key, permission.key, $event)"
|
||||
>
|
||||
<span
|
||||
class="inline-flex h-9 min-w-20 items-center justify-center gap-2 rounded-xl border px-3 text-sm font-bold transition"
|
||||
:class="roleHasPermission(role.key, permission.key)
|
||||
? role.key === 'owner' || role.key === 'creator'
|
||||
? 'border-slate-200 bg-slate-100 text-slate-500'
|
||||
: 'border-violet-500 bg-violet-600 text-white shadow-lg shadow-violet-500/20'
|
||||
: role.key === 'owner' || role.key === 'creator'
|
||||
? 'border-slate-200 bg-slate-50 text-slate-300'
|
||||
: 'border-violet-200 bg-white text-violet-700 hover:bg-violet-50'"
|
||||
>
|
||||
<Check v-if="roleHasPermission(role.key, permission.key)" class="h-4 w-4" :stroke-width="3" />
|
||||
<Minus v-else class="h-4 w-4" :stroke-width="3" />
|
||||
{{ roleHasPermission(role.key, permission.key) ? 'An' : 'Aus' }}
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
|
||||
@@ -77,7 +77,7 @@ function windowSupportText(rule: AdminTrackingMetricRule) {
|
||||
.map((value) => windowOptions.find((item) => item.value === value)?.label ?? value)
|
||||
.join(', ')
|
||||
|
||||
return `Dieses Zeitfenster ist aktuell nicht automatisch verfuegbar. Auto geht nur fuer: ${labels || 'keine Zeitfenster'}.`
|
||||
return `Dieses Zeitfenster ist aktuell nicht automatisch verfügbar. Auto geht nur für: ${labels || 'keine Zeitfenster'}.`
|
||||
}
|
||||
|
||||
function effectSummary(rule: AdminTrackingMetricRule, section: 'important' | 'optional') {
|
||||
@@ -93,9 +93,9 @@ function effectSummary(rule: AdminTrackingMetricRule, section: 'important' | 'op
|
||||
}
|
||||
|
||||
if (rule.sourceSupport === 'auto') {
|
||||
parts.push('Werte kommen direkt aus TwitchTracker, sofern das Zeitfenster unterstuetzt wird.')
|
||||
parts.push('Werte kommen direkt aus TwitchTracker, sofern das Zeitfenster unterstützt wird.')
|
||||
} else if (rule.sourceSupport === 'manual') {
|
||||
parts.push('Diese Metrik ist fuer manuelle Pflege gedacht.')
|
||||
parts.push('Diese Metrik ist für manuelle Pflege gedacht.')
|
||||
} else {
|
||||
parts.push('Diese Metrik dient nur als Review-Kontext.')
|
||||
}
|
||||
@@ -174,7 +174,7 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-500">Tracking Source</p>
|
||||
<h2 class="mt-1 text-xl font-bold text-slate-900">TwitchTracker API-Quelle</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||
Diese Base URL wird fuer automatische TwitchTracker-Lookups verwendet.
|
||||
Diese Base URL wird für automatische TwitchTracker-Lookups verwendet.
|
||||
</p>
|
||||
</div>
|
||||
<Button class="gap-2" :disabled="trackingLoading || sourceSaving || !hasUnsavedTrackingSourceChanges || !canManageTrackingRules" @click="saveTrackingSource">
|
||||
@@ -210,9 +210,9 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-500">Manual Review Notes</p>
|
||||
<h2 class="mt-1 text-xl font-bold text-slate-900">Admin-Notizen fuer Fallback-Quellen</h2>
|
||||
<h2 class="mt-1 text-xl font-bold text-slate-900">Admin-Notizen für Fallback-Quellen</h2>
|
||||
<p class="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||
Diese Notizen erscheinen im Nominierungsreview als Hilfe fuer den manuellen Check.
|
||||
Diese Notizen erscheinen im Nominierungsreview als Hilfe für den manuellen Check.
|
||||
</p>
|
||||
</div>
|
||||
<Button class="gap-2" :disabled="trackingLoading || notesSaving || !hasUnsavedTrackingNotesChanges || !canManageTrackingRules" @click="saveTrackingNotes">
|
||||
@@ -247,7 +247,7 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
rows="10"
|
||||
:disabled="!canManageTrackingRules"
|
||||
class="w-full rounded-2xl border border-violet-200 bg-white px-4 py-3 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100 disabled:cursor-not-allowed disabled:bg-slate-50"
|
||||
placeholder="- SullyGnome Channel Summary - manueller Check fuer Category Fit - Sonderregeln fuer kleine Kanaele"
|
||||
placeholder="- SullyGnome Channel Summary - manueller Check für Category Fit - Sonderregeln für kleine Kanäle"
|
||||
/>
|
||||
</section>
|
||||
</Card>
|
||||
@@ -296,7 +296,7 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
{{ sourceSupportLabel(rule.sourceSupport) }}
|
||||
</span>
|
||||
<span v-if="rule.requiredForAutoClassification" class="rounded-full border border-rose-200 bg-rose-50 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-rose-700">
|
||||
Pflicht fuer Auto-Check
|
||||
Pflicht für Auto-Check
|
||||
</span>
|
||||
<span v-if="rule.showInReview" class="rounded-full border border-sky-200 bg-sky-50 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-sky-700">
|
||||
Im Review sichtbar
|
||||
@@ -328,7 +328,7 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
</label>
|
||||
<AdminSettingsToggle
|
||||
:model-value="rule.requiredForAutoClassification"
|
||||
:label="`${rule.label} fuer Auto-Check erzwingen`"
|
||||
:label="`${rule.label} für Auto-Check erzwingen`"
|
||||
:disabled="!canManageTrackingRules"
|
||||
active-label="Pflicht"
|
||||
inactive-label="Nicht Pflicht"
|
||||
@@ -373,7 +373,7 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
<Card class="overflow-visible">
|
||||
<section class="border-b border-violet-100 p-5">
|
||||
<h2 class="text-xl font-bold text-slate-900">Optionale Metriken</h2>
|
||||
<p class="mt-2 text-sm text-slate-500">Nur aktive optionale Metriken koennen im Nominierungsreview erscheinen. Deaktivierte Metriken werden dort komplett ausgeblendet.</p>
|
||||
<p class="mt-2 text-sm text-slate-500">Nur aktive optionale Metriken können im Nominierungsreview erscheinen. Deaktivierte Metriken werden dort komplett ausgeblendet.</p>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4 p-5">
|
||||
@@ -425,8 +425,8 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
|
||||
<div v-if="isTopCategoriesRule(rule)" class="mt-4 space-y-3 rounded-2xl border border-violet-100 bg-white p-4">
|
||||
<div>
|
||||
<p class="text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Review-Kontext fuer Top Categories</p>
|
||||
<p class="mt-1 text-sm text-slate-600">Diese Einstellungen erzeugen aktuell keinen automatischen TwitchTracker-Abgleich, steuern aber den manuellen Review-Rahmen fuer Admins.</p>
|
||||
<p class="text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Review-Kontext für Top Categories</p>
|
||||
<p class="mt-1 text-sm text-slate-600">Diese Einstellungen erzeugen aktuell keinen automatischen TwitchTracker-Abgleich, steuern aber den manuellen Review-Rahmen für Admins.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
@@ -555,6 +555,7 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
<Card class="overflow-visible">
|
||||
<section class="border-b border-violet-100 p-5">
|
||||
<h2 class="text-xl font-bold text-slate-900">Flags</h2>
|
||||
<p class="mt-2 text-sm text-slate-500">Tracking-Flags markieren Review-Faelle und Hinweise, blockieren aber keine Annahme oder Verwerfung mehr.</p>
|
||||
</section>
|
||||
<section class="grid gap-3 p-5">
|
||||
<article v-for="rule in trackingFlags" :key="rule.key" class="rounded-2xl border border-violet-100 bg-violet-50/35 p-4">
|
||||
@@ -599,23 +600,20 @@ function activateOptionalMetric(ruleKey: string) {
|
||||
:model-value="rule.requiresManualReview"
|
||||
:label="`${rule.label} braucht Review`"
|
||||
:disabled="!canManageTrackingRules"
|
||||
active-label="Review noetig"
|
||||
active-label="Review nötig"
|
||||
inactive-label="Nur Hinweis"
|
||||
@update:model-value="updateTrackingFlag(rule.key, { requiresManualReview: $event })"
|
||||
/>
|
||||
<AdminSettingsToggle
|
||||
:model-value="rule.blocksApproval"
|
||||
:label="`${rule.label} blockiert Approve`"
|
||||
:disabled="!canManageTrackingRules"
|
||||
active-label="Blockiert"
|
||||
inactive-label="Warnt nur"
|
||||
@update:model-value="updateTrackingFlag(rule.key, { blocksApproval: $event })"
|
||||
/>
|
||||
<div class="rounded-2xl border border-violet-100 bg-white px-4 py-3">
|
||||
<p class="text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Approve-Verhalten</p>
|
||||
<p class="mt-2 text-sm font-semibold text-slate-900">Warnt nur</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">Tracking-Flags sind reine Zusatzinfos für den Review-Kontext und sperren die Entscheidung nicht.</p>
|
||||
</div>
|
||||
<AdminSettingsToggle
|
||||
:model-value="rule.adminNoteRequiredOnOverride"
|
||||
:label="`${rule.label} braucht Override-Notiz`"
|
||||
:disabled="!canManageTrackingRules"
|
||||
active-label="Notiz noetig"
|
||||
active-label="Notiz nötig"
|
||||
inactive-label="Optional"
|
||||
@update:model-value="updateTrackingFlag(rule.key, { adminNoteRequiredOnOverride: $event })"
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ const {
|
||||
statusFilter,
|
||||
statusFilters,
|
||||
summaryCards,
|
||||
seasonCandidates,
|
||||
votingGroups,
|
||||
selectedGroupName,
|
||||
focusedCategoryId,
|
||||
@@ -117,6 +118,7 @@ function openLeaderboard(category: AdminVotingWorkspaceRow) {
|
||||
|
||||
<AdminVotingLeaderboardModal
|
||||
:category="leaderboardCategory"
|
||||
:candidates="seasonCandidates"
|
||||
@close="leaderboardCategory = null"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ watchAdminToast(adminMessage, adminError)
|
||||
<div class="space-y-6">
|
||||
<AdminPageHeader
|
||||
eyebrow="Gewinner"
|
||||
description="Finale Gewinner pro Unterkategorie setzen, Guards pruefen und Landingpage-Freigabe vorbereiten."
|
||||
description="Finale Gewinner pro Unterkategorie setzen, Guards prüfen und Landingpage-Freigabe vorbereiten."
|
||||
:icon="Trophy"
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user