"use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import ClueList from "@/components/ClueList"; import Header from "@/components/Header"; import OnboardingModal from "@/components/OnboardingModal"; import Pitch from "@/components/Pitch"; import PlayerSearch from "@/components/PlayerSearch"; import FeedbackModal from "@/components/FeedbackModal"; import ShareResult from "@/components/ShareResult"; import SponsorSlot from "@/components/SponsorSlot"; import StatsPanel from "@/components/StatsPanel"; import { trackEvent } from "@/lib/analytics-events"; import { registerPlay } from "@/lib/plays"; import { buildDifficultyShareText, buildGlobalStatsShareText, formatScoreLine, parseSharePayload, payloadScore, type SharePayload, } from "@/lib/share"; import { localePath } from "@/lib/seo-content"; import type { SponsorConfig } from "@/lib/sponsor"; import { DIFFICULTIES, MAX_ATTEMPTS, POSITIONS, getDailyPuzzles, getFixedGuess, getPuzzleNumber, getTodayKey, isAttemptComplete, isSolved, players, validateGuess, } from "@/lib/game-engine"; import { SUPPORTED_LOCALES, detectLocale, getDictionary, type Locale } from "@/lib/i18n"; import { quoteForDay } from "@/lib/quotes"; import { createInitialProgress, createInitialStats, loadProgress, loadStats, nextDifficulty, recordDailyStats, saveProgress, } from "@/lib/storage"; import type { DailyProgress, Difficulty, GlobalStats, Position } from "@/lib/types"; type HomeProps = { sponsor: SponsorConfig; initialLocale?: Locale; dateKey?: string; archive?: boolean; }; export default function Home({ sponsor, initialLocale, dateKey: dateKeyProp, archive = false }: HomeProps) { const dateKey = useMemo(() => dateKeyProp ?? getTodayKey(), [dateKeyProp]); const dailyPuzzles = useMemo(() => getDailyPuzzles(dateKey), [dateKey]); const [locale, setLocale] = useState(initialLocale ?? "en"); const [progress, setProgress] = useState(() => createInitialProgress()); const [activeDifficulty, setActiveDifficulty] = useState("easy"); const [cluesCollapsed, setCluesCollapsed] = useState(false); const [fieldCollapsed, setFieldCollapsed] = useState(false); const [shareOpen, setShareOpen] = useState(false); const [onboardingOpen, setOnboardingOpen] = useState(false); const [started, setStarted] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false); const [statsOpen, setStatsOpen] = useState(false); const [revealedSolutions, setRevealedSolutions] = useState>>({}); const [selectedPosition, setSelectedPosition] = useState("DEF"); const [stats, setStats] = useState(() => createInitialStats()); const [draft, setDraft] = useState>(() => getFixedGuess(dailyPuzzles[0])); const [message, setMessage] = useState(""); const [challenge, setChallenge] = useState(null); const [challengeExpired, setChallengeExpired] = useState(false); const [earlyShare, setEarlyShare] = useState(null); const activePuzzle = dailyPuzzles.find((puzzle) => puzzle.difficulty === activeDifficulty) ?? dailyPuzzles[0]; const activeProgress = progress[activeDifficulty]; const isLocked = activeProgress.locked; const isFinished = activeProgress.solved || activeProgress.attempts.length >= MAX_ATTEMPTS; const hasFailed = !activeProgress.solved && activeProgress.attempts.length >= MAX_ATTEMPTS; const solutionRevealed = revealedSolutions[activeDifficulty] === true; const pitchDraft = solutionRevealed ? activePuzzle.solution : draft; const t = useMemo(() => getDictionary(locale), [locale]); const quote = useMemo(() => quoteForDay(dateKey, locale), [dateKey, locale]); const globalStatsShareText = useMemo(() => buildGlobalStatsShareText(stats, t), [stats, t]); const totalAttempts = DIFFICULTIES.reduce((sum, difficulty) => sum + progress[difficulty].attempts.length, 0); useEffect(() => { if (initialLocale) { window.localStorage.setItem("hidden11-locale", initialLocale); } else { const savedLocale = window.localStorage.getItem("hidden11-locale") as Locale | null; setLocale(savedLocale ?? detectLocale(window.navigator.language)); } const loaded = loadProgress(dateKey); setProgress(loaded); setStats(loadStats()); const firstPlayable = DIFFICULTIES.find((difficulty) => !loaded[difficulty].locked) ?? "easy"; setActiveDifficulty(firstPlayable); const seenOnboarding = window.localStorage.getItem("hidden11-onboarding-seen") === "true"; setOnboardingOpen(!seenOnboarding && DIFFICULTIES.every((difficulty) => loaded[difficulty].attempts.length === 0)); }, [dateKey, initialLocale]); useEffect(() => { const params = new URLSearchParams(window.location.search); const code = params.get("challenge"); const storageKey = `hidden11-challenge-${dateKey}`; if (code) { const payload = parseSharePayload(code); if (payload && payload.dateKey === dateKey) { setChallenge(payload); window.localStorage.setItem(storageKey, code); trackEvent("challenge_accepted", { puzzle_number: payload.puzzleNumber }); } else if (payload) { setChallengeExpired(true); } return; } const stored = window.localStorage.getItem(storageKey); if (stored) { const payload = parseSharePayload(stored); if (payload && payload.dateKey === dateKey) setChallenge(payload); } }, [dateKey]); useEffect(() => { setDraft(getFixedGuess(activePuzzle)); const firstEmpty = POSITIONS.find((position) => position !== activePuzzle.fixedPosition) ?? "GK"; setSelectedPosition(firstEmpty); setMessage(""); }, [activePuzzle]); useEffect(() => { if (isLocked || isFinished) return; const interval = window.setInterval(() => { setProgress((current) => { const next = structuredClone(current); next[activeDifficulty].elapsedSeconds += 1; saveProgress(dateKey, next); return next; }); }, 1000); return () => window.clearInterval(interval); }, [activeDifficulty, dateKey, isFinished, isLocked]); useEffect(() => { if (!isFinished) return; // Archive replays never touch global stats or the daily streak. if (!archive) { const nextStats = recordDailyStats(dateKey, progress); setStats(nextStats); } setShareOpen(true); if (DIFFICULTIES.every((difficulty) => progress[difficulty].solved)) { const completionKey = `hidden11-daily-completed-event-${dateKey}`; if (window.localStorage.getItem(completionKey) !== "true") { trackEvent("daily_completed", { puzzle_number: getPuzzleNumber(dateKey), total_seconds: DIFFICULTIES.reduce((sum, difficulty) => sum + progress[difficulty].elapsedSeconds, 0), }); window.localStorage.setItem(completionKey, "true"); } } }, [activeDifficulty, archive, dateKey, isFinished, progress]); function updateProgress(next: DailyProgress) { setProgress(next); saveProgress(dateKey, next); } function startGame() { setStarted(true); window.localStorage.setItem("hidden11-onboarding-seen", "true"); setOnboardingOpen(false); trackEvent("game_started", { puzzle_number: getPuzzleNumber(dateKey) }); if (!archive) registerPlay(dateKey); window.requestAnimationFrame(() => document.getElementById("player-search")?.focus()); } function selectDifficulty(difficulty: Difficulty) { if (progress[difficulty].locked) return; setActiveDifficulty(difficulty); } function placePlayer(playerId: string) { if (isLocked || isFinished || selectedPosition === activePuzzle.fixedPosition) return; if (!started && totalAttempts === 0) { setStarted(true); trackEvent("game_started", { puzzle_number: getPuzzleNumber(dateKey) }); if (!archive) registerPlay(dateKey); } setDraft((current) => ({ ...current, [selectedPosition]: playerId })); } function submitGuess() { if (isLocked || isFinished) return; if (!isAttemptComplete(draft)) { setMessage(t.completeLineup); return; } const guess = validateGuess(activePuzzle, draft); const solved = isSolved(guess); const next = structuredClone(progress); next[activeDifficulty].attempts.push(guess); next[activeDifficulty].solved = solved; trackEvent("guess_submitted", { attempts: next[activeDifficulty].attempts.length, difficulty: activeDifficulty, puzzle_number: getPuzzleNumber(dateKey), solved, }); if (solved) { const unlocked = nextDifficulty(activeDifficulty); if (unlocked) next[unlocked].locked = false; if (unlocked) { trackEvent("difficulty_unlocked", { difficulty: unlocked, puzzle_number: getPuzzleNumber(dateKey) }); } setMessage(unlocked ? t.unlocked(t.difficulty[unlocked]) : t.allCompleted); if (unlocked) { setActiveDifficulty(unlocked); // Prompt a share at the emotional peak, while the rest of the day is pending. setEarlyShare(activeDifficulty); } } else if (next[activeDifficulty].attempts.length >= MAX_ATTEMPTS) { setMessage(t.noAttemptsLeft); } else { setMessage(t.attemptsRemaining(MAX_ATTEMPTS - next[activeDifficulty].attempts.length)); } updateProgress(next); } function resetDraft() { setDraft(getFixedGuess(activePuzzle)); setSelectedPosition(POSITIONS.find((position) => position !== activePuzzle.fixedPosition) ?? "GK"); } function revealHint() { if (isLocked || isFinished) return; const positionToReveal = POSITIONS.find( (position) => position !== activePuzzle.fixedPosition && draft[position] !== activePuzzle.solution[position], ); if (!positionToReveal) { setMessage(t.noHintAvailable); return; } setDraft((current) => ({ ...current, [positionToReveal]: activePuzzle.solution[positionToReveal], })); setSelectedPosition(positionToReveal); setMessage(t.hintRevealed(positionToReveal)); } function revealSolution() { if (!hasFailed) return; setRevealedSolutions((current) => ({ ...current, [activeDifficulty]: true })); setDraft(activePuzzle.solution); setMessage(t.solutionRevealed); trackEvent("solution_revealed", { difficulty: activeDifficulty, puzzle_number: getPuzzleNumber(dateKey), }); } function changeLocale(nextLocale: Locale) { setLocale(nextLocale); window.localStorage.setItem("hidden11-locale", nextLocale); } async function shareEarlyResult(difficulty: Difficulty) { const text = buildDifficultyShareText(dateKey, difficulty, progress, t); const homeUrl = window.location.origin.replace(/\/$/, ""); trackEvent("share_clicked", { channel: "early", difficulty, puzzle_number: getPuzzleNumber(dateKey), }); if (navigator.share) { try { await navigator.share({ title: "hidden11", text, url: homeUrl }); } catch { // user dismissed the native sheet } return; } window.open( `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(homeUrl)}`, "_blank", "noopener,noreferrer", ); } async function shareGlobalStats() { const homeUrl = typeof window === "undefined" ? "https://hidden11.app" : window.location.origin.replace(/\/$/, ""); trackEvent("share_clicked", { source: "global_stats", games_played: stats.gamesPlayed }); if (navigator.share) { await navigator.share({ title: "hidden11", text: globalStatsShareText, url: homeUrl, }); return; } window.open( `https://twitter.com/intent/tweet?text=${encodeURIComponent(globalStatsShareText)}&url=${encodeURIComponent(homeUrl)}`, "_blank", "noopener,noreferrer", ); } const actionButtons = (
{hasFailed && !solutionRevealed ? ( ) : null}
); return (
{totalAttempts === 0 && !started ? (

{t.firstMoveTitle}

{t.firstMoveLead}

) : null}

“{quote.text[locale]}” — {quote.author}

{challenge ? (

🎯{" "} {challenge.name ? t.challengeLeadNamed(challenge.name, formatScoreLine(payloadScore(challenge))) : t.challengeLead(formatScoreLine(payloadScore(challenge)))}

) : null} {challengeExpired ? (

⌛ {t.challengeExpired}

) : null}
{DIFFICULTIES.map((difficulty) => ( ))}
{message ?

{message}

: null}
{earlyShare ? (

🎉 {t.earlySharePrompt(t.difficulty[earlyShare], `${progress[earlyShare].attempts.length}/${MAX_ATTEMPTS}`)}

) : null}
setCluesCollapsed((current) => !current)} t={t} />
setFieldCollapsed((current) => !current)} selectedPosition={selectedPosition} solutionRevealed={solutionRevealed} t={t} onSelectPosition={setSelectedPosition} />
{isFinished && shareOpen ? ( setShareOpen(false)} progress={progress} quote={quote} stats={stats} t={t} onOpenStats={() => setStatsOpen(true)} sponsor={sponsor} /> ) : null} {statsOpen ? (

{t.stats}

) : null} {feedbackOpen ? ( setFeedbackOpen(false)} t={t} /> ) : null} {onboardingOpen ? ( { window.localStorage.setItem("hidden11-onboarding-seen", "true"); setOnboardingOpen(false); }} onStart={startGame} t={t} /> ) : null}
); }