feat: WM2026 Tippspiel - Initial Backend + Frontend

This commit is contained in:
Ronny Müller
2026-04-03 21:41:19 +02:00
commit 1c685b90a0
2507 changed files with 997210 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
const BASE = '/api';
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.message || err.error || 'Request failed');
}
return res.json();
}
export const api = {
// Matches
getMatches: (params?: { stage?: string; group?: string }) => {
const q = new URLSearchParams(params as Record<string, string>).toString();
return request<{ matches: Match[]; count: number }>(`/matches${q ? '?' + q : ''}`);
},
// Tips
submitTip: (matchId: number, tipHome: number, tipAway: number) =>
request<{ success: boolean; tip: TipInfo; message: string }>('/tips', {
method: 'POST',
body: JSON.stringify({ matchId, tipHome, tipAway }),
}),
getMyTips: () =>
request<{ tips: MyTip[]; count: number }>('/tips'),
// Leaderboard
getLeaderboard: () =>
request<LeaderboardResponse>('/leaderboard'),
getMyStats: () =>
request<UserStats>('/leaderboard/me'),
// Admin
syncMatches: () =>
request<{ success: boolean; total: number; created: number; updated: number }>(
'/admin/sync',
{ method: 'POST' }
),
evaluateTips: () =>
request<{ success: boolean; matchesEvaluated: number; tipsUpdated: number }>(
'/admin/evaluate',
{ method: 'POST' }
),
};
// Types (gespiegelt vom Backend)
export interface Match {
id: number;
externalId: number;
utcDate: string;
status: string;
stage: string;
group: string | null;
homeTeam: { name: string; shortName: string; crest: string | null };
awayTeam: { name: string; shortName: string; crest: string | null };
score: { home: number | null; away: number | null };
userTip: TipInfo | null;
minutesUntilKickoff: number;
tippable: boolean;
}
export interface TipInfo {
home: number;
away: number;
points: number | null;
}
export interface MyTip {
match_id: number;
tip_home: number;
tip_away: number;
points: number | null;
utc_date: string;
home_team_short: string;
away_team_short: string;
status: string;
}
export interface LeaderboardEntry {
user_id: string;
full_name: string;
total_points: number;
tips_count: number;
exact_count: number;
tendency_count: number;
rank: number;
}
export interface LeaderboardResponse {
entries: LeaderboardEntry[];
currentUserRank: number | null;
totalParticipants: number;
lastUpdated: string;
}
export interface UserStats {
userId: string;
fullName: string;
totalPoints: number;
rank: number | null;
tipsCount: number;
exactCount: number;
tendencyCount: number;
wrongCount: number;
accuracy: number;
}