import { useEffect, useMemo, useState } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { Zap, Clock, Target, Trophy, CheckCircle2, PlayCircle, Lock, ChevronRight, Calendar, Sparkles, Crown, Coins, Medal, Clock3, } from 'lucide-react'; import { codingQuestions, type CodingQuestion } from '@/data/codingQuestions'; import type { AuthUser } from '@/lib/authClient'; import { cn } from '@/lib/utils'; import { getActivityDateKey } from '@/lib/activityDates'; interface DailyContestPageProps { user: AuthUser; solvedQuestionIds: string[]; onSelectQuestion: (question: CodingQuestion, mode: 'code' | 'solution') => void; } const DAILY_CONTEST_STORAGE_PREFIX = 'codequest.dailyContest'; const DAILY_CONTEST_JOIN_STORAGE_PREFIX = 'codequest.dailyContest.joined'; export const DAILY_CONTEST_PRIZE_TIERS = [ { rank: 1, label: '1st place', coins: 100 }, { rank: 2, label: '2nd place', coins: 75 }, { rank: 3, label: '3rd place', coins: 50 }, { rank: 4, label: '4th - 10th', coins: 5 }, ] as const; interface DailyContestQuestion { id: string; label: string; question: CodingQuestion; points: number; } interface DailyContest { id: string; date: Date; questions: DailyContestQuestion[]; } function getDailyContest(now: Date): DailyContest { const dateKey = getActivityDateKey(now); // Use date to deterministically pick 2 questions const dayIndex = now.getDate() % codingQuestions.length; const secondIndex = (dayIndex + Math.floor(codingQuestions.length / 2)) % codingQuestions.length; const q1 = codingQuestions[dayIndex]; const q2 = codingQuestions[secondIndex]; return { id: `daily-contest-${dateKey}`, date: new Date(now.getFullYear(), now.getMonth(), now.getDate()), questions: [ { id: `daily-${dateKey}-q1`, label: 'Q1', question: q1, points: getPointsForDifficulty(q1.difficulty), }, { id: `daily-${dateKey}-q2`, label: 'Q2', question: q2, points: getPointsForDifficulty(q2.difficulty), }, ], }; } function getPointsForDifficulty(difficulty: CodingQuestion['difficulty']) { const points = { Easy: 100, Medium: 200, Hard: 350, }; return points[difficulty]; } function readJoinedContestIds(userId: string): string[] { if (typeof window === 'undefined') return []; const key = `${DAILY_CONTEST_JOIN_STORAGE_PREFIX}.${userId}`; try { const data = localStorage.getItem(key); return data ? JSON.parse(data) : []; } catch { return []; } } function joinDailyContest(userId: string, contestId: string): string[] { const existing = readJoinedContestIds(userId); if (!existing.includes(contestId)) { const updated = [...existing, contestId]; localStorage.setItem(`${DAILY_CONTEST_JOIN_STORAGE_PREFIX}.${userId}`, JSON.stringify(updated)); return updated; } return existing; } function readDailySolvedIds(userId: string): string[] { if (typeof window === 'undefined') return []; const key = `${DAILY_CONTEST_STORAGE_PREFIX}.solved.${userId}`; try { const data = localStorage.getItem(key); return data ? JSON.parse(data) : []; } catch { return []; } } function markDailySolved(userId: string, contestId: string): string[] { const existing = readDailySolvedIds(userId); if (!existing.includes(contestId)) { const updated = [...existing, contestId]; localStorage.setItem(`${DAILY_CONTEST_STORAGE_PREFIX}.solved.${userId}`, JSON.stringify(updated)); return updated; } return existing; } function readDailyStreak(userId: string): { current: number; lastDate: string | null } { if (typeof window === 'undefined') return { current: 0, lastDate: null }; const key = `${DAILY_CONTEST_STORAGE_PREFIX}.streak.${userId}`; try { const data = localStorage.getItem(key); return data ? JSON.parse(data) : { current: 0, lastDate: null }; } catch { return { current: 0, lastDate: null }; } } function updateDailyStreak(userId: string): number { const today = getActivityDateKey(new Date()); const { current, lastDate } = readDailyStreak(userId); let newStreak = current; if (lastDate) { const last = new Date(lastDate); const todayDate = new Date(); const diffDays = Math.floor((todayDate.getTime() - last.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays === 1) { newStreak = current + 1; // Continue streak } else if (diffDays > 1) { newStreak = 1; // Reset streak } } else { newStreak = 1; // First solve } localStorage.setItem(`${DAILY_CONTEST_STORAGE_PREFIX}.streak.${userId}`, JSON.stringify({ current: newStreak, lastDate: today })); return newStreak; } const CONTEST_START_HOUR = 20; // 8 PM const CONTEST_DURATION_MINUTES = 70; // 1 hour 10 minutes const CONTEST_END_MINUTES = CONTEST_START_HOUR * 60 + CONTEST_DURATION_MINUTES; // 9:10 PM in minutes const REGISTRATION_CUTOFF_MINUTES = 30; // Close 30 min before contest (7:30 PM) function getContestStartTime(date: Date): Date { const start = new Date(date); start.setHours(CONTEST_START_HOUR, 0, 0, 0); return start; } function getContestEndTime(date: Date): Date { const end = new Date(date); end.setHours(CONTEST_START_HOUR, CONTEST_DURATION_MINUTES, 0, 0); return end; } function getRegistrationCutoffTime(date: Date): Date { const cutoff = getContestStartTime(date); cutoff.setMinutes(cutoff.getMinutes() - REGISTRATION_CUTOFF_MINUTES); return cutoff; } function isContestLive(now: Date): boolean { const start = getContestStartTime(now); const end = getContestEndTime(now); return now >= start && now < end; } function isRegistrationOpen(now: Date): boolean { const todayStart = getContestStartTime(now); const todayEnd = getContestEndTime(now); const cutoffTime = getRegistrationCutoffTime(now); // If we're in the contest window, registration is closed if (now >= todayStart && now < todayEnd) { return false; } // If we're past the cutoff time (7:30 PM) but before contest start, registration is closed if (now >= cutoffTime && now < todayStart) { return false; } // If contest has ended today, registration is open for next contest if (now >= todayEnd) { return true; } // If we're before cutoff time, registration is open if (now < cutoffTime) { return true; } return false; } function getNextContestTime(now: Date): Date { const todayStart = getContestStartTime(now); const todayEnd = getContestEndTime(now); if (now < todayEnd) { // Current or upcoming contest today return todayStart; } // Next contest is tomorrow at 8 PM const next = new Date(todayStart); next.setDate(next.getDate() + 1); return next; } export default function DailyContestPage({ user, solvedQuestionIds, onSelectQuestion }: DailyContestPageProps) { const now = useTime(); const dailyContest = useMemo(() => getDailyContest(now), [now]); const [solvedIds, setSolvedIds] = useState(() => readDailySolvedIds(user.id)); const [streak, setStreak] = useState(() => readDailyStreak(user.id)); const [joinedContestIds, setJoinedContestIds] = useState(() => readJoinedContestIds(user.id)); const solvedSet = useMemo(() => new Set(solvedQuestionIds), [solvedQuestionIds]); const contestLive = isContestLive(now); const isJoinedCurrentContest = joinedContestIds.includes(dailyContest.id); const canAccessQuestions = contestLive && isJoinedCurrentContest; const nextContestTime = getNextContestTime(now); const timeUntilContest = formatTimeRemaining(now, nextContestTime); const solvedCount = dailyContest.questions.filter(q => solvedIds.includes(q.id) || solvedSet.has(q.question.id) ).length; const isAllSolved = solvedCount === dailyContest.questions.length; const totalPoints = dailyContest.questions.reduce((sum, q) => sum + q.points, 0); useEffect(() => { setSolvedIds(readDailySolvedIds(user.id)); setStreak(readDailyStreak(user.id)); setJoinedContestIds(readJoinedContestIds(user.id)); }, [user.id, dailyContest.id]); const registrationOpen = isRegistrationOpen(now); const handleJoinContest = () => { if (registrationOpen && !isJoinedCurrentContest) { const updated = joinDailyContest(user.id, dailyContest.id); setJoinedContestIds(updated); } }; const handleSolve = (dailyQuestion: DailyContestQuestion) => { if (!canAccessQuestions) return; // Can't solve if not joined or contest not live const isAlreadySolved = solvedIds.includes(dailyQuestion.id) || solvedSet.has(dailyQuestion.question.id); if (isAlreadySolved) { onSelectQuestion(dailyQuestion.question, 'code'); return; } onSelectQuestion(dailyQuestion.question, 'code'); // Mark as solved after navigation setTimeout(() => { const updated = markDailySolved(user.id, dailyQuestion.id); setSolvedIds(updated); // Check if all solved to update streak const newSolvedCount = dailyContest.questions.filter(q => updated.includes(q.id) || solvedSet.has(q.question.id) ).length; if (newSolvedCount === dailyContest.questions.length) { const newStreak = updateDailyStreak(user.id); setStreak({ current: newStreak, lastDate: getActivityDateKey(new Date()) }); } }, 500); }; // Contest status logic const contestStatusLabel = contestLive ? isJoinedCurrentContest ? 'Contest Live' : 'Registration Closed' : isJoinedCurrentContest ? 'Registered' : registrationOpen ? 'Open until 7:30 PM' : 'Upcoming'; const contestStatusTone = contestLive ? isJoinedCurrentContest ? 'border-emerald-400/25 bg-emerald-400/15 text-emerald-100' : 'border-rose-400/25 bg-rose-400/12 text-rose-100' : isJoinedCurrentContest ? 'border-cyan-400/25 bg-cyan-400/12 text-cyan-100' : registrationOpen ? 'border-emerald-400/25 bg-emerald-400/12 text-emerald-100' : 'border-amber-400/25 bg-amber-400/12 text-amber-100'; const contestPrimaryAction = contestLive ? isJoinedCurrentContest ? 'Contest Running' : 'Registration Closed' : isJoinedCurrentContest ? 'Registered for Tonight' : registrationOpen ? 'Join Contest' : 'Registration Closed'; const contestWindowLabel = `Join by 7:30 PM • Contest 8:00-9:10 PM`; const statusTone = !contestLive && !isJoinedCurrentContest ? 'border-amber-400/25 bg-amber-400/15 text-amber-100' : !contestLive ? 'border-cyan-400/25 bg-cyan-400/15 text-cyan-100' : isAllSolved ? 'border-emerald-400/25 bg-emerald-400/15 text-emerald-100' : solvedCount > 0 ? 'border-amber-400/25 bg-amber-400/15 text-amber-100' : 'border-orange-400/25 bg-orange-400/15 text-orange-100'; return (
{/* Header */}

Daily Challenge

Daily Contest

{now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })}

