This repository has been archived on 2026-05-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tippspiel/frontend/src/api/client.ts
T
Ronny edf33fa932 feat: premium achievement badges with Material Symbols icons
Backend:
- New /api/achievements endpoint calculating 6 badges:
  Scharfschütze, Serien-Tipper, Tabellenführer, Frühtipper,
  Globetrotter, Diamant
- Each with progress tracking (current/target)

Frontend:
- AchievementBadge component with Stitch-inspired design
- Material Symbols Outlined font (filled icons)
- Unlocked: colored icon with glow + drop-shadow, rank label
- Locked: grayscale, lock overlay, progress bar
- ProfilePage: real badges replacing emoji placeholders
- Progress bar showing X/6 collected
- Mobile: 2-col grid, Desktop: 6-col grid

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:09:25 +02:00

176 lines
4.3 KiB
TypeScript

const BASE = '/api';
function withDevUser(path: string): string {
const devUser = new URLSearchParams(window.location.search).get('devUser');
if (!devUser) return path;
const sep = path.includes('?') ? '&' : '?';
return `${path}${sep}devUser=${devUser}`;
}
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${withDevUser(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'),
// Profile
updateTeam: (team: string) =>
request<{ success: boolean; team: string }>('/profile/team', {
method: 'PATCH',
body: JSON.stringify({ team }),
}),
// Dashboard
getDashboard: () => request<DashboardData>('/dashboard'),
// Achievements
getAchievements: () => request<AchievementsData>('/achievements'),
// 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 DashboardData {
hero: {
match: {
id: number;
homeTeam: { name: string; shortName: string; crest: string | null };
awayTeam: { name: string; shortName: string; crest: string | null };
utcDate: string;
status: string;
minutesUntilKickoff: number;
};
userTip: { home: number; away: number } | null;
tippable: boolean;
} | null;
stats: { rank: number | null; totalPoints: number; streak: number };
nudges: Array<{ type: string; text: string; matchId?: number }>;
}
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;
team: string | null;
total_points: number;
tips_count: number;
exact_count: number;
tendency_count: number;
rank: number;
}
export interface LeaderboardResponse {
entries: LeaderboardEntry[];
currentUserRank: number | null;
currentUserId: string | null;
totalParticipants: number;
lastUpdated: string;
}
export interface UserStats {
userId: string;
fullName: string;
team: string | null;
totalPoints: number;
rank: number | null;
tipsCount: number;
exactCount: number;
tendencyCount: number;
wrongCount: number;
accuracy: number;
}
export interface Achievement {
id: string;
name: string;
description: string;
icon: string;
color: string;
rankLabel: string;
unlocked: boolean;
progress: number;
current: number;
target: number;
}
export interface AchievementsData {
achievements: Achievement[];
unlockedCount: number;
total: number;
}