306 lines
11 KiB
TypeScript
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
|
|
},
|
|
},
|
|
})
|