Files
vtuber-awards/frontend/src/stores/awards.ts
T
2026-06-17 12:01:57 +02:00

306 lines
11 KiB
TypeScript

import { defineStore } from 'pinia'
import { api } from '../lib/api'
import type {
AdminDashboardResponse,
AdminSeasonDetailResponse,
ApproveNominationPayload,
AdminSeasonListItem,
CreateNominationPayload,
CreateVotePayload,
OverviewResponse,
SeasonCategoriesResponse,
UpdateSeasonPayload,
UpsertCandidatePayload,
UpsertCategoryPayload,
WinnerArchiveResponse,
} from '../types/awards'
const fallbackOverview: OverviewResponse = {
seasonId: 1,
year: 2026,
title: 'VTuber Star Awards 2026',
showDate: '2026-01-24',
currentPhase: 'Community Voting',
isCommunityOnly: true,
loginProvider: 'Twitch',
timeline: [
{ key: 'nomination', title: 'Nominierung', startsAt: '2026-05-01', endsAt: '2026-05-31', state: 'done' },
{ key: 'voting', title: 'Voting', startsAt: '2026-06-01', endsAt: '2026-06-30', state: 'active' },
{ key: 'review', title: 'Auswertung', startsAt: '2026-07-01', endsAt: '2026-07-10', state: 'upcoming' },
{ key: 'show', title: 'Award Show', startsAt: '2026-07-20', endsAt: '2026-07-20', state: 'upcoming' },
],
featuredCategories: [
{ id: 1, groupName: 'Main Awards', name: 'VTuber des Jahres', description: 'Die groesste Auszeichnung des Jahres.', maxNomineesPerUser: 3 },
{ id: 2, groupName: 'Performance', name: 'Bestes Live Event', description: 'Events, Konzerte und Showformate.', maxNomineesPerUser: 3 },
{ id: 3, groupName: 'Clips & Highlights', name: 'Clip des Jahres', description: 'Der lustigste oder emotionalste Clip.', maxNomineesPerUser: 3 },
],
winnersPreview: [
{ year: 2025, category: 'VTuber des Jahres', winnerName: 'Hoshimi Miyu', winnerSlug: '@hoshimimiyu' },
{ year: 2025, category: 'Bestes Live Event', winnerName: 'Kurainu 3D Live', winnerSlug: '@kurainu' },
{ year: 2024, category: 'Clip des Jahres', winnerName: 'Pyonkichi Kingdom', winnerSlug: '@pyonkichikingdom' },
],
faq: [
{ question: 'Wer kann nominieren und voten?', answer: 'Jede Person mit Twitch Login. Das Konto wird beim ersten Login implizit erstellt.' },
{ question: 'Wie werden Gewinner bestimmt?', answer: 'Aktuell rein community-basiert. Eine Mischlogik kann spaeter aktiviert werden.' },
{ question: 'Wer verwaltet Kategorien und Unterkategorien?', answer: 'Das Team pflegt diese pro Jahr im Admin-Bereich.' },
],
}
const fallbackCategories: SeasonCategoriesResponse = {
seasonId: 1,
year: 2026,
categories: [
{
id: 1,
name: 'VTuber des Jahres',
groupName: 'Main Awards',
description: 'Die Hauptkategorie fuer die praegendste Creator-Praesenz des Jahres.',
maxNomineesPerUser: 3,
candidates: [
{ id: 1, displayName: 'Hoshimi Miyu', channelSlug: '@hoshimimiyu', platform: 'Twitch' },
{ id: 2, displayName: 'Kurainu', channelSlug: '@kurainu', platform: 'Twitch' },
{ id: 3, displayName: 'Shiro Ch.', channelSlug: '@shiroch', platform: 'Twitch' },
],
},
{
id: 2,
name: 'Bestes Live Event',
groupName: 'Performance',
description: 'Konzerte, Sonderformate und grosse Community-Shows.',
maxNomineesPerUser: 3,
candidates: [
{ id: 4, displayName: 'Kurainu 3D Live', channelSlug: '@kurainu', platform: 'Twitch' },
{ id: 5, displayName: 'Aoi Sakura Showcase', channelSlug: '@aoisakura', platform: 'YouTube' },
],
},
],
}
const fallbackArchive: WinnerArchiveResponse = {
year: 2025,
items: [
{ category: 'VTuber des Jahres', winnerName: 'Hoshimi Miyu', winnerSlug: '@hoshimimiyu' },
{ category: 'Bestes Live Event', winnerName: 'Kurainu 3D Live', winnerSlug: '@kurainu' },
{ category: 'Clip des Jahres', winnerName: 'Pyonkichi Kingdom', winnerSlug: '@pyonkichikingdom' },
],
}
const fallbackAdmin: AdminDashboardResponse = {
metrics: [
{ label: 'Nominierungen', value: 12341, note: '+12.4% vs. gestern' },
{ label: 'Votes', value: 587231, note: '+8.7% vs. gestern' },
{ label: 'Kategorien', value: 28, note: 'aktiv im Jahr 2026' },
{ label: 'Reviews offen', value: 47, note: '14 neu' },
],
activities: [
{ label: 'Neue Nominierung in Best New VTuber', age: 'vor 2 Min.' },
{ label: 'Clip-Dublette erkannt in Clip des Jahres', age: 'vor 7 Min.' },
{ label: 'Alias-Merge fuer Hoshimi Miyu reviewt', age: 'vor 18 Min.' },
],
topCategories: [
{ category: 'VTuber des Jahres', votes: 186321 },
{ category: 'Bestes Live Event', votes: 132550 },
{ category: 'Clip des Jahres', votes: 98210 },
],
riskFlags: [
{
id: 1,
source: 'vote',
type: 'rapid_vote_updates',
severity: 'high',
status: 'open',
summary: 'Mehrere Voting-Aenderungen in kurzer Zeit erkannt.',
twitchUserId: 'demo_user',
createdFromIp: '127.0.0.1',
createdAt: '2026-06-17T08:40:00Z',
metadataJson: '{"recentVoteSubmissions":3}',
},
],
auditEntries: [
{
id: 1,
adminTwitchUserId: 'jayuhime_admin',
actionType: 'category.update',
entityType: 'category',
entityId: '1',
summary: 'Kategorie VTuber des Jahres wurde aktualisiert.',
createdAt: '2026-06-17T08:32:00Z',
},
],
}
const fallbackAdminSeasons: AdminSeasonListItem[] = [
{ id: 1, year: 2026, name: 'VTuber Star Awards 2026', currentPhase: 'Community Voting', isCurrent: true, categoryCount: 4 },
{ id: 2, year: 2025, name: 'VTuber Star Awards 2025', currentPhase: 'Archived', isCurrent: false, categoryCount: 3 },
]
const fallbackAdminSeasonDetail: AdminSeasonDetailResponse = {
id: 1,
year: 2026,
name: 'VTuber Star Awards 2026',
currentPhase: 'Community Voting',
isCurrent: true,
categories: [
{
id: 1,
groupName: 'Main Awards',
name: 'VTuber des Jahres',
slug: 'vtuber-des-jahres',
description: 'Die groesste Auszeichnung des Jahres.',
sortOrder: 1,
maxNomineesPerUser: 3,
candidateCount: 3,
},
{
id: 2,
groupName: 'Performance',
name: 'Bestes Live Event',
slug: 'bestes-live-event',
description: 'Events, Konzerte und 3D-Shows.',
sortOrder: 2,
maxNomineesPerUser: 3,
candidateCount: 2,
},
],
candidates: [
{ id: 1, categoryId: 1, displayName: 'Hoshimi Miyu', channelSlug: '@hoshimimiyu', platform: 'Twitch' },
{ id: 2, categoryId: 1, displayName: 'Kurainu', channelSlug: '@kurainu', platform: 'Twitch' },
],
pendingNominations: [
{
id: 1,
categoryId: 1,
categoryName: 'VTuber des Jahres',
submittedByTwitchId: 'demo_user',
candidateText: 'Session Nominee',
createdAt: '2026-06-17T08:00:00Z',
},
],
}
const emptyAdmin: AdminDashboardResponse = {
metrics: [],
activities: [],
topCategories: [],
riskFlags: [],
auditEntries: [],
}
const emptyAdminSeasons: AdminSeasonListItem[] = []
const emptyAdminSeasonDetail: AdminSeasonDetailResponse = {
id: 0,
year: 0,
name: '',
currentPhase: '',
isCurrent: false,
categories: [],
candidates: [],
pendingNominations: [],
}
export const useAwardsStore = defineStore('awards', {
state: () => ({
overview: fallbackOverview as OverviewResponse,
categories: fallbackCategories as SeasonCategoriesResponse,
archive: fallbackArchive as WinnerArchiveResponse,
admin: fallbackAdmin as AdminDashboardResponse,
adminSeasons: fallbackAdminSeasons as AdminSeasonListItem[],
adminSeasonDetail: fallbackAdminSeasonDetail as AdminSeasonDetailResponse,
loading: false,
apiMode: 'fallback' as 'api' | 'fallback',
}),
actions: {
async loadHomeData() {
this.loading = true
try {
this.overview = await api.getOverview()
this.categories = await api.getSeasonCategories(this.overview.year)
this.archive = await api.getWinnerArchive(this.overview.winnersPreview[0]?.year ?? this.overview.year - 1)
this.apiMode = 'api'
} catch {
this.apiMode = 'fallback'
} finally {
this.loading = false
}
},
async loadArchive(year: number) {
try {
this.archive = await api.getWinnerArchive(year)
this.apiMode = 'api'
} catch {
this.archive = { ...fallbackArchive, year }
}
},
async loadAdmin() {
try {
this.admin = await api.getAdminDashboard()
this.adminSeasons = await api.getAdminSeasons()
this.adminSeasonDetail = await api.getAdminSeasonDetail(this.adminSeasons[0]?.id ?? 1)
this.apiMode = 'api'
} catch {
this.admin = emptyAdmin
this.adminSeasons = emptyAdminSeasons
this.adminSeasonDetail = emptyAdminSeasonDetail
}
},
async loadAdminSeasonDetail(seasonId: number) {
try {
this.adminSeasonDetail = await api.getAdminSeasonDetail(seasonId)
this.apiMode = 'api'
} catch {
this.adminSeasonDetail = emptyAdminSeasonDetail
}
},
submitNomination(payload: CreateNominationPayload) {
return api.submitNomination(payload)
},
submitVote(payload: CreateVotePayload) {
return api.submitVote(payload)
},
async updateAdminSeason(seasonId: number, payload: UpdateSeasonPayload) {
const result = await api.updateAdminSeason(seasonId, payload)
await this.loadAdmin()
return result
},
async createAdminCategory(seasonId: number, payload: UpsertCategoryPayload) {
const result = await api.createAdminCategory(seasonId, payload)
await this.loadAdminSeasonDetail(seasonId)
return result
},
async updateAdminCategory(categoryId: number, seasonId: number, payload: UpsertCategoryPayload) {
const result = await api.updateAdminCategory(categoryId, payload)
await this.loadAdminSeasonDetail(seasonId)
return result
},
async createAdminCandidate(seasonId: number, payload: UpsertCandidatePayload) {
const result = await api.createAdminCandidate(seasonId, payload)
await this.loadAdminSeasonDetail(seasonId)
return result
},
async updateAdminCandidate(candidateId: number, seasonId: number, payload: UpsertCandidatePayload) {
const result = await api.updateAdminCandidate(candidateId, payload)
await this.loadAdminSeasonDetail(seasonId)
return result
},
async approveAdminNomination(nominationId: number, seasonId: number, payload: ApproveNominationPayload) {
const result = await api.approveAdminNomination(nominationId, payload)
await this.loadAdminSeasonDetail(seasonId)
await this.loadAdmin()
return result
},
async rejectAdminNomination(nominationId: number, seasonId: number) {
const result = await api.rejectAdminNomination(nominationId)
await this.loadAdminSeasonDetail(seasonId)
await this.loadAdmin()
return result
},
async resolveRiskFlag(riskFlagId: number, status = 'resolved') {
const result = await api.resolveRiskFlag(riskFlagId, status)
await this.loadAdmin()
return result
},
},
})