Add winner archive, host image upload and live demo data
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 1m1s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m30s

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:
AzuTear
2026-06-29 21:47:52 +02:00
parent f323e5a82c
commit 6b3b0360e7
92 changed files with 7412 additions and 583 deletions
+83 -25
View File
@@ -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