- curated, bilingual player-facing notes (lib/patch-notes.ts) — a public subset of CHANGELOG.md (which stays the technical source of truth) - "Nouveautés" button on home with a dot badge when the app version changed since last seen (localStorage); opening marks the version seen - app version injected at build via Vite define (__APP_VERSION__) - add shadcn Dialog component (@workspace/ui, via radix-ui) Verified: notes + version baked into the build; typecheck/lint/build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
import { useState } from "react"
|
|
import { useLocation } from "wouter"
|
|
import { motion } from "framer-motion"
|
|
import { LogIn, Sparkles } from "lucide-react"
|
|
import { Button } from "@workspace/ui/components/button"
|
|
import { useRoomStore } from "@/store/room"
|
|
import { useI18n } from "@/i18n/context"
|
|
import { errorMessage } from "@/i18n/error"
|
|
import { LanguageToggle } from "@/i18n/language-toggle"
|
|
import { ThemeToggle } from "@/components/theme-toggle"
|
|
import { WhatsNew } from "@/components/whats-new"
|
|
|
|
const inputClass =
|
|
"border-input bg-background ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
|
|
|
|
// Emojis geek qui flottent en fond.
|
|
const FLOATERS = [
|
|
{ emoji: "🎮", x: "8%", y: "18%", delay: 0 },
|
|
{ emoji: "🎧", x: "82%", y: "22%", delay: 0.6 },
|
|
{ emoji: "🧠", x: "15%", y: "72%", delay: 1.2 },
|
|
{ emoji: "👾", x: "78%", y: "70%", delay: 0.3 },
|
|
{ emoji: "🕹️", x: "45%", y: "12%", delay: 0.9 },
|
|
{ emoji: "🎬", x: "60%", y: "82%", delay: 1.5 },
|
|
]
|
|
|
|
export function HomePage() {
|
|
const [, navigate] = useLocation()
|
|
const { t } = useI18n()
|
|
const createRoom = useRoomStore((s) => s.createRoom)
|
|
const joinRoom = useRoomStore((s) => s.joinRoom)
|
|
const connected = useRoomStore((s) => s.connected)
|
|
|
|
const [code, setCode] = useState("")
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
async function handleCreate() {
|
|
setError(null)
|
|
setBusy(true)
|
|
try {
|
|
const { roomCode } = await createRoom()
|
|
navigate(`/room/${roomCode}`)
|
|
} catch (err) {
|
|
setError(errorMessage(t, err))
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
async function handleJoin() {
|
|
setError(null)
|
|
setBusy(true)
|
|
try {
|
|
const { roomCode } = await joinRoom(code.trim().toUpperCase())
|
|
navigate(`/room/${roomCode}`)
|
|
} catch (err) {
|
|
setError(errorMessage(t, err))
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="relative flex min-h-svh items-center justify-center overflow-hidden p-6">
|
|
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
<WhatsNew />
|
|
<ThemeToggle />
|
|
<LanguageToggle />
|
|
</div>
|
|
{/* Halo animé */}
|
|
<motion.div
|
|
aria-hidden
|
|
className="pointer-events-none absolute -z-10 size-[120vmin] rounded-full bg-gradient-to-br from-fuchsia-500/20 via-purple-500/10 to-cyan-400/20 blur-3xl"
|
|
animate={{ scale: [1, 1.15, 1], rotate: [0, 30, 0] }}
|
|
transition={{ duration: 18, repeat: Infinity, ease: "easeInOut" }}
|
|
/>
|
|
{/* Emojis flottants */}
|
|
{FLOATERS.map((f) => (
|
|
<motion.span
|
|
key={f.emoji}
|
|
aria-hidden
|
|
className="pointer-events-none absolute -z-10 text-4xl opacity-40 select-none sm:text-5xl"
|
|
style={{ left: f.x, top: f.y }}
|
|
animate={{ y: [0, -18, 0], rotate: [-8, 8, -8] }}
|
|
transition={{
|
|
duration: 5,
|
|
repeat: Infinity,
|
|
ease: "easeInOut",
|
|
delay: f.delay,
|
|
}}
|
|
>
|
|
{f.emoji}
|
|
</motion.span>
|
|
))}
|
|
|
|
<motion.div
|
|
className="flex w-full max-w-sm flex-col gap-6"
|
|
initial={{ opacity: 0, y: 16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
>
|
|
<header className="text-center">
|
|
<motion.h1
|
|
className="font-heading bg-gradient-to-r from-fuchsia-500 via-purple-500 to-cyan-400 bg-clip-text text-5xl font-black tracking-tight text-transparent"
|
|
initial={{ scale: 0.6, rotate: -6, opacity: 0 }}
|
|
animate={{ scale: 1, rotate: 0, opacity: 1 }}
|
|
transition={{ type: "spring", stiffness: 260, damping: 14 }}
|
|
>
|
|
NerdWare
|
|
</motion.h1>
|
|
<p className="text-muted-foreground mt-1 flex items-center justify-center gap-1 text-sm">
|
|
<Sparkles className="size-3.5" /> {t.home.tagline}
|
|
</p>
|
|
</header>
|
|
|
|
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
<Button
|
|
size="lg"
|
|
className="w-full"
|
|
disabled={!connected || busy}
|
|
onClick={handleCreate}
|
|
>
|
|
<Sparkles /> {t.home.create}
|
|
</Button>
|
|
</motion.div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="bg-border h-px flex-1" />
|
|
<span className="text-muted-foreground text-xs">{t.home.orJoin}</span>
|
|
<div className="bg-border h-px flex-1" />
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<input
|
|
className={`${inputClass} text-center text-lg font-bold tracking-[0.3em] uppercase`}
|
|
placeholder={t.home.codePlaceholder}
|
|
value={code}
|
|
maxLength={4}
|
|
onChange={(e) => setCode(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && code.trim() && handleJoin()}
|
|
/>
|
|
<Button
|
|
variant="secondary"
|
|
disabled={!connected || busy || code.trim().length === 0}
|
|
onClick={handleJoin}
|
|
>
|
|
<LogIn /> {t.home.join}
|
|
</Button>
|
|
</div>
|
|
|
|
{!connected && (
|
|
<p className="text-muted-foreground text-center text-xs">
|
|
{t.home.connecting}
|
|
</p>
|
|
)}
|
|
{error && (
|
|
<p className="text-destructive text-center text-sm">{error}</p>
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
)
|
|
}
|