import { getDeviceId } from '../lib/device'; export interface AchievementBadge { id: string; title: string; description: string; earned: boolean; progress: number; target: number; } export interface LeaderboardEntry { guest: string; photos: number; likes: number; } export interface TopPhotoHighlight { photoId: number; guest: string; likes: number; task?: string | null; createdAt: string; thumbnail: string | null; } export interface TrendingEmotionHighlight { emotionId: number; name: string; count: number; } export interface TimelinePoint { date: string; photos: number; guests: number; } export interface FeedEntry { photoId: number; guest: string; task?: string | null; likes: number; createdAt: string; thumbnail: string | null; } export interface AchievementsPayload { summary: { totalPhotos: number; uniqueGuests: number; tasksSolved: number; likesTotal: number; }; personal: { guestName: string; photos: number; tasks: number; likes: number; badges: AchievementBadge[]; } | null; leaderboards: { uploads: LeaderboardEntry[]; likes: LeaderboardEntry[]; }; highlights: { topPhoto: TopPhotoHighlight | null; trendingEmotion: TrendingEmotionHighlight | null; timeline: TimelinePoint[]; }; feed: FeedEntry[]; } function toNumber(value: unknown, fallback = 0): number { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value !== '') { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } return fallback; } function safeString(value: unknown): string { return typeof value === 'string' ? value : ''; } export async function fetchAchievements( slug: string, guestName?: string, signal?: AbortSignal ): Promise { const params = new URLSearchParams(); if (guestName && guestName.trim().length > 0) { params.set('guest_name', guestName.trim()); } const response = await fetch(`/api/v1/events/${encodeURIComponent(slug)}/achievements?${params.toString()}`, { method: 'GET', headers: { 'X-Device-Id': getDeviceId(), 'Cache-Control': 'no-store', }, signal, }); if (!response.ok) { const message = await response.text(); throw new Error(message || 'Achievements request failed'); } const json = await response.json(); const summary = json.summary ?? {}; const personalRaw = json.personal ?? null; const leaderboards = json.leaderboards ?? {}; const highlights = json.highlights ?? {}; const feedRaw = Array.isArray(json.feed) ? json.feed : []; const personal = personalRaw ? { guestName: safeString(personalRaw.guest_name), photos: toNumber(personalRaw.photos), tasks: toNumber(personalRaw.tasks), likes: toNumber(personalRaw.likes), badges: Array.isArray(personalRaw.badges) ? personalRaw.badges.map((badge: any): AchievementBadge => ({ id: safeString(badge.id), title: safeString(badge.title), description: safeString(badge.description), earned: Boolean(badge.earned), progress: toNumber(badge.progress), target: toNumber(badge.target, 1), })) : [], } : null; const uploadsBoard = Array.isArray(leaderboards.uploads) ? leaderboards.uploads.map((row: any): LeaderboardEntry => ({ guest: safeString(row.guest), photos: toNumber(row.photos), likes: toNumber(row.likes), })) : []; const likesBoard = Array.isArray(leaderboards.likes) ? leaderboards.likes.map((row: any): LeaderboardEntry => ({ guest: safeString(row.guest), photos: toNumber(row.photos), likes: toNumber(row.likes), })) : []; const topPhotoRaw = highlights.top_photo ?? null; const topPhoto = topPhotoRaw ? { photoId: toNumber(topPhotoRaw.photo_id), guest: safeString(topPhotoRaw.guest), likes: toNumber(topPhotoRaw.likes), task: topPhotoRaw.task ?? null, createdAt: safeString(topPhotoRaw.created_at), thumbnail: topPhotoRaw.thumbnail ? safeString(topPhotoRaw.thumbnail) : null, } : null; const trendingRaw = highlights.trending_emotion ?? null; const trendingEmotion = trendingRaw ? { emotionId: toNumber(trendingRaw.emotion_id), name: safeString(trendingRaw.name), count: toNumber(trendingRaw.count), } : null; const timeline = Array.isArray(highlights.timeline) ? highlights.timeline.map((row: any): TimelinePoint => ({ date: safeString(row.date), photos: toNumber(row.photos), guests: toNumber(row.guests), })) : []; const feed = feedRaw.map((row: any): FeedEntry => ({ photoId: toNumber(row.photo_id), guest: safeString(row.guest), task: row.task ?? null, likes: toNumber(row.likes), createdAt: safeString(row.created_at), thumbnail: row.thumbnail ? safeString(row.thumbnail) : null, })); return { summary: { totalPhotos: toNumber(summary.total_photos), uniqueGuests: toNumber(summary.unique_guests), tasksSolved: toNumber(summary.tasks_solved), likesTotal: toNumber(summary.likes_total), }, personal, leaderboards: { uploads: uploadsBoard, likes: likesBoard, }, highlights: { topPhoto, trendingEmotion, timeline, }, feed, }; }