feat: add about page, global stats counter, and blitz screen updates
- Create about page with game modes, technologies, and paliers - Add stats API endpoint for global game counter - Update home screen with emoji-based tier system (🌱-🚀) - Rewrite BlitzScreen to match new puzzle-based win/lose phases - Add simple-icons package for tech stack icons - Update metadata with wikirush.xyz domain - Fix em dashes to hyphens throughout codebase - Update footer with privacy and about links
This commit is contained in:
@@ -0,0 +1,192 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "À propos - WikiRush",
|
||||||
|
description: "WikiRush, le jeu de navigation Wikipedia.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] px-4 py-12 flex flex-col items-center">
|
||||||
|
<div className="w-full max-w-xl flex flex-col gap-10">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium
|
||||||
|
bg-[#1f1f1f] border border-[#2e2e2e] text-[#e5e5e5]
|
||||||
|
hover:bg-[#2a2a2a] hover:border-[#3a3a3a]
|
||||||
|
transition-all duration-200"
|
||||||
|
>
|
||||||
|
<span className="text-base">←</span>
|
||||||
|
Retour
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Image
|
||||||
|
src="/wikirush.png"
|
||||||
|
alt="WikiRush"
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
className="w-24 h-auto mix-blend-screen"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-[#888] text-sm sm:text-base leading-relaxed max-w-md">
|
||||||
|
Le jeu de navigation Wikipedia. Pars d'un article, atteins la
|
||||||
|
cible en cliquant sur les liens. Simple en apparence, redoutablement
|
||||||
|
stratégique.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modes */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-xs font-bold uppercase tracking-widest text-[#555]">
|
||||||
|
Modes de jeu
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: "🎯",
|
||||||
|
name: "Solo",
|
||||||
|
desc: "À ton rythme, chrono et compteur de clics pour battre tes records.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "⚔️",
|
||||||
|
name: "Multijoueur",
|
||||||
|
desc: "Affronte d'autres joueurs en temps réel. Le premier à la cible gagne.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "🗓",
|
||||||
|
name: "Défi du jour",
|
||||||
|
desc: "Un puzzle identique pour tout le monde. Classement journalier.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "⚡",
|
||||||
|
name: "Mode Blitz",
|
||||||
|
desc: "2 minutes pour atteindre la cible. Chaque seconde compte.",
|
||||||
|
},
|
||||||
|
].map(({ icon, name, desc }) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<div className="text-2xl">{icon}</div>
|
||||||
|
<div className="font-bold text-sm">{name}</div>
|
||||||
|
<div className="text-[#888] text-xs leading-relaxed">
|
||||||
|
{desc}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stack */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-xs font-bold uppercase tracking-widest text-[#555]">
|
||||||
|
Technologies
|
||||||
|
</h2>
|
||||||
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-wrap gap-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
name: "Next.js",
|
||||||
|
color: "#fff",
|
||||||
|
path: "M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "React",
|
||||||
|
color: "#61DAFB",
|
||||||
|
path: "M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tailwind CSS",
|
||||||
|
color: "#06B6D4",
|
||||||
|
path: "M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Prisma",
|
||||||
|
color: "#fff",
|
||||||
|
path: "M21.8068 18.2848L13.5528.7565c-.207-.4382-.639-.7273-1.1286-.7541-.5023-.0293-.9523.213-1.2062.6253L2.266 15.1271c-.2773.4518-.2718 1.0091.0158 1.4555l4.3759 6.7786c.2608.4046.7127.6388 1.1823.6388.1332 0 .267-.0188.3987-.0577l12.7019-3.7568c.3891-.1151.7072-.3904.8737-.7553s.1633-.7828-.0075-1.1454zm-1.8481.7519L9.1814 22.2242c-.3292.0975-.6448-.1873-.5756-.5194l3.8501-18.4386c.072-.3448.5486-.3996.699-.0803l7.1288 15.138c.1344.2856-.019.6224-.325.7128z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SQLite",
|
||||||
|
color: "#003B57",
|
||||||
|
path: "M21.678.521c-1.032-.92-2.28-.55-3.513.544a8.71 8.71 0 0 0-.547.535c-2.109 2.237-4.066 6.38-4.674 9.544.237.48.422 1.093.544 1.561a13.044 13.044 0 0 1 .164.703s-.019-.071-.096-.296l-.05-.146a1.689 1.689 0 0 0-.033-.08c-.138-.32-.518-.995-.686-1.289-.143.423-.27.818-.376 1.176.484.884.778 2.4.778 2.4s-.025-.099-.147-.442c-.107-.303-.644-1.244-.772-1.464-.217.804-.304 1.346-.226 1.478.152.256.296.698.422 1.186.286 1.1.485 2.44.485 2.44l.017.224a22.41 22.41 0 0 0 .056 2.748c.095 1.146.273 2.13.5 2.657l.155-.084c-.334-1.038-.47-2.399-.41-3.967.09-2.398.642-5.29 1.661-8.304 1.723-4.55 4.113-8.201 6.3-9.945-1.993 1.8-4.692 7.63-5.5 9.788-.904 2.416-1.545 4.684-1.931 6.857.666-2.037 2.821-2.912 2.821-2.912s1.057-1.304 2.292-3.166c-.74.169-1.955.458-2.362.629-.6.251-.762.337-.762.337s1.945-1.184 3.613-1.72C21.695 7.9 24.195 2.767 21.678.521m-18.573.543A1.842 1.842 0 0 0 1.27 2.9v16.608a1.84 1.84 0 0 0 1.835 1.834h9.418a22.953 22.953 0 0 1-.052-2.707c-.006-.062-.011-.141-.016-.2a27.01 27.01 0 0 0-.473-2.378c-.121-.47-.275-.898-.369-1.057-.116-.197-.098-.31-.097-.432 0-.12.015-.245.037-.386a9.98 9.98 0 0 1 .234-1.045l.217-.028c-.017-.035-.014-.065-.031-.097l-.041-.381a32.8 32.8 0 0 1 .382-1.194l.2-.019c-.008-.016-.01-.038-.018-.053l-.043-.316c.63-3.28 2.587-7.443 4.8-9.791.066-.069.133-.128.198-.194Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wikipedia",
|
||||||
|
color: "#fff",
|
||||||
|
path: "M12.09 13.119c-.936 1.932-2.217 4.548-2.853 5.728-.616 1.074-1.127.931-1.532.029-1.406-3.321-4.293-9.144-5.651-12.409-.251-.601-.441-.987-.619-1.139-.181-.15-.554-.24-1.122-.271C.103 5.033 0 4.982 0 4.898v-.455l.052-.045c.924-.005 5.401 0 5.401 0l.051.045v.434c0 .119-.075.176-.225.176l-.564.031c-.485.029-.727.164-.727.436 0 .135.053.33.166.601 1.082 2.646 4.818 10.521 4.818 10.521l.136.046 2.411-4.81-.482-1.067-1.658-3.264s-.318-.654-.428-.872c-.728-1.443-.712-1.518-1.447-1.617-.207-.023-.313-.05-.313-.149v-.468l.06-.045h4.292l.113.037v.451c0 .105-.076.15-.227.15l-.308.047c-.792.061-.661.381-.136 1.422l1.582 3.252 1.758-3.504c.293-.64.233-.801.111-.947-.07-.084-.305-.22-.812-.24l-.201-.021c-.052 0-.098-.015-.145-.051-.045-.031-.067-.076-.067-.129v-.427l.061-.045c1.247-.008 4.043 0 4.043 0l.059.045v.436c0 .121-.059.178-.193.178-.646.03-.782.095-1.023.439-.12.186-.375.589-.646 1.039l-2.301 4.273-.065.135 2.792 5.712.17.048 4.396-10.438c.154-.422.129-.722-.064-.895-.197-.172-.346-.273-.857-.295l-.42-.016c-.061 0-.105-.014-.152-.045-.043-.029-.072-.075-.072-.119v-.436l.059-.045h4.961l.041.045v.437c0 .119-.074.18-.209.18-.648.03-1.127.18-1.443.421-.314.255-.557.616-.736 1.067 0 0-4.043 9.258-5.426 12.339-.525 1.007-1.053.917-1.503-.031-.571-1.171-1.773-3.786-2.646-5.71l.053-.036z",
|
||||||
|
},
|
||||||
|
].map(({ name, color, path }) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-[#242424] border border-[#2e2e2e]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="w-4 h-4 shrink-0"
|
||||||
|
style={{ fill: color }}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d={path} />
|
||||||
|
</svg>
|
||||||
|
<span className="text-xs font-medium text-[#aaa]">{name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Paliers */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-xs font-bold uppercase tracking-widest text-[#555]">
|
||||||
|
Paliers du compteur total
|
||||||
|
</h2>
|
||||||
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl divide-y divide-[#2e2e2e]">
|
||||||
|
{[
|
||||||
|
{ emoji: "🌱", label: "Moins de 10 parties", range: "< 10" },
|
||||||
|
{ emoji: "👾", label: "10 parties et plus", range: "10+" },
|
||||||
|
{ emoji: "🎮", label: "100 parties et plus", range: "100+" },
|
||||||
|
{ emoji: "🎯", label: "500 parties et plus", range: "500+" },
|
||||||
|
{ emoji: "⚡", label: "1 000 parties et plus", range: "1 000+" },
|
||||||
|
{ emoji: "🔥", label: "5 000 parties et plus", range: "5 000+" },
|
||||||
|
{ emoji: "🚀", label: "10 000 parties et plus", range: "10 000+" },
|
||||||
|
].map(({ emoji, label, range }) => (
|
||||||
|
<div key={range} className="flex items-center gap-3 px-4 py-2.5">
|
||||||
|
<span className="text-xl w-7 text-center shrink-0">{emoji}</span>
|
||||||
|
<span className="text-sm text-[#aaa] flex-1">{label}</span>
|
||||||
|
<span className="text-xs font-mono text-[#555]">{range}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-xs font-bold uppercase tracking-widest text-[#555]">
|
||||||
|
Contact
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href="https://github.com/jessy-david-dev/WikiRush"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-3 bg-[#1a1a1a] border border-[#2e2e2e] hover:border-[#3a3a3a] hover:bg-[#222] transition-colors rounded-xl px-4 py-3 text-sm font-medium text-[#f0f0f0]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="w-5 h-5 fill-current shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
|
jessy-david-dev/WikiRush
|
||||||
|
<span className="ml-auto text-[#555] text-xs">GitHub →</span>
|
||||||
|
</a>
|
||||||
|
<p className="text-[#888] text-xs leading-relaxed">
|
||||||
|
Une idée, un bug, une suggestion ? Ouvre une issue sur le dépôt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ async function getOrCreatePuzzle() {
|
|||||||
return prisma.dailyPuzzle.create({ data: { date, startArticle: start, targetArticle: target } });
|
return prisma.dailyPuzzle.create({ data: { date, startArticle: start, targetArticle: target } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET — retourne le puzzle du jour + si l'utilisateur a déjà joué
|
// GET - retourne le puzzle du jour + si l'utilisateur a déjà joué
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const puzzle = await getOrCreatePuzzle();
|
const puzzle = await getOrCreatePuzzle();
|
||||||
@@ -35,7 +35,7 @@ export async function GET() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST — soumettre un résultat
|
// POST - soumettre un résultat
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user?.id) return NextResponse.json({ error: "Non connecté" }, { status: 401 });
|
if (!session?.user?.id) return NextResponse.json({ error: "Non connecté" }, { status: 401 });
|
||||||
@@ -50,7 +50,7 @@ export async function POST(req: Request) {
|
|||||||
return NextResponse.json({ error: "Puzzle invalide" }, { status: 400 });
|
return NextResponse.json({ error: "Puzzle invalide" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert — on n'enregistre qu'une fois
|
// Upsert - on n'enregistre qu'une fois
|
||||||
const result = await prisma.dailyResult.upsert({
|
const result = await prisma.dailyResult.upsert({
|
||||||
where: { puzzleId_userId: { puzzleId, userId: session.user.id } },
|
where: { puzzleId_userId: { puzzleId, userId: session.user.id } },
|
||||||
create: { puzzleId, userId: session.user.id, path: JSON.stringify(path), clicks, timeSeconds, won },
|
create: { puzzleId, userId: session.user.id, path: JSON.stringify(path), clicks, timeSeconds, won },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { auth } from "../../../auth";
|
import { auth } from "../../../auth";
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
|
|
||||||
// POST /api/games — sauvegarder une partie
|
// POST /api/games - sauvegarder une partie
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -36,7 +36,7 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ id: game.id });
|
return NextResponse.json({ id: game.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/games — historique de l'utilisateur connecté
|
// GET /api/games - historique de l'utilisateur connecté
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "../../../lib/prisma";
|
||||||
|
|
||||||
|
export const revalidate = 60; // revalide toutes les 60s
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const todayStart = new Date();
|
||||||
|
todayStart.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const todayCount = await prisma.game.count({
|
||||||
|
where: { playedAt: { gte: todayStart } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = await prisma.game.count();
|
||||||
|
|
||||||
|
return NextResponse.json({ today: todayCount, total: totalCount });
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
|
|||||||
<div className="text-5xl">⚡</div>
|
<div className="text-5xl">⚡</div>
|
||||||
<h2 className="text-2xl sm:text-3xl font-black">Mode Blitz</h2>
|
<h2 className="text-2xl sm:text-3xl font-black">Mode Blitz</h2>
|
||||||
<p className="text-[#888] text-sm sm:text-base leading-relaxed">
|
<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.
|
Tu as <span className="text-[#f0f0f0] font-bold">2 minutes</span>{" "}pour naviguer de l'article de départ jusqu'à l'article cible en cliquant sur les liens Wikipedia. Plus tu es rapide, mieux c'est !
|
||||||
</p>
|
</p>
|
||||||
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
||||||
{game.loading ? "Préparation..." : "Lancer le chrono !"}
|
{game.loading ? "Préparation..." : "Lancer le chrono !"}
|
||||||
@@ -33,26 +33,37 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (game.phase === "ended") return (
|
if (game.phase === "won") return (
|
||||||
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex items-center justify-center px-4 py-8">
|
<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="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>
|
<div className="text-5xl sm:text-6xl leading-none">🎉</div>
|
||||||
<h2 className="text-2xl sm:text-3xl font-black">Temps écoulé !</h2>
|
<h2 className="text-2xl sm:text-3xl font-black">Article trouvé !</h2>
|
||||||
|
{game.puzzle && (
|
||||||
|
<p className="text-[#888] text-sm">
|
||||||
|
<span className="text-[#f0f0f0] font-semibold">{game.puzzle.start}</span>
|
||||||
|
{" → "}
|
||||||
|
<span className="text-[#7c3aed] font-semibold">{game.puzzle.target}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="flex gap-8 justify-center">
|
<div className="flex gap-8 justify-center">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-4xl font-black text-[#7c3aed]">{game.visited.length}</span>
|
<span className="text-4xl font-black text-[#7c3aed]">{fmtLeft(game.timeLeft)}</span>
|
||||||
<span className="text-xs text-[#888]">articles visités</span>
|
<span className="text-xs text-[#888]">temps restant</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-4xl font-black text-[#7c3aed]">{game.clicks}</span>
|
<span className="text-4xl font-black text-[#7c3aed]">{game.clicks}</span>
|
||||||
<span className="text-xs text-[#888]">clics</span>
|
<span className="text-xs text-[#888]">clics</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-4xl font-black text-[#7c3aed]">{game.history.length}</span>
|
||||||
|
<span className="text-xs text-[#888]">articles</span>
|
||||||
|
</div>
|
||||||
</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">
|
<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) => (
|
{game.history.map((t, i) => (
|
||||||
<span key={i} className="flex items-center gap-1">
|
<span key={i} className="flex items-center gap-1">
|
||||||
{i > 0 && <span className="text-[#555]">›</span>}
|
{i > 0 && <span className="text-[#555]">›</span>}
|
||||||
<span>{t}</span>
|
<span className={t === game.puzzle?.target ? "text-[#7c3aed] font-bold" : ""}>{t}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +71,48 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
|
|||||||
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
||||||
Rejouer
|
Rejouer
|
||||||
</button>
|
</button>
|
||||||
<button className={btnGhost} onClick={onBack}>Accueil</button>
|
<button className={btnGhost} onClick={() => { game.reset(); onBack(); }}>Accueil</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (game.phase === "lost") 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>
|
||||||
|
{game.puzzle && (
|
||||||
|
<p className="text-[#888] text-sm">
|
||||||
|
L'objectif était d'atteindre{" "}
|
||||||
|
<span className="text-[#7c3aed] font-semibold">{game.puzzle.target}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-8 justify-center">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-4xl font-black text-red-400">{game.clicks}</span>
|
||||||
|
<span className="text-xs text-[#888]">clics</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-4xl font-black text-red-400">{game.history.length}</span>
|
||||||
|
<span className="text-xs text-[#888]">articles visités</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{game.history.length > 0 && (
|
||||||
|
<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.history.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}>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
<button className={btnGhost} onClick={() => { game.reset(); onBack(); }}>Accueil</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,12 +142,9 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom bar */}
|
{/* 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="fixed bottom-0 left-0 right-0 z-50 bg-[#0f0f0f]/97 backdrop-blur-sm border-t border-[#2e2e2e] px-3 sm:px-4 py-2 flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center 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">
|
<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-[8px] font-bold uppercase tracking-wider text-[#888]">Clics</span>
|
||||||
<span className="text-sm font-black tabular-nums">{game.clicks}</span>
|
<span className="text-sm font-black tabular-nums">{game.clicks}</span>
|
||||||
@@ -103,13 +152,22 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timer */}
|
{/* Timer */}
|
||||||
<div className={`text-2xl sm:text-3xl font-black tabular-nums transition-colors ${danger ? "text-red-400" : "text-[#f0f0f0]"}`}>
|
<div className={`text-2xl sm:text-3xl font-black tabular-nums transition-colors ${danger ? "text-red-400 animate-pulse" : "text-[#f0f0f0]"}`}>
|
||||||
{fmtLeft(game.timeLeft)}
|
{fmtLeft(game.timeLeft)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-[#888] text-right max-w-28 truncate">
|
{game.puzzle && (
|
||||||
{game.visited.length > 0 ? game.visited[game.visited.length - 1] : ""}
|
<div className="text-right">
|
||||||
|
<div className="text-[8px] font-bold uppercase tracking-wider text-[#888]">Cible</div>
|
||||||
|
<div className="text-xs font-bold text-[#7c3aed] max-w-28 truncate">{game.puzzle.target}</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{game.history.length > 0 && (
|
||||||
|
<div className="text-[10px] text-[#555] truncate">
|
||||||
|
{game.history.join(" › ")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="text-center">
|
<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-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">
|
<div className="text-2xl sm:text-3xl font-black mb-1">
|
||||||
{game.phase === "won" ? "🎉 Réussi !" : game.phase === "gave_up" ? "😔 Abandonné" : "✅ Déjà joué"}
|
{game.phase === "won" ? "🎉 Réussi !" : game.phase === "gave_up" ? "😔 Abandonné" : "✅ Déjà joué"}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function GameScreen({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar — desktop seulement */}
|
{/* Sidebar - desktop seulement */}
|
||||||
<aside className="hidden md:flex w-36 lg:w-44 shrink-0 bg-[#1a1a1a] border-l border-[#2e2e2e] flex-col px-3 py-4 overflow-y-auto gap-3">
|
<aside className="hidden md:flex w-36 lg:w-44 shrink-0 bg-[#1a1a1a] border-l border-[#2e2e2e] flex-col px-3 py-4 overflow-y-auto gap-3">
|
||||||
<h4 className="text-[10px] font-bold uppercase tracking-wider text-[#888]">Joueurs</h4>
|
<h4 className="text-[10px] font-bold uppercase tracking-wider text-[#888]">Joueurs</h4>
|
||||||
<ul className="flex flex-col gap-2.5">
|
<ul className="flex flex-col gap-2.5">
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
function todayEmoji(n: number): string {
|
||||||
|
if (n >= 10000) return "🚀";
|
||||||
|
if (n >= 5000) return "🔥";
|
||||||
|
if (n >= 1000) return "⚡";
|
||||||
|
if (n >= 500) return "🎯";
|
||||||
|
if (n >= 100) return "🎮";
|
||||||
|
if (n >= 10) return "👾";
|
||||||
|
return "🌱";
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDailyStats() {
|
||||||
|
const [today, setToday] = useState<number | null>(null); // renommé plus bas en total
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/stats")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => setToday(d.total))
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
return today;
|
||||||
|
}
|
||||||
|
|
||||||
type HomeScreenProps = {
|
type HomeScreenProps = {
|
||||||
playerName: string;
|
playerName: string;
|
||||||
setPlayerName: (v: string) => void;
|
setPlayerName: (v: string) => void;
|
||||||
@@ -23,17 +45,34 @@ type HomeScreenProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function HomeScreen({
|
export function HomeScreen({
|
||||||
playerName, setPlayerName, joinCode, setJoinCode,
|
playerName,
|
||||||
error, setError, loading, onCreateRoom, onJoinRoom, onSolo,
|
setPlayerName,
|
||||||
session, onShowAuth, onShowProfile, onShowLeaderboard, onDaily, onBlitz,
|
joinCode,
|
||||||
|
setJoinCode,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
loading,
|
||||||
|
onCreateRoom,
|
||||||
|
onJoinRoom,
|
||||||
|
onSolo,
|
||||||
|
session,
|
||||||
|
onShowAuth,
|
||||||
|
onShowProfile,
|
||||||
|
onShowLeaderboard,
|
||||||
|
onDaily,
|
||||||
|
onBlitz,
|
||||||
}: HomeScreenProps) {
|
}: 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";
|
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";
|
||||||
|
const today = useDailyStats();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh w-full bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col items-center justify-center px-4 py-16 gap-6 sm:gap-8">
|
<div className="min-h-dvh w-full bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col items-center justify-center px-4 py-16 gap-6 sm:gap-8">
|
||||||
{/* Topbar */}
|
{/* Topbar */}
|
||||||
<div className="fixed top-3 right-3 flex items-center gap-1.5 sm:gap-2 z-50">
|
<div className="fixed top-3 right-3 flex items-center gap-1.5 sm:gap-2 z-50">
|
||||||
<button className={btnGhost} onClick={onShowLeaderboard}>🏆 Classement</button>
|
<button className={btnGhost} onClick={onShowLeaderboard}>
|
||||||
|
🏆 Classement
|
||||||
|
</button>
|
||||||
{session?.user ? (
|
{session?.user ? (
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 sm:gap-2 bg-[#242424] border border-[#2e2e2e] rounded-full pl-1 pr-2.5 sm:pr-3 py-1 cursor-pointer hover:bg-[#1a1a1a]"
|
className="flex items-center gap-1.5 sm:gap-2 bg-[#242424] border border-[#2e2e2e] rounded-full pl-1 pr-2.5 sm:pr-3 py-1 cursor-pointer hover:bg-[#1a1a1a]"
|
||||||
@@ -42,23 +81,41 @@ export function HomeScreen({
|
|||||||
<span className="w-7 h-7 rounded-full bg-[#7c3aed] text-white flex items-center justify-center text-xs font-bold shrink-0">
|
<span className="w-7 h-7 rounded-full bg-[#7c3aed] text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||||||
{session.user.name?.[0]?.toUpperCase() ?? "?"}
|
{session.user.name?.[0]?.toUpperCase() ?? "?"}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden sm:block max-w-28 truncate text-sm font-medium">{session.user.name}</span>
|
<span className="hidden sm:block max-w-28 truncate text-sm font-medium">
|
||||||
|
{session.user.name}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button className={btnGhost} onClick={onShowAuth}>Connexion</button>
|
<button className={btnGhost} onClick={onShowAuth}>
|
||||||
|
Connexion
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Image
|
<Image
|
||||||
src="/wikirush.png" alt="WikiRush" width={320} height={320}
|
src="/wikirush.png"
|
||||||
|
alt="WikiRush"
|
||||||
|
width={320}
|
||||||
|
height={320}
|
||||||
className="w-40 sm:w-56 md:w-72 h-auto mx-auto mix-blend-screen"
|
className="w-40 sm:w-56 md:w-72 h-auto mx-auto mix-blend-screen"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<p className="mt-3 text-sm sm:text-base font-semibold text-[#f0f0f0] max-w-xs sm:max-w-sm mx-auto leading-relaxed tracking-wide">
|
<p className="mt-3 text-sm sm:text-base font-semibold text-[#f0f0f0] max-w-xs sm:max-w-sm mx-auto leading-relaxed tracking-wide">
|
||||||
Navigue entre les articles Wikipedia pour atteindre la cible en premier !
|
Navigue entre les articles Wikipedia pour atteindre la cible en
|
||||||
|
premier !
|
||||||
</p>
|
</p>
|
||||||
|
{today !== null && (
|
||||||
|
<p className="mt-2 text-xs text-[#888]">
|
||||||
|
{todayEmoji(today)}{" "}
|
||||||
|
<span className="font-semibold text-[#f0f0f0]">
|
||||||
|
{today.toLocaleString("fr-FR")}
|
||||||
|
</span>{" "}
|
||||||
|
partie{today > 1 ? "s" : ""} jouée{today > 1 ? "s" : ""} au total
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
@@ -66,37 +123,54 @@ export function HomeScreen({
|
|||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-center justify-between gap-3 bg-red-950/40 border border-red-600 text-red-300 px-3 py-2.5 rounded-xl text-xs sm:text-sm">
|
<div className="flex items-center justify-between gap-3 bg-red-950/40 border border-red-600 text-red-300 px-3 py-2.5 rounded-xl text-xs sm:text-sm">
|
||||||
{error}
|
{error}
|
||||||
<button onClick={() => setError(null)} className="shrink-0 px-1.5 py-0.5 rounded hover:bg-white/10 cursor-pointer">✕</button>
|
<button
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
className="shrink-0 px-1.5 py-0.5 rounded hover:bg-white/10 cursor-pointer"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="w-full min-h-11 px-3.5 bg-[#1a1a1a] border-[1.5px] border-[#2e2e2e] rounded-xl text-[#f0f0f0] text-sm outline-none focus:border-[#7c3aed] transition-colors"
|
className="w-full min-h-11 px-3.5 bg-[#1a1a1a] border-[1.5px] border-[#2e2e2e] rounded-xl text-[#f0f0f0] text-sm outline-none focus:border-[#7c3aed] transition-colors"
|
||||||
type="text" placeholder="Ton pseudo"
|
type="text"
|
||||||
value={playerName} onChange={(e) => setPlayerName(e.target.value)}
|
placeholder="Ton pseudo"
|
||||||
maxLength={20} onKeyDown={(e) => e.key === "Enter" && onCreateRoom()}
|
value={playerName}
|
||||||
|
onChange={(e) => setPlayerName(e.target.value)}
|
||||||
|
maxLength={20}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && onCreateRoom()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Multijoueur */}
|
{/* Multijoueur */}
|
||||||
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 sm:p-5 flex flex-col gap-3">
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 sm:p-5 flex flex-col gap-3">
|
||||||
<h3 className="text-[10px] sm:text-xs font-bold text-[#888] uppercase tracking-wider">Multijoueur</h3>
|
<h3 className="text-[10px] sm:text-xs font-bold text-[#888] uppercase tracking-wider">
|
||||||
|
Multijoueur
|
||||||
|
</h3>
|
||||||
<button
|
<button
|
||||||
className="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"
|
className="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"
|
||||||
onClick={onCreateRoom} disabled={loading}
|
onClick={onCreateRoom}
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? "Création..." : "Créer une partie"}
|
{loading ? "Création..." : "Créer une partie"}
|
||||||
</button>
|
</button>
|
||||||
{/* Join row — colonne sur mobile, ligne sur sm+ */}
|
{/* Join row - colonne sur mobile, ligne sur sm+ */}
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<input
|
<input
|
||||||
className="flex-1 min-h-11 px-3.5 bg-[#0f0f0f] border-[1.5px] border-[#2e2e2e] rounded-xl text-[#f0f0f0] font-mono text-base sm:text-lg tracking-widest uppercase outline-none focus:border-[#7c3aed] transition-colors"
|
className="flex-1 min-h-11 px-3.5 bg-[#0f0f0f] border-[1.5px] border-[#2e2e2e] rounded-xl text-[#f0f0f0] font-mono text-base sm:text-lg tracking-widest uppercase outline-none focus:border-[#7c3aed] transition-colors"
|
||||||
type="text" placeholder="CODE (ex: WIKI)"
|
type="text"
|
||||||
value={joinCode} onChange={(e) => setJoinCode(e.target.value.toUpperCase().slice(0, 4))}
|
placeholder="Code de la partie"
|
||||||
maxLength={4} onKeyDown={(e) => e.key === "Enter" && onJoinRoom()}
|
value={joinCode}
|
||||||
|
onChange={(e) =>
|
||||||
|
setJoinCode(e.target.value.toUpperCase().slice(0, 4))
|
||||||
|
}
|
||||||
|
maxLength={4}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && onJoinRoom()}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="w-full sm:w-auto min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#2563eb] text-white hover:bg-[#1d4ed8] disabled:opacity-50 cursor-pointer shrink-0 transition-colors"
|
className="w-full sm:w-auto min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#2563eb] text-white hover:bg-[#1d4ed8] disabled:opacity-50 cursor-pointer shrink-0 transition-colors"
|
||||||
onClick={onJoinRoom} disabled={loading}
|
onClick={onJoinRoom}
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Rejoindre
|
Rejoindre
|
||||||
</button>
|
</button>
|
||||||
@@ -111,7 +185,9 @@ export function HomeScreen({
|
|||||||
|
|
||||||
{/* Solo */}
|
{/* Solo */}
|
||||||
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 sm:p-5 flex flex-col gap-3">
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 sm:p-5 flex flex-col gap-3">
|
||||||
<h3 className="text-[10px] sm:text-xs font-bold text-[#888] uppercase tracking-wider">Solo</h3>
|
<h3 className="text-[10px] sm:text-xs font-bold text-[#888] uppercase tracking-wider">
|
||||||
|
Solo
|
||||||
|
</h3>
|
||||||
<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"
|
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}
|
onClick={onSolo}
|
||||||
@@ -128,7 +204,7 @@ 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"
|
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}
|
onClick={onBlitz}
|
||||||
>
|
>
|
||||||
⚡ Mode Blitz — 2 min
|
⚡ Mode Blitz - 2 min
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,19 +99,19 @@ export function ProfileScreen({ userName, onBack }: { userName: string; onBack:
|
|||||||
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Victoires</span>
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Victoires</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={statCard}>
|
<div className={statCard}>
|
||||||
<span className="text-lg sm:text-[22px] font-black">{stats.avgClicks > 0 ? stats.avgClicks : "—"}</span>
|
<span className="text-lg sm:text-[22px] font-black">{stats.avgClicks > 0 ? stats.avgClicks : "-"}</span>
|
||||||
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Clics moy.</span>
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Clics moy.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={statCard}>
|
<div className={statCard}>
|
||||||
<span className="text-lg sm:text-[22px] font-black">{stats.avgTime > 0 ? fmt(stats.avgTime) : "—"}</span>
|
<span className="text-lg sm:text-[22px] font-black">{stats.avgTime > 0 ? fmt(stats.avgTime) : "-"}</span>
|
||||||
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Temps moy.</span>
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Temps moy.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${statCard} border-[#7c3aed]`}>
|
<div className={`${statCard} border-[#7c3aed]`}>
|
||||||
<span className="text-lg sm:text-[22px] font-black text-[#7c3aed]">{stats.bestClicks > 0 ? stats.bestClicks : "—"}</span>
|
<span className="text-lg sm:text-[22px] font-black text-[#7c3aed]">{stats.bestClicks > 0 ? stats.bestClicks : "-"}</span>
|
||||||
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Meilleur clics</span>
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Meilleur clics</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${statCard} border-[#7c3aed]`}>
|
<div className={`${statCard} border-[#7c3aed]`}>
|
||||||
<span className="text-lg sm:text-[22px] font-black text-[#7c3aed]">{stats.bestTime > 0 ? fmt(stats.bestTime) : "—"}</span>
|
<span className="text-lg sm:text-[22px] font-black text-[#7c3aed]">{stats.bestTime > 0 ? fmt(stats.bestTime) : "-"}</span>
|
||||||
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Meilleur temps</span>
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">Meilleur temps</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
Article Wikipedia (HTML externe — ne peut pas être tailwindifié)
|
Article Wikipedia (HTML externe - ne peut pas être tailwindifié)
|
||||||
================================ */
|
================================ */
|
||||||
|
|
||||||
.article-container {
|
.article-container {
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ const geistMono = Geist_Mono({
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "WikiRush - Jeu de navigation Wikipedia",
|
title: "WikiRush - Jeu de navigation Wikipedia",
|
||||||
description: "Navigue entre les articles Wikipedia pour atteindre la cible en premier !",
|
description: "Navigue entre les articles Wikipedia pour atteindre la cible en premier !",
|
||||||
|
metadataBase: new URL("https://wikirush.xyz"),
|
||||||
|
openGraph: {
|
||||||
|
title: "WikiRush",
|
||||||
|
description: "Navigue entre les articles Wikipedia pour atteindre la cible en premier !",
|
||||||
|
url: "https://wikirush.xyz",
|
||||||
|
siteName: "WikiRush",
|
||||||
|
locale: "fr_FR",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -35,6 +44,8 @@ export default function RootLayout({
|
|||||||
<span>© {new Date().getFullYear()} WikiRush</span>
|
<span>© {new Date().getFullYear()} WikiRush</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<a href="/privacy" className="hover:text-[#888] transition-colors">Politique de confidentialité</a>
|
<a href="/privacy" className="hover:text-[#888] transition-colors">Politique de confidentialité</a>
|
||||||
|
<span>·</span>
|
||||||
|
<a href="/about" className="hover:text-[#888] transition-colors">À propos</a>
|
||||||
</footer>
|
</footer>
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Politique de confidentialité — WikiRush",
|
title: "Politique de confidentialité - WikiRush",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
@@ -27,9 +27,9 @@ export default function PrivacyPage() {
|
|||||||
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">2. Données collectées</h2>
|
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">2. Données collectées</h2>
|
||||||
<p>Lors de la création d'un compte, nous collectons :</p>
|
<p>Lors de la création d'un compte, nous collectons :</p>
|
||||||
<ul className="list-disc pl-5 flex flex-col gap-1.5 text-[#ccc]">
|
<ul className="list-disc pl-5 flex flex-col gap-1.5 text-[#ccc]">
|
||||||
<li><span className="text-[#f0f0f0] font-semibold">Pseudo</span> — affiché dans les parties et le classement.</li>
|
<li><span className="text-[#f0f0f0] font-semibold">Pseudo</span> - affiché dans les parties et le classement.</li>
|
||||||
<li><span className="text-[#f0f0f0] font-semibold">Adresse e-mail</span> — utilisée uniquement pour l'authentification. Elle n'est jamais affichée publiquement.</li>
|
<li><span className="text-[#f0f0f0] font-semibold">Adresse e-mail</span> - utilisée uniquement pour l'authentification. Elle n'est jamais affichée publiquement.</li>
|
||||||
<li><span className="text-[#f0f0f0] font-semibold">Mot de passe</span> — stocké sous forme hachée (bcrypt). Nous n'avons jamais accès à votre mot de passe en clair.</li>
|
<li><span className="text-[#f0f0f0] font-semibold">Mot de passe</span> - stocké sous forme hachée (bcrypt). Nous n'avons jamais accès à votre mot de passe en clair.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Pour chaque partie jouée, nous enregistrons :</p>
|
<p>Pour chaque partie jouée, nous enregistrons :</p>
|
||||||
<ul className="list-disc pl-5 flex flex-col gap-1.5 text-[#ccc]">
|
<ul className="list-disc pl-5 flex flex-col gap-1.5 text-[#ccc]">
|
||||||
|
|||||||
+52
-44
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from "react";
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
import { fetchArticle, pickTwoArticles } from "./wiki";
|
import { fetchArticle, pickTwoArticles, normalizeTitle } from "./wiki";
|
||||||
|
|
||||||
const BLITZ_DURATION = 120; // 2 minutes en secondes
|
const BLITZ_DURATION = 120; // 2 minutes
|
||||||
|
|
||||||
export type BlitzPhase = "setup" | "playing" | "ended";
|
export type BlitzPhase = "setup" | "playing" | "won" | "lost";
|
||||||
|
|
||||||
export function useBlitzGame() {
|
export function useBlitzGame() {
|
||||||
const [phase, setPhase] = useState<BlitzPhase>("setup");
|
const [phase, setPhase] = useState<BlitzPhase>("setup");
|
||||||
@@ -13,18 +13,21 @@ export function useBlitzGame() {
|
|||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [timeLeft, setTimeLeft] = useState(BLITZ_DURATION);
|
const [timeLeft, setTimeLeft] = useState(BLITZ_DURATION);
|
||||||
const [clicks, setClicks] = useState(0);
|
const [clicks, setClicks] = useState(0);
|
||||||
const [visited, setVisited] = useState<string[]>([]); // articles uniques
|
const [history, setHistory] = useState<string[]>([]);
|
||||||
|
const [puzzle, setPuzzle] = useState<{ start: string; target: string } | null>(null);
|
||||||
|
|
||||||
const clicksRef = useRef(0);
|
const clicksRef = useRef(0);
|
||||||
const visitedRef = useRef<Set<string>>(new Set());
|
const pathRef = useRef<string[]>([]);
|
||||||
const visitedListRef = useRef<string[]>([]);
|
|
||||||
const loadingRef = useRef(false);
|
const loadingRef = useRef(false);
|
||||||
const endedRef = useRef(false);
|
const endedRef = useRef(false);
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const startTimeRef = useRef<number>(0);
|
const startTimeRef = useRef(0);
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; }
|
||||||
|
}
|
||||||
|
|
||||||
function startTimer() {
|
function startTimer() {
|
||||||
startTimeRef.current = Date.now();
|
startTimeRef.current = Date.now();
|
||||||
@@ -32,33 +35,34 @@ export function useBlitzGame() {
|
|||||||
const elapsed = (Date.now() - startTimeRef.current) / 1000;
|
const elapsed = (Date.now() - startTimeRef.current) / 1000;
|
||||||
const left = Math.max(0, BLITZ_DURATION - elapsed);
|
const left = Math.max(0, BLITZ_DURATION - elapsed);
|
||||||
setTimeLeft(left);
|
setTimeLeft(left);
|
||||||
if (left <= 0) endGame();
|
if (left <= 0 && !endedRef.current) {
|
||||||
}, 200);
|
endedRef.current = true;
|
||||||
|
stopTimer();
|
||||||
|
setPhase("lost");
|
||||||
|
saveGame(false, pathRef.current, clicksRef.current, BLITZ_DURATION);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function endGame() {
|
useEffect(() => () => stopTimer(), []);
|
||||||
if (endedRef.current) return;
|
|
||||||
endedRef.current = true;
|
async function saveGame(won: boolean, path: string[], clicks: number, timeSeconds: number) {
|
||||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
if (!puzzle) return;
|
||||||
setPhase("ended");
|
|
||||||
// Sauvegarder
|
|
||||||
fetch("/api/games", {
|
fetch("/api/games", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
mode: "blitz",
|
mode: "blitz",
|
||||||
startArticle: visitedListRef.current[0] ?? "",
|
startArticle: puzzle.start,
|
||||||
targetArticle: "",
|
targetArticle: puzzle.target,
|
||||||
path: visitedListRef.current,
|
path,
|
||||||
clicks: clicksRef.current,
|
clicks,
|
||||||
timeSeconds: BLITZ_DURATION,
|
timeSeconds: BLITZ_DURATION - timeLeft,
|
||||||
won: true,
|
won,
|
||||||
}),
|
}),
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => () => { if (intervalRef.current) clearInterval(intervalRef.current); }, []);
|
|
||||||
|
|
||||||
async function loadArticle(t: string) {
|
async function loadArticle(t: string) {
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -74,21 +78,20 @@ export function useBlitzGame() {
|
|||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
stopTimer();
|
||||||
clicksRef.current = 0; setClicks(0);
|
clicksRef.current = 0; setClicks(0);
|
||||||
visitedRef.current = new Set();
|
pathRef.current = []; setHistory([]);
|
||||||
visitedListRef.current = [];
|
|
||||||
endedRef.current = false;
|
endedRef.current = false;
|
||||||
setTimeLeft(BLITZ_DURATION);
|
setTimeLeft(BLITZ_DURATION);
|
||||||
setVisited([]);
|
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
|
|
||||||
const { start: startArticle } = await pickTwoArticles();
|
const p = await pickTwoArticles();
|
||||||
const canonical = await loadArticle(startArticle);
|
setPuzzle(p);
|
||||||
|
const canonical = await loadArticle(p.start);
|
||||||
if (!canonical) return;
|
if (!canonical) return;
|
||||||
|
|
||||||
visitedRef.current.add(canonical);
|
pathRef.current = [canonical];
|
||||||
visitedListRef.current = [canonical];
|
setHistory([canonical]);
|
||||||
setVisited([canonical]);
|
|
||||||
setPhase("playing");
|
setPhase("playing");
|
||||||
startTimer();
|
startTimer();
|
||||||
}
|
}
|
||||||
@@ -101,31 +104,36 @@ export function useBlitzGame() {
|
|||||||
const canonical = await loadArticle(t);
|
const canonical = await loadArticle(t);
|
||||||
if (!canonical) return;
|
if (!canonical) return;
|
||||||
|
|
||||||
if (!visitedRef.current.has(canonical)) {
|
const newPath = [...pathRef.current, canonical];
|
||||||
visitedRef.current.add(canonical);
|
pathRef.current = newPath;
|
||||||
visitedListRef.current = [...visitedListRef.current, canonical];
|
setHistory(newPath);
|
||||||
setVisited([...visitedListRef.current]);
|
|
||||||
}
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
|
||||||
|
if (puzzle && normalizeTitle(canonical) === normalizeTitle(puzzle.target)) {
|
||||||
|
endedRef.current = true;
|
||||||
|
stopTimer();
|
||||||
|
setPhase("won");
|
||||||
|
saveGame(true, newPath, clicksRef.current, timeLeft);
|
||||||
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [puzzle, timeLeft]);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
stopTimer();
|
||||||
endedRef.current = false;
|
endedRef.current = false;
|
||||||
clicksRef.current = 0; setClicks(0);
|
clicksRef.current = 0; setClicks(0);
|
||||||
visitedRef.current = new Set();
|
pathRef.current = []; setHistory([]);
|
||||||
visitedListRef.current = [];
|
|
||||||
setVisited([]);
|
|
||||||
setHtml(""); setTitle("");
|
setHtml(""); setTitle("");
|
||||||
setTimeLeft(BLITZ_DURATION);
|
setTimeLeft(BLITZ_DURATION);
|
||||||
|
setPuzzle(null);
|
||||||
setPhase("setup");
|
setPhase("setup");
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
phase, html, title, loading, loadError,
|
phase, puzzle, html, title, loading, loadError,
|
||||||
timeLeft, clicks, visited,
|
timeLeft, clicks, history,
|
||||||
|
canGoBack: false, // pas de retour en blitz
|
||||||
start, navigate, reset,
|
start, navigate, reset,
|
||||||
retryLoad: () => title && loadArticle(title),
|
retryLoad: () => title && loadArticle(title),
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -19,5 +19,5 @@ export async function saveGame(data: {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
} catch { /* silencieux — pas de compte ou hors ligne */ }
|
} catch { /* silencieux - pas de compte ou hors ligne */ }
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -15,7 +15,8 @@
|
|||||||
"next": "16.2.3",
|
"next": "16.2.3",
|
||||||
"next-auth": "5.0.0-beta.30",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4",
|
||||||
|
"simple-icons": "^16.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prisma/adapter-better-sqlite3": "^7.7.0",
|
"@prisma/adapter-better-sqlite3": "^7.7.0",
|
||||||
|
|||||||
Generated
+15
-2880
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
Reference in New Issue
Block a user