feat: add search toggle for game setup and improve RGPD compliance

This commit is contained in:
jessy-david-dev
2026-04-11 13:24:30 +02:00
parent 76c7967a9a
commit b2f28bbb75
7 changed files with 286 additions and 50 deletions
+8
View File
@@ -68,6 +68,14 @@ export function AuthModal({ onClose, onSuccess }: { onClose: () => void; onSucce
<button className="w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer mt-1" type="submit" disabled={loading}> <button className="w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer mt-1" type="submit" disabled={loading}>
{loading ? "Chargement..." : mode === "login" ? "Se connecter" : "Créer un compte"} {loading ? "Chargement..." : mode === "login" ? "Se connecter" : "Créer un compte"}
</button> </button>
{mode === "register" && (
<p className="text-xs text-[#555] text-center leading-relaxed">
En créant un compte, vous acceptez notre{" "}
<a href="/privacy" className="text-[#888] hover:text-[#f0f0f0] underline underline-offset-2 transition-colors">
politique de confidentialité
</a>.
</p>
)}
</form> </form>
</div> </div>
</div> </div>
+12
View File
@@ -3,6 +3,7 @@
import { useBlitzGame } from "../../lib/useBlitzGame"; import { useBlitzGame } from "../../lib/useBlitzGame";
import { ArticleView } from "./ArticleView"; import { ArticleView } from "./ArticleView";
import { ShareBar } from "./ShareBar"; import { ShareBar } from "./ShareBar";
import { useSearchAllowed } from "../../lib/useSearchAllowed";
function fmtLeft(s: number): string { function fmtLeft(s: number): string {
const m = Math.floor(s / 60); const m = Math.floor(s / 60);
@@ -12,6 +13,8 @@ function fmtLeft(s: number): string {
export function BlitzScreen({ onBack }: { onBack: () => void }) { export function BlitzScreen({ onBack }: { onBack: () => void }) {
const game = useBlitzGame(); const game = useBlitzGame();
const { allowed: searchAllowed, toggle: toggleSearch } = useSearchAllowed();
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 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 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";
@@ -28,6 +31,15 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
<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 naviguer de l&apos;article de départ jusqu&apos;à l&apos;article cible en cliquant sur les liens Wikipedia. Plus tu es rapide, mieux c&apos;est ! Tu as <span className="text-[#f0f0f0] font-bold">2 minutes</span>{" "}pour naviguer de l&apos;article de départ jusqu&apos;à l&apos;article cible en cliquant sur les liens Wikipedia. Plus tu es rapide, mieux c&apos;est !
</p> </p>
<button
onClick={toggleSearch}
className={`w-full min-h-11 rounded-xl text-sm font-semibold border transition-colors cursor-pointer flex items-center justify-between px-4 ${searchAllowed ? "bg-[#7c3aed]/10 border-[#7c3aed] text-[#a78bfa]" : "bg-[#1a1a1a] border-[#2e2e2e] text-[#888]"}`}
>
<span>🔍 Recherche Ctrl+F</span>
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${searchAllowed ? "bg-[#7c3aed]/30 text-[#a78bfa]" : "bg-[#242424] text-[#555]"}`}>
{searchAllowed ? "Autorisée" : "Bloquée"}
</span>
</button>
<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 !"}
</button> </button>
+1 -1
View File
@@ -117,7 +117,7 @@ export function GameScreen({
</div> </div>
<Breadcrumbs history={history} endRef={breadcrumbEndRef} /> <Breadcrumbs history={history} endRef={breadcrumbEndRef} />
</div> </div>
<div className="flex gap-2 sm:gap-3 shrink-0 text-[10px] sm:text-xs font-bold text-[#888] tabular-nums"> <div className="flex items-center gap-2 sm:gap-3 shrink-0 text-[10px] sm:text-xs font-bold text-[#888] tabular-nums">
<span>{elapsed}</span> <span>{elapsed}</span>
<span>{clicks} clics</span> <span>{clicks} clics</span>
<span className="hidden sm:inline">{myPlayer?.score ?? 0} pts</span> <span className="hidden sm:inline">{myPlayer?.score ?? 0} pts</span>
+12
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { useSearchAllowed } from "../../lib/useSearchAllowed";
import type { Room } from "../api/rooms/route"; import type { Room } from "../api/rooms/route";
type LobbyScreenProps = { type LobbyScreenProps = {
@@ -19,6 +20,7 @@ export function LobbyScreen({
}: LobbyScreenProps) { }: LobbyScreenProps) {
const isHost = room.players.find((p) => p.id === playerId)?.isHost ?? false; const isHost = room.players.find((p) => p.id === playerId)?.isHost ?? false;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const { allowed: searchAllowed, toggle: toggleSearch } = useSearchAllowed();
function copyCode() { function copyCode() {
navigator.clipboard.writeText(room.code).then(() => { navigator.clipboard.writeText(room.code).then(() => {
@@ -74,6 +76,16 @@ export function LobbyScreen({
</ul> </ul>
</div> </div>
<button
onClick={toggleSearch}
className={`w-full min-h-11 rounded-xl text-sm font-semibold border transition-colors cursor-pointer flex items-center justify-between px-4 ${searchAllowed ? "bg-[#7c3aed]/10 border-[#7c3aed] text-[#a78bfa]" : "bg-[#1a1a1a] border-[#2e2e2e] text-[#888]"}`}
>
<span>🔍 Recherche Ctrl+F</span>
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${searchAllowed ? "bg-[#7c3aed]/30 text-[#a78bfa]" : "bg-[#242424] text-[#555]"}`}>
{searchAllowed ? "Autorisée" : "Bloquée"}
</span>
</button>
{room.round > 0 && <p className="text-xs text-[#888] text-center">Manche {room.round} terminée</p>} {room.round > 0 && <p className="text-xs text-[#888] text-center">Manche {room.round} terminée</p>}
{isHost ? ( {isHost ? (
+12
View File
@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { ArticleView } from "./ArticleView"; import { ArticleView } from "./ArticleView";
import { Breadcrumbs } from "./Breadcrumbs"; import { Breadcrumbs } from "./Breadcrumbs";
import { ShareBar } from "./ShareBar"; import { ShareBar } from "./ShareBar";
import { useSearchAllowed } from "../../lib/useSearchAllowed";
import type { Puzzle } from "../../lib/types"; import type { Puzzle } from "../../lib/types";
type SoloPhase = "setup" | "playing" | "won"; type SoloPhase = "setup" | "playing" | "won";
@@ -32,8 +33,10 @@ export function SoloScreen({
elapsedDisplay, canGoBack, onStart, onNavigate, onBack, onQuit, onNewGame, onRetry, elapsedDisplay, canGoBack, onStart, onNavigate, onBack, onQuit, onNewGame, onRetry,
}: SoloScreenProps) { }: SoloScreenProps) {
const breadcrumbEndRef = useRef<HTMLDivElement>(null); const breadcrumbEndRef = useRef<HTMLDivElement>(null);
const { allowed: searchAllowed, toggle: toggleSearch } = useSearchAllowed();
useEffect(() => { useEffect(() => {
breadcrumbEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "end" }); breadcrumbEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "end" });
}, [history]); }, [history]);
@@ -49,6 +52,15 @@ export function SoloScreen({
<p className="text-[#888] text-sm sm:text-base leading-relaxed"> <p className="text-[#888] text-sm sm:text-base leading-relaxed">
Deux articles aléatoires seront choisis. Atteins l&apos;article cible en cliquant uniquement sur les liens ! Deux articles aléatoires seront choisis. Atteins l&apos;article cible en cliquant uniquement sur les liens !
</p> </p>
<button
onClick={toggleSearch}
className={`w-full min-h-11 rounded-xl text-sm font-semibold border transition-colors cursor-pointer flex items-center justify-between px-4 ${searchAllowed ? "bg-[#7c3aed]/10 border-[#7c3aed] text-[#a78bfa]" : "bg-[#1a1a1a] border-[#2e2e2e] text-[#888]"}`}
>
<span>🔍 Recherche Ctrl+F</span>
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${searchAllowed ? "bg-[#7c3aed]/30 text-[#a78bfa]" : "bg-[#242424] text-[#555]"}`}>
{searchAllowed ? "Autorisée" : "Bloquée"}
</span>
</button>
<button className={btnPrimary} onClick={onStart} disabled={loading}> <button className={btnPrimary} onClick={onStart} disabled={loading}>
{loading ? "Préparation..." : "Lancer une partie"} {loading ? "Préparation..." : "Lancer une partie"}
</button> </button>
+206 -49
View File
@@ -1,3 +1,5 @@
import Link from "next/link";
export const metadata = { export const metadata = {
title: "Politique de confidentialité - WikiRush", title: "Politique de confidentialité - WikiRush",
}; };
@@ -6,34 +8,71 @@ export default function PrivacyPage() {
return ( return (
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0]"> <div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0]">
<div className="max-w-2xl mx-auto px-4 py-12 sm:py-16"> <div className="max-w-2xl mx-auto px-4 py-12 sm:py-16">
<a href="/" className="inline-flex items-center gap-2 text-sm text-[#888] hover:text-[#f0f0f0] mb-8 transition-colors"> <Link
href="/"
className="inline-flex items-center gap-2 text-sm text-[#888] hover:text-[#f0f0f0] mb-8 transition-colors"
>
Retour Retour
</a> </Link>
<h1 className="text-3xl sm:text-4xl font-black mb-2">Politique de confidentialité</h1> <h1 className="text-3xl sm:text-4xl font-black mb-2">
<p className="text-sm text-[#888] mb-10">Dernière mise à jour : avril 2025</p> Politique de confidentialité
</h1>
<p className="text-sm text-[#888] mb-10">
Dernière mise à jour : avril 2026
</p>
<div className="flex flex-col gap-8 text-sm sm:text-base leading-relaxed text-[#ccc]"> <div className="flex flex-col gap-8 text-sm sm:text-base leading-relaxed text-[#ccc]">
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">1. Présentation</h2> <h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
1. Présentation
</h2>
<p> <p>
WikiRush est un jeu de navigation Wikipedia jouable en solo et en multijoueur. WikiRush est un jeu de navigation Wikipedia jouable en solo et en
Cette politique explique quelles données sont collectées, pourquoi, et comment elles sont protégées. multijoueur. Cette politique explique quelles données sont
collectées, pourquoi, et comment elles sont protégées.
</p>
<p>
Responsable du traitement :{" "}
<span className="text-[#f0f0f0] font-semibold">DAVID Jessy</span>{" "}
-{" "}
<a
href="mailto:contact@jessy-david.dev"
className="text-[#7c3aed] hover:underline"
>
contact@jessy-david.dev
</a>
</p> </p>
</section> </section>
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<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&apos;un compte, nous collectons :</p> <p>Lors de la création d&apos;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>
<li><span className="text-[#f0f0f0] font-semibold">Adresse e-mail</span> - utilisée uniquement pour l&apos;authentification. Elle n&apos;est jamais affichée publiquement.</li> <span className="text-[#f0f0f0] font-semibold">Pseudo</span> -
<li><span className="text-[#f0f0f0] font-semibold">Mot de passe</span> - stocké sous forme hachée (bcrypt). Nous n&apos;avons jamais accès à votre mot de passe en clair.</li> affiché dans les parties et le classement.
</li>
<li>
<span className="text-[#f0f0f0] font-semibold">
Adresse e-mail
</span>{" "}
- utilisée uniquement pour l&apos;authentification. Elle
n&apos;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&apos;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]">
<li>Mode de jeu (solo / multijoueur)</li> <li>Mode de jeu (solo / multijoueur / blitz / défi du jour)</li>
<li>Articles de départ et cible</li> <li>Articles de départ et cible</li>
<li>Chemin parcouru, nombre de clics, temps</li> <li>Chemin parcouru, nombre de clics, temps</li>
<li>Résultat (victoire / abandon)</li> <li>Résultat (victoire / abandon)</li>
@@ -42,53 +81,171 @@ export default function PrivacyPage() {
</section> </section>
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">3. Utilisation des données</h2> <h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
<p>Les données collectées servent exclusivement à :</p> 3. Finalités et bases légales
</h2>
<ul className="list-disc pl-5 flex flex-col gap-1.5"> <ul className="list-disc pl-5 flex flex-col gap-1.5">
<li>Afficher votre historique de parties dans votre profil.</li>
<li>Alimenter le classement global (pseudo + statistiques uniquement).</li>
<li>Maintenir votre session de connexion.</li>
</ul>
<p>Aucune donnée n&apos;est vendue, partagée ou utilisée à des fins commerciales ou publicitaires.</p>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">4. Stockage et sécurité</h2>
<p>
Les données sont stockées dans une base SQLite hébergée localement sur le serveur de l&apos;application.
Les mots de passe sont hachés avec bcrypt avant tout stockage.
Les sessions sont gérées via JWT (JSON Web Token) et ne contiennent pas d&apos;informations sensibles.
</p>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">5. Données de navigation</h2>
<p>
WikiRush n&apos;utilise pas de cookies tiers, de trackers analytiques (Google Analytics, etc.) ni de réseaux publicitaires.
La session de connexion est stockée dans un cookie HTTP sécurisé et httpOnly, nécessaire au fonctionnement de l&apos;authentification.
</p>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">6. Vos droits</h2>
<p>Vous pouvez à tout moment :</p>
<ul className="list-disc pl-5 flex flex-col gap-1.5">
<li><span className="text-[#f0f0f0] font-semibold">Consulter</span> vos données via votre page de profil.</li>
<li> <li>
<span className="text-[#f0f0f0] font-semibold">Supprimer</span> votre compte directement depuis votre profil, <span className="text-[#f0f0f0] font-semibold">
rubrique <span className="italic">Zone dangereuse</span>. Cette action supprime immédiatement et définitivement Gestion du compte
votre compte ainsi que l&apos;intégralité de vos parties enregistrées. </span>{" "}
- base légale : exécution du contrat (Art. 6.1.b RGPD).
</li>
<li>
<span className="text-[#f0f0f0] font-semibold">
Historique de parties et classement
</span>{" "}
- base légale : exécution du contrat (Art. 6.1.b RGPD).
</li>
<li>
<span className="text-[#f0f0f0] font-semibold">
Session de connexion
</span>{" "}
- base légale : intérêt légitime (Art. 6.1.f RGPD) - nécessaire
au fonctionnement du service.
</li>
</ul>
<p>
Aucune donnée n&apos;est vendue, partagée ou utilisée à des fins
commerciales ou publicitaires.
</p>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
4. Durées de conservation
</h2>
<ul className="list-disc pl-5 flex flex-col gap-1.5">
<li>
Les données de compte (pseudo, email) sont conservées
jusqu&apos;à la suppression du compte.
</li>
<li>
Les données de parties sont liées au compte et supprimées avec
lui.
</li>
<li>
Aucune archive n&apos;est conservée après suppression du compte.
</li> </li>
</ul> </ul>
</section> </section>
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">7. Contact</h2> <h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
5. Stockage et sécurité
</h2>
<p> <p>
Pour toute question relative à vos données personnelles, contactez-nous via la page GitHub du projet. Les données sont stockées dans une base SQLite hébergée sur le
serveur de l&apos;application. Les mots de passe sont hachés avec
bcrypt avant tout stockage. Les sessions sont gérées via JWT (JSON
Web Token) et ne contiennent pas d&apos;informations sensibles.
</p> </p>
</section> </section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
6. Hébergement et transferts
</h2>
<p>
WikiRush est hébergé par{" "}
<span className="text-[#f0f0f0] font-semibold">
QuantumCraft Studios
</span>{" "}
(58 Rue de Monceau, 75008 Paris). L&apos;infrastructure de
déploiement peut faire appel à des prestataires établis hors UE.
Dans ce cas, les transferts sont encadrés par des clauses
contractuelles types (SCC) conformes au RGPD.
</p>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
7. Données de navigation
</h2>
<p>
WikiRush n&apos;utilise pas de cookies tiers, de trackers
analytiques (Google Analytics, etc.) ni de réseaux publicitaires.
La session de connexion est stockée dans un cookie HTTP sécurisé
et httpOnly, nécessaire au fonctionnement de
l&apos;authentification.
</p>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
8. Vos droits
</h2>
<p>Conformément au RGPD, vous disposez des droits suivants :</p>
<ul className="list-disc pl-5 flex flex-col gap-1.5">
<li>
<span className="text-[#f0f0f0] font-semibold">Accès</span> -
consulter vos données via votre page de profil.
</li>
<li>
<span className="text-[#f0f0f0] font-semibold">
Portabilité
</span>{" "}
- télécharger l&apos;intégralité de vos données au format JSON
depuis votre profil (bouton &laquo;&nbsp;Télécharger mes
données&nbsp;&raquo;).
</li>
<li>
<span className="text-[#f0f0f0] font-semibold">
Rectification
</span>{" "}
- corriger des données inexactes en nous contactant.
</li>
<li>
<span className="text-[#f0f0f0] font-semibold">
Suppression
</span>{" "}
- supprimer votre compte depuis votre profil, rubrique{" "}
<span className="italic">Zone dangereuse</span>. Cette action
supprime immédiatement et définitivement votre compte ainsi que
l&apos;intégralité de vos données.
</li>
<li>
<span className="text-[#f0f0f0] font-semibold">Opposition</span>{" "}
- vous opposer à un traitement en nous contactant.
</li>
</ul>
<p>
Pour exercer ces droits :{" "}
<a
href="mailto:contact@jessy-david.dev"
className="text-[#7c3aed] hover:underline"
>
contact@jessy-david.dev
</a>
</p>
<p>
Vous pouvez également introduire une réclamation auprès de la{" "}
<a
href="https://www.cnil.fr/fr/plaintes"
target="_blank"
rel="noopener noreferrer"
className="text-[#7c3aed] hover:underline"
>
CNIL
</a>
.
</p>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg sm:text-xl font-bold text-[#f0f0f0]">
9. Contact
</h2>
<p>
Pour toute question relative à vos données personnelles :{" "}
<a
href="mailto:contact@jessy-david.dev"
className="text-[#7c3aed] hover:underline"
>
contact@jessy-david.dev
</a>
</p>
</section>
</div> </div>
</div> </div>
</div> </div>
+35
View File
@@ -0,0 +1,35 @@
"use client";
import { useEffect, useState } from "react";
const KEY = "wikirush_search_allowed";
export function useSearchAllowed() {
const [allowed, setAllowed] = useState<boolean>(() => {
if (typeof window === "undefined") return false;
return localStorage.getItem(KEY) === "true";
});
function toggle() {
setAllowed((prev) => {
const next = !prev;
localStorage.setItem(KEY, String(next));
return next;
});
}
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
if (!allowed) {
e.preventDefault();
e.stopPropagation();
}
}
}
window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => window.removeEventListener("keydown", handleKeyDown, { capture: true });
}, [allowed]);
return { allowed, toggle };
}