Initialize project with basic structure, including Docker configuration, backend and frontend setup, environment configuration, and essential files for development.
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit is contained in:
2026-07-19 00:17:31 +03:00
parent d401e9009c
commit 1239fcee08
69 changed files with 12391 additions and 10 deletions

248
frontend/src/api/demo.ts Normal file
View File

@@ -0,0 +1,248 @@
import type {
BalanceCandidate,
Bracket,
MixEvent,
Player,
Registration,
Series,
Session,
Team,
} from './client'
import type { RankOrdinal } from '../ranks'
// Fixtures keep local development and visual review useful without a backend.
// Production builds always use the real API.
export const demoMode =
import.meta.env.DEV && import.meta.env.VITE_DEMO_MODE !== 'false'
const now = new Date()
const daysFromNow = (days: number, hour = 19) => {
const date = new Date(now)
date.setDate(date.getDate() + days)
date.setHours(hour, 0, 0, 0)
return date.toISOString()
}
const makePlayer = (
id: string,
displayName: string,
tank: RankOrdinal,
damage: RankOrdinal,
support: RankOrdinal,
): Player => ({
id,
displayName,
battleTag: `${displayName.replace(/\s/g, '')}#${1000 + Number(id.replace(/\D/g, '') || 0)}`,
ratings: { tank, damage, support, updatedAt: daysFromNow(-4) },
preferredRoles: tank >= damage && tank >= support ? ['tank'] : damage >= support ? ['damage'] : ['support'],
preferredPlayerIds: [],
})
export const demoProfile: Player = {
...makePlayer('p1', 'Lemintare', 30, 33, 27),
preferredRoles: ['damage', 'tank'],
preferredPlayerIds: ['p2', 'p5'],
}
export const demoSession: Session = {
account: {
id: 'account-1',
discordId: '125661705720381444',
displayName: 'Lemintare',
role: 'admin',
},
player: demoProfile,
}
export const demoEvents: MixEvent[] = [
{
id: 'event-1',
title: 'Saturday Night Scrim',
description: 'Community draft night · 4 teams · Bo3 single elimination',
startsAt: daysFromNow(2),
endsAt: daysFromNow(2, 23),
registrationDeadline: daysFromNow(1, 21),
status: 'registration',
rulesetName: 'Community Bo3 v2',
counts: { going: 18, maybe: 4, not_going: 3 },
myRsvp: 'going',
},
{
id: 'event-2',
title: 'Midweek Mix',
description: 'Fast queue with two balanced teams',
startsAt: daysFromNow(5),
endsAt: daysFromNow(5, 23),
registrationDeadline: daysFromNow(4),
status: 'registration',
rulesetName: 'Quick Mix v1',
counts: { going: 9, maybe: 6, not_going: 2 },
myRsvp: 'maybe',
},
{
id: 'event-3',
title: 'Summer Clash #4',
description: 'Live playoff bracket',
startsAt: daysFromNow(0, 20),
endsAt: daysFromNow(1, 1),
registrationDeadline: daysFromNow(-1),
status: 'live',
rulesetName: 'Tournament Bo3 v2',
counts: { going: 22, maybe: 0, not_going: 4 },
myRsvp: 'going',
},
]
const players = [
demoProfile,
makePlayer('p2', 'Nova', 36, 28, 29),
makePlayer('p3', 'Kairo', 24, 35, 26),
makePlayer('p4', 'MercyMain', 18, 27, 37),
makePlayer('p5', 'Pulse', 25, 34, 29),
makePlayer('p6', 'Astra', 31, 25, 36),
makePlayer('p7', 'Hex', 35, 29, 23),
makePlayer('p8', 'Orbit', 22, 34, 30),
makePlayer('p9', 'Miko', 27, 30, 36),
makePlayer('p10', 'Rook', 37, 26, 24),
makePlayer('p11', 'Echo', 26, 32, 31),
makePlayer('p12', 'Zenith', 28, 30, 35),
]
players[1].preferredPlayerIds = ['p1']
players[4].preferredPlayerIds = ['p1']
export const demoRegistrations: Registration[] = players.map((player, index) => ({
id: `reg-${index + 1}`,
player,
status: index < 9 ? 'going' : index < 11 ? 'maybe' : 'not_going',
updatedAt: daysFromNow(-(index % 4)),
changedBy:
index === 4
? { id: 'account-1', displayName: 'Lemintare', source: 'admin' }
: { id: player.id, displayName: player.displayName, source: 'self' },
}))
const alpha: Team = {
id: 'team-alpha',
name: 'Ember Wolves',
side: 'alpha',
captainId: 'p2',
averageRating: 35.6,
members: [
{ player: players[1], assignedRole: 'tank' },
{ player: players[2], assignedRole: 'damage' },
{ player: players[4], assignedRole: 'damage' },
{ player: players[3], assignedRole: 'support' },
{ player: players[5], assignedRole: 'support' },
],
}
const beta: Team = {
id: 'team-beta',
name: 'Azure Phantoms',
side: 'beta',
captainId: 'p7',
averageRating: 34.4,
members: [
{ player: players[6], assignedRole: 'tank' },
{ player: players[7], assignedRole: 'damage' },
{ player: players[10], assignedRole: 'damage' },
{ player: players[8], assignedRole: 'support' },
{ player: players[11], assignedRole: 'support' },
],
}
export const demoBalanceCandidates: BalanceCandidate[] = [
{
id: 'candidate-1',
score: 4,
teams: [alpha, beta],
reserve: [players[0], players[9]],
explanations: ['9 preferred role assignments', '2 preferred teammate pairs kept together'],
},
{
id: 'candidate-2',
score: 9,
teams: [
{ ...alpha, averageRating: 35.2 },
{ ...beta, averageRating: 34.6 },
],
reserve: [players[0], players[9]],
explanations: ['8 preferred role assignments', '2 preferred teammate pairs kept together'],
},
{
id: 'candidate-3',
score: 13,
teams: [
{ ...alpha, averageRating: 35.4 },
{ ...beta, averageRating: 34.5 },
],
reserve: [players[0], players[9]],
explanations: ['10 preferred role assignments', '1 preferred teammate pair kept together'],
},
]
export const demoSeries: Series = {
id: 'series-1',
status: 'hero_draft',
roundLabel: 'Semifinal 1',
teamAlpha: alpha,
teamBeta: beta,
score: { alpha: 1, beta: 0 },
version: 12,
currentStep: {
title: 'Hero ban 2 of 4',
instruction: 'Azure Phantoms choose a hero to remove for Map 2.',
activeTeam: 'beta',
kind: 'hero_ban',
version: 12,
},
options: [
{ id: 'hero-1', name: 'Ana', role: 'support' },
{ id: 'hero-2', name: 'Kiriko', role: 'support' },
{ id: 'hero-3', name: 'Tracer', role: 'damage' },
{ id: 'hero-4', name: 'Sojourn', role: 'damage' },
{ id: 'hero-5', name: 'Winston', role: 'tank', disabled: true, disabledReason: 'Already banned by this team' },
{ id: 'hero-6', name: 'Sigma', role: 'tank' },
],
maps: [
{
number: 1,
name: 'Lijiang Tower',
mode: 'Control',
status: 'completed',
winner: 'alpha',
heroBans: [
{ hero: 'Sombra', team: 'alpha', role: 'damage' },
{ hero: 'Winston', team: 'beta', role: 'tank' },
{ hero: 'Ana', team: 'alpha', role: 'support' },
{ hero: 'Tracer', team: 'beta', role: 'damage' },
],
},
{
number: 2,
name: 'Kings Row',
mode: 'Hybrid',
status: 'drafting',
heroBans: [{ hero: 'D.Va', team: 'alpha', role: 'tank' }],
},
{ number: 3, name: 'TBD', mode: 'Control', status: 'ready', heroBans: [] },
],
audit: [
{ id: 'a1', kind: 'coin_toss', summary: 'Ember Wolves won the toss', actorName: 'System', createdAt: daysFromNow(0, 20) },
{ id: 'a2', kind: 'map_selected', summary: 'Lijiang Tower selected', actorName: 'Nova', createdAt: daysFromNow(0, 20) },
{ id: 'a3', kind: 'result', summary: 'Map 1 won by Ember Wolves', actorName: 'Lemintare', createdAt: daysFromNow(0, 21) },
{ id: 'a4', kind: 'map_selected', summary: 'Kings Row selected', actorName: 'Hex', createdAt: daysFromNow(0, 21) },
{ id: 'a5', kind: 'hero_ban', summary: 'D.Va banned by Ember Wolves', actorName: 'Nova', createdAt: daysFromNow(0, 21) },
],
}
export const demoBracket: Bracket = {
id: 'bracket-1',
title: 'Summer Clash #4',
rounds: ['Semifinals', 'Grand Final'],
matches: [
{ id: 'm1', round: 0, slot: 0, label: 'Semifinal 1', teamAlpha: 'Ember Wolves', teamBeta: 'Azure Phantoms', scoreAlpha: 1, scoreBeta: 0, status: 'live', seriesId: 'series-1' },
{ id: 'm2', round: 0, slot: 1, label: 'Semifinal 2', teamAlpha: 'Neon Foxes', teamBeta: 'Void Runners', scoreAlpha: 2, scoreBeta: 1, winner: 'alpha', status: 'completed', seriesId: 'series-2' },
{ id: 'm3', round: 1, slot: 0, label: 'Grand Final', teamBeta: 'Neon Foxes', status: 'pending' },
],
}