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 flagship = list.find((g) => g.slug === FLAGSHIP_SLUG) ?? list.find((g) => g.isFeatured) ?? list[0] ?? null;25 const continuePlaying = list.filter((g) => g.lastPlayedAt).sort((a, b) => new Date(b.lastPlayedAt!).getTime() - new Date(a.lastPlayedAt!).getTime());26 const featured = list.filter((g) => g.isFeatured);27 const fresh = list.filter((g) => g.isNew);28 const popular = [...list].sort((a, b) => b.popularity - a.popularity).slice(0, 10);29 const highVol = list.filter((g) => g.volatility === "high" || g.volatility === "extreme");30 const relaxed = list.filter((g) => g.volatility === "low" || g.volatility === "medium");31 const jackpots = list.filter((g) => g.isJackpot);32 const picks = recommendations.map((s) => list.find((g) => g.slug === s)).filter((g): g is GameCardData => !!g);33 const welcomePicks = picks.length ? picks : list.slice(0, 3);3435 return (36 <div className="space-y-10 lg:space-y-12">37 <Suspense fallback={null}>38 <WelcomeOverlay picks={welcomePicks} />39 </Suspense>4041 {/* ---------------------------------------------------------- hero */}42 {flagship ? (43 <section className="relative overflow-hidden rounded-xl border border-line bg-bg-1" aria-labelledby="lobby-hero-title">44 <div className="absolute inset-0">45 <GameArt slug={flagship.slug} name={flagship.name} palette={flagship.palette} variant="hero" showName={false} />46 <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%)]" />47 <div className="absolute inset-x-0 bottom-0 h-2/3 bg-[linear-gradient(180deg,transparent,rgba(7,8,12,0.85))]" />48 </div>49 <div className="relative flex min-h-[340px] flex-col justify-end p-6 sm:min-h-[420px] sm:p-10">50 <div className="eyebrow mb-3 flex items-center gap-2 text-accent">51 <Sparkles className="h-3.5 w-3.5" /> {flagship.slug === FLAGSHIP_SLUG ? "Flagship title" : "Featured"}52 </div>53 <h1 id="lobby-hero-title" className="text-4xl font-semibold leading-[0.95] tracking-[-0.04em] sm:text-6xl">54 {flagship.name}55 </h1>56 <p className="mt-3 max-w-md text-[15px] text-fg-2 sm:text-base">{flagship.tagline}</p>57 <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px] text-fg-3">58 <VolatilityMeter volatility={flagship.volatility} />59 <span>{flagship.grid.reels}×{flagship.grid.rows} · {flagship.features.slice(0, 2).join(" · ")}</span>60 </div>61 <div className="mt-6 flex flex-wrap gap-3">62 <Button size="lg" href={`/games/${flagship.slug}`} className="min-w-[160px]">63 <Play className="h-4 w-4 fill-current" /> Play now64 </Button>65 <Button variant="outline" size="lg" href={`/games/${flagship.slug}#info`} className="glass">66 <Info className="h-4 w-4" /> Game info67 </Button>68 </div>69 </div>70 </section>71 ) : games === null ? (72 <ServerUnavailable />73 ) : (74 <section className="surface rounded-xl p-8 text-center sm:p-12">75 <div className="eyebrow mb-2">Lobby</div>76 <h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Welcome back, {user.username}.</h1>77 <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>78 <Button className="mt-6" variant="secondary" href="/rewards">79 Visit rewards80 </Button>81 </section>82 )}8384 {/* --------------------------------------------------------- strip */}85 <RewardsStrip livePlayers={livePlayers} />8687 {games === null && flagship ? <ServerUnavailable compact /> : null}8889 {/* ------------------------------------------------------ sections */}90 {continuePlaying.length ? (91 <section aria-labelledby="continue-title">92 <div className="mb-3">93 <div className="eyebrow mb-1">Pick up where you left off</div>94 <h2 id="continue-title" className="text-xl font-semibold tracking-tight sm:text-2xl">95 Continue playing96 </h2>97 </div>98 <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">99 {continuePlaying.slice(0, 6).map((g) => (100 <GameTile key={g.slug} game={g} meta={`Played ${timeAgo(g.lastPlayedAt!)}`} />101 ))}102 </div>103 </section>104 ) : null}105106 <GameRow id="featured" title="Featured games" eyebrow="Curated tonight" games={featured} href="/games?filter=featured" />107 <GameRow id="new" title="New releases" eyebrow="Fresh from the studio" games={fresh} href="/games?filter=new" />108 <GameRow id="popular" title="Popular" eyebrow="Most played this week" games={popular} href="/games" />109 <GameRow id="high" title="High volatility" eyebrow="Big swings, big moments" games={highVol} href="/games?filter=high" />110 <GameRow id="relaxed" title="Relaxed games" eyebrow="Steady, easy sessions" games={relaxed} href="/games?filter=relaxed" />111 <GameRow id="jackpot" title="Jackpot games" eyebrow="Hold, respin, fill the grid" games={jackpots} href="/games?filter=jackpot" />112113 {list.length ? (114 <section aria-labelledby="all-title">115 <div className="mb-4 flex items-end justify-between">116 <div>117 <div className="eyebrow mb-1">{list.length} titles</div>118 <h2 id="all-title" className="text-xl font-semibold tracking-tight sm:text-2xl">119 All games120 </h2>121 </div>122 <Link href="/games" className="tap inline-flex h-9 min-h-0 items-center text-sm font-medium text-fg-3 hover:text-fg">123 Filter & search124 </Link>125 </div>126 <div className="grid grid-cols-2 gap-x-3 gap-y-6 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">127 {list.map((g) => (128 <GameCard key={g.slug} game={g} className="w-full sm:w-full" />129 ))}130 </div>131 </section>132 ) : null}133134 <p className="pt-4 text-center text-[12px] text-fg-4">Virtual credits only. No deposits. No withdrawals. No cash value. 18+.</p>135 </div>136 );137}138