141 lines
4.6 KiB
TypeScript
141 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
import { GAME_META, GameId } from "@/lib/levels";
|
|
import { GAME_RULES } from "@/lib/rules";
|
|
|
|
interface Props {
|
|
game: GameId;
|
|
/** Force-show regardless of localStorage (e.g. when user clicks "?") */
|
|
forceShow?: boolean;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
function seenKey(game: GameId) { return `rule-seen-${game}`; }
|
|
|
|
export function useRuleOverlay(game: GameId) {
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === "undefined") return;
|
|
const seen = localStorage.getItem(seenKey(game));
|
|
if (!seen) setVisible(true);
|
|
}, [game]);
|
|
|
|
const dismiss = useCallback(() => {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem(seenKey(game), "1");
|
|
}
|
|
setVisible(false);
|
|
}, [game]);
|
|
|
|
const open = useCallback(() => setVisible(true), []);
|
|
|
|
return { visible, dismiss, open };
|
|
}
|
|
|
|
export default function RuleOverlay({ game, forceShow, onClose }: Props) {
|
|
const { visible, dismiss } = useRuleOverlay(game);
|
|
const isOpen = forceShow || visible;
|
|
|
|
const handleClose = useCallback(() => {
|
|
dismiss();
|
|
onClose?.();
|
|
}, [dismiss, onClose]);
|
|
|
|
// Close on Escape
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") handleClose(); };
|
|
window.addEventListener("keydown", handler);
|
|
return () => window.removeEventListener("keydown", handler);
|
|
}, [isOpen, handleClose]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const { name, accent, symbol } = GAME_META[game];
|
|
const rules = GAME_RULES[game];
|
|
if (!rules) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4"
|
|
style={{ background: "rgba(0,0,0,0.45)" }}
|
|
onClick={handleClose}
|
|
>
|
|
<div
|
|
className="w-full max-w-sm bg-white rounded-3xl p-6 flex flex-col gap-5 shadow-2xl"
|
|
style={{ animation: "slideUp 0.28s cubic-bezier(0.34,1.56,0.64,1) both" }}
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className="w-9 h-9 rounded-xl flex items-center justify-center text-lg font-bold"
|
|
style={{ background: `${accent}18`, color: accent }}
|
|
aria-hidden
|
|
>
|
|
{symbol}
|
|
</span>
|
|
<div>
|
|
<h2 className="text-base font-bold text-gray-900">{name}</h2>
|
|
<p className="text-xs text-gray-400">{rules.duration}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleClose}
|
|
className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors flex items-center justify-center"
|
|
aria-label="Fermer"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Subtitle */}
|
|
<p className="text-sm text-gray-500 -mt-1">{rules.subtitle}</p>
|
|
|
|
{/* Rules */}
|
|
<div className="flex flex-col gap-2.5">
|
|
{rules.howToPlay.map((rule, i) => (
|
|
<div key={i} className="flex items-start gap-3">
|
|
<span
|
|
className="w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5"
|
|
style={{ background: `${accent}18`, color: accent }}
|
|
>
|
|
{i + 1}
|
|
</span>
|
|
<p className="text-sm text-gray-700 leading-snug">{rule}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tip */}
|
|
{rules.tip && (
|
|
<div className="flex items-start gap-2.5 px-3 py-2.5 rounded-xl" style={{ background: `${accent}0d` }}>
|
|
<span style={{ color: accent }} className="text-sm shrink-0 mt-px">💡</span>
|
|
<p className="text-xs text-gray-600 leading-snug">{rules.tip}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* CTA */}
|
|
<button
|
|
onClick={handleClose}
|
|
className="w-full py-3 rounded-2xl text-white font-semibold text-sm transition-opacity hover:opacity-90"
|
|
style={{ background: accent }}
|
|
>
|
|
C'est parti !
|
|
</button>
|
|
</div>
|
|
|
|
<style>{`
|
|
@keyframes slideUp {
|
|
from { opacity: 0; transform: translateY(24px) scale(0.97); }
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|