SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
7.6 KB · 143 lines tsx
Raw Blame History
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 &amp; 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