131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
// app/builds/page.tsx
|
|
import Link from "next/link";
|
|
|
|
type BuildClass = "Rifle" | "Pistol" | "NFA";
|
|
type Caliber = string;
|
|
|
|
type BuildCard = {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
creator: string;
|
|
caliber: Caliber;
|
|
buildClass: BuildClass;
|
|
price: number;
|
|
votes: number;
|
|
tags: string[];
|
|
coverImageUrl?: string | null;
|
|
};
|
|
|
|
type BuildFeedCardDto = {
|
|
uuid: string;
|
|
title?: string | null;
|
|
slug?: string | null;
|
|
creator?: string | null;
|
|
caliber?: string | null;
|
|
buildClass?: BuildClass | null;
|
|
price?: number | null;
|
|
votes?: number | null;
|
|
tags?: string[] | null;
|
|
coverImageUrl?: string | null;
|
|
};
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
|
|
|
function safeArray(v: unknown): string[] {
|
|
return Array.isArray(v) ? v.filter(Boolean).map(String) : [];
|
|
}
|
|
|
|
function fallbackSlug(dto: BuildFeedCardDto) {
|
|
return dto.slug?.trim() || dto.uuid;
|
|
}
|
|
|
|
function normalizeCard(dto: BuildFeedCardDto): BuildCard {
|
|
const title = (dto.title ?? "Untitled Build").trim() || "Untitled Build";
|
|
|
|
return {
|
|
id: dto.uuid,
|
|
title,
|
|
slug: fallbackSlug(dto),
|
|
creator: (dto.creator ?? "anonymous").trim() || "anonymous",
|
|
caliber: (dto.caliber ?? "—").trim() || "—",
|
|
buildClass: (dto.buildClass ?? "Rifle") as BuildClass,
|
|
price: typeof dto.price === "number" ? dto.price : 0,
|
|
votes: typeof dto.votes === "number" ? dto.votes : 0,
|
|
tags: safeArray(dto.tags),
|
|
coverImageUrl: dto.coverImageUrl ?? null,
|
|
};
|
|
}
|
|
|
|
// Import the client component that will handle filters & voting
|
|
import BuildsClient from "./BuildsClient";
|
|
|
|
export default async function BuildsPage() {
|
|
let cards: BuildCard[] = [];
|
|
let error: string | null = null;
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/api/v1/builds?limit=50`, {
|
|
method: "GET",
|
|
// Disable full-page static caching; you can adjust this per your needs:
|
|
cache: "no-store",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const txt = await res.text().catch(() => "");
|
|
throw new Error(txt || `Failed to load builds (${res.status})`);
|
|
}
|
|
|
|
const data = (await res.json()) as BuildFeedCardDto[];
|
|
cards = (Array.isArray(data) ? data : []).map(normalizeCard);
|
|
} catch (e: any) {
|
|
error = e?.message || "Failed to load builds feed";
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-screen bg-black text-zinc-50">
|
|
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
|
{/* Header */}
|
|
<header className="mb-6 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
|
<div>
|
|
<p className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
|
Battl Builder · Build Book
|
|
</p>
|
|
<h1
|
|
className="mt-2 text-3xl md:text-4xl font-extrabold tracking-tight
|
|
bg-gradient-to-r from-amber-400 via-amber-200 to-amber-400
|
|
text-transparent bg-clip-text drop-shadow-[0_2px_6px_rgba(255,193,7,0.25)]"
|
|
>
|
|
Community Builds
|
|
</h1>
|
|
<p className="mt-2 text-sm md:text-base text-zinc-400 max-w-xl">
|
|
Browse community builds, vote them up or down, and steal good
|
|
ideas without shame. This feed is backed by public builds
|
|
(isPublic=true).
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-2 md:gap-3">
|
|
<Link
|
|
href="/builder"
|
|
className="inline-flex items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/60 px-4 py-2 text-xs md:text-sm font-medium text-zinc-200 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
|
>
|
|
Open Builder
|
|
</Link>
|
|
<Link
|
|
href="/vault"
|
|
className="inline-flex items-center justify-center rounded-md border border-amber-400/70 bg-amber-400/10 px-4 py-2 text-xs md:text-sm font-medium text-amber-200 hover:bg-amber-400/15 transition-colors"
|
|
>
|
|
+ Submit Build
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Pass initial data + any load error into the client component */}
|
|
<BuildsClient initialBuilds={cards} initialError={error} />
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|