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%
6.7 KB · 153 lines tsx
Raw Blame History
1"use client";23import Link from "next/link";4import { usePathname, useRouter } from "next/navigation";5import { Home, LayoutGrid, Gift, Trophy, User, LogOut, Settings, History, Shield } from "lucide-react";6import { SpinzaWordmark, SpinzaMark } from "@/components/brand/logo";7import { useSession } from "@/lib/store";8import { Button, Credits } from "@/components/ui";9import { cn } from "@/lib/utils";1011const NAV = [12  { href: "/", label: "Home", icon: Home },13  { href: "/games", label: "Games", icon: LayoutGrid },14  { href: "/rewards", label: "Rewards", icon: Gift },15  { href: "/leaderboard", label: "Leaderboard", icon: Trophy },16  { href: "/profile", label: "Profile", icon: User },17];1819const SIDE_EXTRA = [20  { href: "/profile/history", label: "History", icon: History },21  { href: "/settings", label: "Settings", icon: Settings },22  { href: "/responsible-play", label: "Responsible play", icon: Shield },23];2425function isActive(path: string, href: string) {26  return href === "/" ? path === "/" : path.startsWith(href);27}2829/** Desktop: sidebar + top bar. Mobile: top logo + balance, bottom navigation. */30export function AppShell({ children, wide }: { children: React.ReactNode; wide?: boolean }) {31  const path = usePathname();32  const router = useRouter();33  const { status, user, wallet, signOut } = useSession();34  const authed = status === "authenticated";3536  return (37    <div className="min-h-dvh lg:grid lg:grid-cols-[248px_1fr]">38      {/* Sidebar (desktop) */}39      <aside className="sticky top-0 hidden h-dvh flex-col border-r border-line bg-bg-1/60 px-4 py-6 lg:flex">40        <Link href="/" className="px-2">41          <SpinzaWordmark />42        </Link>43        <nav className="mt-8 flex flex-col gap-1">44          {NAV.map((n) => (45            <Link key={n.href} href={n.href} className={cn("flex items-center gap-3 rounded-md px-3 py-2.5 text-[15px] font-medium transition-colors", isActive(path, n.href) ? "bg-surface-2 text-fg" : "text-fg-3 hover:bg-surface hover:text-fg-2")}>46              <n.icon className="h-[18px] w-[18px]" />47              {n.label}48            </Link>49          ))}50          <div className="my-3 h-px bg-line" />51          {SIDE_EXTRA.map((n) => (52            <Link key={n.href} href={n.href} className={cn("flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors", isActive(path, n.href) ? "bg-surface-2 text-fg" : "text-fg-3 hover:bg-surface hover:text-fg-2")}>53              <n.icon className="h-4 w-4" />54              {n.label}55            </Link>56          ))}57        </nav>58        <div className="mt-auto">59          {authed && user ? (60            <div className="surface rounded-lg p-3">61              <div className="flex items-center justify-between">62                <div className="truncate text-sm font-semibold">{user.username}</div>63                <span className="rounded-full bg-accent-soft px-2 py-0.5 text-[11px] font-bold text-accent-2">LVL {user.level}</span>64              </div>65              <div className="mt-1">66                <Credits amount={wallet?.balance ?? 0} size="md" />67              </div>68              <button69                onClick={() =>70                  void signOut().then(() => {71                    router.push("/");72                    router.refresh();73                  })74                }75                className="mt-3 flex items-center gap-2 text-[13px] text-fg-3 hover:text-fg"76              >77                <LogOut className="h-3.5 w-3.5" /> Sign out78              </button>79            </div>80          ) : (81            <div className="flex flex-col gap-2">82              <Button variant="accent" href="/register">83                Play free84              </Button>85              <Button variant="ghost" href="/login">86                Sign in87              </Button>88            </div>89          )}90          <p className="mt-4 px-1 text-[11px] leading-relaxed text-fg-4">Virtual credits only. No deposits. No withdrawals. No cash value. 18+.</p>91        </div>92      </aside>9394      <div className="flex min-h-dvh flex-col">95        {/* Top bar */}96        <header className="sticky top-0 glass border-x-0 border-t-0" style={{ zIndex: "var(--z-nav)", paddingTop: "var(--safe-top)" }}>97          <div className={cn("flex h-14 items-center justify-between gap-3 px-4 lg:px-8", wide ? "" : "mx-auto max-w-[1320px]")}>98            <Link href="/" className="lg:hidden">99              <SpinzaMark className="h-8 w-8" />100            </Link>101            <div className="hidden text-sm text-fg-3 lg:block">{pageTitle(path)}</div>102            <div className="flex items-center gap-2">103              {authed ? (104                <>105                  <Link href="/rewards" className="surface flex h-10 items-center gap-2 rounded-full px-3.5">106                    <Credits amount={wallet?.balance ?? 0} size="sm" />107                  </Link>108                  <Link href="/profile" className="metal hidden h-10 items-center gap-2 rounded-full px-3 text-sm font-semibold sm:flex">109                    <span className="grid h-6 w-6 place-items-center rounded-full bg-accent text-[11px] font-bold text-bg">{user?.username.slice(0, 1).toUpperCase()}</span>110                    <span className="max-w-[120px] truncate">{user?.username}</span>111                  </Link>112                </>113              ) : status === "guest" ? (114                <>115                  <Button variant="ghost" size="sm" href="/login">116                    Sign in117                  </Button>118                  <Button variant="accent" size="sm" href="/register">119                    Play free120                  </Button>121                </>122              ) : null}123            </div>124          </div>125        </header>126127        <main className={cn("flex-1 pb-[calc(76px+var(--safe-bottom))] lg:pb-10", wide ? "" : "page py-6 lg:py-8")}>{children}</main>128129        {/* Bottom nav (mobile) */}130        <nav className="fixed inset-x-0 bottom-0 glass border-x-0 border-b-0 lg:hidden" style={{ zIndex: "var(--z-nav)", paddingBottom: "var(--safe-bottom)" }}>131          <div className="grid h-[64px] grid-cols-5">132            {NAV.map((n) => {133              const active = isActive(path, n.href);134              return (135                <Link key={n.href} href={n.href} className={cn("flex flex-col items-center justify-center gap-1 text-[11px] font-medium transition-colors", active ? "text-accent-2" : "text-fg-3")}>136                  <n.icon className={cn("h-[22px] w-[22px]", active && "drop-shadow-[0_0_8px_rgba(201,169,97,0.6)]")} />137                  {n.label}138                </Link>139              );140            })}141          </div>142        </nav>143      </div>144    </div>145  );146}147148function pageTitle(path: string): string {149  if (path === "/") return "Lobby";150  const seg = path.split("/")[1];151  return seg ? seg.charAt(0).toUpperCase() + seg.slice(1) : "";152}153