Update release notes and deploy workspace
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 59s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 53s

This commit is contained in:
AzuTear
2026-06-29 17:49:54 +02:00
parent c466348d18
commit 441ef2b850
196 changed files with 9279 additions and 39292 deletions
@@ -11,7 +11,6 @@ const props = defineProps<{
setArchiveYear: (year: number) => Promise<void>
archiveYearButtonStyle: (active: boolean) => string
winnerPlatformStyle: (url: string) => string
winnerPlatformKey: (url: string) => string
winnerPlatformLabel: (url: string) => string
}>()
@@ -57,10 +56,17 @@ function initialsFor(value: string) {
<div style="font-size:13px;color:rgba(255,246,251,.78);">{{ props.selectedArchive.winners.length }} Kategorien archiviert</div>
</div>
<div class="home-archive-modal__winners" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;">
<article
<component
:is="winner.url ? 'a' : 'article'"
v-for="winner in props.selectedArchive.winners"
:key="`${props.selectedArchive.year}-${winner.category}`"
style="display:grid;grid-template-columns:84px minmax(0,1fr);gap:16px;padding:18px;border-radius:24px;background:rgba(255,255,255,.78);border:1px solid rgba(139,108,219,.12);box-shadow:0 12px 28px rgba(139,108,219,.08);align-items:start;"
:href="winner.url || undefined"
:target="winner.url ? '_blank' : undefined"
:rel="winner.url ? 'noopener' : undefined"
:aria-label="winner.url ? `${winner.name} auf ${props.winnerPlatformLabel(winner.url)} öffnen` : undefined"
style="display:grid;grid-template-columns:84px minmax(0,1fr);gap:16px;padding:18px;border-radius:24px;background:rgba(255,255,255,.78);border:1px solid rgba(139,108,219,.12);box-shadow:0 12px 28px rgba(139,108,219,.08);align-items:start;color:inherit;text-decoration:none;cursor:pointer;transition:transform .16s ease,box-shadow .16s ease,border-color .16s ease;"
style-hover="transform:translateY(-2px);box-shadow:0 18px 38px rgba(139,108,219,.16);border-color:rgba(139,108,219,.26);"
style-focus="outline:3px solid rgba(139,108,219,.28);outline-offset:3px;"
>
<div style="width:84px;height:84px;border-radius:24px;background:linear-gradient(135deg,#ffe1a3,#e7b13e);display:grid;place-items:center;color:#2f1b00;font-family:'Fredoka',sans-serif;font-size:26px;font-weight:800;box-shadow:0 12px 24px rgba(139,108,219,.16);">
{{ initialsFor(winner.name) }}
@@ -68,54 +74,14 @@ function initialsFor(value: string) {
<div style="min-width:0;">
<div style="font-size:10px;font-weight:700;letter-spacing:1.6px;text-transform:uppercase;color:#d9942a;margin-bottom:7px;">{{ winner.category }}</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:20px;line-height:1.2;color:#3f3556;">{{ winner.name }}</div>
<div style="font-size:13px;color:#8a8398;margin-top:5px;">{{ winner.handle }}</div>
<div style="font-size:13px;color:#8a8398;margin-top:5px;">Unterkategorie · {{ winner.subcategory }}</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:9px;margin-top:12px;">
<a :href="winner.url" target="_blank" rel="noopener" :style="props.winnerPlatformStyle(winner.url)" style-hover="transform:translateY(-1px);opacity:.88;">
<span :style="props.winnerPlatformStyle(winner.url)">
{{ props.winnerPlatformLabel(winner.url) }}
</a>
<a
v-if="winner.clipUrl"
:href="winner.clipUrl"
target="_blank"
rel="noopener noreferrer"
referrerpolicy="no-referrer"
style="display:inline-flex;align-items:center;gap:6px;color:#b7791f;text-decoration:none;font-family:'Outfit',sans-serif;font-weight:800;font-size:12px;"
>
{{ winner.clipTitle }}
</a>
</div>
<iframe
v-if="winner.clipEmbedUrl"
:src="winner.clipEmbedUrl"
:title="winner.clipEmbedTitle"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin"
style="width:100%;aspect-ratio:16/9;border:0;border-radius:14px;background:#1b0d2d;margin-top:12px;"
/>
<div
v-else-if="winner.clipUrl"
style="display:flex;align-items:center;justify-content:center;aspect-ratio:16/9;border-radius:14px;background:rgba(255,255,255,.88);border:1px solid rgba(139,108,219,.12);margin-top:12px;padding:14px;text-align:center;"
>
<a
:href="winner.clipUrl"
target="_blank"
rel="noopener noreferrer"
referrerpolicy="no-referrer"
style="color:#7c5bb4;text-decoration:none;font-family:'Outfit',sans-serif;font-size:13px;font-weight:700;"
>
Clip extern ansehen
</a>
</div>
<div
v-else
style="display:flex;align-items:center;justify-content:center;aspect-ratio:16/9;border-radius:14px;background:rgba(255,248,232,.92);border:1px dashed rgba(231,177,62,.42);margin-top:12px;padding:14px;text-align:center;color:#b7791f;font-family:'Outfit',sans-serif;font-size:13px;font-weight:700;"
>
Fuer diesen Gewinner ist kein Clip hinterlegt.
</span>
</div>
</div>
</article>
</component>
</div>
</div>
</div>
@@ -2,21 +2,52 @@
<section id="kategorien" class="home-section" style="max-width:1200px;margin:0 auto;padding:90px 24px;">
<div style="text-align:center;margin-bottom:52px;">
<div style="font-size:13px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent,#ff5fa2);margin-bottom:12px;"> Die Awards</div>
<h2 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:clamp(32px,4vw,48px);margin:0 0 14px;letter-spacing:0;">{{ landingCategories.length }} Hauptkategorien · 1 Sternenhimmel</h2>
<p style="font-size:17px;color:var(--muted,#c9b8da);max-width:560px;margin:0 auto;">Von Gaming bis Creative - jede Hauptkategorie bringt ihre eigenen Viewer-Tiers und Favoriten mit.</p>
<h2 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:clamp(32px,4vw,48px);margin:0 0 14px;letter-spacing:0;">{{ awardsTitle }}</h2>
<p style="font-size:17px;color:var(--muted,#c9b8da);max-width:680px;margin:0 auto;line-height:1.65;">{{ awardsDescription }}</p>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:18px;" data-cat-grid>
<div v-for="category in landingCategories" :key="category.id" style="padding:26px 22px;border-radius:22px;background:var(--card,#22123a);border:1px solid var(--line,rgba(255,255,255,.12));transition:transform .2s,border-color .2s;" style-hover="transform:translateY(-5px);border-color:var(--accent,#ff5fa2);">
<div style="display:inline-flex;align-items:center;justify-content:center;width:50px;height:50px;border-radius:15px;background:linear-gradient(135deg,var(--accent,#ff5fa2),var(--accent2,#a06bff));color:#fff;font-size:24px;margin-bottom:16px;box-shadow:0 8px 20px var(--glow,rgba(255,95,162,.4));">{{ category.icon }}</div>
<h3 style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:19px;margin:0 0 6px;">{{ category.name }}</h3>
<p style="font-size:14px;color:var(--muted,#c9b8da);margin:0;line-height:1.5;">{{ category.subcategoryCount }} Viewer-Tiers · {{ category.candidateCount }} Kandidat:innen aktuell.</p>
<p v-if="category.description" style="font-size:13px;color:var(--muted,#c9b8da);margin:12px 0 0;line-height:1.5;">{{ category.description }}</p>
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;" data-cat-grid>
<article v-for="category in landingCategories" :key="category.id" style="position:relative;display:flex;flex-direction:column;gap:18px;min-height:100%;padding:28px 22px 24px;border-radius:28px;background:radial-gradient(circle at 18% 16%,rgba(255,255,255,.98),transparent 26%),radial-gradient(circle at 84% 20%,rgba(255,214,120,.18),transparent 20%),linear-gradient(160deg,rgba(255,255,255,.97),rgba(247,239,255,.96) 52%,rgba(239,229,255,.94));border:1px solid rgba(178,150,232,.28);box-shadow:0 20px 54px rgba(43,20,74,.10), inset 0 1px 0 rgba(255,255,255,.92), inset 0 -1px 0 rgba(176,145,232,.10);transition:transform .2s,border-color .2s,box-shadow .2s;" style-hover="transform:translateY(-5px);border-color:rgba(255,95,162,.34);box-shadow:0 28px 68px rgba(43,20,74,.14), inset 0 1px 0 rgba(255,255,255,.96), inset 0 -1px 0 rgba(176,145,232,.14);">
<span aria-hidden="true" style="position:absolute;top:14px;right:16px;font-size:10px;color:rgba(231,177,62,.9);text-shadow:0 0 12px rgba(231,177,62,.42);"></span>
<span aria-hidden="true" style="position:absolute;top:42px;right:34px;font-size:7px;color:rgba(164,122,238,.72);text-shadow:0 0 10px rgba(164,122,238,.32);"></span>
<div style="display:inline-flex;align-items:center;justify-content:center;width:60px;height:60px;border-radius:20px;background:radial-gradient(circle at 30% 28%,rgba(255,255,255,.34),transparent 30%),linear-gradient(135deg,var(--accent2,#8b6cdb),#b490ff 58%,#ffd678 140%);color:#fff;font-size:26px;box-shadow:0 16px 36px rgba(139,108,219,.30), inset 0 1px 0 rgba(255,255,255,.32);">{{ category.icon }}</div>
<div>
<h3 style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:clamp(21px,2.2vw,27px);line-height:1.15;color:#3d2f58;margin:0 0 10px;">{{ category.name }}</h3>
<p v-if="category.description" style="font-size:15px;color:#7a6f93;margin:0;line-height:1.65;">{{ category.description }}</p>
</div>
</article>
</div>
<div style="margin-top:34px;padding:28px 24px;border-radius:30px;background:linear-gradient(135deg,rgba(255,255,255,.94),rgba(245,236,255,.92));border:1px solid rgba(139,108,219,.14);box-shadow:0 18px 54px rgba(43,20,74,.08);">
<div style="max-width:780px;margin:0 auto 28px;text-align:center;">
<div style="font-size:13px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:#a05acb;margin-bottom:12px;"> Unterkategorien</div>
<h3 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:clamp(28px,3.2vw,40px);line-height:1.15;color:#3d2f58;margin:0 0 14px;">{{ subcategoriesTitle }}</h3>
<p style="font-size:16px;color:#7a6f93;line-height:1.7;margin:0;">{{ subcategoriesDescription }}</p>
</div>
<div v-if="allSubcategoryNames.length" style="display:flex;flex-wrap:wrap;justify-content:center;gap:12px 10px;">
<span v-for="subcategoryName in allSubcategoryNames" :key="subcategoryName" style="position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:10px 18px;border-radius:999px;background:radial-gradient(circle at 22% 18%,rgba(255,255,255,.95),transparent 34%),linear-gradient(135deg,rgba(255,255,255,.94),rgba(241,232,255,.92) 52%,rgba(234,222,255,.96));border:1px solid rgba(178,150,232,.36);color:#4f3d70;font-size:14px;font-weight:700;line-height:1.2;box-shadow:0 10px 24px rgba(139,108,219,.14), inset 0 1px 0 rgba(255,255,255,.9), inset 0 -1px 0 rgba(176,145,232,.12);">
<span aria-hidden="true" style="display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:999px;background:linear-gradient(135deg,rgba(164,122,238,.18),rgba(255,214,120,.22));color:#9a62df;font-size:11px;box-shadow:0 4px 10px rgba(154,98,223,.16), inset 0 1px 0 rgba(255,255,255,.78);"></span>
<span>{{ subcategoryName }}</span>
<span aria-hidden="true" style="position:absolute;top:8px;right:12px;font-size:8px;color:rgba(231,177,62,.82);text-shadow:0 0 10px rgba(231,177,62,.38);"></span>
</span>
</div>
<p v-else style="font-size:13px;color:#8d83a0;margin:0;line-height:1.5;text-align:center;">Noch keine Unterkategorien veröffentlicht.</p>
</div>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { plainTextContentFromStorage } from '../../lib/privacyContent'
type LandingSiteContent = {
awardsSectionTitle?: string | null
awardsSectionDescription?: string | null
subcategoriesSectionTitle?: string | null
subcategoriesSectionDescription?: string | null
}
type LandingCategory = {
id: string
groupName: string
@@ -24,11 +55,47 @@ type LandingCategory = {
icon: string
description: string
maxNomineesPerUser: number
candidateCount: number
subcategoryCount: number
subcategoryNames: string[]
}
defineProps<{
const props = defineProps<{
siteContent: LandingSiteContent
landingCategories: LandingCategory[]
}>()
const allSubcategoryNames = computed(() => {
const seen = new Set<string>()
const names: string[] = []
for (const category of props.landingCategories) {
for (const subcategoryName of category.subcategoryNames) {
const normalized = subcategoryName.trim()
if (!normalized) continue
const key = normalized.toLowerCase()
if (seen.has(key)) continue
seen.add(key)
names.push(normalized)
}
}
return names
})
const awardsTitle = computed(() =>
plainTextContentFromStorage(props.siteContent.awardsSectionTitle ?? '') || 'Die Kategorien, die diese Award-Season zum Funkeln bringen',
)
const awardsDescription = computed(() =>
plainTextContentFromStorage(props.siteContent.awardsSectionDescription ?? '') || 'Von großen Community-Favoriten bis zu liebgewonnenen Szenen-Highlights: Jede Hauptkategorie entfaltet ihre eigenen Unterkategorien und macht die Show noch spannender.',
)
const subcategoriesTitle = computed(() =>
plainTextContentFromStorage(props.siteContent.subcategoriesSectionTitle ?? '') || 'Hier bekommt jede Story ihre Chance auf den großen Moment',
)
const subcategoriesDescription = computed(() =>
plainTextContentFromStorage(props.siteContent.subcategoriesSectionDescription ?? '') || 'Zu jeder Kategorie gehören eigene Unterkategorien, damit wirklich jede Reise ihren Platz hat: ob ganz frisch entdeckt, gerade im großen Aufwind oder schon fest in der Szene etabliert. Am Ende kann jede Person den Spot bekommen, der zu ihrer Geschichte passt.',
)
</script>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { computed, onUnmounted, reactive, ref, watch } from 'vue'
import { Handshake, Mic2, Send, X } from '@lucide/vue'
import { privacyContentToHtml } from '../../lib/privacyContent'
@@ -126,12 +126,19 @@ function closeShowactInfoModal() {
emit('update:modalOpen', false)
}
// For multiselect: values are stored as comma-separated strings
function getMultiselectValues(fieldId: string): string[] {
function getChoiceValues(fieldId: string): string[] {
const raw = fieldValues[fieldId] ?? ''
return raw ? raw.split(',').map((v) => v.trim()).filter(Boolean) : []
}
function getMultiselectValues(fieldId: string): string[] {
return getChoiceValues(fieldId)
}
function getCheckboxValues(fieldId: string): string[] {
return getChoiceValues(fieldId)
}
function otherFieldId(fieldId: string) {
return `${fieldId}__other`
}
@@ -140,6 +147,11 @@ function getOtherValue(fieldId: string) {
return (fieldValues[otherFieldId(fieldId)] ?? '').trim()
}
function setChoiceValues(fieldId: string, values: string[]) {
fieldValues[fieldId] = values.join(', ')
delete fieldErrors[fieldId]
}
function setMultiselectValue(fieldId: string, option: string, checked: boolean) {
const current = getMultiselectValues(fieldId)
if (checked) {
@@ -148,18 +160,20 @@ function setMultiselectValue(fieldId: string, option: string, checked: boolean)
const idx = current.indexOf(option)
if (idx !== -1) current.splice(idx, 1)
}
fieldValues[fieldId] = current.join(', ')
delete fieldErrors[fieldId]
setChoiceValues(fieldId, current)
}
function setCheckboxValue(field: ShowactFormField, checked: boolean) {
const value = field.options[0] || 'Ja'
fieldValues[field.id] = checked ? value : ''
delete fieldErrors[field.id]
}
function setCheckboxOptionValue(field: ShowactFormField, option: string, checked: boolean) {
const current = getCheckboxValues(field.id)
if (checked) {
if (!current.includes(option)) current.push(option)
} else {
const next = current.filter((value) => value !== option)
setChoiceValues(field.id, next)
return
}
function isCheckboxChecked(field: ShowactFormField) {
return Boolean((fieldValues[field.id] ?? '').trim())
setChoiceValues(field.id, current)
}
function updateOtherValue(field: ShowactFormField, value: string) {
@@ -173,7 +187,7 @@ function validateField(field: ShowactFormField): string {
if (field.required && !value && !otherValue) {
if (field.type === 'checkbox') {
return `Bitte bestaetige${field.label}".`
return `Bitte waehle bei${field.label}" mindestens eine Checkbox aus.`
}
if (field.type === 'radio' || field.type === 'select' || field.type === 'multiselect') {
@@ -333,7 +347,6 @@ async function submitShowact() {
const inputStyle = 'padding:13px 16px;border-radius:12px;border:1.5px solid #e6dcf6;background:#fbf9ff;font-family:\'Outfit\',sans-serif;font-size:15px;color:#3f3556;outline:none;transition:border-color .15s;'
const labelStyle = 'font-family:\'Outfit\',sans-serif;font-size:13px;font-weight:600;color:#5f44ad;'
const errorStyle = 'font-family:\'Outfit\',sans-serif;font-size:12px;font-weight:600;color:#d14b72;line-height:1.45;'
const checkboxStyle = 'display:flex;align-items:center;gap:9px;padding:10px 14px;border-radius:12px;border:1.5px solid #e6dcf6;background:#fbf9ff;font-family:\'Outfit\',sans-serif;font-size:14px;color:#3f3556;cursor:pointer;'
function onFocus(event: FocusEvent) {
;(event.target as HTMLElement).style.borderColor = '#8b6cdb'
@@ -398,6 +411,14 @@ watch(
},
{ immediate: true },
)
watch([localModalOpen, showactInfoModalOpen], ([form, info]) => {
document.body.style.overflow = form || info ? 'hidden' : ''
})
onUnmounted(() => {
document.body.style.overflow = ''
})
</script>
<template>
@@ -450,7 +471,7 @@ watch(
<h2 class="mt-0.5 text-xl font-black text-[#3f3556]">Als Showact auftreten</h2>
<p class="mt-1 text-sm leading-6 text-[#746b86]">
<template v-if="featureFlags.showactApplicationsEnabled">
Hast du eine Idee fuer einen Beitrag? Bewirb dich jetzt und zeig der Community, was du kannst.
Hast du eine Idee für einen Beitrag? Bewirb dich jetzt und zeig der Community, was du kannst.
</template>
<template v-else>
Showacts bleiben ein Teil der Award-Show. Aktuell ist das Bewerbungsfenster geschlossen, aber alle Infos bleiben hier erreichbar.
@@ -601,52 +622,65 @@ watch(
<div v-else-if="field.type === 'multiselect'" style="display:flex;flex-direction:column;gap:6px;margin-bottom:14px;">
<span :style="labelStyle">{{ field.label }}{{ field.required ? ' *' : '' }}</span>
<span v-if="field.helpText" style="font-family:'Outfit',sans-serif;font-size:12px;line-height:1.45;color:#8a819d;">{{ field.helpText }}</span>
<div :style="'display:flex;flex-direction:column;gap:6px;padding:10px 14px;border-radius:12px;border:1.5px solid #e6dcf6;background:#fbf9ff;' + (fieldErrors[field.id] ? 'border-color:#d14b72;background:#fff7fa;' : '')">
<div
class="home-showact-choice-group"
:class="{ 'home-showact-choice-group--invalid': Boolean(fieldErrors[field.id]) }"
>
<label
v-for="opt in field.options"
:key="opt"
style="display:flex;align-items:center;gap:8px;font-family:'Outfit',sans-serif;font-size:14px;color:#3f3556;cursor:pointer;"
class="home-showact-choice-option"
>
<input
type="checkbox"
:checked="getMultiselectValues(field.id).includes(opt)"
style="width:16px;height:16px;"
class="home-showact-choice-input"
@change="setMultiselectValue(field.id, opt, ($event.target as HTMLInputElement).checked); updateFieldError(field)"
/>
{{ opt }}
<span>{{ opt }}</span>
</label>
<label
v-if="field.allowOther"
style="display:flex;align-items:center;gap:8px;font-family:'Outfit',sans-serif;font-size:14px;color:#3f3556;cursor:pointer;"
class="home-showact-choice-option home-showact-choice-option--other"
>
<span style="width:16px;height:16px;border-radius:4px;border:1.5px solid #d8c9eb;background:#fff;"></span>
Andere:
<span class="home-showact-choice-faux-input" aria-hidden="true"></span>
<span>Andere:</span>
<input
:value="getOtherValue(field.id)"
type="text"
placeholder="Eigene Antwort"
style="min-width:0;flex:1;border:0;border-bottom:1.5px solid #d8c9eb;background:transparent;padding:4px 2px;font-family:'Outfit',sans-serif;font-size:14px;color:#3f3556;outline:none;"
class="home-showact-choice-other-input"
@input="updateOtherValue(field, ($event.target as HTMLInputElement).value)"
/>
</label>
<span v-if="field.options.length === 0" style="font-size:13px;color:#9ca3af;">Keine Optionen konfiguriert.</span>
<span v-if="field.options.length === 0" class="home-showact-choice-empty">Keine Optionen konfiguriert.</span>
</div>
<span v-if="fieldErrors[field.id]" :style="errorStyle">{{ fieldErrors[field.id] }}</span>
</div>
<!-- single checkbox confirmation -->
<!-- checkbox group -->
<div v-else-if="field.type === 'checkbox'" style="display:flex;flex-direction:column;gap:6px;margin-bottom:14px;">
<span :style="labelStyle">{{ field.label }}{{ field.required ? ' *' : '' }}</span>
<span v-if="field.helpText" style="font-family:'Outfit',sans-serif;font-size:12px;line-height:1.45;color:#8a819d;">{{ field.helpText }}</span>
<label :style="checkboxStyle + (fieldErrors[field.id] ? 'border-color:#d14b72;background:#fff7fa;' : '')">
<input
type="checkbox"
:checked="isCheckboxChecked(field)"
style="width:17px;height:17px;accent-color:#7c56c4;"
@change="setCheckboxValue(field, ($event.target as HTMLInputElement).checked); updateFieldError(field)"
/>
{{ field.options[0] || 'Ja' }}
</label>
<div
class="home-showact-choice-group"
:class="{ 'home-showact-choice-group--invalid': Boolean(fieldErrors[field.id]) }"
>
<label
v-for="opt in field.options"
:key="opt"
class="home-showact-choice-option"
>
<input
type="checkbox"
:checked="getCheckboxValues(field.id).includes(opt)"
class="home-showact-choice-input"
@change="setCheckboxOptionValue(field, opt, ($event.target as HTMLInputElement).checked); updateFieldError(field)"
/>
<span>{{ opt }}</span>
</label>
<span v-if="field.options.length === 0" class="home-showact-choice-empty">Keine Checkboxen konfiguriert.</span>
</div>
<span v-if="fieldErrors[field.id]" :style="errorStyle">{{ fieldErrors[field.id] }}</span>
</div>
</template>
@@ -693,7 +727,7 @@ watch(
<div style="overflow-y:auto;padding:28px 36px 32px;flex:1;font-family:'Outfit',sans-serif;font-size:14px;line-height:1.7;color:#6f6685;">
<div v-if="showactInfoHtml" class="home-showact-info-content" v-html="showactInfoHtml" />
<p v-else style="margin:0;padding:14px 16px;border-radius:16px;border:1px solid #fde68a;background:#fffbeb;color:#92400e;font-weight:600;">
Fuer Showacts ist aktuell noch kein Landingpage-Inhalt hinterlegt.
Für Showacts ist aktuell noch kein Landingpage-Inhalt hinterlegt.
</p>
</div>
</div>
@@ -721,12 +755,88 @@ watch(
.home-showact-info-content :deep(li) {
margin: 3px 0;
}
</style>
<style scoped>
.home-showact-choice-group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 14px;
border: 1.5px solid #e6dcf6;
border-radius: 14px;
background: #fbf9ff;
transition: border-color .15s ease, background .15s ease;
}
.home-showact-choice-group--invalid {
border-color: #d14b72;
background: #fff7fa;
}
.home-showact-choice-option {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(139, 108, 219, 0.12);
background: rgba(255, 255, 255, 0.86);
color: #3f3556;
font-family: 'Outfit', sans-serif;
font-size: 14px;
font-weight: 600;
line-height: 1.5;
cursor: pointer;
}
.home-showact-choice-option--other {
align-items: center;
}
.home-showact-choice-input {
width: 16px;
height: 16px;
margin-top: 2px;
accent-color: #7c56c4;
flex: none;
}
.home-showact-choice-faux-input {
width: 16px;
height: 16px;
margin-top: 1px;
border-radius: 4px;
border: 1.5px solid #d8c9eb;
background: #fff;
flex: none;
}
.home-showact-choice-other-input {
min-width: 0;
flex: 1;
border: 0;
border-bottom: 1.5px solid #d8c9eb;
background: transparent;
padding: 4px 2px;
color: #3f3556;
font-family: 'Outfit', sans-serif;
font-size: 14px;
outline: none;
}
.home-showact-choice-empty {
color: #9ca3af;
font-size: 13px;
font-weight: 600;
}
@media (max-width: 560px) {
.home-showact-grid {
grid-template-columns: 1fr !important;
}
.home-showact-choice-option--other {
align-items: flex-start;
flex-direction: column;
}
}
</style>
@@ -18,7 +18,7 @@
<div class="home-hero__content" style="position:relative;z-index:3;max-width:1200px;margin:0 auto;padding:60px 24px 70px;">
<div class="home-hero__copy" style="max-width:560px;">
<div class="home-hero__eyebrow" style="display:flex;align-items:center;gap:10px;font-size:13px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;color:#a98ddb;margin-bottom:22px;">
<span style="color:#e7b13e;animation:spinSlow 9s linear infinite;display:inline-block;"></span> Die grosse Community-Auszeichnung
<span style="color:#e7b13e;animation:spinSlow 9s linear infinite;display:inline-block;"></span> Die große Community-Auszeichnung
</div>
<h1 style="margin:0 0 4px;font-family:'Cormorant Garamond',serif;font-weight:700;line-height:.92;">
<span style="display:block;font-size:clamp(56px,8.4vw,116px);letter-spacing:3px;color:#5f44ad;">VTUBER</span>
@@ -37,10 +37,12 @@ defineProps<{
phasePrimaryActionStyle: string
streamEyebrow: string
streamTitle: string
streamText: string
streamMeta: string
streamLive: boolean
streamLocked: boolean
streamLockedLabel: string
streamButtonLabel: string
streamButtonUrl: string
streamLockedTitle: string
statOneValue: string
statOneLabel: string
@@ -95,10 +97,12 @@ defineProps<{
:public-stream-url="publicStreamUrl"
:stream-eyebrow="streamEyebrow"
:stream-title="streamTitle"
:stream-text="streamText"
:stream-meta="streamMeta"
:stream-live="streamLive"
:stream-locked="streamLocked"
:stream-locked-label="streamLockedLabel"
:stream-button-label="streamButtonLabel"
:stream-button-url="streamButtonUrl"
:stream-locked-title="streamLockedTitle"
/>
@@ -1,41 +1,50 @@
<template>
<section class="home-stream-band" style="position:relative;overflow:hidden;background:linear-gradient(115deg,#241640 0%,#3a2168 48%,#5b3aa0 100%);">
<HomeStarField :count="13" variant="light" />
<div class="home-stream-band__inner" style="max-width:1200px;margin:0 auto;padding:26px 24px;display:flex;align-items:center;justify-content:space-between;gap:26px;flex-wrap:wrap;">
<div class="home-stream-band__lead" style="display:flex;align-items:center;gap:18px;">
<div class="home-stream-band__inner" style="max-width:1200px;margin:0 auto;padding:26px 24px;display:flex;align-items:center;justify-content:space-between;gap:26px;">
<div class="home-stream-band__lead" style="display:flex;align-items:center;justify-content:center;gap:18px;">
<div style="flex:none;display:inline-flex;align-items:center;justify-content:center;width:54px;height:54px;border-radius:15px;background:rgba(145,70,255,.22);border:1px solid rgba(255,255,255,.18);">
<svg width="26" height="26" viewBox="0 0 24 24" fill="#c9b1ff"><path d="M4 2L2.4 6v14.5h5V23h2.7l2.5-2.5h4L21.6 16V2H4zm15.3 13.1l-2.9 2.9h-4.4l-2.5 2.5v-2.5H6.7V3.7h12.6v11.4z"/><path d="M11.1 7.1h1.7v5h-1.7zM15.7 7.1h1.7v5h-1.7z"/></svg>
</div>
<div>
<div style="font-size:11px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;color:#c9b1ff;margin-bottom:5px;">{{ streamEyebrow }}</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:26px;color:#fff;line-height:1.1;">{{ streamTitle }}</div>
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:8px 14px;font-size:14px;color:rgba(255,255,255,.78);margin-top:6px;">
<div class="home-stream-band__copy">
<div class="home-stream-band__eyebrow" style="font-size:11px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;color:#c9b1ff;margin-bottom:7px;">{{ streamEyebrow }}</div>
<div class="home-stream-band__headline-row" style="display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;">
<span class="home-stream-band__title" style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:clamp(28px,3vw,36px);color:#fff;line-height:1.02;">
{{ streamTitle }}
</span>
<span class="home-stream-band__countdown-chip" aria-label="Countdown bis zum Finale" style="display:inline-flex;align-items:center;gap:10px;padding:10px 16px;border-radius:999px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.14);box-shadow:inset 0 1px 0 rgba(255,255,255,.08);">
<span
data-dc-ref="streamCountdownLabelRef"
class="home-stream-band__countdown-label"
style="font-family:'Outfit',sans-serif;font-weight:700;font-size:14px;color:#e9dcff;line-height:1;"
>
Finale in
</span>
<span class="home-stream-band__countdown-time" style="display:inline-flex;align-items:baseline;gap:4px;font-family:'Outfit',sans-serif;font-size:clamp(22px,2.4vw,28px);font-weight:800;line-height:1;color:#fff;">
<span data-dc-ref="bdRef">00</span><span class="home-stream-band__countdown-unit" style="opacity:.7;">T</span>
<span data-dc-ref="bhRef">00</span><span class="home-stream-band__countdown-unit" style="opacity:.45;">:</span>
<span data-dc-ref="bmRef">00</span><span class="home-stream-band__countdown-unit" style="opacity:.45;">:</span>
<span data-dc-ref="bsRef">00</span>
</span>
</span>
</div>
<div class="home-stream-band__meta" style="display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:8px 14px;font-size:14px;color:rgba(255,255,255,.78);margin-top:8px;">
<span style="display:inline-flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9b1ff" stroke-width="2"><rect x="3" y="4.5" width="18" height="17" rx="2.5"/><path d="M3 9h18M8 2.5v4M16 2.5v4" stroke-linecap="round"/></svg>{{ streamMeta }}</span>
<span style="width:4px;height:4px;border-radius:50%;background:rgba(255,255,255,.4);"></span>
<span>Countdown bis zur grossen Live-Show</span>
<span>{{ streamText }}</span>
</div>
</div>
</div>
<template v-if="streamLive || streamLocked">
<div class="home-stream-band__actions" :class="{ 'home-stream-band__actions--locked': streamLocked }" style="display:flex;flex-direction:column;align-items:center;gap:9px;">
<div style="display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;letter-spacing:.5px;color:rgba(255,255,255,.7);">
<span data-dc-ref="streamCountdownLabelRef">Finale startet in</span>
<span style="display:inline-flex;align-items:center;gap:4px;font-family:'Outfit',sans-serif;font-weight:700;color:#fff;">
<span data-dc-ref="bdRef">00</span><span style="opacity:.6;">T</span>
<span data-dc-ref="bhRef">00</span><span style="opacity:.6;">:</span>
<span data-dc-ref="bmRef">00</span><span style="opacity:.6;">:</span>
<span data-dc-ref="bsRef">00</span>
</span>
</div>
<a v-if="streamLive" :href="publicStreamUrl" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:11px;padding:16px 28px;border-radius:14px;background:#ec3b5a;color:#fff;text-decoration:none;font-family:'Outfit',sans-serif;font-weight:700;font-size:16px;box-shadow:0 12px 30px rgba(236,59,90,.45);" style-hover="transform:translateY(-2px);">
<span style="width:9px;height:9px;border-radius:50%;background:#fff;display:inline-block;animation:pulseGlow 1.4s ease-in-out infinite;"></span>
Jetzt live · Zum Stream
<div class="home-stream-band__actions" :class="{ 'home-stream-band__actions--locked': streamLocked }" style="display:flex;flex-direction:column;align-items:flex-end;gap:9px;">
<a v-if="streamButtonHref" :href="streamButtonHref" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:11px;padding:16px 28px;border-radius:14px;background:#ec3b5a;color:#fff;text-decoration:none;font-family:'Outfit',sans-serif;font-weight:700;font-size:16px;box-shadow:0 12px 30px rgba(236,59,90,.45);" style-hover="transform:translateY(-2px);">
<span v-if="streamLive" style="width:9px;height:9px;border-radius:50%;background:#fff;display:inline-block;animation:pulseGlow 1.4s ease-in-out infinite;"></span>
{{ streamButtonLabel }}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</a>
<div v-else :title="streamLockedTitle" style="display:inline-flex;align-items:center;gap:9px;padding:14px 24px;border-radius:14px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);color:rgba(255,255,255,.62);font-family:'Outfit',sans-serif;font-weight:600;font-size:15px;cursor:not-allowed;">
<svg width="15" height="15" viewBox="0 0 24 24" fill="rgba(255,255,255,.62)"><path d="M17 9V7a5 5 0 0 0-10 0v2H5v13h14V9h-2zM9 7a3 3 0 0 1 6 0v2H9V7zm3 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>
{{ streamLockedLabel }}
{{ streamButtonLabel }}
</div>
</div>
</template>
@@ -44,16 +53,22 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import HomeStarField from './HomeStarField.vue'
defineProps<{
const props = defineProps<{
publicStreamUrl: string
streamEyebrow: string
streamTitle: string
streamText: string
streamMeta: string
streamLive: boolean
streamLocked: boolean
streamLockedLabel: string
streamButtonLabel: string
streamButtonUrl: string
streamLockedTitle: string
}>()
const streamButtonHref = computed(() => props.streamButtonUrl || (props.streamLive ? props.publicStreamUrl : ''))
</script>
@@ -195,12 +195,12 @@ const nominationRailItems = computed(() => props.catOptions.map((option) => {
}
}))
const nominationProgressText = computed(() =>
`${nominationReviewItems.value.length} / ${props.catOptions.length} Kategorien ausgefuellt`,
`${nominationReviewItems.value.length} / ${props.catOptions.length} Kategorien ausgefüllt`,
)
const nominationHelperText = computed(() =>
nominationTotalLinks.value > 0
? `${nominationTotalLinks.value} Link${nominationTotalLinks.value === 1 ? '' : 's'} bereit. Leere Kategorien werden uebersprungen.`
: 'Leere Kategorien kannst du einfach ueberspringen.',
? `${nominationTotalLinks.value} Link${nominationTotalLinks.value === 1 ? '' : 's'} bereit. Leere Kategorien werden übersprungen.`
: 'Leere Kategorien kannst du einfach überspringen.',
)
const isLastNominationCategory = computed(() => {
const activeIndex = props.catOptions.findIndex((option) => option.id === nominationCatValue.value)
@@ -329,7 +329,7 @@ function stripCategoryIcon(value: string) {
<div class="home-modal__show" style="padding:38px 40px 40px;">
<div style="display:inline-flex;align-items:center;gap:8px;padding:5px 14px;border-radius:999px;background:#f3eefb;color:#a98ddb;font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:18px;">Award-Show</div>
<h3 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:28px;line-height:1.14;margin:0 0 10px;color:#3f3556;">Sei live dabei </h3>
<p style="font-size:15.5px;line-height:1.6;color:#7d7491;margin:0 0 24px;">Die grosse Live-Show findet am <strong style="color:#5f44ad;">{{ props.formatShowDate() }}</strong> statt. Aktiviere eine Erinnerung, damit du nichts verpasst.</p>
<p style="font-size:15.5px;line-height:1.6;color:#7d7491;margin:0 0 24px;">Die große Live-Show findet am <strong style="color:#5f44ad;">{{ props.formatShowDate() }}</strong> statt. Aktiviere eine Erinnerung, damit du nichts verpasst.</p>
<div class="home-modal__reminder-form" style="display:flex;gap:10px;margin-bottom:14px;">
<input data-dc-ref="emailRef" type="email" placeholder="deine@email.de" style="flex:1;padding:14px 16px;border-radius:12px;border:1.5px solid #e6dcf6;background:#fbf9ff;font-family:'Outfit',sans-serif;font-size:15px;color:#3f3556;outline:none;" style-focus="border-color:#8b6cdb;" />
<button @click="props.submitReminder" style="flex:none;padding:14px 24px;border-radius:12px;border:none;background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;font-family:'Outfit',sans-serif;font-weight:600;font-size:15px;cursor:pointer;box-shadow:0 10px 22px rgba(124,86,196,.3);" style-hover="transform:translateY(-2px);">Erinnern</button>
@@ -384,12 +384,12 @@ function stripCategoryIcon(value: string) {
:helper-text="nominationHelperText"
:actions="nominationReviewMode
? [
{ label: 'Zurueck', tone: 'secondary', onClick: closeNominationReview },
{ label: 'Zurück', tone: 'secondary', onClick: closeNominationReview },
{ label: props.submitting ? 'Speichert ...' : 'Nominierungen einreichen', disabled: props.submitting || nominationHasErrors || nominationTotalLinks === 0, onClick: submitNominationDrafts },
]
: [
{ label: 'Zurueck', tone: 'secondary', disabled: nominationCatValue === props.catOptions[0]?.id, onClick: goToPreviousNominationCategory },
{ label: isLastNominationCategory ? 'Nominierungen pruefen' : 'Weiter', disabled: nominationHasErrors, onClick: isLastNominationCategory ? openNominationReview : goToNextNominationCategory },
{ label: 'Zurück', tone: 'secondary', disabled: nominationCatValue === props.catOptions[0]?.id, onClick: goToPreviousNominationCategory },
{ label: isLastNominationCategory ? 'Nominierungen prüfen' : 'Weiter', disabled: nominationHasErrors, onClick: isLastNominationCategory ? openNominationReview : goToNextNominationCategory },
]"
/>
</div>
@@ -436,7 +436,7 @@ function stripCategoryIcon(value: string) {
<label style="display:block;font-size:13px;font-weight:700;color:#5f44ad;margin-bottom:6px;">Kategorie</label>
<HomeSelectDropdown
v-model="clipCatValue"
label="Kategorie fuer Clip auswaehlen"
label="Kategorie für Clip auswählen"
:options="props.catOptions"
@change="handleClipCategoryChange"
/>
@@ -66,8 +66,10 @@ const {
phasePrimaryActionStyle,
streamEyebrow,
streamTitle,
streamText,
streamMeta,
streamLockedLabel,
streamButtonLabel,
streamButtonUrl,
streamLockedTitle,
statOneValue,
statOneLabel,
@@ -93,6 +95,7 @@ const {
clipSubmitStyle,
archiveYears,
selectedArchive,
winnerShowcaseYear,
winnerShowcase,
activeCat,
submitted,
@@ -128,7 +131,6 @@ const {
onCloseArchive,
archiveYearButtonStyle,
winnerPlatformStyle,
winnerPlatformKey,
winnerPlatformLabel,
privacyModalStop,
accountModalStop,
@@ -239,10 +241,12 @@ const { setRootEl, landingLoaderVisible, stickyCountdownVisible, handleClipSubmi
:phase-primary-action-style="phasePrimaryActionStyle"
:stream-eyebrow="streamEyebrow"
:stream-title="streamTitle"
:stream-text="streamText"
:stream-meta="streamMeta"
:stream-live="streamLive"
:stream-locked="streamLocked"
:stream-locked-label="streamLockedLabel"
:stream-button-label="streamButtonLabel"
:stream-button-url="streamButtonUrl"
:stream-locked-title="streamLockedTitle"
:stat-one-value="statOneValue"
:stat-one-label="statOneLabel"
@@ -273,10 +277,10 @@ const { setRootEl, landingLoaderVisible, stickyCountdownVisible, handleClipSubmi
:on-timeline-final-action="onTimelineFinalAction"
/>
<HomeCategoriesSection :landing-categories="landingCategories" />
<HomeCategoriesSection :site-content="siteContent" :landing-categories="landingCategories" />
<HomeWinnerShowcaseSection
:selected-archive="selectedArchive"
:winner-showcase-year="winnerShowcaseYear"
:winner-showcase="winnerShowcase"
:initials-for="initialsFor"
:on-open-archive="onOpenArchive"
@@ -356,7 +360,6 @@ const { setRootEl, landingLoaderVisible, stickyCountdownVisible, handleClipSubmi
:set-archive-year="setArchiveYear"
:archive-year-button-style="archiveYearButtonStyle"
:winner-platform-style="winnerPlatformStyle"
:winner-platform-key="winnerPlatformKey"
:winner-platform-label="winnerPlatformLabel"
:privacy-modal-open="privacyModalOpen"
:privacy-content-html="privacyContentHtml"
@@ -59,7 +59,6 @@ defineProps<{
setArchiveYear: (year: number) => Promise<void>
archiveYearButtonStyle: (active: boolean) => string
winnerPlatformStyle: (url: string) => string
winnerPlatformKey: (url: string) => string
winnerPlatformLabel: (url: string) => string
privacyModalOpen: boolean
privacyContentHtml: string
@@ -133,7 +132,6 @@ defineProps<{
:set-archive-year="setArchiveYear"
:archive-year-button-style="archiveYearButtonStyle"
:winner-platform-style="winnerPlatformStyle"
:winner-platform-key="winnerPlatformKey"
:winner-platform-label="winnerPlatformLabel"
/>
@@ -11,18 +11,18 @@ const props = defineProps<{
<template>
<section class="home-missing-votes">
<p class="home-vote-picker__eyebrow">Pruefen</p>
<h4>Nicht alle Kategorien gewaehlt</h4>
<p class="home-vote-picker__eyebrow">Prüfen</p>
<h4>Nicht alle Kategorien gewählt</h4>
<p>
In {{ props.missingCategories.length }} Kategorien fehlt noch eine Stimme. Du kannst trotzdem speichern
oder zurueckgehen und weitere Kategorien auswaehlen.
oder zurückgehen und weitere Kategorien auswählen.
</p>
<div class="home-missing-votes__chips">
<span v-for="category in props.missingCategories" :key="category">{{ category }}</span>
</div>
<div v-if="!props.hideActions" class="home-missing-votes__actions">
<button type="button" class="home-wizard-footer__button home-wizard-footer__button--secondary" @click="props.onBack">
Zurueck zum Voting
Zurück zum Voting
</button>
<button
type="button"
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
export interface HomeNominationReviewItem {
categoryIndex: number
categoryName: string
@@ -11,36 +13,73 @@ const props = defineProps<{
totalCategories: number
onEditCategory: (index: number) => void
}>()
const skippedCategoryCount = computed(() =>
Math.max(0, props.totalCategories - props.items.length),
)
const reviewStats = computed(() => [
{
label: 'Links bereit',
value: `${props.totalLinks}`,
},
{
label: 'Kategorien gefuellt',
value: `${props.items.length}/${props.totalCategories}`,
},
{
label: 'Werden uebersprungen',
value: `${skippedCategoryCount.value}`,
},
])
</script>
<template>
<section class="home-nomination-review">
<div class="home-nomination-review__hero">
<p class="home-vote-picker__eyebrow">Pruefen</p>
<p class="home-vote-picker__eyebrow">Prüfen</p>
<h4>Nominierungen einreichen?</h4>
<p>
Du reichst {{ props.totalLinks }} Links in {{ props.items.length }} von
{{ props.totalCategories }} Kategorien ein. Leere Kategorien werden uebersprungen.
{{ props.totalCategories }} Kategorien ein. Leere Kategorien werden übersprungen.
</p>
<div class="home-nomination-review__stats">
<div v-for="stat in reviewStats" :key="stat.label" class="home-nomination-review__stat">
<span>{{ stat.label }}</span>
<strong>{{ stat.value }}</strong>
</div>
</div>
</div>
<div v-if="props.items.length" class="home-nomination-review__list">
<article v-for="item in props.items" :key="item.categoryName" class="home-nomination-review__item">
<div>
<h5>{{ item.categoryName }}</h5>
<div class="home-nomination-review__item-header">
<div class="home-nomination-review__item-copy">
<p class="home-nomination-review__item-eyebrow">Kategorie</p>
<h5>{{ item.categoryName }}</h5>
</div>
<span>{{ item.links.length }} Link{{ item.links.length === 1 ? '' : 's' }}</span>
</div>
<ul>
<li v-for="link in item.links" :key="link">{{ link }}</li>
<ul class="home-nomination-review__links">
<li v-for="link in item.links" :key="link">
<span class="home-nomination-review__link-dot" aria-hidden="true"></span>
<span>{{ link }}</span>
</li>
</ul>
<button type="button" @click="props.onEditCategory(item.categoryIndex)">
<button class="home-nomination-review__edit" type="button" @click="props.onEditCategory(item.categoryIndex)">
Bearbeiten
</button>
</article>
</div>
<div v-else class="home-vote-picker__empty">
Noch keine Links eingetragen.
<div v-else class="home-nomination-review__empty">
<div class="home-nomination-review__empty-icon" aria-hidden="true"></div>
<div class="home-nomination-review__empty-copy">
<p class="home-nomination-review__empty-eyebrow">Noch leer</p>
<h5>Noch keine Links eingetragen.</h5>
<p>Fülle links mindestens eine Kategorie aus, dann erscheint hier deine finale Einreichungsübersicht.</p>
</div>
</div>
</section>
</template>
@@ -51,8 +51,8 @@ const props = defineProps<{
class="home-nomination-pane__add-link"
@click="props.onAddLinkField"
>
<span>+ Weiteren Link hinzufuegen</span>
<small>Noch {{ props.remainingLinkCount }} moeglich</small>
<span>+ Weiteren Link hinzufügen</span>
<small>Noch {{ props.remainingLinkCount }} möglich</small>
</button>
</section>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import { privacyContentToHtml } from '../../lib/privacyContent'
@@ -41,12 +41,29 @@ const props = defineProps<{
platformKey: (platform: string | null | undefined) => string
}>()
const openFaqIndex = ref<number | null>(null)
function toggleFaq(index: number) {
openFaqIndex.value = openFaqIndex.value === index ? null : index
}
const activeFooterLink = ref<FooterLink | null>(null)
const linkCopied = ref(false)
const activeFooterHtml = computed(() => privacyContentToHtml(activeFooterLink.value?.content || ''))
const safeNewsletterUrl = computed(() =>
isSafePublicUrl(props.siteContent.newsletterUrl) ? props.siteContent.newsletterUrl : '',
)
const isSponsorFooterModal = computed(() => activeFooterLink.value?.key === 'sponsors')
const footerModalEyebrow = computed(() =>
isSponsorFooterModal.value ? 'Support the show' : 'Footer Seite',
)
const footerModalIntro = computed(() => {
if (isSponsorFooterModal.value) {
return 'Partner, Sponsoren und kreative Wegbegleiter machen diese Award-Nacht erst möglich.'
}
return 'Alle Infos kommen direkt aus dem Landingpage-Content und bleiben dadurch jederzeit aktuell.'
})
function openFooterLink(event: Event, link: FooterLink) {
if (link.key === 'showacts' && props.onShowactApply) {
@@ -89,6 +106,14 @@ function isSafePublicUrl(value: string | null | undefined) {
return false
}
}
watch(activeFooterLink, (link) => {
document.body.style.overflow = link ? 'hidden' : ''
})
onUnmounted(() => {
document.body.style.overflow = ''
})
</script>
<template>
@@ -155,10 +180,21 @@ function isSafePublicUrl(value: string | null | undefined) {
<p style="font-size:17px;color:var(--muted,#8a8398);max-width:520px;margin:0 auto;">Alles, was du über Nominierung, Voting und die Show wissen musst.</p>
</div>
<div style="display:flex;flex-direction:column;gap:14px;">
<details v-for="item in props.faqItems" :key="item.question" style="background:#ffffff;border:1px solid var(--line,rgba(124,86,196,.16));border-radius:16px;padding:4px 22px;box-shadow:0 8px 24px rgba(124,86,196,.07);">
<summary style="display:flex;align-items:center;justify-content:space-between;gap:16px;cursor:pointer;list-style:none;padding:18px 0;font-family:'Outfit',sans-serif;font-weight:600;font-size:18px;color:var(--ink,#3f3556);">{{ item.question }}<span style="flex:none;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:#f1ecfb;color:#8b6cdb;font-size:16px;transition:transform .2s;">+</span></summary>
<p style="margin:0 0 18px;font-size:15px;line-height:1.65;color:var(--muted,#8a8398);">{{ item.answer }}</p>
</details>
<div
v-for="(item, index) in props.faqItems"
:key="item.question"
class="faq-item"
:class="{ 'faq-item--open': openFaqIndex === index }"
>
<button class="faq-trigger" @click="toggleFaq(index)" :aria-expanded="openFaqIndex === index">
<span class="faq-question">
<span class="faq-star" aria-hidden="true"></span>
{{ item.question }}
</span>
<span class="faq-toggle" aria-hidden="true">{{ openFaqIndex === index ? '×' : '+' }}</span>
</button>
<p v-show="openFaqIndex === index" class="faq-answer">{{ item.answer }}</p>
</div>
</div>
</section>
@@ -199,16 +235,35 @@ function isSafePublicUrl(value: string | null | undefined) {
<template v-if="activeFooterLink">
<div class="home-modal-overlay" @click="closeFooterLink" style="position:fixed;inset:0;z-index:420;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(40,24,70,.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);">
<div class="home-modal" @click.stop style="position:relative;width:100%;max-width:720px;max-height:88vh;overflow:hidden;display:flex;flex-direction:column;background:#fff;border-radius:24px;box-shadow:0 40px 90px rgba(40,24,70,.4);">
<div style="display:flex;align-items:center;justify-content:space-between;gap:18px;padding:24px 32px 18px;border-bottom:1px solid #f1ecfb;flex:none;">
<div>
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:4px;">Footer Seite</div>
<h2 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:26px;margin:0;color:#3f3556;">{{ activeFooterLink.label }}</h2>
<div class="home-modal" @click.stop style="position:relative;width:100%;max-width:780px;max-height:88vh;overflow:hidden;display:flex;flex-direction:column;background:linear-gradient(180deg,#fffaff 0%,#ffffff 42%,#faf7ff 100%);border:1px solid rgba(221,208,242,.9);border-radius:32px;box-shadow:0 40px 90px rgba(40,24,70,.4);">
<div style="position:relative;display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:28px 32px 24px;border-bottom:1px solid #f1ecfb;flex:none;overflow:hidden;background:radial-gradient(circle at top left,rgba(193,163,255,.22),transparent 42%),radial-gradient(circle at top right,rgba(255,191,219,.2),transparent 36%),linear-gradient(135deg,#fbf7ff 0%,#ffffff 54%,#fff7fb 100%);">
<div style="position:absolute;right:88px;top:20px;font-size:14px;color:#d6c3fb;"> </div>
<div style="max-width:560px;position:relative;z-index:1;">
<div style="display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border-radius:999px;background:rgba(255,255,255,.82);border:1px solid rgba(221,208,242,.95);box-shadow:0 10px 24px rgba(124,86,196,.08);font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:14px;">
<span style="font-size:12px;"></span>
{{ footerModalEyebrow }}
</div>
<h2 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:clamp(28px,4vw,34px);line-height:1.05;margin:0;color:#3f3556;">{{ activeFooterLink.label }}</h2>
<p style="margin:14px 0 0;font-family:'Outfit',sans-serif;font-size:15px;line-height:1.7;color:#7d7491;">
{{ footerModalIntro }}
</p>
</div>
<button @click="closeFooterLink" style="width:34px;height:34px;border-radius:50%;border:none;background:#f1ecfb;color:#8b6cdb;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;" style-hover="background:#e6dcf6;"></button>
<button type="button" @click="closeFooterLink" style="position:relative;z-index:1;width:42px;height:42px;border-radius:50%;border:1px solid rgba(221,208,242,.95);background:rgba(255,255,255,.92);color:#8b6cdb;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 12px 28px rgba(124,86,196,.12);" style-hover="background:#f3ebff;"></button>
</div>
<div style="overflow-y:auto;padding:28px 32px;flex:1;font-family:'Outfit',sans-serif;font-size:14px;line-height:1.7;color:#6f6685;">
<div v-if="activeFooterHtml" class="home-footer-page-content" v-html="activeFooterHtml" />
<div style="overflow-y:auto;padding:28px 32px 32px;flex:1;font-family:'Outfit',sans-serif;font-size:14px;line-height:1.7;color:#6f6685;">
<div
v-if="isSponsorFooterModal"
style="display:grid;gap:14px;margin-bottom:18px;"
>
<div style="display:flex;flex-wrap:wrap;gap:10px;">
<span class="home-sponsor-pill">Für Newcomer, Rising Stars und Legenden</span>
<span class="home-sponsor-pill">Community, Show und Sichtbarkeit</span>
<span class="home-sponsor-pill">Gemeinsam tragen wir die Bühne</span>
</div>
</div>
<div v-if="activeFooterHtml" class="home-footer-modal-surface">
<div class="home-footer-page-content" v-html="activeFooterHtml" />
</div>
<p v-else style="margin:0;padding:14px 16px;border-radius:16px;border:1px solid #fde68a;background:#fffbeb;color:#92400e;font-weight:600;">
Für diese Footer-Seite ist noch kein Inhalt hinterlegt.
</p>
@@ -219,6 +274,89 @@ function isSafePublicUrl(value: string | null | undefined) {
</template>
<style scoped>
/* FAQ accordion */
.faq-item {
background: #ffffff;
border: 1px solid rgba(124, 86, 196, 0.16);
border-radius: 16px;
padding: 4px 22px;
box-shadow: 0 8px 24px rgba(124, 86, 196, 0.07);
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
}
.faq-item--open {
border-color: rgba(139, 108, 219, 0.45);
box-shadow: 0 8px 28px rgba(124, 86, 196, 0.15);
background: linear-gradient(135deg, #fdfbff 0%, #f5f0ff 100%);
}
.faq-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
width: 100%;
padding: 18px 0;
background: none;
border: none;
cursor: pointer;
font-family: 'Outfit', sans-serif;
font-weight: 600;
font-size: 18px;
color: #3f3556;
text-align: left;
transition: color 0.2s;
}
.faq-item--open .faq-trigger {
color: #6b44c0;
}
.faq-question {
display: flex;
align-items: center;
gap: 10px;
}
.faq-star {
flex: none;
font-size: 11px;
color: #c4aef7;
transition: color 0.2s;
}
.faq-item--open .faq-star {
color: #8b6cdb;
}
.faq-toggle {
flex: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: #f1ecfb;
color: #8b6cdb;
font-size: 18px;
line-height: 1;
transition: background 0.2s;
}
.faq-item--open .faq-toggle {
background: #e4d8ff;
}
.faq-answer {
margin: 0 0 18px;
font-size: 15px;
line-height: 1.65;
color: #8a8398;
padding-left: 21px;
border-left: 2px solid #d8c8ff;
}
.home-footer-page-content :deep(p) {
margin: 0 0 14px;
}
@@ -238,4 +376,37 @@ function isSafePublicUrl(value: string | null | undefined) {
.home-footer-page-content :deep(li) {
margin: 3px 0;
}
.home-footer-modal-surface {
border: 1px solid rgba(221, 208, 242, 0.9);
border-radius: 24px;
padding: 22px 22px 20px;
background:
radial-gradient(circle at top right, rgba(255, 206, 232, 0.16), transparent 26%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(249, 244, 255, 0.96) 100%);
box-shadow: 0 20px 44px rgba(124, 86, 196, 0.1);
}
.home-sponsor-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-radius: 999px;
border: 1px solid rgba(212, 194, 247, 0.95);
background:
radial-gradient(circle at top, rgba(255, 255, 255, 0.95), rgba(245, 238, 255, 0.98)),
linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(243, 235, 255, 0.96));
color: #6d55a8;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.03em;
box-shadow: 0 12px 26px rgba(124, 86, 196, 0.08);
}
.home-sponsor-pill::before {
content: '✦';
color: #b089ff;
font-size: 12px;
}
</style>
@@ -3,13 +3,13 @@
<div style="text-align:center;margin-bottom:56px;">
<div style="font-size:13px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent,#8b6cdb);margin-bottom:12px;"> Der Ablauf</div>
<h2 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:clamp(32px,4vw,48px);margin:0 0 14px;letter-spacing:0;color:#3f3556;">In vier Schritten auf die Bühne</h2>
<p style="font-size:17px;color:var(--muted,#8a8398);max-width:620px;margin:0 auto;">Von Nominierung und Voting über die Aufbereitung bis ganz zum Schluss zur grossen Show.</p>
<p style="font-size:17px;color:var(--muted,#8a8398);max-width:620px;margin:0 auto;">Von Nominierung und Voting über die Aufbereitung bis ganz zum Schluss zur großen Show.</p>
</div>
<div style="position:relative;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:20px;align-items:start;" data-timeline>
<div :style="timelineLineStyle"></div>
<div class="timeline-grid" style="position:relative;display:grid;gap:20px;align-items:start;" data-timeline>
<div :style="timelineLineStyle" class="timeline-connector-line"></div>
<div style="position:relative;z-index:1;text-align:center;">
<div class="timeline-phase" style="position:relative;z-index:1;text-align:center;">
<div :style="nominationPhase ? 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#2bbd6e,#1f9d5a);display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 6px rgba(43,189,110,.18),0 8px 20px rgba(31,157,90,.35);border:4px solid #f4eefb;animation:pulseGlow 2.2s ease-in-out infinite;' : 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#8b6cdb,#7355c8);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(124,86,196,.32);border:4px solid #f4eefb;'">
<template v-if="nominationPhase">
<svg width="22" height="22" viewBox="0 0 24 24" fill="#fff"><path d="M12 17.3l-6.2 3.3 1.2-7-5.1-5 7.1-1 3-6.4 3 6.4 7.1 1-5.1 5 1.2 7z"/></svg>
@@ -27,7 +27,7 @@
</div>
</div>
<div style="position:relative;z-index:1;text-align:center;">
<div class="timeline-phase" style="position:relative;z-index:1;text-align:center;">
<div :style="votingPhase ? 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#2bbd6e,#1f9d5a);display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 6px rgba(43,189,110,.18),0 8px 20px rgba(31,157,90,.35);border:4px solid #f4eefb;animation:pulseGlow 2.2s ease-in-out infinite;' : nominationPhase ? 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:#fff;border:4px solid #e2d6f4;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(124,86,196,.1);' : 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#8b6cdb,#7355c8);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(124,86,196,.32);border:4px solid #f4eefb;'">
<template v-if="nominationPhase">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#b9a9dd" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.6 1.4 6.8L12 17.8 5.9 20.4l1.4-6.8L2.2 9l6.9-.7z"/></svg>
@@ -57,7 +57,7 @@
</div>
</div>
<div style="position:relative;z-index:1;text-align:center;">
<div class="timeline-phase" style="position:relative;z-index:1;text-align:center;">
<div :style="nominationPhase || votingPhase ? 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:#fff;border:4px solid #e2d6f4;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(124,86,196,.1);' : preparationPhase ? 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#f7c76a,#b7791f);display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 6px rgba(247,199,106,.18),0 8px 20px rgba(183,121,31,.28);border:4px solid #f4eefb;animation:pulseGlow 2.2s ease-in-out infinite;' : 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#8b6cdb,#7355c8);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(124,86,196,.32);border:4px solid #f4eefb;'">
<template v-if="nominationPhase || votingPhase">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#b9a9dd" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
@@ -85,7 +85,7 @@
</div>
</div>
<div style="position:relative;z-index:1;text-align:center;">
<div class="timeline-phase" style="position:relative;z-index:1;text-align:center;">
<div :style="showPhase ? 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#ec3b5a,#b91c43);display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 6px rgba(236,59,90,.18),0 8px 20px rgba(185,28,67,.28);border:4px solid #f4eefb;animation:pulseGlow 2.2s ease-in-out infinite;' : completedPhase ? 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:linear-gradient(135deg,#8b6cdb,#7355c8);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 20px rgba(124,86,196,.32);border:4px solid #f4eefb;' : 'width:54px;height:54px;margin:0 auto 20px;border-radius:50%;background:#fff;border:4px solid #e2d6f4;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(124,86,196,.1);'">
<template v-if="showPhase">
<svg width="18" height="18" viewBox="0 0 24 24" fill="#fff"><path d="M8 5v14l11-7z"/></svg>
@@ -101,7 +101,7 @@
<div :style="showPhase ? 'display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;background:#ffe5ec;color:#ec3b5a;font-size:11px;font-weight:700;letter-spacing:.5px;margin-bottom:12px;' : completedPhase ? 'display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;background:#f1ecfb;color:#8b6cdb;font-size:11px;font-weight:700;letter-spacing:.5px;margin-bottom:12px;' : 'display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;background:#f3eefb;color:#a98ddb;font-size:11px;font-weight:700;letter-spacing:.5px;margin-bottom:12px;'">{{ showPhase ? 'JETZT LIVE' : completedPhase ? 'ABGESCHLOSSEN' : 'FINALE' }}</div>
<h3 style="font-family:'Outfit',sans-serif;font-weight:700;font-size:21px;margin:0 0 6px;color:#3f3556;">Show</h3>
<div :style="showPhase ? 'font-size:13px;font-weight:600;color:#ec3b5a;margin-bottom:12px;' : 'font-size:13px;font-weight:600;color:#a99fc0;margin-bottom:12px;'">{{ formatTimelineRange('show') }}</div>
<p style="font-size:14.5px;line-height:1.6;color:#7d7491;margin:0 0 16px;">Die grosse Live-Show auf Twitch: Gewinner werden gekrönt, Clips gezeigt und die Szene feiert gemeinsam.</p>
<p style="font-size:14.5px;line-height:1.6;color:#7d7491;margin:0 0 16px;">Die große Live-Show auf Twitch: Gewinner werden gekrönt, Clips gezeigt und die Szene feiert gemeinsam.</p>
<a
v-if="showPhase"
:href="publicStreamUrl"
@@ -180,3 +180,35 @@ function finalActionStyle(showPhase: boolean, completedPhase = false) {
: "display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:11px 20px;border-radius:11px;background:#f3eefb;color:#a06bd8;border:1px solid #e4d8f6;cursor:pointer;font-family:'Outfit',sans-serif;font-weight:600;font-size:14px;text-decoration:none;"
}
</script>
<style scoped>
.timeline-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.timeline-phase {
display: flex;
flex-direction: column;
align-items: stretch;
}
.timeline-phase > :first-child {
align-self: center;
}
@media (max-width: 1024px) {
.timeline-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.timeline-connector-line {
display: none;
}
}
@media (max-width: 600px) {
.timeline-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -59,18 +59,18 @@ const footerPrimaryLabel = computed(() => {
})
const footerHelperText = computed(() =>
confirmMissingVotes.value
? 'Pruefe die offenen Kategorien oder speichere deine aktuelle Auswahl.'
? 'Prüfe die offenen Kategorien oder speichere deine aktuelle Auswahl.'
: props.readonlyMode
? 'Hier siehst du die freigegebenen Kandidat:innen der abgeschlossenen Nominierungsphase.'
: props.savedVoteActive
? 'Du bearbeitest dein gespeichertes Voting. Speichern ersetzt deine bisherige Auswahl.'
: 'Du kannst Kategorien ueberspringen und vor dem Speichern pruefen.',
: 'Du kannst Kategorien überspringen und vor dem Speichern prüfen.',
)
const footerActions = computed(() =>
props.readonlyMode
? [
{
label: 'Zurueck',
label: 'Zurück',
tone: 'secondary' as const,
disabled: props.activeCatIndex === 0,
onClick: () => props.catList[Math.max(0, props.activeCatIndex - 1)]?.onClick(),
@@ -84,12 +84,12 @@ const footerActions = computed(() =>
:
confirmMissingVotes.value
? [
{ label: 'Zurueck zum Voting', tone: 'secondary' as const, disabled: props.submitting, onClick: hideMissingVoteConfirm },
{ label: 'Zurück zum Voting', tone: 'secondary' as const, disabled: props.submitting, onClick: hideMissingVoteConfirm },
{ label: props.submitting ? 'Speichert ...' : submitLabel.value, disabled: props.submitting, onClick: submitConfirmedVote },
]
: [
{
label: 'Zurueck',
label: 'Zurück',
tone: 'secondary' as const,
disabled: props.activeCatIndex === 0,
onClick: () => props.catList[Math.max(0, props.activeCatIndex - 1)]?.onClick(),
@@ -230,17 +230,18 @@ async function submitConfirmedVote() {
</article>
<article v-else class="home-vote-card" :class="{ 'home-vote-card--selected': nom.selected, 'home-vote-card--missing-clip': !nom.hasClip }">
<div v-if="nom.selected" class="home-vote-card__burst" aria-hidden="true">
<span class="home-vote-card__burst-flare"></span>
<span class="home-vote-card__burst-ring"></span>
<span class="home-vote-card__burst-trail home-vote-card__burst-trail--left"></span>
<span class="home-vote-card__burst-trail home-vote-card__burst-trail--right"></span>
<span class="home-vote-card__burst-trail home-vote-card__burst-trail--up"></span>
<span class="home-vote-card__burst-star home-vote-card__burst-star--up"></span>
<span class="home-vote-card__burst-star home-vote-card__burst-star--left"></span>
<span class="home-vote-card__burst-star home-vote-card__burst-star--right"></span>
<span class="home-vote-card__burst-star home-vote-card__burst-star--left-small"></span>
<span class="home-vote-card__burst-star home-vote-card__burst-star--right-small"></span>
<span class="home-vote-card__burst-star home-vote-card__burst-star--top-small"></span>
<span class="home-vote-card__sky-glow"></span>
<span class="home-vote-card__sky-sweep"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--one"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--two"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--three"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--four"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--five"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--six"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--seven"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--eight"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--nine"></span>
<span class="home-vote-card__sky-star home-vote-card__sky-star--ten"></span>
</div>
<div class="home-vote-card__top">
<div class="home-vote-card__identity">
@@ -1,63 +1,8 @@
<template>
<section id="nominierte" style="position:relative;overflow:hidden;background:linear-gradient(115deg,#241640 0%,#3a2168 48%,#5b3aa0 100%);border-top:1px solid rgba(255,255,255,.12);border-bottom:1px solid rgba(255,255,255,.12);">
<HomeStarField :count="17" variant="light" />
<div class="home-section" style="max-width:1200px;margin:0 auto;padding:90px 24px;">
<div style="display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:46px;">
<div>
<div style="font-size:13px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:#ff5fa2;margin-bottom:12px;">&#9733; Die Stars</div>
<h2 style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:clamp(32px,4vw,48px);margin:0;letter-spacing:0;color:#fff6fb;">Gewinner {{ selectedArchive.year }}</h2>
</div>
<button @click="onOpenArchive" style="display:inline-flex;align-items:center;gap:8px;padding:12px 22px;border-radius:999px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);color:#fff6fb;font-weight:600;font-size:15px;cursor:pointer;" style-hover="transform:translateY(-2px);background:rgba(255,255,255,.09);">Archiv ansehen &#8594;</button>
</div>
<div class="home-winner-rail" style="overflow-x:auto;overflow-y:hidden;padding:0 18px 12px 0;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.34) rgba(255,255,255,.06);scroll-snap-type:x mandatory;scroll-padding-left:0;">
<div class="home-winner-track" style="display:flex;gap:22px;width:max-content;">
<article v-for="winner in winnerShowcase" :key="`${selectedArchive.year}-${winner.category}`" class="home-winner-card" style="flex:none;width:390px;border-radius:24px;overflow:hidden;background:#22123a;border:1px solid rgba(255,255,255,.12);">
<div style="position:relative;padding:24px 24px 20px;background:radial-gradient(circle at 28% 24%,rgba(255,95,162,.22),transparent 28%),radial-gradient(circle at 74% 78%,rgba(160,107,255,.2),transparent 32%),linear-gradient(180deg,#26133d 0%,#201132 100%);">
<div style="display:flex;align-items:center;gap:18px;margin-top:28px;margin-bottom:20px;min-height:120px;">
<div style="flex:none;width:104px;height:104px;border-radius:28px;background:linear-gradient(135deg,#ffe1a3,#e7b13e);display:flex;align-items:center;justify-content:center;color:#2f1b00;font-family:'Fredoka',sans-serif;font-size:34px;font-weight:700;box-shadow:0 18px 42px rgba(0,0,0,.22);">{{ initialsFor(winner.name) }}</div>
<div style="min-width:0;">
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#ffd27a;margin-bottom:8px;">{{ winner.category }}</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:24px;line-height:1.15;color:#fff6fb;">{{ winner.name }}</div>
<div style="font-size:13px;color:#c9b8da;margin-top:6px;">{{ winner.handle }}</div>
</div>
</div>
<a :href="winner.url" target="_blank" rel="noopener" style="display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:13px 18px;border-radius:14px;background:linear-gradient(135deg,#ff5fa2,#a06bff);color:#fff;font-family:'Fredoka',sans-serif;font-weight:600;font-size:14px;text-decoration:none;box-sizing:border-box;">{{ winner.platform }}</a>
</div>
<div style="padding:12px 24px 24px;background:#1b0d2d;">
<div style="font-size:11px;font-weight:700;letter-spacing:1.6px;text-transform:uppercase;color:#ffb9d4;margin-bottom:10px;">{{ winner.hasClip ? winner.clipPlatform : 'Archivierter Gewinner' }}</div>
<iframe
v-if="winner.clipEmbedUrl"
:src="winner.clipEmbedUrl"
:title="winner.clipEmbedTitle"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin"
style="width:100%;aspect-ratio:16/9;border:0;border-radius:18px;background:#12091d;box-shadow:0 16px 34px rgba(0,0,0,.22);"
/>
<a
v-else-if="winner.clipUrl"
:href="winner.clipUrl"
target="_blank"
rel="noopener noreferrer"
referrerpolicy="no-referrer"
style="border-radius:18px;background:#12091d;border:1px solid rgba(255,255,255,.08);aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;color:#fff6fb;font-family:'Fredoka',sans-serif;font-size:17px;text-decoration:none;text-align:center;padding:18px;"
>
{{ winner.clipTitle }}
</a>
<div v-else style="border-radius:18px;overflow:hidden;background:#12091d;border:1px solid rgba(255,255,255,.08);aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;color:#c9b8da;font-family:'Fredoka',sans-serif;font-size:18px;">{{ winner.platform }} · {{ winner.handle }}</div>
</div>
</article>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import HomeStarField from './HomeStarField.vue'
type WinnerCard = {
categoryGroup: string
category: string
name: string
handle: string
@@ -72,12 +17,382 @@ type WinnerCard = {
}
defineProps<{
selectedArchive: {
year: number
winners: WinnerCard[]
}
winnerShowcaseYear: number
winnerShowcase: WinnerCard[]
initialsFor: (value: string) => string
onOpenArchive: () => Promise<void>
}>()
</script>
<template>
<section id="nominierte" class="home-winner-section">
<HomeStarField :count="17" variant="light" />
<div class="home-section home-winner-section__inner">
<div class="home-winner-section__header">
<div>
<div class="home-winner-section__eyebrow">&#9733; Die Stars</div>
<h2 class="home-winner-section__title">Gewinner {{ winnerShowcaseYear }}</h2>
</div>
<button
type="button"
class="home-winner-section__archive-button"
style-hover="transform:translateY(-2px);background:rgba(255,255,255,.09);"
@click="onOpenArchive"
>
Archiv ansehen &rarr;
</button>
</div>
<div v-if="winnerShowcase.length > 0" class="home-winner-rail">
<div class="home-winner-track">
<article
v-for="winner in winnerShowcase"
:key="`${winnerShowcaseYear}-${winner.category}-${winner.handle}`"
class="home-winner-card"
>
<div class="home-winner-card__spotlight">
<div class="home-winner-card__portrait" aria-hidden="true">
{{ initialsFor(winner.name) }}
</div>
<div class="home-winner-card__status">Gewinner</div>
<div class="home-winner-card__plaque">
<div class="home-winner-card__category">{{ winner.categoryGroup }}</div>
<h3 class="home-winner-card__name">{{ winner.name }}</h3>
<p class="home-winner-card__subcategory">{{ winner.category }}</p>
</div>
</div>
<div class="home-winner-card__actions">
<a
:href="winner.url"
target="_blank"
rel="noopener noreferrer"
class="home-winner-card__profile"
>
{{ winner.platform }}
</a>
</div>
</article>
</div>
</div>
<div v-else class="home-winner-empty">
<div class="home-winner-empty__badge">&#9733;</div>
<div>
<h3>Noch keine Gewinner veröffentlicht</h3>
<p>Sobald ein Award-Jahr im Adminbereich freigegeben ist, erscheinen die Gewinner hier.</p>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.home-winner-section{
position:relative;
overflow:hidden;
border-top:1px solid rgba(255,255,255,.12);
border-bottom:1px solid rgba(255,255,255,.12);
background:
radial-gradient(circle at 20% 12%,rgba(255,95,162,.16),transparent 30%),
radial-gradient(circle at 82% 24%,rgba(201,177,255,.18),transparent 34%),
linear-gradient(115deg,#241640 0%,#3a2168 48%,#5b3aa0 100%);
}
.home-winner-section__inner{
position:relative;
z-index:1;
max-width:1200px;
margin:0 auto;
padding:90px 24px;
}
.home-winner-section__header{
display:flex;
flex-wrap:wrap;
align-items:flex-end;
justify-content:space-between;
gap:20px;
margin-bottom:46px;
}
.home-winner-section__eyebrow{
margin-bottom:12px;
color:#ff5fa2;
font-size:13px;
font-weight:700;
letter-spacing:1.5px;
text-transform:uppercase;
}
.home-winner-section__title{
margin:0;
color:#fff6fb;
font-family:'Fredoka',sans-serif;
font-size:clamp(32px,4vw,48px);
font-weight:700;
letter-spacing:0;
}
.home-winner-section__archive-button{
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
min-height:48px;
padding:12px 22px;
border:1px solid rgba(255,255,255,.12);
border-radius:999px;
background:rgba(255,255,255,.05);
color:#fff6fb;
cursor:pointer;
font-family:'Outfit',sans-serif;
font-size:15px;
font-weight:700;
transition:transform .18s ease, background .18s ease, border-color .18s ease;
}
.home-winner-rail{
overflow-x:auto;
overflow-y:hidden;
padding:0 18px 14px 0;
scroll-padding-left:0;
scrollbar-color:rgba(255,255,255,.34) rgba(255,255,255,.06);
scrollbar-width:thin;
}
.home-winner-track{
display:flex;
gap:24px;
width:max-content;
scroll-snap-type:x mandatory;
}
.home-winner-card{
flex:none;
width:374px;
min-height:478px;
display:flex;
flex-direction:column;
justify-content:space-between;
border:1px solid rgba(255,255,255,.13);
border-radius:28px;
background:
radial-gradient(circle at 22% 15%,rgba(255,95,162,.14),transparent 30%),
radial-gradient(circle at 82% 84%,rgba(160,107,255,.17),transparent 32%),
linear-gradient(180deg,rgba(47,29,68,.96),rgba(34,21,50,.98));
box-shadow:0 24px 62px rgba(16,7,32,.28);
overflow:hidden;
scroll-snap-align:start;
scroll-snap-stop:always;
}
.home-winner-card__spotlight{
position:relative;
display:grid;
min-height:372px;
place-items:center;
padding:28px 26px 34px;
}
.home-winner-card__portrait{
display:grid;
place-items:center;
width:min(248px,100%);
aspect-ratio:1;
border-radius:34%;
color:#3a2d30;
background:
radial-gradient(circle at 32% 28%,rgba(255,244,203,.78),transparent 24%),
linear-gradient(145deg,#ffe39a 0%,#f4bf4f 58%,#d6a342 100%);
box-shadow:
0 24px 58px rgba(0,0,0,.28),
inset 0 1px 0 rgba(255,255,255,.46),
inset 0 -12px 26px rgba(114,71,0,.12);
font-family:'Fredoka',sans-serif;
font-size:64px;
font-weight:800;
letter-spacing:0;
}
.home-winner-card__status{
position:absolute;
right:22px;
top:28px;
padding:7px 12px;
border:1px solid rgba(255,255,255,.14);
border-radius:999px;
color:#ffd27a;
background:rgba(23,12,36,.48);
font-size:11px;
font-weight:900;
letter-spacing:.14em;
text-transform:uppercase;
}
.home-winner-card__plaque{
position:absolute;
left:50%;
bottom:22px;
width:min(236px,calc(100% - 48px));
padding:10px 14px 11px;
border:1px solid rgba(255,210,122,.24);
border-radius:8px;
background:linear-gradient(180deg,rgba(58,28,92,.96),rgba(40,19,65,.98));
box-shadow:0 18px 36px rgba(14,6,27,.34);
text-align:center;
transform:translateX(-50%);
}
.home-winner-card__category{
color:#ffd27a;
font-size:10px;
font-weight:900;
letter-spacing:.13em;
line-height:1.15;
text-transform:uppercase;
}
.home-winner-card__name{
margin:4px 0 0;
color:#fff6fb;
font-family:'Fredoka',sans-serif;
font-size:22px;
font-weight:700;
letter-spacing:0;
line-height:1.05;
overflow-wrap:anywhere;
}
.home-winner-card__subcategory{
margin:4px 0 0;
color:#d9c9ee;
font-size:13px;
font-weight:800;
line-height:1.2;
overflow-wrap:anywhere;
}
.home-winner-card__actions{
display:grid;
gap:10px;
padding:0 26px 26px;
}
.home-winner-card__profile{
display:flex;
align-items:center;
justify-content:center;
min-height:48px;
padding:12px 18px;
border-radius:15px;
font-family:'Fredoka',sans-serif;
font-size:14px;
font-weight:700;
text-align:center;
text-decoration:none;
transition:transform .18s ease, opacity .18s ease, border-color .18s ease;
}
.home-winner-card__profile{
color:#fff;
background:linear-gradient(135deg,#d36fa7,#a06bff);
box-shadow:0 16px 34px rgba(123,76,196,.26);
}
.home-winner-card__profile:hover{
opacity:.92;
transform:translateY(-2px);
}
.home-winner-empty{
display:flex;
align-items:center;
gap:18px;
max-width:720px;
padding:24px;
border:1px solid rgba(255,255,255,.12);
border-radius:24px;
color:#fff6fb;
background:rgba(32,17,50,.62);
box-shadow:0 22px 54px rgba(16,7,32,.22);
}
.home-winner-empty__badge{
flex:none;
display:grid;
place-items:center;
width:58px;
height:58px;
border-radius:20px;
color:#3a2d30;
background:linear-gradient(145deg,#ffe39a,#e7b13e);
font-size:24px;
}
.home-winner-empty h3{
margin:0 0 5px;
font-family:'Fredoka',sans-serif;
font-size:24px;
}
.home-winner-empty p{
margin:0;
color:#d9c9ee;
font-size:15px;
line-height:1.5;
}
@media (max-width:720px){
.home-winner-section__inner{
padding:72px 16px;
}
.home-winner-section__header{
align-items:flex-start;
margin-bottom:34px;
}
.home-winner-section__archive-button{
width:100%;
}
.home-winner-rail{
margin-right:-16px;
padding-right:16px;
}
.home-winner-track{
gap:16px;
}
.home-winner-card{
min-height:430px;
border-radius:24px;
}
.home-winner-card__spotlight{
min-height:324px;
padding:24px 22px 30px;
}
.home-winner-card__portrait{
width:min(218px,100%);
font-size:54px;
}
.home-winner-card__status{
right:18px;
top:22px;
}
.home-winner-card__actions{
padding:0 22px 22px;
}
.home-winner-empty{
align-items:flex-start;
padding:20px;
}
}
</style>
@@ -4,13 +4,9 @@
html,body{margin:0;padding:0;background:#160a26;}
@keyframes twinkle{0%,100%{opacity:.25;transform:scale(.7);}50%{opacity:1;transform:scale(1.15);}}
@keyframes starAppearRandom{0%,16%,100%{opacity:0;transform:translate(-50%,-50%) scale(.55) rotate(var(--star-rotate));}30%,68%{opacity:var(--star-alpha);transform:translate(-50%,-50%) scale(1) rotate(var(--star-rotate));}84%{opacity:0;transform:translate(-50%,-50%) scale(1.16) rotate(var(--star-rotate));}}
@keyframes voteBurstFlare{0%{opacity:.42;transform:translate3d(-50%,-50%,0) scale(.26);}14%{opacity:.88;transform:translate3d(-50%,-50%,0) scale(.88);}64%{opacity:.34;transform:translate3d(-50%,-50%,0) scale(1.42);}100%{opacity:0;transform:translate3d(-50%,-50%,0) scale(1.72);}}
@keyframes voteBurstRing{0%{opacity:.36;transform:translate3d(-50%,-50%,0) scale(.36);}16%{opacity:.72;transform:translate3d(-50%,-50%,0) scale(.82);}100%{opacity:0;transform:translate3d(-50%,-50%,0) scale(2.22);}}
@keyframes voteTrail{0%{opacity:.3;transform:translate3d(0,-50%,0) rotate(var(--trail-rotate)) scaleX(.12);}18%{opacity:.72;transform:translate3d(0,-50%,0) rotate(var(--trail-rotate)) scaleX(.72);}100%{opacity:0;transform:translate3d(var(--trail-x),calc(-50% + var(--trail-y)),0) rotate(var(--trail-rotate)) scaleX(1.08);}}
@keyframes voteStarUp{0%{opacity:.38;transform:translate3d(-50%,-50%,0) scale(.28) rotate(-12deg);}18%{opacity:1;transform:translate3d(-50%,-70%,0) scale(.88) rotate(-2deg);}66%{opacity:1;transform:translate3d(-50%,-170%,0) scale(1.24) rotate(12deg);}100%{opacity:0;transform:translate3d(-50%,-226%,0) scale(.7) rotate(24deg);}}
@keyframes voteStarLeft{0%{opacity:.34;transform:translate3d(-50%,-50%,0) scale(.26) rotate(8deg);}18%{opacity:1;transform:translate3d(-70%,-66%,0) scale(.74) rotate(-4deg);}66%{opacity:.98;transform:translate3d(-176%,-128%,0) scale(1) rotate(-20deg);}100%{opacity:0;transform:translate3d(-236%,-82%,0) scale(.56) rotate(-36deg);}}
@keyframes voteStarRight{0%{opacity:.34;transform:translate3d(-50%,-50%,0) scale(.26) rotate(-8deg);}18%{opacity:1;transform:translate3d(-32%,-66%,0) scale(.68) rotate(4deg);}66%{opacity:.98;transform:translate3d(88%,-122%,0) scale(.88) rotate(18deg);}100%{opacity:0;transform:translate3d(150%,-76%,0) scale(.5) rotate(34deg);}}
@keyframes voteStarDriftSmall{0%{opacity:.28;transform:translate3d(-50%,-50%,0) scale(.22) rotate(0deg);}30%{opacity:.92;transform:translate3d(calc(-50% + var(--star-dx-start)),calc(-50% + var(--star-dy-start)),0) scale(.72) rotate(14deg);}100%{opacity:0;transform:translate3d(calc(-50% + var(--star-dx-end)),calc(-50% + var(--star-dy-end)),0) scale(.28) rotate(42deg);}}
@keyframes voteSkyGlow{0%{opacity:0;transform:scale(.98);}24%{opacity:.88;transform:scale(1);}76%{opacity:.34;transform:scale(1.015);}100%{opacity:0;transform:scale(1.03);}}
@keyframes voteSkySweep{0%{opacity:0;transform:translate3d(-46%,18%,0) rotate(-9deg) scaleX(.7);}22%{opacity:.62;}64%{opacity:.5;}100%{opacity:0;transform:translate3d(46%,-18%,0) rotate(-9deg) scaleX(1.22);}}
@keyframes voteSkyStar{0%,12%{opacity:0;transform:translate(-50%,-50%) scale(.58) rotate(var(--star-rotate));}34%{opacity:1;transform:translate(-50%,-52%) scale(1.12) rotate(var(--star-rotate));}72%{opacity:.72;transform:translate(-50%,-55%) scale(.94) rotate(var(--star-rotate));}100%{opacity:0;transform:translate(-50%,-58%) scale(.72) rotate(var(--star-rotate));}}
@keyframes floaty{0%,100%{transform:translateY(0);}50%{transform:translateY(-16px);}}
@keyframes floaty2{0%,100%{transform:translateY(0) rotate(-4deg);}50%{transform:translateY(-22px) rotate(4deg);}}
@keyframes pulseGlow{0%,100%{opacity:.55;}50%{opacity:1;}}
@@ -142,7 +138,9 @@ html,body{margin:0;padding:0;background:#160a26;}
.home-sticky-countdown,
.home-sticky-countdown__dot,
.home-vote-card__burst-star{
.home-vote-card__sky-star,
.home-vote-card__sky-glow,
.home-vote-card__sky-sweep{
animation:none;
transition:none;
}
@@ -254,6 +252,57 @@ html,body{margin:0;padding:0;background:#160a26;}
transform:translateY(-1px);
}
.home-stream-band__copy{
min-width:0;
flex:1 1 auto;
display:flex;
flex-direction:column;
align-items:center;
}
.home-stream-band__headline-row{
width:100%;
}
.home-stream-band__lead{
flex:1 1 auto;
}
.home-stream-band__eyebrow,
.home-stream-band__meta{
text-align:center;
}
.home-stream-band__headline{
min-width:0;
}
.home-stream-band__title{
white-space:nowrap;
letter-spacing:.01em;
}
.home-stream-band__countdown-label{
white-space:nowrap;
opacity:.92;
}
.home-stream-band__countdown-chip{
backdrop-filter:blur(10px);
-webkit-backdrop-filter:blur(10px);
}
.home-stream-band__countdown-time{
font-variant-numeric:tabular-nums;
white-space:nowrap;
text-shadow:0 4px 18px rgba(11,6,26,.18);
}
.home-stream-band__actions{
flex:none;
margin-left:auto;
}
.home-landing img,
.home-landing svg{
max-width:100%;
@@ -509,14 +558,17 @@ html,body{margin:0;padding:0;background:#160a26;}
}
.home-nomination-review__hero{
margin-bottom:18px;
padding:18px 20px;
position:relative;
overflow:hidden;
margin-bottom:20px;
padding:22px 24px;
border:1px solid rgba(139,108,219,.14);
border-radius:18px;
border-radius:24px;
background:
linear-gradient(135deg,rgba(255,255,255,.94),rgba(252,248,255,.94)),
radial-gradient(circle at 100% 0,rgba(231,177,62,.12),transparent 34%);
box-shadow:0 14px 34px rgba(82,61,128,.07);
radial-gradient(circle at top right,rgba(231,177,62,.16),transparent 34%),
radial-gradient(circle at 12% 0,rgba(201,177,255,.26),transparent 28%),
linear-gradient(145deg,rgba(255,255,255,.96),rgba(250,245,255,.96));
box-shadow:0 18px 40px rgba(82,61,128,.08);
}
.home-nomination-review__item h5{
@@ -534,6 +586,39 @@ html,body{margin:0;padding:0;background:#160a26;}
line-height:1.5;
}
.home-nomination-review__stats{
display:grid;
grid-template-columns:repeat(3,minmax(0,1fr));
gap:12px;
margin-top:18px;
}
.home-nomination-review__stat{
display:grid;
gap:4px;
padding:14px 16px;
border:1px solid rgba(139,108,219,.1);
border-radius:18px;
background:rgba(255,255,255,.72);
box-shadow:inset 0 1px 0 rgba(255,255,255,.7);
}
.home-nomination-review__stat span{
color:#8f84a7;
font-size:11px;
font-weight:800;
letter-spacing:.08em;
text-transform:uppercase;
}
.home-nomination-review__stat strong{
color:#4a356d;
font-family:'Fredoka',sans-serif;
font-size:22px;
font-weight:700;
line-height:1;
}
.home-nomination-pane__fields{
display:grid;
gap:14px;
@@ -670,26 +755,39 @@ html,body{margin:0;padding:0;background:#160a26;}
.home-nomination-review__list{
display:grid;
gap:12px;
gap:14px;
}
.home-nomination-review__item{
display:grid;
gap:12px;
padding:16px;
border:1px solid #efe7fb;
border-radius:18px;
background:#fff;
box-shadow:0 12px 28px rgba(82,61,128,.07);
gap:14px;
padding:18px;
border:1px solid rgba(139,108,219,.12);
border-radius:24px;
background:linear-gradient(135deg,rgba(255,255,255,.96),rgba(249,244,255,.96));
box-shadow:0 16px 34px rgba(82,61,128,.08);
}
.home-nomination-review__item > div{
.home-nomination-review__item-header{
display:flex;
align-items:center;
align-items:flex-start;
justify-content:space-between;
gap:12px;
}
.home-nomination-review__item-copy{
min-width:0;
}
.home-nomination-review__item-eyebrow{
margin:0 0 6px;
color:#9d85d9;
font-size:11px;
font-weight:900;
letter-spacing:.12em;
text-transform:uppercase;
}
.home-nomination-review__item span{
padding:4px 8px;
border-radius:999px;
@@ -699,26 +797,117 @@ html,body{margin:0;padding:0;background:#160a26;}
font-weight:900;
}
.home-nomination-review__item ul{
.home-nomination-review__links{
display:grid;
gap:9px;
margin:0;
padding-left:18px;
padding:0;
list-style:none;
}
.home-nomination-review__links li{
display:flex;
align-items:flex-start;
gap:10px;
padding:12px 14px;
border:1px solid rgba(139,108,219,.1);
border-radius:16px;
background:rgba(255,255,255,.74);
color:#6f6685;
font-size:13px;
font-weight:700;
line-height:1.5;
overflow-wrap:anywhere;
}
.home-nomination-review__item button{
.home-nomination-review__link-dot{
flex:none;
width:10px;
height:10px;
margin-top:5px;
border-radius:50%;
background:linear-gradient(135deg,#8b6cdb,#e9a8c9);
box-shadow:0 0 0 5px rgba(139,108,219,.12);
}
.home-nomination-review__edit{
justify-self:start;
border:0;
border-radius:11px;
background:#f1ecfb;
border-radius:999px;
background:linear-gradient(135deg,#f4effd,#f0e8fb);
color:#7658c4;
cursor:pointer;
font-family:'Outfit',sans-serif;
font-size:13px;
font-weight:900;
padding:9px 13px;
padding:10px 14px;
box-shadow:0 10px 22px rgba(139,108,219,.1);
transition:transform .16s ease,box-shadow .16s ease,background .16s ease,color .16s ease;
}
.home-nomination-review__edit:hover{
background:linear-gradient(135deg,#8b6cdb,#7355c8);
color:#fff;
box-shadow:0 14px 28px rgba(139,108,219,.22);
transform:translateY(-1px);
}
.home-nomination-review__empty{
display:grid;
place-items:center;
gap:16px;
min-height:240px;
padding:34px 24px;
border:1px dashed rgba(139,108,219,.32);
border-radius:28px;
background:
radial-gradient(circle at top,rgba(231,177,62,.16),transparent 28%),
linear-gradient(145deg,rgba(255,255,255,.92),rgba(247,240,255,.9));
box-shadow:inset 0 1px 0 rgba(255,255,255,.78);
text-align:center;
}
.home-nomination-review__empty-icon{
display:grid;
place-items:center;
width:62px;
height:62px;
border-radius:22px;
background:linear-gradient(135deg,#8b6cdb,#7355c8);
color:#fff2bd;
font-size:24px;
box-shadow:0 18px 38px rgba(139,108,219,.22);
}
.home-nomination-review__empty-copy{
display:grid;
gap:6px;
max-width:420px;
}
.home-nomination-review__empty-eyebrow{
margin:0;
color:#9d85d9;
font-size:11px;
font-weight:900;
letter-spacing:.16em;
text-transform:uppercase;
}
.home-nomination-review__empty-copy h5{
margin:0;
color:#3f3556;
font-family:'Fredoka',sans-serif;
font-size:28px;
line-height:1.05;
}
.home-nomination-review__empty-copy p{
margin:0;
color:#8a8398;
font-size:14px;
font-weight:700;
line-height:1.6;
}
.home-wizard-footer{
@@ -1245,133 +1434,152 @@ html,body{margin:0;padding:0;background:#160a26;}
contain:paint;
}
.home-vote-card__burst-flare,
.home-vote-card__burst-ring{
.home-vote-card__sky-glow,
.home-vote-card__sky-sweep,
.home-vote-card__sky-star{
position:absolute;
left:50%;
top:36%;
opacity:0;
pointer-events:none;
transform:translate(-50%,-50%) scale(.2);
will-change:transform,opacity;
}
.home-vote-card__burst-flare{
width:124px;
height:124px;
border-radius:50%;
.home-vote-card__sky-glow{
inset:-1px;
border-radius:inherit;
opacity:0;
background:
radial-gradient(circle,rgba(255,255,255,.92) 0 9%,rgba(255,210,122,.55) 26%,rgba(233,168,201,.24) 48%,transparent 72%);
filter:blur(1.2px);
animation:voteBurstFlare .9s cubic-bezier(.2,.78,.22,1) forwards;
radial-gradient(circle at 24% 22%,rgba(255,242,189,.46),transparent 12%),
radial-gradient(circle at 72% 28%,rgba(201,177,255,.4),transparent 15%),
radial-gradient(circle at 44% 76%,rgba(233,168,201,.26),transparent 18%),
linear-gradient(135deg,rgba(37,20,72,.24),rgba(139,108,219,.18) 44%,rgba(255,210,122,.12));
box-shadow:inset 0 0 0 1px rgba(255,242,189,.28),0 18px 46px rgba(78,46,139,.18);
animation:voteSkyGlow 1.05s cubic-bezier(.22,.8,.24,1) forwards;
}
.home-vote-card__burst-ring{
width:82px;
height:82px;
border:1.5px solid rgba(255,242,189,.74);
border-radius:50%;
box-shadow:0 0 16px rgba(255,210,122,.34),0 0 28px rgba(201,177,255,.18);
animation:voteBurstRing .92s cubic-bezier(.2,.78,.22,1) .02s forwards;
}
.home-vote-card__burst-trail{
position:absolute;
left:50%;
top:36%;
width:86px;
height:5px;
.home-vote-card__sky-sweep{
left:-18%;
right:-18%;
top:4%;
height:92%;
border-radius:999px;
opacity:0;
pointer-events:none;
transform-origin:left center;
transform:translate3d(0,-50%,0) rotate(var(--trail-rotate)) scaleX(.12);
background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,242,189,.92),rgba(201,177,255,.42),rgba(255,255,255,0));
box-shadow:0 0 14px rgba(255,210,122,.42),0 0 22px rgba(139,108,219,.16);
will-change:transform,opacity;
background:
linear-gradient(90deg,transparent 0%,rgba(255,210,122,.16) 18%,rgba(255,143,192,.2) 34%,rgba(201,177,255,.24) 50%,rgba(122,215,255,.18) 66%,rgba(255,242,189,.2) 82%,transparent 100%),
radial-gradient(ellipse at center,rgba(255,255,255,.36),transparent 62%);
filter:blur(14px);
transform-origin:center;
animation:voteSkySweep 1.45s cubic-bezier(.18,.84,.22,1) .05s forwards;
}
.home-vote-card__burst-trail--left{
--trail-rotate:-150deg;
--trail-x:-20px;
--trail-y:-13px;
animation:voteTrail .58s cubic-bezier(.22,.76,.24,1) .06s forwards;
}
.home-vote-card__burst-trail--right{
--trail-rotate:-36deg;
--trail-x:16px;
--trail-y:-13px;
animation:voteTrail .56s cubic-bezier(.22,.76,.24,1) .08s forwards;
}
.home-vote-card__burst-trail--up{
--trail-rotate:-90deg;
--trail-x:0px;
--trail-y:-20px;
width:96px;
animation:voteTrail .62s cubic-bezier(.22,.76,.24,1) .04s forwards;
}
.home-vote-card__burst-star{
position:absolute;
.home-vote-card__sky-star{
left:50%;
top:36%;
color:#ffd27a;
top:50%;
color:#fff2bd;
font-family:'Fredoka',sans-serif;
line-height:1;
opacity:0;
text-shadow:0 0 8px rgba(255,255,255,.85),0 0 14px rgba(255,210,122,.78),0 0 24px rgba(139,108,219,.28);
transform:translate(-50%,-50%) scale(.2);
text-shadow:0 0 8px rgba(255,255,255,.86),0 0 18px rgba(255,210,122,.72),0 0 28px rgba(139,108,219,.22);
transform:translate(-50%,-50%) scale(.58) rotate(var(--star-rotate));
will-change:transform,opacity;
animation:voteSkyStar var(--star-duration) cubic-bezier(.2,.76,.2,1) var(--star-delay) forwards;
}
.home-vote-card__burst-star--up{
color:#fff2bd;
font-size:36px;
animation:voteStarUp .96s cubic-bezier(.2,.78,.22,1) .02s forwards;
.home-vote-card__sky-star--one{
--star-rotate:-12deg;
--star-duration:.92s;
--star-delay:.1s;
left:24%;
top:30%;
color:#ffd27a;
font-size:19px;
}
.home-vote-card__burst-star--left{
color:#e9a8c9;
font-size:24px;
animation:voteStarLeft .9s cubic-bezier(.2,.78,.22,1) .06s forwards;
}
.home-vote-card__burst-star--right{
color:#c9b1ff;
font-size:21px;
animation:voteStarRight .88s cubic-bezier(.2,.78,.22,1) .08s forwards;
}
.home-vote-card__burst-star--left-small{
--star-dx-start:-18px;
--star-dy-start:-14px;
--star-dx-end:-92px;
--star-dy-end:-2px;
color:#fff2bd;
.home-vote-card__sky-star--two{
--star-rotate:18deg;
--star-duration:.98s;
--star-delay:.18s;
left:42%;
top:22%;
color:#fff7d8;
font-size:13px;
animation:voteStarDriftSmall .68s cubic-bezier(.2,.78,.22,1) .12s forwards;
}
.home-vote-card__burst-star--right-small{
--star-dx-start:18px;
--star-dy-start:-12px;
--star-dx-end:88px;
--star-dy-end:-8px;
.home-vote-card__sky-star--three{
--star-rotate:0deg;
--star-duration:1.04s;
--star-delay:.06s;
left:58%;
top:38%;
color:#fff;
font-size:12px;
}
.home-vote-card__sky-star--four{
--star-rotate:22deg;
--star-duration:.96s;
--star-delay:.24s;
left:72%;
top:26%;
color:#c9b1ff;
font-size:17px;
}
.home-vote-card__sky-star--five{
--star-rotate:-18deg;
--star-duration:.88s;
--star-delay:.3s;
left:80%;
top:54%;
color:#e9a8c9;
font-size:12px;
animation:voteStarDriftSmall .66s cubic-bezier(.2,.78,.22,1) .15s forwards;
}
.home-vote-card__burst-star--top-small{
--star-dx-start:4px;
--star-dy-start:-18px;
--star-dx-end:34px;
--star-dy-end:-88px;
.home-vote-card__sky-star--six{
--star-rotate:12deg;
--star-duration:1.02s;
--star-delay:.2s;
left:18%;
top:58%;
color:#fff7d8;
font-size:11px;
}
.home-vote-card__sky-star--seven{
--star-rotate:-24deg;
--star-duration:1.08s;
--star-delay:.34s;
left:34%;
top:68%;
color:#c9b1ff;
font-size:14px;
animation:voteStarDriftSmall .72s cubic-bezier(.2,.78,.22,1) .18s forwards;
font-size:15px;
}
.home-vote-card__sky-star--eight{
--star-rotate:8deg;
--star-duration:.96s;
--star-delay:.16s;
left:51%;
top:64%;
color:#ffd27a;
font-size:10px;
}
.home-vote-card__sky-star--nine{
--star-rotate:-8deg;
--star-duration:1.1s;
--star-delay:.28s;
left:66%;
top:70%;
color:#fff;
font-size:13px;
}
.home-vote-card__sky-star--ten{
--star-rotate:26deg;
--star-duration:.94s;
--star-delay:.4s;
left:88%;
top:38%;
color:#ffb8d8;
font-size:11px;
}
.home-vote-picker__empty{
@@ -1763,13 +1971,43 @@ html,body{margin:0;padding:0;background:#160a26;}
.home-stream-band__lead{
align-items:flex-start!important;
justify-content:flex-start!important;
gap:14px!important;
width:100%!important;
}
.home-stream-band__copy{
align-items:flex-start!important;
}
.home-stream-band__eyebrow,
.home-stream-band__meta{
text-align:left!important;
}
.home-stream-band__actions{
width:100%!important;
align-items:stretch!important;
justify-content:center!important;
margin-left:0!important;
}
.home-stream-band__headline-row{
justify-content:flex-start!important;
gap:8px!important;
}
.home-stream-band__title{
white-space:normal!important;
}
.home-stream-band__countdown-chip{
flex-wrap:wrap!important;
row-gap:6px!important;
}
.home-stream-band__countdown-time{
font-size:clamp(22px,7vw,28px)!important;
}
.home-stream-band__actions > a,
@@ -1821,7 +2059,7 @@ html,body{margin:0;padding:0;background:#160a26;}
grid-template-columns:1fr!important;
}
[data-cat-grid] > div{
[data-cat-grid] > *{
border-radius:18px!important;
padding:22px 20px!important;
}
@@ -2016,6 +2254,15 @@ html,body{margin:0;padding:0;background:#160a26;}
flex-direction:column!important;
}
.home-nomination-review__stats{
grid-template-columns:1fr!important;
}
.home-nomination-review__item-header{
flex-direction:column!important;
align-items:flex-start!important;
}
.home-nomination-field__header{
align-items:flex-start!important;
flex-direction:column!important;
@@ -35,6 +35,5 @@ export interface HomeLandingCategory {
icon: string
description: string
maxNomineesPerUser: number
candidateCount: number
subcategoryCount: number
subcategoryNames: string[]
}
@@ -52,16 +52,9 @@ export interface HomeArchiveYearItem {
export interface HomeArchiveWinnerItem {
category: string
subcategory: string
name: string
handle: string
platform: string
url: string
clipUrl: string | null
clipTitle: string
clipPlatform: string
clipEmbedUrl: string | null
clipEmbedTitle: string
hasClip: boolean
}
export interface HomeSelectedArchive {
@@ -12,15 +12,6 @@ export function useHomeArchivePresentation(store: AwardsStore, archiveYear: Ref<
knownYears.set(entry.year, entry.winnerCount)
}
for (const entry of store.overview.winnersPreview) {
if (!knownYears.has(entry.year)) {
knownYears.set(
entry.year,
store.overview.winnersPreview.filter((winner) => winner.year === entry.year).length,
)
}
}
if (store.archive.items.length > 0) {
knownYears.set(store.archive.year, store.archive.items.length)
}
@@ -31,7 +22,7 @@ export function useHomeArchivePresentation(store: AwardsStore, archiveYear: Ref<
year,
label: String(year),
winnerCount: year === store.archive.year ? store.archive.items.length : winnerCount,
winners: year === store.archive.year ? store.archive.items : store.overview.winnersPreview.filter((entry) => entry.year === year),
winners: year === store.archive.year ? store.archive.items : [],
active: archiveYear.value === year,
}))
})
@@ -41,7 +32,15 @@ export function useHomeArchivePresentation(store: AwardsStore, archiveYear: Ref<
winners: store.archive.items.map((winner) => toArchiveWinnerItem(winner)),
}))
const winnerShowcase = computed(() => selectedArchive.value.winners.slice(0, 4))
const winnerShowcaseYear = computed(() =>
store.overview.winnersPreview[0]?.year ?? selectedArchive.value.year,
)
const winnerShowcase = computed(() => {
const publishedWinners = store.overview.winnersPreview.map((winner) => toWinnerShowcaseItem(winner))
const archiveFallback = store.archive.items.map((winner) => toWinnerShowcaseItem(winner))
return (publishedWinners.length > 0 ? publishedWinners : archiveFallback).slice(0, 8)
})
function archiveYearButtonStyle(active: boolean) {
return active
@@ -50,6 +49,21 @@ export function useHomeArchivePresentation(store: AwardsStore, archiveYear: Ref<
}
function toArchiveWinnerItem(winner: {
categoryGroup?: string | null
category: string
winnerName: string
winnerUrl: string
}) {
return {
category: winner.category,
subcategory: winner.categoryGroup?.trim() || winner.category,
name: winner.winnerName,
url: winner.winnerUrl,
}
}
function toWinnerShowcaseItem(winner: {
categoryGroup?: string | null
category: string
winnerName: string
winnerSlug: string
@@ -66,7 +80,9 @@ export function useHomeArchivePresentation(store: AwardsStore, archiveYear: Ref<
const clipEmbed = clipUrl && winner.clipEmbedStatus !== 'link_only' && winner.clipEmbedStatus !== 'blocked'
? buildClipEmbed(clipUrl)
: null
return {
categoryGroup: winner.categoryGroup?.trim() || winner.category,
category: winner.category,
name: winner.winnerName,
handle: winner.winnerSlug,
@@ -114,9 +130,9 @@ export function useHomeArchivePresentation(store: AwardsStore, archiveYear: Ref<
return {
archiveYears,
selectedArchive,
winnerShowcaseYear,
winnerShowcase,
archiveYearButtonStyle,
winnerPlatformKey,
winnerPlatformLabel,
winnerPlatformStyle,
}
@@ -24,7 +24,10 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
const faqItems = computed(() =>
(store.overview.faq ?? []).filter((item) => item?.question && item.answer),
)
const publicStreamUrl = computed(() => store.overview.showStreamUrl || 'https://twitch.tv/jayuhime')
const publicStreamUrl = computed(() =>
store.overview.siteContent.streamBanner.liveButtonUrl
|| 'https://twitch.tv/jayuhime',
)
const showDate = computed(() => store.overview.showDate)
const showStartsAt = computed(() => store.overview.showStartsAt || '20:00:00')
const currentYear = computed(() => store.overview.year ? String(store.overview.year) : '')
@@ -42,7 +45,9 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
if (store.overview.featuredCategories.length > 0) {
return store.overview.featuredCategories.map((category, index) => {
const matchingSubcategories = displayCategories.value.filter((item) => item.groupName === category.groupName)
const candidateCount = matchingSubcategories.reduce((sum, item) => sum + item.candidates.length, 0)
const subcategoryNames = matchingSubcategories
.map((item) => item.name.trim())
.filter(Boolean)
return {
id: String(category.id),
groupName: category.groupName,
@@ -50,8 +55,7 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦',
description: category.description,
maxNomineesPerUser: category.maxNomineesPerUser,
candidateCount,
subcategoryCount: matchingSubcategories.length,
subcategoryNames,
}
})
}
@@ -65,6 +69,9 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
seenGroups.add(groupKey)
const matchingSubcategories = displayCategories.value.filter((item) => item.groupName.trim().toLowerCase() === groupKey)
const subcategoryNames = matchingSubcategories
.map((item) => item.name.trim())
.filter(Boolean)
return [{
id: category.id,
groupName: category.groupName,
@@ -72,8 +79,7 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦',
description: '',
maxNomineesPerUser: category.maxNomineesPerUser,
candidateCount: matchingSubcategories.reduce((sum, item) => sum + item.candidates.length, 0),
subcategoryCount: matchingSubcategories.length,
subcategoryNames,
}]
})
})
@@ -83,8 +89,6 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
const bootstrapArchiveYears = computed<Array<{ year: number }>>(() =>
(store.overview.archiveYears ?? []).length > 0
? (store.overview.archiveYears ?? []).map((entry) => ({ year: entry.year }))
: store.overview.winnersPreview.length > 0
? [...new Set(store.overview.winnersPreview.map((winner) => winner.year))].map((year) => ({ year }))
: [{ year: store.overview.year - 1 }],
)
@@ -95,9 +95,9 @@ export function useHomeLandingState() {
const {
archiveYears,
selectedArchive,
winnerShowcaseYear,
winnerShowcase,
archiveYearButtonStyle,
winnerPlatformKey,
winnerPlatformLabel,
winnerPlatformStyle,
} = useHomeArchivePresentation(store, archiveYear)
@@ -120,8 +120,10 @@ export function useHomeLandingState() {
phasePrimaryActionStyle,
streamEyebrow,
streamTitle,
streamText,
streamMeta,
streamLockedLabel,
streamButtonLabel,
streamButtonUrl,
streamLockedTitle,
statOneValue,
statOneLabel,
@@ -140,6 +142,7 @@ export function useHomeLandingState() {
previewPhase,
landingCategories,
candidateCount,
siteContent,
publicStreamUrl,
showDate,
showStartsAt,
@@ -299,8 +302,10 @@ export function useHomeLandingState() {
phasePrimaryActionStyle,
streamEyebrow,
streamTitle,
streamText,
streamMeta,
streamLockedLabel,
streamButtonLabel,
streamButtonUrl,
streamLockedTitle,
statOneValue,
statOneLabel,
@@ -328,6 +333,7 @@ export function useHomeLandingState() {
clipSubmissionDisabledMessage,
archiveYears,
selectedArchive,
winnerShowcaseYear,
winnerShowcase,
activeCat,
submitted,
@@ -365,7 +371,6 @@ export function useHomeLandingState() {
onCloseArchive,
archiveYearButtonStyle,
winnerPlatformStyle,
winnerPlatformKey,
winnerPlatformLabel,
privacyModalStop: stop,
accountModalStop: stop,
@@ -86,6 +86,17 @@ export function useHomeLandingViewEffects(params: UseHomeLandingViewEffectsParam
let timer: ReturnType<typeof setInterval> | null = null
let landingLoaderTimer: ReturnType<typeof setTimeout> | null = null
function syncScrollLock() {
const shouldLock = landingLoaderVisible.value
|| modalOpen.value
|| accountModalOpen.value
|| privacyModalOpen.value
|| archiveModalOpen.value
document.documentElement.style.overflow = shouldLock ? 'hidden' : ''
document.body.style.overflow = shouldLock ? 'hidden' : ''
}
function setRootEl(element: Element | ComponentPublicInstance | null) {
rootEl.value = element instanceof HTMLElement ? element : null
}
@@ -170,7 +181,7 @@ export function useHomeLandingViewEffects(params: UseHomeLandingViewEffectsParam
const showCountdown = split(showCompleted ? 0 : showStarted ? now - showTarget : showTarget - now)
setText(countdownRefs.labelEl.value, activeTarget.label)
setText(countdownRefs.streamLabelEl.value, showCompleted ? 'Award abgeschlossen' : showStarted ? 'Stream läuft seit' : 'Finale startet in')
setText(countdownRefs.streamLabelEl.value, showCompleted ? 'Award abgeschlossen' : showStarted ? 'Live seit' : 'Finale in')
setText(countdownRefs.dEl.value, pad(activeCountdown.d))
setText(countdownRefs.hEl.value, pad(activeCountdown.h))
setText(countdownRefs.mEl.value, pad(activeCountdown.m))
@@ -209,8 +220,8 @@ export function useHomeLandingViewEffects(params: UseHomeLandingViewEffectsParam
return 'show'
}
watch([modalOpen, accountModalOpen, privacyModalOpen, archiveModalOpen], () => {
document.body.style.overflow = modalOpen.value || accountModalOpen.value || privacyModalOpen.value || archiveModalOpen.value ? 'hidden' : ''
watch([landingLoaderVisible, modalOpen, accountModalOpen, privacyModalOpen, archiveModalOpen], () => {
syncScrollLock()
nextTick(setupDom)
updateStickyCountdownVisibility()
})
@@ -220,6 +231,7 @@ export function useHomeLandingViewEffects(params: UseHomeLandingViewEffectsParam
})
onMounted(async () => {
syncScrollLock()
if (!authStore.hydrated) {
await authStore.hydrate()
}
@@ -244,6 +256,7 @@ export function useHomeLandingViewEffects(params: UseHomeLandingViewEffectsParam
})
onBeforeUnmount(() => {
document.documentElement.style.overflow = ''
document.body.style.overflow = ''
window.removeEventListener('scroll', updateStickyCountdownVisibility)
window.removeEventListener('resize', updateStickyCountdownVisibility)
@@ -226,7 +226,7 @@ export function useHomeParticipationSubmitActions(params: {
candidateId: selectedCreator?.id ?? null,
twitchUserId: session.twitchUserId,
clipUrl: url,
title: clipContext.description || `Clip fuer ${selectedCreator?.displayName ?? 'die Award-Show'}`,
title: clipContext.description || `Clip für ${selectedCreator?.displayName ?? 'die Award-Show'}`,
creator: selectedCreator?.displayName ?? fallbackCreatorName.value,
})
submitted.value = true
@@ -1,11 +1,14 @@
import { computed, onBeforeUnmount, ref, type ComputedRef, type Ref } from 'vue'
import { plainTextContentFromStorage } from '../../lib/privacyContent'
import type { OverviewResponse } from '../../types/awards'
import type { HomeLandingCategory, HomePreviewPhase } from './homeLandingTypes'
export function useHomePhasePresentation(params: {
previewPhase: Ref<HomePreviewPhase>
landingCategories: ComputedRef<HomeLandingCategory[]>
candidateCount: ComputedRef<number>
siteContent: ComputedRef<OverviewResponse['siteContent']>
publicStreamUrl: ComputedRef<string>
showDate: ComputedRef<string>
showStartsAt: ComputedRef<string>
@@ -17,6 +20,7 @@ export function useHomePhasePresentation(params: {
previewPhase,
landingCategories,
candidateCount,
siteContent,
publicStreamUrl,
showDate,
showStartsAt,
@@ -39,8 +43,10 @@ export function useHomePhasePresentation(params: {
const showStartMs = computed(() => parseShowStartMs(showDate.value, showStartsAt.value))
const streamLive = computed(() => showPhase.value && !Number.isNaN(showStartMs.value) && currentTimestamp.value >= showStartMs.value)
const streamLocked = computed(() => !streamLive.value)
const streamBanner = computed(() => siteContent.value.streamBanner ?? defaultStreamBannerContent())
const useCompletedStreamBanner = computed(() => completedPhase.value && streamBanner.value.useCompletedContent)
const phaseCardTitle = computed(() => completedPhase.value ? 'Award-Jahr abgeschlossen' : nominationPhase.value ? 'Community Nominierung' : votingPhase.value ? 'Community Voting' : preparationPhase.value ? 'Aufbereitung bis zur Show' : 'Award Show Live')
const phaseCardDescription = computed(() => completedPhase.value ? 'Die grosse Award-Show ist beendet. Das Jahr ist abgeschlossen und alle Teilnahme-Aktionen sind gesperrt.' : nominationPhase.value ? 'Die Nominierungsphase laeuft gerade. Reiche pro Kategorie die erlaubte Anzahl an Stream- oder Kanal-Links ein.' : votingPhase.value ? 'Die Nominierungsphase ist abgeschlossen.\nJetzt liegt es an dir: Stimme fuer deine Favoriten!' : preparationPhase.value ? 'Das Voting ist abgeschlossen. Das Team bereitet Clips, Ablauf und Gewinner-Momente fuer die Show vor.' : 'Die Award-Show laeuft jetzt live. Zeit fuer Buehne, Gewinner:innen und ganz viel Glitzer.')
const phaseCardDescription = computed(() => completedPhase.value ? 'Die große Award-Show ist beendet. Das Jahr ist abgeschlossen und alle Teilnahme-Aktionen sind gesperrt.' : nominationPhase.value ? 'Die Nominierungsphase läuft gerade. Reiche pro Kategorie die erlaubte Anzahl an Stream- oder Kanal-Links ein.' : votingPhase.value ? 'Die Nominierungsphase ist abgeschlossen.\nJetzt liegt es an dir: Stimme für deine Favoriten!' : preparationPhase.value ? 'Das Voting ist abgeschlossen. Das Team bereitet Clips, Ablauf und Gewinner-Momente für die Show vor.' : 'Die Award-Show läuft jetzt live. Zeit für Bühne, Gewinner:innen und ganz viel Glitzer.')
const phaseCardRange = computed(() => completedPhase.value ? `Finale abgeschlossen · ${formatShowDate()}` : nominationPhase.value ? `Nominierungszeitraum · ${formatRange('nomination')}` : votingPhase.value ? `Voting-Zeitraum · ${formatRange('voting')}` : preparationPhase.value ? `Aufbereitungszeit · ${formatRange('preparation')}` : `Live · ${formatShowDate()} · ${formatTimeLabel(showStartsAt.value)} Uhr`)
const phaseStatusLabel = computed(() => completedPhase.value ? 'ABGESCHLOSSEN' : streamLive.value ? 'LIVE' : showPhase.value ? 'STARTET BALD' : preparationPhase.value ? 'IN AUFBEREITUNG' : 'AKTIV')
const phaseStatusStyle = computed(() => completedPhase.value ? 'display:inline-flex;align-items:center;gap:5px;padding:4px 11px;border-radius:999px;background:#f1ecfb;color:#8b6cdb;font-size:11px;font-weight:700;letter-spacing:.5px;' : showPhase.value ? 'display:inline-flex;align-items:center;gap:5px;padding:4px 11px;border-radius:999px;background:#ffe5ec;color:#ec3b5a;font-size:11px;font-weight:700;letter-spacing:.5px;' : preparationPhase.value ? 'display:inline-flex;align-items:center;gap:5px;padding:4px 11px;border-radius:999px;background:#fff1d6;color:#b7791f;font-size:11px;font-weight:700;letter-spacing:.5px;' : 'display:inline-flex;align-items:center;gap:5px;padding:4px 11px;border-radius:999px;background:#e3f7ec;color:#1f9d5a;font-size:11px;font-weight:700;letter-spacing:.5px;')
@@ -49,10 +55,30 @@ export function useHomePhasePresentation(params: {
const phasePrimaryActionStyle = computed(() => phasePrimaryDisabled.value
? 'display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:15px 18px;border-radius:13px;background:#eee7f8;color:#9b8abf;border:none;text-decoration:none;font-family:\'Outfit\',sans-serif;font-weight:600;font-size:15px;box-shadow:none;cursor:not-allowed;'
: 'display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:15px 18px;border-radius:13px;background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;border:none;text-decoration:none;font-family:\'Outfit\',sans-serif;font-weight:600;font-size:15px;box-shadow:0 10px 22px rgba(124,86,196,.32);cursor:pointer;')
const streamEyebrow = computed(() => 'Das grosse Finale')
const streamTitle = computed(() => 'Award-Show Countdown')
const streamMeta = computed(() => `Finale am ${formatShowDate()} · ${formatTimeLabel(showStartsAt.value)} Uhr`)
const streamLockedLabel = computed(() => completedPhase.value ? 'Award abgeschlossen' : 'Stream noch gesperrt')
const streamEyebrow = computed(() => useCompletedStreamBanner.value
? firstText(streamBanner.value.completedEyebrow, 'Danke fürs Mitfiebern')
: firstText(streamBanner.value.eyebrow, 'Das große Finale'))
const streamTitle = computed(() => useCompletedStreamBanner.value
? firstText(streamBanner.value.completedTitle, 'Award-Show abgeschlossen')
: firstText(streamBanner.value.title, 'Award-Show Finale'))
const streamText = computed(() => useCompletedStreamBanner.value
? firstText(streamBanner.value.completedText, 'Die große Award-Show ist vorbei. Danke an alle, die live dabei waren.')
: firstText(streamBanner.value.text, 'Countdown bis zur großen Live-Show'))
const streamMeta = computed(() => completedPhase.value ? `Finale abgeschlossen · ${formatShowDate()}` : `Finale am ${formatShowDate()} · ${formatTimeLabel(showStartsAt.value)} Uhr`)
const streamButtonLabel = computed(() => {
if (useCompletedStreamBanner.value) {
return firstText(streamBanner.value.completedButtonLabel, 'Highlights ansehen')
}
return streamLive.value
? firstText(streamBanner.value.liveButtonLabel, 'Jetzt live · Zum Stream')
: firstText(streamBanner.value.lockedButtonLabel, 'Stream noch gesperrt')
})
const streamButtonUrl = computed(() => useCompletedStreamBanner.value
? (streamBanner.value.completedButtonUrl ?? '').trim()
: streamLive.value
? (streamBanner.value.liveButtonUrl ?? '').trim()
: '')
const streamLockedTitle = computed(() => completedPhase.value ? 'Die Award-Show ist beendet' : 'Verfügbar, sobald die Show startet')
const statOneValue = computed(() => completedPhase.value ? 'DONE' : streamLive.value ? 'LIVE' : String(candidateCount.value))
const statOneLabel = computed(() => completedPhase.value ? 'Jahr-Status' : showPhase.value ? 'Show-Status' : 'Kandidat:innen')
@@ -101,8 +127,10 @@ export function useHomePhasePresentation(params: {
phasePrimaryActionStyle,
streamEyebrow,
streamTitle,
streamText,
streamMeta,
streamLockedLabel,
streamButtonLabel,
streamButtonUrl,
streamLockedTitle,
statOneValue,
statOneLabel,
@@ -134,3 +162,25 @@ function normalizeTime(value: string) {
function formatTimeLabel(value: string) {
return normalizeTime(value).slice(0, 5)
}
function firstText(value: string | null | undefined, fallback: string) {
const trimmed = plainTextContentFromStorage(value ?? '')
return trimmed || fallback
}
function defaultStreamBannerContent(): OverviewResponse['siteContent']['streamBanner'] {
return {
eyebrow: 'Das große Finale',
title: 'Award-Show Finale',
text: 'Countdown bis zur großen Live-Show',
liveButtonLabel: 'Jetzt live · Zum Stream',
liveButtonUrl: '',
lockedButtonLabel: 'Stream noch gesperrt',
useCompletedContent: false,
completedEyebrow: 'Danke fürs Mitfiebern',
completedTitle: 'Award-Show abgeschlossen',
completedText: 'Die große Award-Show ist vorbei. Danke an alle, die live dabei waren.',
completedButtonLabel: 'Highlights ansehen',
completedButtonUrl: '',
}
}