TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import { Suspense } from "react";2import Link from "next/link";3import { Play, Info, Sparkles } from "lucide-react";4import type { GameCard as GameCardData, PublicUser } from "@spinza/shared";5import { Button } from "@/components/ui";6import { ServerUnavailable } from "@/components/shell/api-error";7import { GameArt } from "./game-art";8import { GameCard, GameRow, GameTile, VolatilityMeter } from "./game-card";9import { RewardsStrip } from "./rewards-strip";10import { WelcomeOverlay } from "./welcome-overlay";11import { timeAgo } from "@/lib/utils";1213export interface LobbyProps {14 user: PublicUser;15 games: GameCardData[] | null;16 livePlayers: number;17 recommendations: string[];18}1920const FLAGSHIP_SLUG = "spinza-original";2122export function Lobby({ user, games, livePlayers, recommendations }: LobbyProps) {23 const list = games ?? [];24 const slots = list.filter((g) => (g.kind ?? "slot") === "slot");25 const risk = list.filter((g) => g.kind === "crash");26 const originals = list.filter((g) => g.kind === "arcade");27 const flagship = list.find((g) => g.slug === FLAGSHIP_SLUG) ?? list.find((g) => g.isFeatured) ?? list[0] ?? null;28 const continuePlaying = list.filter((g) => g.lastPlayedAt).sort((a, b) => new Date(b.lastPlayedAt!).getTime() - new Date(a.lastPlayedAt!).getTime());29 const featured = list.filter((g) => g.isFeatured);30 const fresh = list.filter((g) => g.isNew);31 const popular = [...list].sort((a, b) => b.popularity - a.popularity).slice(0, 10);32 const highVol = slots.filter((g) => g.volatility === "high" || g.volatility === "extreme");33 const relaxed = slots.filter((g) => g.volatility === "low" || g.volatility === "medium");34 const jackpots = slots.filter((g) => g.isJackpot);35 const picks = recommendations.map((s) => list.find((g) => g.slug === s)).filter((g): g is GameCardData => !!g);36 const welcomePicks = picks.length ? picks : list.slice(0, 3);3738 return (39 <div className="space-y-10 lg:space-y-12">40 <Suspense fallback={null}>41 <WelcomeOverlay picks={welcomePicks} />42 </Suspense>4344 {/* ---------------------------------------------------------- hero */}45 {flagship ? (46 <section className="relative overflow-hidden rounded-xl border border-line bg-bg-1" aria-labelledby="lobby-hero-title">47 <div className="absolute inset-0">48 <GameArt slug={flagship.slug} name={flagship.name} palette={flagship.palette} variant="hero" showName={false} />49 <div className="absolute inset-0 bg-[linear-gradient(90deg,rgba(7,8,12,0.92)_0%,rgba(7,8,12,0.7)_45%,rgba(7,8,12,0.15)_100%)]" />50 <div className="absolute inset-x-0 bottom-0 h-2/3 bg-[linear-gradient(180deg,transparent,rgba(7,8,12,0.85))]" />51 </div>52 <div className="relative flex min-h-[340px] flex-col justify-end p-6 sm:min-h-[420px] sm:p-10">53 <div className="eyebrow mb-3 flex items-center gap-2 text-accent">54 <Sparkles className="h-3.5 w-3.5" /> {flagship.slug === FLAGSHIP_SLUG ? "Flagship title" : "Featured"}55 </div>56 <h1 id="lobby-hero-title" className="text-4xl font-semibold leading-[0.95] tracking-[-0.04em] sm:text-6xl">57 {flagship.name}58 </h1>59 <p className="mt-3 max-w-md text-[15px] text-fg-2 sm:text-base">{flagship.tagline}</p>60 <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px] text-fg-3">61 <VolatilityMeter volatility={flagship.volatility} />62 <span>{flagship.grid.reels}×{flagship.grid.rows} · {flagship.features.slice(0, 2).join(" · ")}</span>63 </div>64 <div className="mt-6 flex flex-wrap gap-3">65 <Button size="lg" href={`/games/${flagship.slug}`} className="min-w-[160px]">66 <Play className="h-4 w-4 fill-current" /> Play now67 </Button>68 <Button variant="outline" size="lg" href={`/games/${flagship.slug}#info`} className="glass">69 <Info className="h-4 w-4" /> Game info70 </Button>71 </div>72 </div>73 </section>74 ) : games === null ? (75 <ServerUnavailable />76 ) : (77 <section className="surface rounded-xl p-8 text-center sm:p-12">78 <div className="eyebrow mb-2">Lobby</div>79 <h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Welcome back, {user.username}.</h1>80 <p className="mx-auto mt-2 max-w-md text-sm text-fg-3">New games are being certified and will appear here as soon as they pass. Your credits and progress are safe.</p>81 <Button className="mt-6" variant="secondary" href="/rewards">82 Visit rewards83 </Button>84 </section>85 )}8687 {/* --------------------------------------------------------- strip */}88 <RewardsStrip livePlayers={livePlayers} />8990 {games === null && flagship ? <ServerUnavailable compact /> : null}9192 {/* ------------------------------------------------------ sections */}93 {continuePlaying.length ? (94 <section aria-labelledby="continue-title">95 <div className="mb-3">96 <div className="eyebrow mb-1">Pick up where you left off</div>97 <h2 id="continue-title" className="text-xl font-semibold tracking-tight sm:text-2xl">98 Continue playing99 </h2>100 </div>101 <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">102 {continuePlaying.slice(0, 6).map((g) => (103 <GameTile key={g.slug} game={g} meta={`Played ${timeAgo(g.lastPlayedAt!)}`} />104 ))}105 </div>106 </section>107 ) : null}108109 <GameRow id="risk" title="Risk Games — Cash Out" eyebrow="One decision: take it now, or push?" games={risk} href="/games?filter=risk" />110 <GameRow id="originals" title="Spinza Originals — Beyond Slots" eyebrow="Interactive originals, no reels" games={originals} href="/games?filter=originals" />111 <GameRow id="featured" title="Featured games" eyebrow="Curated tonight" games={featured} href="/games?filter=featured" />112 <GameRow id="new" title="New releases" eyebrow="Fresh from the studio" games={fresh} href="/games?filter=new" />113 <GameRow id="popular" title="Popular" eyebrow="Most played this week" games={popular} href="/games" />114 <GameRow id="high" title="High volatility" eyebrow="Big swings, big moments" games={highVol} href="/games?filter=high" />115 <GameRow id="relaxed" title="Relaxed games" eyebrow="Steady, easy sessions" games={relaxed} href="/games?filter=relaxed" />116 <GameRow id="jackpot" title="Jackpot games" eyebrow="Hold, respin, fill the grid" games={jackpots} href="/games?filter=jackpot" />117118 {list.length ? (119 <section aria-labelledby="all-title">120 <div className="mb-4 flex items-end justify-between">121 <div>122 <div className="eyebrow mb-1">{list.length} titles</div>123 <h2 id="all-title" className="text-xl font-semibold tracking-tight sm:text-2xl">124 All games125 </h2>126 </div>127 <Link href="/games" className="tap inline-flex h-9 min-h-0 items-center text-sm font-medium text-fg-3 hover:text-fg">128 Filter & search129 </Link>130 </div>131 <div className="grid grid-cols-2 gap-x-3 gap-y-6 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">132 {list.map((g) => (133 <GameCard key={g.slug} game={g} className="w-full sm:w-full" />134 ))}135 </div>136 </section>137 ) : null}138139 <p className="pt-4 text-center text-[12px] text-fg-4">Virtual credits only. No deposits. No withdrawals. No cash value. 18+.</p>140 </div>141 );142}143