feat: add daily challenge and blitz mode (2min speedrun)
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const puzzle = await prisma.dailyPuzzle.findUnique({ where: { date } });
|
||||
if (!puzzle) return NextResponse.json([]);
|
||||
|
||||
const results = await prisma.dailyResult.findMany({
|
||||
where: { puzzleId: puzzle.id, won: true },
|
||||
include: { user: { select: { name: true } } },
|
||||
orderBy: [{ clicks: "asc" }, { timeSeconds: "asc" }],
|
||||
take: 20,
|
||||
});
|
||||
|
||||
return NextResponse.json(results.map((r, i) => ({
|
||||
rank: i + 1,
|
||||
name: r.user.name,
|
||||
clicks: r.clicks,
|
||||
timeSeconds: r.timeSeconds,
|
||||
userId: r.userId,
|
||||
})));
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "../../../auth";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { pickTwoArticles } from "../../../lib/wiki";
|
||||
|
||||
function todayKey() {
|
||||
return new Date().toISOString().slice(0, 10); // "YYYY-MM-DD"
|
||||
}
|
||||
|
||||
async function getOrCreatePuzzle() {
|
||||
const date = todayKey();
|
||||
const existing = await prisma.dailyPuzzle.findUnique({ where: { date } });
|
||||
if (existing) return existing;
|
||||
|
||||
const { start, target } = await pickTwoArticles();
|
||||
return prisma.dailyPuzzle.create({ data: { date, startArticle: start, targetArticle: target } });
|
||||
}
|
||||
|
||||
// GET — retourne le puzzle du jour + si l'utilisateur a déjà joué
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
const puzzle = await getOrCreatePuzzle();
|
||||
|
||||
let myResult = null;
|
||||
if (session?.user?.id) {
|
||||
myResult = await prisma.dailyResult.findUnique({
|
||||
where: { puzzleId_userId: { puzzleId: puzzle.id, userId: session.user.id } },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
puzzle: { id: puzzle.id, date: puzzle.date, startArticle: puzzle.startArticle, targetArticle: puzzle.targetArticle },
|
||||
alreadyPlayed: !!myResult,
|
||||
myResult: myResult ? { clicks: myResult.clicks, timeSeconds: myResult.timeSeconds, won: myResult.won, path: JSON.parse(myResult.path) } : null,
|
||||
});
|
||||
}
|
||||
|
||||
// POST — soumettre un résultat
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non connecté" }, { status: 401 });
|
||||
|
||||
const { puzzleId, path, clicks, timeSeconds, won } = await req.json() as {
|
||||
puzzleId: string; path: string[]; clicks: number; timeSeconds: number; won: boolean;
|
||||
};
|
||||
|
||||
// Vérifier que le puzzle est bien celui du jour
|
||||
const puzzle = await prisma.dailyPuzzle.findUnique({ where: { id: puzzleId } });
|
||||
if (!puzzle || puzzle.date !== todayKey()) {
|
||||
return NextResponse.json({ error: "Puzzle invalide" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Upsert — on n'enregistre qu'une fois
|
||||
const result = await prisma.dailyResult.upsert({
|
||||
where: { puzzleId_userId: { puzzleId, userId: session.user.id } },
|
||||
create: { puzzleId, userId: session.user.id, path: JSON.stringify(path), clicks, timeSeconds, won },
|
||||
update: {},
|
||||
});
|
||||
|
||||
return NextResponse.json({ id: result.id });
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { useBlitzGame } from "../../lib/useBlitzGame";
|
||||
import { ArticleView } from "./ArticleView";
|
||||
|
||||
function fmtLeft(s: number): string {
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = Math.floor(s % 60);
|
||||
return `${m}:${String(sec).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function BlitzScreen({ onBack }: { onBack: () => void }) {
|
||||
const game = useBlitzGame();
|
||||
|
||||
const btnPrimary = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors";
|
||||
const btnGhost = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors";
|
||||
|
||||
const danger = game.timeLeft < 30;
|
||||
|
||||
if (game.phase === "setup") return (
|
||||
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col items-center justify-center px-4 py-8 gap-5 max-w-sm mx-auto text-center">
|
||||
<button className="self-start min-h-9 px-3 rounded-lg text-sm font-semibold bg-[#242424] border border-[#2e2e2e] hover:bg-[#1a1a1a] cursor-pointer" onClick={onBack}>
|
||||
Retour
|
||||
</button>
|
||||
<div className="text-5xl">⚡</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-black">Mode Blitz</h2>
|
||||
<p className="text-[#888] text-sm sm:text-base leading-relaxed">
|
||||
Tu as <span className="text-[#f0f0f0] font-bold">2 minutes</span> pour visiter un maximum d'articles Wikipedia différents en cliquant sur les liens. Ton score = nombre d'articles uniques visités.
|
||||
</p>
|
||||
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
||||
{game.loading ? "Préparation..." : "Lancer le chrono !"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (game.phase === "ended") return (
|
||||
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex items-center justify-center px-4 py-8">
|
||||
<div className="w-full max-w-sm sm:max-w-md text-center flex flex-col gap-5">
|
||||
<div className="text-5xl sm:text-6xl leading-none">⚡</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-black">Temps écoulé !</h2>
|
||||
<div className="flex gap-8 justify-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-4xl font-black text-[#7c3aed]">{game.visited.length}</span>
|
||||
<span className="text-xs text-[#888]">articles visités</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-4xl font-black text-[#7c3aed]">{game.clicks}</span>
|
||||
<span className="text-xs text-[#888]">clics</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#1a1a1a] rounded-xl px-4 py-3 flex flex-wrap gap-1 text-xs text-[#888] text-left max-h-48 overflow-y-auto">
|
||||
{game.visited.map((t, i) => (
|
||||
<span key={i} className="flex items-center gap-1">
|
||||
{i > 0 && <span className="text-[#555]">›</span>}
|
||||
<span>{t}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
||||
Rejouer
|
||||
</button>
|
||||
<button className={btnGhost} onClick={onBack}>Accueil</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Playing
|
||||
return (
|
||||
<div className="min-h-dvh w-full bg-[#0f0f0f] flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto bg-white">
|
||||
<div className="article-container">
|
||||
{game.title && <h1 className="article-title">{game.title}</h1>}
|
||||
{game.loading && (
|
||||
<div className="flex items-center gap-3 py-10 px-4 text-[#888]"><div className="spinner" /> Chargement...</div>
|
||||
)}
|
||||
{game.loadError && (
|
||||
<div className="flex flex-col items-center gap-4 py-10 px-4 text-center">
|
||||
<p className="text-[#888] text-sm">{game.loadError}</p>
|
||||
<button className="min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#2563eb] text-white cursor-pointer" onClick={() => game.retryLoad()}>
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!game.loading && !game.loadError && game.html && (
|
||||
<ArticleView html={game.html} onNavigate={game.navigate} disabled={game.loading} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-[#0f0f0f]/97 backdrop-blur-sm border-t border-[#2e2e2e] flex items-center justify-between px-3 sm:px-4 py-2 gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span className="text-[8px] font-bold uppercase tracking-wider text-[#888]">⚡ Articles</span>
|
||||
<span className="text-sm font-black text-[#7c3aed] tabular-nums">{game.visited.length}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span className="text-[8px] font-bold uppercase tracking-wider text-[#888]">Clics</span>
|
||||
<span className="text-sm font-black tabular-nums">{game.clicks}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timer */}
|
||||
<div className={`text-2xl sm:text-3xl font-black tabular-nums transition-colors ${danger ? "text-red-400" : "text-[#f0f0f0]"}`}>
|
||||
{fmtLeft(game.timeLeft)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-[#888] text-right max-w-28 truncate">
|
||||
{game.visited.length > 0 ? game.visited[game.visited.length - 1] : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDailyGame } from "../../lib/useDailyGame";
|
||||
import { fmt } from "../../lib/utils";
|
||||
import { ArticleView } from "./ArticleView";
|
||||
import { Breadcrumbs } from "./Breadcrumbs";
|
||||
|
||||
const MEDALS = ["🥇", "🥈", "🥉"];
|
||||
|
||||
type LeaderboardEntry = { rank: number; name: string; clicks: number; timeSeconds: number; userId: string };
|
||||
|
||||
export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; currentUserId?: string }) {
|
||||
const game = useDailyGame();
|
||||
const breadcrumbEndRef = useRef<HTMLDivElement>(null);
|
||||
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
||||
const [loadingLb, setLoadingLb] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
breadcrumbEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "end" });
|
||||
}, [game.history]);
|
||||
|
||||
function fetchLeaderboard() {
|
||||
setLoadingLb(true);
|
||||
fetch("/api/daily/leaderboard")
|
||||
.then((r) => r.json())
|
||||
.then(setLeaderboard)
|
||||
.finally(() => setLoadingLb(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (game.phase === "won" || game.phase === "gave_up" || game.phase === "already_played") {
|
||||
fetchLeaderboard();
|
||||
}
|
||||
}, [game.phase]);
|
||||
|
||||
const btnPrimary = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors";
|
||||
const btnGhost = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors";
|
||||
|
||||
// Loading initial
|
||||
if (game.phase === "loading") return (
|
||||
<div className="min-h-dvh bg-[#0f0f0f] flex items-center justify-center">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
);
|
||||
|
||||
// Erreur initiale
|
||||
if (game.loadError && game.phase === "loading") return (
|
||||
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] flex flex-col items-center justify-center gap-4 px-4">
|
||||
<p className="text-[#888]">{game.loadError}</p>
|
||||
<button className={btnGhost} style={{ width: "auto", padding: "0 20px" }} onClick={onBack}>Retour</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Résultat (won / gave_up / already_played)
|
||||
if (game.phase === "won" || game.phase === "gave_up" || game.phase === "already_played") {
|
||||
const result = game.myResult;
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col max-w-lg mx-auto px-4 py-8 gap-6">
|
||||
<button className="self-start min-h-9 px-3 rounded-lg text-sm font-semibold bg-[#242424] border border-[#2e2e2e] hover:bg-[#1a1a1a] cursor-pointer" onClick={onBack}>
|
||||
← Retour
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-bold text-[#888] uppercase tracking-widest mb-1">Défi du jour — {game.puzzle?.date}</div>
|
||||
<div className="text-2xl sm:text-3xl font-black mb-1">
|
||||
{game.phase === "won" ? "🎉 Réussi !" : game.phase === "gave_up" ? "😔 Abandonné" : "✅ Déjà joué"}
|
||||
</div>
|
||||
<div className="text-sm text-[#888]">
|
||||
<span className="text-[#f0f0f0] font-semibold">{game.puzzle?.startArticle}</span>
|
||||
<span className="mx-2">→</span>
|
||||
<span className="text-[#7c3aed] font-semibold">{game.puzzle?.targetArticle}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result && (
|
||||
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-col gap-3">
|
||||
<div className="flex justify-center gap-8">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-black text-[#7c3aed]">{result.clicks}</div>
|
||||
<div className="text-xs text-[#888]">clics</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-black text-[#7c3aed]">{fmt(result.timeSeconds)}</div>
|
||||
<div className="text-xs text-[#888]">temps</div>
|
||||
</div>
|
||||
</div>
|
||||
{result.path.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 text-xs text-[#888] pt-3 border-t border-[#2e2e2e]">
|
||||
{result.path.map((t, i) => (
|
||||
<span key={i} className="flex items-center gap-0.5">
|
||||
{i > 0 && <span className="text-[#555]">›</span>}
|
||||
<span className={i === result.path.length - 1 && result.won ? "text-[#16a34a] font-semibold" : ""}>{t}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Classement du jour */}
|
||||
<div>
|
||||
<h3 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-3">Classement du jour</h3>
|
||||
{loadingLb ? (
|
||||
<div className="flex justify-center py-6"><div className="spinner" /></div>
|
||||
) : leaderboard.length === 0 ? (
|
||||
<p className="text-sm text-[#888] text-center py-4">Aucun résultat pour l'instant.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{leaderboard.map((entry, i) => (
|
||||
<div key={i} className={`flex items-center gap-3 px-3.5 py-2.5 rounded-xl border ${entry.userId === currentUserId ? "border-[#7c3aed] bg-[#7c3aed]/8" : "border-[#2e2e2e] bg-[#1a1a1a]"}`}>
|
||||
<span className="text-lg w-7 text-center shrink-0">{MEDALS[i] ?? `#${entry.rank}`}</span>
|
||||
<span className="flex-1 font-semibold text-sm truncate">{entry.name}</span>
|
||||
<span className="text-xs text-[#888] tabular-nums shrink-0">{entry.clicks} clics · {fmt(entry.timeSeconds)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className={btnGhost} onClick={onBack}>Retour à l'accueil</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Playing
|
||||
return (
|
||||
<div className="min-h-dvh w-full bg-[#0f0f0f] flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto bg-white">
|
||||
<div className="article-container">
|
||||
{game.title && <h1 className="article-title">{game.title}</h1>}
|
||||
{game.loading && (
|
||||
<div className="flex items-center gap-3 py-10 px-4 text-[#888]"><div className="spinner" /> Chargement...</div>
|
||||
)}
|
||||
{game.loadError && (
|
||||
<div className="flex flex-col items-center gap-4 py-10 px-4 text-center">
|
||||
<p className="text-[#888] text-sm">{game.loadError}</p>
|
||||
<button className="min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#2563eb] text-white cursor-pointer" onClick={() => game.retryLoad()}>
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!game.loading && !game.loadError && game.html && (
|
||||
<ArticleView html={game.html} onNavigate={game.navigate} disabled={game.loading} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-[#0f0f0f]/97 backdrop-blur-sm border-t border-[#2e2e2e] flex items-center justify-between px-3 sm:px-4 py-2 gap-2">
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[8px] font-bold uppercase tracking-widest text-[#888] shrink-0">🗓 Défi du jour · cible</span>
|
||||
<span className="text-xs font-black text-[#7c3aed] truncate">{game.puzzle?.targetArticle}</span>
|
||||
</div>
|
||||
<Breadcrumbs history={game.history} endRef={breadcrumbEndRef} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex flex-col items-center gap-0.5 min-w-10">
|
||||
<span className="text-[8px] font-bold tracking-wider text-[#888] uppercase">Clics</span>
|
||||
<span className="text-xs font-black tabular-nums">{game.clicks}</span>
|
||||
</div>
|
||||
{game.canGoBack && (
|
||||
<button className="min-h-8 px-2 rounded-lg text-xs font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] disabled:opacity-50 cursor-pointer" onClick={game.goBack} disabled={game.loading}>
|
||||
← +1
|
||||
</button>
|
||||
)}
|
||||
<button className="min-h-8 px-2 rounded-lg text-xs font-semibold bg-red-950/40 border border-red-900 text-red-400 hover:bg-red-900/40 cursor-pointer" onClick={game.giveUp}>
|
||||
Abandonner
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,12 +18,14 @@ type HomeScreenProps = {
|
||||
onShowAuth: () => void;
|
||||
onShowProfile: () => void;
|
||||
onShowLeaderboard: () => void;
|
||||
onDaily: () => void;
|
||||
onBlitz: () => void;
|
||||
};
|
||||
|
||||
export function HomeScreen({
|
||||
playerName, setPlayerName, joinCode, setJoinCode,
|
||||
error, setError, loading, onCreateRoom, onJoinRoom, onSolo,
|
||||
session, onShowAuth, onShowProfile, onShowLeaderboard,
|
||||
session, onShowAuth, onShowProfile, onShowLeaderboard, onDaily, onBlitz,
|
||||
}: HomeScreenProps) {
|
||||
const btnGhost = "min-h-9 px-3 rounded-lg text-xs sm:text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer whitespace-nowrap";
|
||||
|
||||
@@ -114,7 +116,19 @@ export function HomeScreen({
|
||||
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"
|
||||
onClick={onSolo}
|
||||
>
|
||||
Jouer en solo
|
||||
🎯 Jouer en solo
|
||||
</button>
|
||||
<button
|
||||
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"
|
||||
onClick={onDaily}
|
||||
>
|
||||
🗓 Défi du jour
|
||||
</button>
|
||||
<button
|
||||
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"
|
||||
onClick={onBlitz}
|
||||
>
|
||||
⚡ Mode Blitz — 2 min
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,8 @@ import { GameScreen } from "./GameScreen";
|
||||
import { AuthModal } from "./AuthModal";
|
||||
import { ProfileScreen } from "./ProfileScreen";
|
||||
import { LeaderboardScreen } from "./LeaderboardScreen";
|
||||
import { DailyScreen } from "./DailyScreen";
|
||||
import { BlitzScreen } from "./BlitzScreen";
|
||||
|
||||
type Props = {
|
||||
screen: Screen;
|
||||
@@ -46,6 +48,14 @@ export function ScreenRouter({
|
||||
<LeaderboardScreen onBack={() => setScreen("home")} />
|
||||
);
|
||||
|
||||
if (screen === "daily") return (
|
||||
<DailyScreen onBack={() => setScreen("home")} currentUserId={session?.user?.id ?? undefined} />
|
||||
);
|
||||
|
||||
if (screen === "blitz") return (
|
||||
<BlitzScreen onBack={() => setScreen("home")} />
|
||||
);
|
||||
|
||||
if (screen === "home") return (
|
||||
<>
|
||||
<HomeScreen
|
||||
@@ -59,6 +69,8 @@ export function ScreenRouter({
|
||||
onShowAuth={() => setShowAuth(true)}
|
||||
onShowProfile={() => setScreen("profile")}
|
||||
onShowLeaderboard={() => setScreen("leaderboard")}
|
||||
onDaily={() => setScreen("daily")}
|
||||
onBlitz={() => setScreen("blitz")}
|
||||
/>
|
||||
{showAuth && <AuthModal onClose={() => setShowAuth(false)} onSuccess={() => setShowAuth(false)} />}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user