TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
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