Show three difficulty cards on daily challenge page
This commit is contained in:
@@ -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">⇵</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">⇵</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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user