feat: build game catalog page with filters and grid
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
32
apps/web/src/app/[locale]/catalog/page.tsx
Normal file
32
apps/web/src/app/[locale]/catalog/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Suspense } from "react";
|
||||
import { getGames } from "@/lib/api";
|
||||
import { CatalogFilters } from "@/components/catalog/CatalogFilters";
|
||||
import { CatalogGrid } from "@/components/catalog/CatalogGrid";
|
||||
|
||||
export default async function CatalogPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
|
||||
let games: any[] = [];
|
||||
try {
|
||||
const res = await getGames(locale);
|
||||
games = res.data;
|
||||
} catch {
|
||||
// Strapi not running
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-12">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
{locale === "es" ? "Catálogo de Juegos" : "Game Catalog"}
|
||||
</h1>
|
||||
<Suspense>
|
||||
<CatalogFilters />
|
||||
</Suspense>
|
||||
<CatalogGrid games={games} locale={locale} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
apps/web/src/components/catalog/CatalogFilters.tsx
Normal file
56
apps/web/src/components/catalog/CatalogFilters.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import type { Genre, ServerStatus } from "@afterlife/shared";
|
||||
|
||||
const GENRES: Genre[] = ["MMORPG", "FPS", "Casual", "Strategy", "Sports", "Other"];
|
||||
const STATUSES: ServerStatus[] = ["online", "maintenance", "coming_soon"];
|
||||
|
||||
export function CatalogFilters() {
|
||||
const t = useTranslations("catalog");
|
||||
const tGame = useTranslations("game");
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const currentGenre = searchParams.get("genre") || "";
|
||||
const currentStatus = searchParams.get("status") || "";
|
||||
|
||||
function setFilter(key: string, value: string) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (value) {
|
||||
params.set(key, value);
|
||||
} else {
|
||||
params.delete(key);
|
||||
}
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-4 mb-8">
|
||||
<select
|
||||
value={currentGenre}
|
||||
onChange={(e) => setFilter("genre", e.target.value)}
|
||||
className="bg-gray-900 border border-white/10 rounded-lg px-4 py-2 text-sm text-white"
|
||||
>
|
||||
<option value="">{t("filter_genre")}: {t("all")}</option>
|
||||
{GENRES.map((g) => (
|
||||
<option key={g} value={g}>{g}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={currentStatus}
|
||||
onChange={(e) => setFilter("status", e.target.value)}
|
||||
className="bg-gray-900 border border-white/10 rounded-lg px-4 py-2 text-sm text-white"
|
||||
>
|
||||
<option value="">{t("filter_status")}: {t("all")}</option>
|
||||
{STATUSES.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{tGame(`status_${s}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
apps/web/src/components/catalog/CatalogGrid.tsx
Normal file
28
apps/web/src/components/catalog/CatalogGrid.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { Game } from "@afterlife/shared";
|
||||
import { GameCard } from "../shared/GameCard";
|
||||
|
||||
interface CatalogGridProps {
|
||||
games: Game[];
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function CatalogGrid({ games, locale }: CatalogGridProps) {
|
||||
const t = useTranslations("catalog");
|
||||
|
||||
if (games.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-gray-500 text-lg">{t("no_results")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{games.map((game) => (
|
||||
<GameCard key={game.id} game={game} locale={locale} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user