RYP / src /components /DailyContestPage.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
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<string[]>(() => 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 (
<div className="mx-auto flex h-full max-w-5xl flex-col gap-5 overflow-y-auto scrollbar-auto-hide text-white pb-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="shrink-0 flex flex-wrap items-start justify-between gap-4"
>
<div>
<p className="text-[10px] font-semibold uppercase tracking-[0.3em] text-orange-300/70">
Daily Challenge
</p>
<h1 className="mt-0.5 text-2xl font-bold tracking-tight text-white lg:text-3xl">
Daily Contest
</h1>
<p className="mt-1 text-xs text-slate-500">
{now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })}
</p>
</div>
<div className="flex flex-col items-end gap-2">
{/* Simple Text Countdown */}
{!contestLive && (
<div className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-2.5 text-right backdrop-blur-xl">
<p className="text-[10px] font-black uppercase tracking-[0.22em] text-slate-400">
{registrationOpen ? 'Registration closes in' : 'Contest starts in'}
</p>
<p className="mt-1 text-sm font-semibold text-white tabular-nums tracking-widest">
{formatTimeRemaining(now, registrationOpen ? getRegistrationCutoffTime(now) : nextContestTime)}
</p>
</div>
)}
{contestLive && (
<div className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-2.5 text-right backdrop-blur-xl">
<p className="text-[10px] font-black uppercase tracking-[0.22em] text-slate-400">Contest ends in</p>
<p className="mt-1 text-sm font-semibold text-white">
{formatTimeRemaining(now, getContestEndTime(now))}
</p>
</div>
)}
</div>
</motion.div>
{/* Stats Row */}
<div className="shrink-0 grid grid-cols-2 gap-4 lg:grid-cols-4">
<StatCard
label="Status"
value={contestStatusLabel}
hint={contestWindowLabel}
icon={contestLive ? (isJoinedCurrentContest ? CheckCircle2 : Lock) : (isJoinedCurrentContest ? Calendar : Lock)}
grad={contestLive
? (isJoinedCurrentContest ? 'from-emerald-400 to-cyan-500' : 'from-rose-400 to-red-500')
: (isJoinedCurrentContest ? 'from-cyan-400 to-blue-500' : 'from-amber-400 to-orange-500')}
glow={contestLive
? (isJoinedCurrentContest ? 'bg-emerald-500/10' : 'bg-rose-500/10')
: (isJoinedCurrentContest ? 'bg-cyan-500/10' : 'bg-amber-500/10')}
badge={contestStatusLabel}
badgeClassName={contestStatusTone}
index={0}
/>
<RewardsStatCard
label="Daily Rewards"
icon={Crown}
grad="from-amber-400 to-orange-500"
glow="bg-amber-500/10"
index={1}
/>
<StatCard
label="Points"
value={`+${totalPoints}`}
hint="For solving both"
icon={Trophy}
grad="from-violet-400 to-fuchsia-500"
glow="bg-violet-500/10"
index={2}
/>
<StatCard
label="Questions"
value={`${dailyContest.questions.length}`}
hint="Daily challenges"
icon={Target}
grad="from-cyan-400 to-blue-500"
glow="bg-cyan-500/10"
index={3}
/>
</div>
{/* Main Content - Problem Arena */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="shrink-0"
>
<div className="h-full rounded-2xl border border-white/[0.08] bg-[#0b111d]/95 p-5 shadow-xl backdrop-blur-sm">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.24em] text-slate-500">
Problem Arena
</p>
<h2 className="mt-1.5 text-xl font-black text-white">Problems of the Day</h2>
</div>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500/20 to-rose-500/10 text-orange-300">
<Calendar size={22} />
</div>
</div>
{/* Not Joined State */}
{!isJoinedCurrentContest && (
<div className="mt-6">
<div className="relative overflow-hidden rounded-2xl border border-amber-500/20 bg-[linear-gradient(180deg,rgba(251,191,36,0.08),rgba(251,191,36,0.02))] p-8">
<div className="absolute inset-0 bg-[linear-gradient(90deg,transparent,rgba(251,191,36,0.1),transparent)]" />
<div className="relative text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-2xl bg-amber-500/20 border border-amber-500/30">
<Lock size={28} className="text-amber-400" />
</div>
<h3 className="mt-4 text-lg font-bold text-white">Join to Unlock Questions</h3>
<p className="mt-2 text-sm text-slate-400 max-w-md mx-auto">
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.
</p>
<button
type="button"
onClick={handleJoinContest}
disabled={!registrationOpen}
className={cn(
'mt-5 inline-flex items-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold transition-all duration-200',
!registrationOpen
? 'bg-slate-700/50 text-slate-500 cursor-not-allowed'
: 'bg-gradient-to-r from-amber-500 to-orange-500 text-white hover:from-amber-400 hover:to-orange-400 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-amber-500/20'
)}
>
{!registrationOpen ? 'Registration Closed' : 'Join Contest'}
<ChevronRight size={16} />
</button>
</div>
</div>
</div>
)}
{/* Questions Grid - Only show if joined */}
{isJoinedCurrentContest && (
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{dailyContest.questions.map((dailyQuestion) => {
const isSolved = solvedIds.includes(dailyQuestion.id) || solvedSet.has(dailyQuestion.question.id);
const canSolve = contestLive && !isSolved;
return (
<div
key={dailyQuestion.id}
className={cn(
'group relative overflow-hidden rounded-2xl border p-5 shadow-lg transition-all duration-300',
!contestLive
? 'border-slate-500/20 bg-slate-500/5'
: isSolved
? 'border-emerald-400/20 bg-emerald-400/5 hover:border-emerald-400/30'
: 'border-white/[0.08] bg-[linear-gradient(180deg,rgba(16,24,38,0.88),rgba(10,15,28,0.92))] hover:border-white/[0.12] hover:shadow-xl'
)}
>
<div className="pointer-events-none absolute inset-x-0 top-0 h-[2px] bg-[linear-gradient(90deg,transparent,rgba(148,163,184,0.35),transparent)] opacity-80" />
{/* Lock overlay when contest not live */}
{!contestLive && (
<div className="absolute inset-0 flex items-center justify-center bg-[#0b111d]/60 backdrop-blur-[2px] z-10 rounded-2xl">
<div className="flex flex-col items-center gap-2">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-slate-700/50 border border-slate-600/50">
<Clock3 size={24} className="text-slate-400" />
</div>
<p className="text-xs font-semibold text-slate-400">Unlocks at 8:00 PM</p>
</div>
</div>
)}
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className={cn(
'flex h-12 w-12 items-center justify-center rounded-xl border text-base font-black shadow-sm',
!contestLive
? 'border-slate-600/30 bg-slate-700/20 text-slate-500'
: 'border-white/10 bg-white/[0.05] text-slate-100'
)}>
{dailyQuestion.label}
</div>
<div className="min-w-0">
<h3 className={cn('text-base font-bold truncate', !contestLive ? 'text-slate-500' : 'text-white')}>
{dailyQuestion.question.title}
</h3>
<p className="text-sm text-slate-400">{dailyQuestion.question.category}</p>
</div>
</div>
<span
className={cn(
'rounded-full px-2.5 py-1 text-xs font-semibold shrink-0',
!contestLive
? 'bg-slate-600/20 text-slate-500'
: dailyQuestion.question.difficulty === 'Easy'
? 'bg-emerald-400/10 text-emerald-200'
: dailyQuestion.question.difficulty === 'Medium'
? 'bg-amber-400/10 text-amber-200'
: 'bg-rose-400/10 text-rose-200'
)}
>
{!contestLive ? 'Locked' : dailyQuestion.question.difficulty}
</span>
</div>
<div className="mt-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
<Trophy size={14} className={!contestLive ? 'text-slate-600' : 'text-amber-300'} />
<span className={cn('text-sm font-semibold', !contestLive ? 'text-slate-500' : 'text-slate-300')}>
+{dailyQuestion.points}
</span>
</div>
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-semibold',
!contestLive
? 'border-slate-500/20 bg-slate-500/10 text-slate-400'
: isSolved
? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-100'
: 'border-orange-400/20 bg-orange-400/10 text-orange-100'
)}
>
{!contestLive ? (
<>
<Lock size={12} />
Locked
</>
) : isSolved ? (
<>
<CheckCircle2 size={12} />
Solved
</>
) : (
<>
<PlayCircle size={12} />
Ready
</>
)}
</span>
</div>
<button
type="button"
onClick={() => handleSolve(dailyQuestion)}
disabled={!canSolve}
className={cn(
'inline-flex items-center gap-1.5 rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-200',
!contestLive
? 'bg-slate-700/30 text-slate-500 cursor-not-allowed'
: canSolve
? 'bg-gradient-to-r from-orange-500 to-rose-500 text-white hover:from-orange-400 hover:to-rose-400 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-orange-500/20'
: 'bg-slate-700/50 text-slate-300 hover:bg-slate-700/70'
)}
>
{!contestLive ? 'Locked' : isSolved ? 'View' : 'Solve'}
<ChevronRight size={14} />
</button>
</div>
</div>
);
})}
</div>
)}
{/* Contest Info - 3 Beautiful Boxes */}
<div className="mt-5 grid grid-cols-3 gap-3">
<InfoCard
icon={isJoinedCurrentContest ? CheckCircle2 : Lock}
title={isJoinedCurrentContest ? 'Registered' : 'Not Registered'}
desc={isJoinedCurrentContest ? 'Ready to compete at 8 PM' : 'Join before 7:30 PM'}
variant={isJoinedCurrentContest ? 'success' : 'muted'}
/>
<InfoCard
icon={Trophy}
title="Points"
desc={`${totalPoints} points available`}
variant="default"
/>
<InfoCard
icon={Target}
title="Questions"
desc={`${dailyContest.questions.length} challenges daily`}
variant="default"
/>
</div>
</div>
</motion.div>
</div>
);
}
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 (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay: index * 0.06 }}
className="group relative flex h-full min-h-[100px] flex-col justify-between overflow-hidden rounded-2xl border border-white/[0.08] bg-[linear-gradient(180deg,rgba(13,19,34,0.96),rgba(10,15,28,0.92))] p-4 shadow-lg backdrop-blur-xl transition-all duration-300 hover:border-white/[0.12] hover:shadow-xl"
>
<div className={cn('pointer-events-none absolute -right-4 -top-4 h-24 w-24 rounded-full opacity-25 blur-2xl transition-opacity duration-300 group-hover:opacity-40', glow)} />
<div className={cn('absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r', grad)} />
<div className="flex items-center justify-between gap-3">
<div className={cn('flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg opacity-90 transition-transform duration-300 group-hover:scale-105', grad)}>
<Icon size={18} className="text-white" />
</div>
{badge ? (
<span className={cn('rounded-full border px-2.5 py-1 text-[9px] font-black uppercase tracking-[0.18em] transition-colors duration-200', badgeClassName)}>
{badge}
</span>
) : null}
</div>
<div className="mt-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-500">{label}</p>
<p className="mt-1.5 text-xl font-bold tracking-tight text-white">{value}</p>
<p className="mt-1 text-[11px] text-slate-400">{hint}</p>
</div>
</motion.div>
);
}
function RewardsStatCard({
label,
icon,
grad,
glow,
index,
}: {
label: string;
icon: typeof Crown;
grad: string;
glow: string;
index: number;
}) {
const Icon = icon;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay: index * 0.06 }}
className="group relative flex h-full min-h-[100px] flex-col overflow-hidden rounded-2xl border border-white/[0.08] bg-[linear-gradient(180deg,rgba(13,19,34,0.96),rgba(10,15,28,0.92))] p-3.5 shadow-lg backdrop-blur-xl transition-all duration-300 hover:border-white/[0.12] hover:shadow-xl"
>
<div className={cn('pointer-events-none absolute -right-4 -top-4 h-24 w-24 rounded-full opacity-25 blur-2xl transition-opacity duration-300 group-hover:opacity-40', glow)} />
<div className={cn('absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r', grad)} />
<div className="flex items-center justify-between gap-2">
<div className={cn('flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br shadow-lg opacity-90 transition-transform duration-300 group-hover:scale-105', grad)}>
<Icon size={16} className="text-white" />
</div>
<span className="rounded-full border border-amber-500/25 bg-amber-500/12 px-2 py-0.5 text-[9px] font-black uppercase tracking-[0.15em] text-amber-100">
Prize Pool
</span>
</div>
<div className="mt-1.5">
<p className="text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-500">{label}</p>
{/* Prize Tiers - Clear Position Labels */}
<div className="mt-2 space-y-1.5">
{/* 1st Place */}
<div className="flex items-center justify-between rounded-lg bg-gradient-to-r from-amber-500/15 to-amber-500/5 border border-amber-500/25 px-2.5 py-1.5">
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 items-center justify-center rounded bg-amber-500/25">
<Crown size={12} className="text-amber-300" />
</div>
<span className="text-xs font-semibold text-amber-100">1st Place</span>
</div>
<div className="flex items-center gap-1">
<span className="text-sm font-bold text-amber-200">100</span>
<Coins size={11} className="text-amber-400" />
</div>
</div>
{/* 2nd Place */}
<div className="flex items-center justify-between rounded-lg bg-gradient-to-r from-slate-400/15 to-slate-400/5 border border-slate-400/25 px-2.5 py-1.5">
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 items-center justify-center rounded bg-slate-400/25">
<Medal size={12} className="text-slate-300" />
</div>
<span className="text-xs font-semibold text-slate-200">2nd Place</span>
</div>
<div className="flex items-center gap-1">
<span className="text-sm font-bold text-slate-200">75</span>
<Coins size={11} className="text-slate-400" />
</div>
</div>
{/* 3rd Place */}
<div className="flex items-center justify-between rounded-lg bg-gradient-to-r from-orange-500/15 to-orange-500/5 border border-orange-500/25 px-2.5 py-1.5">
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 items-center justify-center rounded bg-orange-500/25">
<Target size={12} className="text-orange-300" />
</div>
<span className="text-xs font-semibold text-orange-100">3rd Place</span>
</div>
<div className="flex items-center gap-1">
<span className="text-sm font-bold text-orange-200">50</span>
<Coins size={11} className="text-orange-400" />
</div>
</div>
{/* 4th-10th */}
<div className="flex items-center justify-between rounded-lg bg-gradient-to-r from-slate-600/15 to-slate-600/5 border border-slate-600/25 px-2.5 py-1.5">
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 items-center justify-center rounded bg-slate-600/25">
<span className="text-[9px] font-bold text-slate-400">4-10</span>
</div>
<span className="text-xs font-semibold text-slate-300">4th - 10th Place</span>
</div>
<div className="flex items-center gap-1">
<span className="text-sm font-bold text-slate-200">5</span>
<span className="text-[10px] text-slate-400">each</span>
<Coins size={11} className="text-slate-500" />
</div>
</div>
</div>
</div>
</motion.div>
);
}
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 (
<div className="group flex h-full items-center gap-3 rounded-xl border border-white/[0.08] bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)] transition-all duration-200 hover:border-white/[0.12] hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.03))]">
<div className={cn('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-colors', iconBgClass)}>
<Icon size={16} />
</div>
<div className="min-w-0">
<p className="text-sm font-semibold text-white truncate">{title}</p>
<p className="text-xs text-slate-400 leading-relaxed">{desc}</p>
</div>
</div>
);
}
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')}`;
}