SPB Git

spb/spinza

Public
6.2 KB · 130 lines tsx
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
Files · 3b4179a710ab1cd5f12ca7cfd253b81674f1521e
Menu

Theme