"use client"; import { useMemo, useState, type Dispatch, type SetStateAction } from "react"; import { achievements as achievementSeeds, activeChallenges, dailyTrivia, fanGoals, initialFanProfile, leaderboard as leaderboardSeeds, matches as matchSeeds, memories as memorySeeds, navItems, predictions as predictionSeeds, stadiums as stadiumSeeds, stickers as stickerSeeds, teams as teamSeeds, } from "../data/passportData"; import type { Achievement, FanProfile, Match, Memory, NewMemoryInput, Prediction, ScreenId, Stadium, Sticker, Team, TriviaAnswer, } from "../types/passport"; import { calculatePassportStats, getCompletedGroups, } from "../utils/passportMetrics"; import AppHeader from "./navigation/AppHeader"; import MobileTabBar from "./navigation/MobileTabBar"; import AchievementsScreen from "./screens/AchievementsScreen"; import DailyTriviaScreen from "./screens/DailyTriviaScreen"; import DashboardScreen from "./screens/DashboardScreen"; import FanProfileScreen from "./screens/FanProfileScreen"; import LeaderboardScreen from "./screens/LeaderboardScreen"; import MatchJourneyScreen from "./screens/MatchJourneyScreen"; import MemoryTimelineScreen from "./screens/MemoryTimelineScreen"; import OnboardingScreen, { type OnboardingSelection } from "./screens/OnboardingScreen"; import PredictionCenterScreen from "./screens/PredictionCenterScreen"; import StadiumCollectionScreen from "./screens/StadiumCollectionScreen"; import StickerAlbumScreen from "./screens/StickerAlbumScreen"; import TeamCollectionScreen from "./screens/TeamCollectionScreen"; function toggleSetValue( setter: Dispatch>>, id: string, ): void { setter((current) => { const next = new Set(current); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); } export default function PassportApp() { const [activeScreen, setActiveScreen] = useState("onboarding"); const [profile, setProfile] = useState(initialFanProfile); const [collectedTeamIds, setCollectedTeamIds] = useState>( () => new Set(teamSeeds.filter((team) => team.collected).map((team) => team.id)), ); const [collectedStadiumIds, setCollectedStadiumIds] = useState>( () => new Set(stadiumSeeds.filter((stadium) => stadium.collected).map((stadium) => stadium.id)), ); const [watchedMatchIds, setWatchedMatchIds] = useState>( () => new Set(matchSeeds.filter((match) => match.watched).map((match) => match.id)), ); const [predictionPicks, setPredictionPicks] = useState>( () => Object.fromEntries( predictionSeeds .filter((prediction) => Boolean(prediction.userPickOptionId)) .map((prediction) => [prediction.id, prediction.userPickOptionId]), ), ); const [triviaAnswers, setTriviaAnswers] = useState>({}); const [stickerInventory, setStickerInventory] = useState< Record >( () => Object.fromEntries( stickerSeeds.map((sticker) => [ sticker.id, { obtained: sticker.obtained, duplicateCount: sticker.duplicateCount }, ]), ), ); const [claimedAchievementIds, setClaimedAchievementIds] = useState>( () => new Set( achievementSeeds .filter((achievement) => achievement.claimed) .map((achievement) => achievement.id), ), ); const [memories, setMemories] = useState(memorySeeds); const teams = useMemo( () => teamSeeds.map((team) => ({ ...team, collected: collectedTeamIds.has(team.id), })), [collectedTeamIds], ); const stadiums = useMemo( () => stadiumSeeds.map((stadium) => ({ ...stadium, collected: collectedStadiumIds.has(stadium.id), })), [collectedStadiumIds], ); const matches = useMemo( () => matchSeeds.map((match) => ({ ...match, watched: watchedMatchIds.has(match.id), collected: match.collected || watchedMatchIds.has(match.id), })), [watchedMatchIds], ); const predictions = useMemo( () => predictionSeeds.map((prediction) => ({ ...prediction, userPickOptionId: predictionPicks[prediction.id] ?? prediction.userPickOptionId, })), [predictionPicks], ); const stickers = useMemo( () => stickerSeeds.map((sticker) => { const inventoryItem = stickerInventory[sticker.id]; return { ...sticker, obtained: inventoryItem?.obtained ?? sticker.obtained, duplicateCount: inventoryItem?.duplicateCount ?? sticker.duplicateCount, }; }), [stickerInventory], ); const achievements = useMemo(() => { const completedGroups = getCompletedGroups(teams).length; const totalGroups = Math.max(1, new Set(teams.map((team) => team.group)).size); const englandWatched = matches.filter( (match) => match.watched && (match.homeTeamId === "england" || match.awayTeamId === "england"), ).length; const englandTotal = Math.max( 1, matches.filter((match) => match.homeTeamId === "england" || match.awayTeamId === "england") .length, ); const submittedPredictions = predictions.filter((prediction) => Boolean(prediction.userPickOptionId), ).length; const wonGiantKilling = predictions.some( (prediction) => prediction.id.includes("giant") && prediction.result === "won", ) ? 1 : 0; const obtainedStickerCount = stickers.filter((sticker) => sticker.obtained).length; return achievementSeeds.map((achievement) => { let progress = achievement.progress; let total = achievement.total; switch (achievement.id) { case "welcome-aboard": progress = 1; total = 1; break; case "team-collector": progress = teams.filter((team) => team.collected).length; total = teams.length; break; case "england-loyalist": progress = englandWatched; total = englandTotal; break; case "stadium-hopper": progress = stadiums.filter((stadium) => stadium.collected).length; total = stadiums.length; break; case "predictor-rookie": progress = submittedPredictions > 0 ? 1 : 0; total = 1; break; case "giant-killing-oracle": progress = wonGiantKilling; total = 1; break; case "trivia-streak": progress = profile.streakDays; total = 7; break; case "sticker-page": progress = obtainedStickerCount; total = stickers.length; break; case "memory-maker": progress = memories.length; total = 3; break; case "group-master": progress = completedGroups; total = totalGroups; break; case "leaderboard-climber": progress = 1; total = 1; break; case "derby-storyteller": progress = memories.some((memory) => memory.title.toLowerCase().includes("derby")) ? 1 : 0; total = 1; break; default: break; } const cappedProgress = Math.min(progress, total); const unlocked = achievement.unlocked || cappedProgress >= total; return { ...achievement, progress: cappedProgress, total, unlocked, claimed: claimedAchievementIds.has(achievement.id) || Boolean(achievement.claimed), }; }); }, [claimedAchievementIds, matches, memories, predictions, profile.streakDays, stadiums, stickers, teams]); const stats = useMemo( () => calculatePassportStats({ teams, stadiums, matches, stickers, achievements, predictions, triviaAnsweredCount: Object.keys(triviaAnswers).length, triviaCorrectCount: Object.values(triviaAnswers).filter((answer) => answer.correct).length, triviaTotalCount: dailyTrivia.length, memoryCount: memories.length, currentStreak: profile.streakDays, }), [achievements, matches, memories.length, predictions, profile.streakDays, stadiums, stickers, teams, triviaAnswers], ); const displayProfile = useMemo( () => ({ ...profile, passportScore: stats.totalScore, }), [profile, stats.totalScore], ); function handleNavigate(screen: ScreenId) { setActiveScreen(screen); } function handleOnboardingComplete(selection: OnboardingSelection) { setProfile((current) => ({ ...current, displayName: selection.displayName, favoriteTeamId: selection.favoriteTeamId, selectedGoalIds: selection.selectedGoalIds, })); setCollectedTeamIds((current) => new Set(current).add(selection.favoriteTeamId)); const starterSticker = stickerSeeds.find( (sticker) => sticker.category === "team" && sticker.linkedEntityId === selection.favoriteTeamId, ) ?? stickerSeeds[0]; setStickerInventory((current) => ({ ...current, [starterSticker.id]: { obtained: true, duplicateCount: current[starterSticker.id]?.duplicateCount ?? 0, }, })); setActiveScreen("dashboard"); } function handleChoosePrediction(predictionId: string, optionId: string) { setPredictionPicks((current) => { const next = { ...current }; if (next[predictionId] === optionId) { delete next[predictionId]; } else { next[predictionId] = optionId; } return next; }); } function handleTriviaAnswer(questionId: string, selectedIndex: number, correct: boolean) { setTriviaAnswers((current) => { if (current[questionId]) { return current; } return { ...current, [questionId]: { selectedIndex, correct, answeredAt: new Date().toISOString(), }, }; }); } function handleOpenStickerPack(stickerIds: string[]) { setStickerInventory((current) => { const next = { ...current }; stickerIds.forEach((stickerId) => { const existing = next[stickerId] ?? { obtained: false, duplicateCount: 0 }; next[stickerId] = { obtained: true, duplicateCount: existing.obtained ? existing.duplicateCount + 1 : existing.duplicateCount, }; }); return next; }); } function handleClaimAchievement(achievementId: string) { setClaimedAchievementIds((current) => new Set(current).add(achievementId)); } function handleAddMemory(input: NewMemoryInput) { const memory: Memory = { id: `memory-${Date.now()}`, date: new Date().toISOString(), ...input, }; setMemories((current) => [memory, ...current]); } function handleProfileUpdate(updates: Partial) { setProfile((current) => ({ ...current, ...updates, })); } if (activeScreen === "onboarding") { return ; } let screenContent; switch (activeScreen) { case "dashboard": screenContent = ( ); break; case "profile": screenContent = ( ); break; case "teams": screenContent = ( toggleSetValue(setCollectedTeamIds, teamId)} teams={teams} /> ); break; case "matches": screenContent = ( toggleSetValue(setWatchedMatchIds, matchId)} stadiums={stadiums} teams={teams} /> ); break; case "stadiums": screenContent = ( toggleSetValue(setCollectedStadiumIds, stadiumId)} stadiums={stadiums} /> ); break; case "predictions": screenContent = ( ); break; case "trivia": screenContent = ( ); break; case "stickers": screenContent = ; break; case "achievements": screenContent = ( ); break; case "leaderboard": screenContent = ( ); break; case "memories": screenContent = ( ); break; default: screenContent = null; } return (
setActiveScreen("onboarding")} passportScore={stats.totalScore} profile={displayProfile} />
{screenContent}
); }