TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import { useEffect, useState } from "react";4import Link from "next/link";5import { useRouter, useSearchParams } from "next/navigation";6import { AnimatePresence, animate, motion } from "framer-motion";7import { X, Play } from "lucide-react";8import type { GameCard as GameCardData } from "@spinza/shared";9import { STARTING_BALANCE } from "@spinza/shared";10import { useSession } from "@/lib/store";11import { Button } from "@/components/ui";12import { GameArt } from "./game-art";13import { VolatilityMeter } from "./game-card";1415/** First-time experience shown after registration (`/?welcome=1`). */16export function WelcomeOverlay({ picks }: { picks: GameCardData[] }) {17 const params = useSearchParams();18 const router = useRouter();19 const user = useSession((s) => s.user);20 const reduceMotion = useSession((s) => s.settings?.reduceMotion) ?? false;21 const [open, setOpen] = useState(params.get("welcome") === "1");22 const [phase, setPhase] = useState<"credits" | "choose">("credits");23 const [count, setCount] = useState(0);2425 useEffect(() => {26 if (!open) return;27 document.body.style.overflow = "hidden";28 const ctrl = animate(0, STARTING_BALANCE, { duration: reduceMotion ? 0.2 : 1.8, ease: [0.16, 1, 0.3, 1], onUpdate: (v) => setCount(Math.round(v)) });29 const t = setTimeout(() => setPhase("choose"), reduceMotion ? 600 : 2600);30 return () => {31 ctrl.stop();32 clearTimeout(t);33 document.body.style.overflow = "";34 };35 }, [open, reduceMotion]);3637 const dismiss = () => {38 setOpen(false);39 router.replace("/", { scroll: false });40 };4142 return (43 <AnimatePresence>44 {open ? (45 <motion.div className="fixed inset-0 flex flex-col overflow-y-auto bg-bg/95 backdrop-blur-xl" style={{ zIndex: "var(--z-modal)" }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} role="dialog" aria-modal aria-labelledby="welcome-title">46 <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_50%_30%,rgba(139,92,246,0.22),transparent_55%)]" />47 <button onClick={dismiss} className="tap absolute right-4 top-4 grid place-items-center rounded-full glass text-fg-2 hover:text-fg focus-ring" style={{ marginTop: "var(--safe-top)" }} aria-label="Skip">48 <X className="h-5 w-5" />49 </button>50 <div className="page relative flex flex-1 flex-col items-center justify-center py-16 text-center">51 <motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}>52 <div className="eyebrow mb-3 text-accent">{user ? `@${user.username}` : "New player"}</div>53 <h1 id="welcome-title" className="text-4xl font-semibold tracking-[-0.04em] sm:text-6xl">54 Welcome to Spinza.55 </h1>56 </motion.div>57 <motion.div className="mt-8" initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.3, duration: 0.7, ease: [0.16, 1, 0.3, 1] }}>58 <div className="text-[64px] font-semibold leading-none tracking-[-0.05em] text-credit tabular sm:text-[96px]" aria-live="polite">59 +{count.toLocaleString("en-US")}60 <span className="ml-2 text-[0.35em] font-bold tracking-wider text-credit/70">SC</span>61 </div>62 <p className="mt-3 text-sm text-fg-3">Your starting Spinza Credits. Fictional, free, and refilled every day you come back.</p>63 </motion.div>6465 <AnimatePresence>66 {phase === "choose" ? (67 <motion.div className="mt-12 w-full max-w-3xl" initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}>68 <h2 className="text-xl font-semibold tracking-tight sm:text-2xl">Choose your first game</h2>69 <p className="mt-1 text-sm text-fg-3">Three picks to start with. You can always come back to the lobby.</p>70 <div className="mt-6 grid grid-cols-3 gap-2 sm:gap-3">71 {picks.map((g, i) => (72 <motion.div key={g.slug} initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 + i * 0.1, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}>73 <Link href={`/games/${g.slug}`} onClick={() => setOpen(false)} className="group block overflow-hidden rounded-lg border border-line bg-bg-1 text-left shadow-card transition-transform hover:-translate-y-1 focus-ring" aria-label={`Play ${g.name}`}>74 <div className="aspect-[3/4]">75 <GameArt slug={g.slug} name={g.name} palette={g.palette} variant="card" />76 </div>77 <div className="flex items-center justify-between gap-2 p-2 sm:p-3">78 <div className="min-w-0">79 <div className="hidden truncate text-[12px] text-fg-3 sm:block">{g.tagline}</div>80 <VolatilityMeter volatility={g.volatility} className="sm:mt-1" />81 </div>82 <span className="hidden h-9 w-9 shrink-0 place-items-center rounded-full bg-fg text-bg sm:grid">83 <Play className="ml-0.5 h-4 w-4 fill-current" />84 </span>85 </div>86 </Link>87 </motion.div>88 ))}89 </div>90 <div className="mt-8">91 <Button variant="ghost" onClick={dismiss}>92 Browse the whole lobby instead93 </Button>94 </div>95 </motion.div>96 ) : null}97 </AnimatePresence>98 </div>99 </motion.div>100 ) : null}101 </AnimatePresence>102 );103}104