feat: WM2026 Tippspiel - Initial Backend + Frontend
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user