import { computed, type ComputedRef, type Ref } from 'vue' import { buildClipEmbed } from '../../lib/clipEmbeds' import { useAuthStore } from '../../stores/auth' import { useAwardsStore } from '../../stores/awards' import type { HomeDisplayCategory, HomeInteractionModalKind, HomeLandingCategory } from './homeLandingTypes' type AwardsStore = ReturnType type AuthStore = ReturnType type HomeTimelineKey = 'nomination' | 'voting' | 'preparation' | 'show' const CATEGORY_ICONS = ['✦', '★', '✧', '♬', '⚔', '☻', '♡', '✶'] as const export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore: AuthStore) { const role = computed<'guest' | 'user' | 'admin'>(() => { if (!authStore.session) return 'guest' return authStore.isAdmin ? 'admin' : 'user' }) const isGuest = computed(() => role.value === 'guest') const isUser = computed(() => role.value === 'user') const isAdmin = computed(() => role.value === 'admin') const twitchUser = computed(() => authStore.session?.twitchUserId ?? 'local_user') const siteContent = computed(() => store.overview.siteContent) const faqItems = computed(() => (store.overview.faq ?? []).filter((item) => item?.question && item.answer), ) const publicStreamUrl = computed(() => store.overview.showStreamUrl || '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) : '') const displayCategories = computed(() => store.categories.categories.map((category, index) => ({ id: String(category.id), groupName: category.groupName, name: category.name, icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦', maxNomineesPerUser: category.maxNomineesPerUser, candidates: category.candidates, })), ) const landingCategories = computed(() => { 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) return { id: String(category.id), groupName: category.groupName, name: category.groupName, icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦', description: category.description, maxNomineesPerUser: category.maxNomineesPerUser, candidateCount, subcategoryCount: matchingSubcategories.length, } }) } const seenGroups = new Set() return displayCategories.value.flatMap((category, index) => { const groupKey = category.groupName.trim().toLowerCase() if (seenGroups.has(groupKey)) { return [] } seenGroups.add(groupKey) const matchingSubcategories = displayCategories.value.filter((item) => item.groupName.trim().toLowerCase() === groupKey) return [{ id: category.id, groupName: category.groupName, name: category.groupName, icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦', description: '', maxNomineesPerUser: category.maxNomineesPerUser, candidateCount: matchingSubcategories.reduce((sum, item) => sum + item.candidates.length, 0), subcategoryCount: matchingSubcategories.length, }] }) }) const candidateCount = computed(() => displayCategories.value.reduce((sum, category) => sum + category.candidates.length, 0), ) const bootstrapArchiveYears = computed>(() => (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 }], ) function timelineItem(key: HomeTimelineKey) { return store.overview.timeline.find((entry) => entry.key === key) } function formatRange(key: Exclude) { const item = timelineItem(key) if (!item) return '' return `${formatDateLabel(item.startsAt)} – ${formatDateLabel(item.endsAt)}` } function formatTimelineRange(key: HomeTimelineKey) { const item = timelineItem(key) if (!item) return 'Noch offen' return item.startsAt === item.endsAt ? formatDateLabel(item.startsAt) : `${formatDateLabel(item.startsAt)} – ${formatDateLabel(item.endsAt)}` } function formatShowDate() { return formatDateLabel(store.overview.showDate) } return { role, isGuest, isUser, isAdmin, twitchUser, siteContent, faqItems, publicStreamUrl, showDate, showStartsAt, currentYear, displayCategories, landingCategories, candidateCount, bootstrapArchiveYears, formatRange, formatTimelineRange, formatShowDate, initialsFor, } } export function useHomeModalCandidatePresentation(params: { displayCategories: ComputedRef activeCat: Ref modal: Ref votes: Ref> activeCategory: ComputedRef setCat: (index: number) => void pickNominee: (categoryId: string, index: number) => void }) { const { displayCategories, activeCat, modal, votes, activeCategory, setCat, pickNominee, } = params const catList = computed(() => buildCatList(displayCategories, activeCat, votes, setCat)) const noms = computed(() => { const cat = activeCategory.value const list = cat?.candidates ?? [] const voteMode = modal.value === 'vote' return list.map((candidate, idx) => { const selected = cat ? votes.value[cat.id] === idx : false const clipUrl = candidate.clipUrl?.trim() || null const clipTitle = candidate.clipTitle?.trim() || 'Highlight-Clip ansehen' const clipPlatform = clipUrl ? candidate.clipPlatform?.trim() || candidate.platform : 'Clip fehlt' const clipEmbed = clipUrl && candidate.clipEmbedStatus !== 'link_only' ? buildClipEmbed(clipUrl) : null return { name: candidate.displayName, handle: candidate.channelSlug, url: resolveCandidateUrl(candidate.channelUrl, candidate.channelSlug), platform: candidate.platform, initials: initialsFor(candidate.displayName), clipUrl, clipTitle, clipPlatform, clipEmbedUrl: clipEmbed?.src ?? null, clipEmbedTitle: clipEmbed?.title ?? clipTitle, idx, selected, hasClip: Boolean(clipUrl), showPick: voteMode, onPick: () => cat && pickNominee(cat.id, idx), cardStyle: `display:flex;align-items:center;justify-content:space-between;gap:12px;padding:13px 15px;border-radius:13px;transition:all .15s;border:1.5px solid ${selected ? '#8b6cdb;background:#f6f1fd;' : '#ece4f6;background:#fff;'}`, btnStyle: "flex:none;white-space:nowrap;padding:8px 16px;border-radius:9px;border:none;cursor:pointer;font-family:'Outfit',sans-serif;font-weight:600;font-size:13px;transition:all .15s;" + (selected ? 'background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;' : 'background:#f1ecfb;color:#8b6cdb;'), btnLabel: selected ? 'Auswahl entfernen' : 'Für Clip voten', } }) }) return { catList, noms, } } function resolveCandidateUrl(channelUrl: string | null | undefined, channelSlug: string) { const url = channelUrl?.trim() || channelSlug.trim() if (!isHttpUrl(url) || url === '#') return null return url } function isHttpUrl(value: string) { try { const url = new URL(value) return url.protocol === 'http:' || url.protocol === 'https:' } catch { return false } } function formatDateLabel(value: string) { if (!value) return 'Noch nicht terminiert' const date = new Date(`${value}T00:00:00`) return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' }) } function initialsFor(value: string) { return value .split(/[\s&.-]+/) .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase() ?? '') .join('') } function buildCatList( displayCategories: ComputedRef, activeCat: Ref, votes: Ref>, setCat: (index: number) => void, ) { const baseRow = "display:flex;align-items:center;gap:10px;padding:11px 13px;border-radius:11px;cursor:pointer;font-size:14px;font-weight:600;transition:all .15s;border:1px solid transparent;outline:none;-webkit-tap-highlight-color:transparent;text-align:left;width:100%;background:transparent;font-family:'Outfit',sans-serif;" return displayCategories.value.map((category, index) => { const active = index === activeCat.value const done = votes.value[category.id] != null return { name: category.name, groupName: category.groupName, displayName: `${category.groupName} · ${category.name}`, icon: category.icon, idx: index, done, candidateCount: category.candidates.length, onClick: () => setCat(index), rowStyle: baseRow + (active ? 'background:#f1ecfb;border-color:#d8c9f2;color:#5f44ad;' : 'border-color:transparent;color:#6f6685;'), iconStyle: "flex:none;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;font-size:14px;" + (active ? 'background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;' : 'background:#efe7fb;color:#9a7fce;'), checkStyle: `flex:none;margin-left:auto;color:#1f9d5a;font-size:14px;font-weight:700;display:${done ? 'inline' : 'none'};`, } }) }