1"use client";23import { useMemo, useState, useEffect } from "react";4import { useRouter, useSearchParams } from "next/navigation";5import { Search, X, Heart } from "lucide-react";6import type { GameCard as GameCardData } from "@spinza/shared";7import { useSession } from "@/lib/store";8import { cn } from "@/lib/utils";9import { Button, Empty } from "@/components/ui";10import { GameCard } from "./game-card";1112const FILTERS = [13 { key: "all", label: "All" },14 { key: "featured", label: "Featured" },15 { key: "new", label: "New" },16 { key: "high", label: "High volatility" },17 { key: "relaxed", label: "Relaxed" },18 { key: "jackpot", label: "Jackpot" },19 { key: "favourites", label: "Favourites" },20] as const;21type FilterKey = (typeof FILTERS)[number]["key"];2223const isFilter = (v: string | null): v is FilterKey => !!v && FILTERS.some((f) => f.key === v);2425export function GamesBrowser({ games }: { games: GameCardData[] }) {26 const params = useSearchParams();27 const router = useRouter();28 const status = useSession((s) => s.status);29 const [filter, setFilter] = useState<FilterKey>(isFilter(params.get("filter")) ? (params.get("filter") as FilterKey) : "all");30 const [query, setQuery] = useState(params.get("q") ?? "");31 const [favs, setFavs] = useState<Record<string, boolean>>(() => Object.fromEntries(games.map((g) => [g.slug, !!g.favorite])));3233 useEffect(() => {34 const p = new URLSearchParams();35 if (filter !== "all") p.set("filter", filter);36 if (query.trim()) p.set("q", query.trim());37 const qs = p.toString();38 router.replace(qs ? `/games?${qs}` : "/games", { scroll: false });39 }, [filter, query, router]);4041 const list = useMemo(() => {42 const q = query.trim().toLowerCase();43 return games44 .filter((g) => {45 switch (filter) {46 case "featured":47 return g.isFeatured;48 case "new":49 return g.isNew;50 case "high":51 return g.volatility === "high" || g.volatility === "extreme";52 case "relaxed":53 return g.volatility === "low" || g.volatility === "medium";54 case "jackpot":55 return g.isJackpot;56 case "favourites":57 return favs[g.slug];58 default:59 return true;60 }61 })62 .filter((g) => !q || g.name.toLowerCase().includes(q) || g.tagline.toLowerCase().includes(q) || g.theme.toLowerCase().includes(q) || g.tags.some((t) => t.includes(q)) || g.features.some((f) => f.toLowerCase().includes(q)));63 }, [games, filter, query, favs]);6465 const counts = useMemo<Record<FilterKey, number>>(66 () => ({67 all: games.length,68 featured: games.filter((g) => g.isFeatured).length,69 new: games.filter((g) => g.isNew).length,70 high: games.filter((g) => g.volatility === "high" || g.volatility === "extreme").length,71 relaxed: games.filter((g) => g.volatility === "low" || g.volatility === "medium").length,72 jackpot: games.filter((g) => g.isJackpot).length,73 favourites: Object.values(favs).filter(Boolean).length,74 }),75 [games, favs],76 );7778 return (79 <div className="space-y-6">80 <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">81 <div className="snap-row -mx-4 px-4 sm:mx-0 sm:px-0 sm:flex-wrap" role="tablist" aria-label="Filter games">82 {FILTERS.map((f) => (83 <button key={f.key} role="tab" aria-selected={filter === f.key} onClick={() => setFilter(f.key)} className={cn("tap inline-flex h-10 min-w-0 items-center gap-1.5 whitespace-nowrap rounded-full border px-4 text-[13px] font-semibold transition-colors focus-ring", filter === f.key ? "border-accent/50 bg-accent-soft text-accent-2" : "border-line text-fg-3 hover:border-line-2 hover:text-fg")}>84 {f.key === "favourites" ? <Heart className={cn("h-3.5 w-3.5", filter === f.key && "fill-current")} /> : null}85 {f.label}86 <span className="tabular text-[11px] opacity-70">{counts[f.key]}</span>87 </button>88 ))}89 </div>90 <label className="relative block lg:w-72">91 <Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-fg-3" />92 <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search games, features…" aria-label="Search games" className="h-11 w-full rounded-full border border-line-2 bg-bg-1 pl-10 pr-10 text-[15px] placeholder:text-fg-4 focus-ring focus:border-accent/60" />93 {query ? (94 <button type="button" onClick={() => setQuery("")} className="absolute right-2 top-1/2 grid h-8 w-8 -translate-y-1/2 place-items-center rounded-full text-fg-3 hover:bg-surface-2 hover:text-fg" aria-label="Clear search">95 <X className="h-4 w-4" />96 </button>97 ) : null}98 </label>99 </div>100101 {filter === "favourites" && status !== "authenticated" ? (102 <Empty title="Sign in to keep favourites" description="Tap the heart on any game to pin it here. Favourites sync across your devices." icon={<Heart className="h-5 w-5" />} action={<Button href="/login?next=%2Fgames%3Ffilter%3Dfavourites">Sign in</Button>} />103 ) : list.length ? (104 <div className="grid grid-cols-2 gap-x-3 gap-y-6 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">105 {list.map((g) => (106 <GameCard key={g.slug} game={{ ...g, favorite: favs[g.slug] }} className="w-full sm:w-full" onFavoriteChange={(slug, fav) => setFavs((f) => ({ ...f, [slug]: fav }))} />107 ))}108 </div>109 ) : (110 <Empty111 title={filter === "favourites" ? "No favourites yet" : query ? `No games match “${query}”` : "Nothing here yet"}112 description={filter === "favourites" ? "Tap the heart on any game to pin it here." : query ? "Try a feature name like “cascades”, “jackpot” or “free spins”." : "New titles are being certified and will appear here soon."}113 icon={filter === "favourites" ? <Heart className="h-5 w-5" /> : <Search className="h-5 w-5" />}114 action={115 <Button116 variant="secondary"117 onClick={() => {118 setFilter("all");119 setQuery("");120 }}121 >122 Show all games123 </Button>124 }125 />126 )}127 </div>128 );129}130