Show three difficulty cards on daily challenge page

This commit is contained in:
2026-03-14 19:56:39 -07:00
parent f3c703d414
commit 222f40c902
+89 -64
View File
@@ -5,48 +5,62 @@ import { DifficultyBadge } from '@/components/ui/difficulty-badge';
import { Skeleton } from '@/components/ui/skeleton';
import { useGameStore } from '@/stores/game-store';
import PageLayout from '@/components/layout/PageLayout';
import { getTodaysChallenge, type DailyChallenge as DailyChallengeType } from '@/api/daily-challenges';
import { checkDailyCompletion } from '@/api/leaderboards';
import { getTodaysChallenges, type DailyChallenge as DailyChallengeType } from '@/api/daily-challenges';
import { checkDailyCompletion, type DailyCompletionStatus } from '@/api/leaderboards';
import { useAuthStore } from '@/stores/auth-store';
import { Loader2, Calendar, Trophy, Clock } from 'lucide-react';
type Difficulty = 'easy' | 'medium' | 'hard';
export default function DailyChallenge() {
const [challenge, setChallenge] = useState<DailyChallengeType | null>(null);
const [challenges, setChallenges] = useState<DailyChallengeType[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [alreadyCompleted, setAlreadyCompleted] = useState(false);
const [completed, setCompleted] = useState<Record<Difficulty, boolean>>({
easy: false,
medium: false,
hard: false,
});
const startGame = useGameStore((s) => s.startGame);
const isValidating = useGameStore((s) => s.isValidating);
const user = useAuthStore((s) => s.user);
const navigate = useNavigate();
useEffect(() => {
getTodaysChallenge()
.then((c) => {
setChallenge(c);
getTodaysChallenges()
.then((list) => {
setChallenges(list);
// Check localStorage for anonymous replay prevention
if (localStorage.getItem('daily-completed-' + c.date)) {
setAlreadyCompleted(true);
const localCompleted: Record<string, boolean> = {};
for (const c of list) {
const key = `daily-completed-${c.date}-${c.difficulty}`;
if (localStorage.getItem(key)) {
localCompleted[c.difficulty] = true;
}
}
setCompleted((prev) => ({ ...prev, ...localCompleted }));
// Check server for logged-in users (more reliable)
if (user) {
checkDailyCompletion()
.then(({ completed }) => {
if (completed) setAlreadyCompleted(true);
.then((status: DailyCompletionStatus) => {
setCompleted((prev) => ({
easy: prev.easy || status.easy,
medium: prev.medium || status.medium,
hard: prev.hard || status.hard,
}));
})
.catch(() => {
// Fall back to localStorage check only
});
}
})
.catch(() => setError('Failed to load today\'s challenge.'))
.catch(() => setError('Failed to load today\'s challenges.'))
.finally(() => setLoading(false));
}, [user]);
const handlePlay = async () => {
if (!challenge) return;
const handlePlay = async (challenge: DailyChallengeType) => {
await startGame(
{
movieA: { id: challenge.movieAId, title: challenge.movieATitle },
@@ -68,7 +82,6 @@ export default function DailyChallenge() {
};
const getLocalResetTime = () => {
// Next midnight UTC, displayed in the user's local timezone
const now = new Date();
const nextMidnightUtc = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
@@ -86,11 +99,11 @@ export default function DailyChallenge() {
<div className="space-y-2 text-center">
<div className="flex items-center justify-center gap-2">
<Calendar className="h-5 w-5 text-muted-foreground" />
<h1 className="text-2xl font-bold">Daily Challenge</h1>
<h1 className="text-2xl font-bold">Daily Challenges</h1>
</div>
{challenge && (
{challenges.length > 0 && (
<p className="text-sm text-muted-foreground">
{formatDate(challenge.date)}
{formatDate(challenges[0].date)}
</p>
)}
</div>
@@ -98,6 +111,8 @@ export default function DailyChallenge() {
{loading && (
<div className="w-full max-w-md space-y-4">
<Skeleton className="h-40 w-full" />
<Skeleton className="h-40 w-full" />
<Skeleton className="h-40 w-full" />
</div>
)}
@@ -105,57 +120,67 @@ export default function DailyChallenge() {
<p className="text-destructive">{error}</p>
)}
{challenge && (
<div className="w-full max-w-md space-y-6">
<div className="rounded-lg border border-border bg-card p-6">
<div className="flex items-center justify-between">
<DifficultyBadge difficulty={challenge.difficulty} />
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Trophy className="h-3.5 w-3.5" />
Par: {challenge.par} links
</div>
</div>
{challenges.length > 0 && (
<div className="w-full max-w-md space-y-4">
{challenges.map((challenge) => {
const diff = challenge.difficulty as Difficulty;
const isCompleted = completed[diff];
<div className="mt-4 space-y-3">
<div className="rounded-md bg-muted/50 p-3 text-center">
<p className="text-xs font-medium uppercase text-muted-foreground">
Movie A
</p>
<p className="text-lg font-semibold">{challenge.movieATitle}</p>
</div>
<div className="flex justify-center text-muted-foreground">
<span className="text-xl">&#8693;</span>
</div>
<div className="rounded-md bg-muted/50 p-3 text-center">
<p className="text-xs font-medium uppercase text-muted-foreground">
Movie B
</p>
<p className="text-lg font-semibold">{challenge.movieBTitle}</p>
</div>
</div>
return (
<div
key={challenge.id}
className="rounded-lg border border-border bg-card p-6"
>
<div className="flex items-center justify-between">
<DifficultyBadge difficulty={challenge.difficulty} />
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Trophy className="h-3.5 w-3.5" />
Par: {challenge.par} links
</div>
</div>
<Button
className="mt-6 w-full"
size="lg"
onClick={handlePlay}
disabled={isValidating || alreadyCompleted}
>
{alreadyCompleted ? (
'Already completed today\'s challenge'
) : isValidating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</>
) : (
'Play Today\'s Challenge'
)}
</Button>
</div>
<div className="mt-4 space-y-3">
<div className="rounded-md bg-muted/50 p-3 text-center">
<p className="text-xs font-medium uppercase text-muted-foreground">
Movie A
</p>
<p className="text-lg font-semibold">{challenge.movieATitle}</p>
</div>
<div className="flex justify-center text-muted-foreground">
<span className="text-xl">&#8693;</span>
</div>
<div className="rounded-md bg-muted/50 p-3 text-center">
<p className="text-xs font-medium uppercase text-muted-foreground">
Movie B
</p>
<p className="text-lg font-semibold">{challenge.movieBTitle}</p>
</div>
</div>
<Button
className="mt-6 w-full"
size="lg"
onClick={() => handlePlay(challenge)}
disabled={isValidating || isCompleted}
>
{isCompleted ? (
'Completed'
) : isValidating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</>
) : (
'Play'
)}
</Button>
</div>
);
})}
<div className="flex items-center justify-center gap-1.5 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>New challenge at midnight UTC ({getLocalResetTime()} local)</span>
<span>New challenges at midnight UTC ({getLocalResetTime()} local)</span>
</div>
</div>
)}