{/* Simple Text Countdown */} {!contestLive && (

{registrationOpen ? 'Registration closes in' : 'Contest starts in'}

{formatTimeRemaining(now, registrationOpen ? getRegistrationCutoffTime(now) : nextContestTime)}

)} {contestLive && (

Contest ends in

{formatTimeRemaining(now, getContestEndTime(now))}

)}
{/* Stats Row */}
{/* Main Content - Problem Arena */}

Problem Arena

Problems of the Day

{/* Not Joined State */} {!isJoinedCurrentContest && (

Join to Unlock Questions

Join the daily contest to access today's coding problems. Registration opens after 9:10 PM and closes at 7:30 PM the next day.

)} {/* Questions Grid - Only show if joined */} {isJoinedCurrentContest && (
{dailyContest.questions.map((dailyQuestion) => { const isSolved = solvedIds.includes(dailyQuestion.id) || solvedSet.has(dailyQuestion.question.id); const canSolve = contestLive && !isSolved; return (
{/* Lock overlay when contest not live */} {!contestLive && (

Unlocks at 8:00 PM

)}
{dailyQuestion.label}

{dailyQuestion.question.title}

{dailyQuestion.question.category}

{!contestLive ? 'Locked' : dailyQuestion.question.difficulty}
+{dailyQuestion.points}
{!contestLive ? ( <> Locked ) : isSolved ? ( <> Solved ) : ( <> Ready )}
); })}
)} {/* Contest Info - 3 Beautiful Boxes */}
); } function StatCard({ label, value, hint, icon, grad, glow, badge, badgeClassName, index, }: { label: string; value: string; hint: string; icon: typeof Trophy; grad: string; glow: string; badge?: string; badgeClassName?: string; index: number; }) { const Icon = icon; return (
{badge ? ( {badge} ) : null}

{label}

{value}

{hint}

); } function RewardsStatCard({ label, icon, grad, glow, index, }: { label: string; icon: typeof Crown; grad: string; glow: string; index: number; }) { const Icon = icon; return (
Prize Pool

{label}

{/* Prize Tiers - Clear Position Labels */}
{/* 1st Place */}
1st Place
100
{/* 2nd Place */}
2nd Place
75
{/* 3rd Place */}
3rd Place
50
{/* 4th-10th */}
4-10
4th - 10th Place
5 each
); } function InfoCard({ icon, title, desc, variant = 'default' }: { icon: typeof Clock; title: string; desc: string; variant?: 'default' | 'success' | 'muted' }) { const Icon = icon; const iconBgClass = variant === 'success' ? 'bg-emerald-500/20 text-emerald-300 group-hover:bg-emerald-500/30' : variant === 'muted' ? 'bg-slate-600/30 text-slate-400 group-hover:bg-slate-600/40' : 'bg-slate-700/30 text-slate-300 group-hover:bg-slate-700/50'; return (

{title}

{desc}

); } function useTime() { const [now, setNow] = useState(new Date()); useEffect(() => { const timer = setInterval(() => setNow(new Date()), 1_000); return () => clearInterval(timer); }, []); return now; } function formatTimeRemaining(now: Date, target: Date): string { const diff = target.getTime() - now.getTime(); if (diff <= 0) return '00:00:00'; const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; }