rswag-online/frontend/app/products/page.tsx

122 lines
4.9 KiB
TypeScript

import Link from "next/link";
import { cookies } from "next/headers";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
interface Product {
slug: string;
name: string;
description: string;
category: string;
product_type: string;
image_url: string;
base_price: number;
}
function getMockupType(productType: string): string {
if (productType.includes("shirt") || productType.includes("tee") || productType.includes("hoodie")) return "shirt";
if (productType.includes("sticker")) return "sticker";
if (productType.includes("print")) return "print";
return "shirt";
}
async function getProducts(spaceId: string): Promise<Product[]> {
try {
const params = new URLSearchParams();
if (spaceId && spaceId !== "default") {
params.set("space", spaceId);
}
const url = `${API_URL}/products${params.toString() ? `?${params}` : ""}`;
const res = await fetch(url, { next: { revalidate: 3600 } });
if (!res.ok) return [];
return res.json();
} catch {
return [];
}
}
export default async function ProductsPage() {
const cookieStore = await cookies();
const spaceId = cookieStore.get("space_id")?.value || "default";
const products = await getProducts(spaceId);
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-12">
<div className="mb-12">
<h1 className="text-4xl font-bold tracking-tight">Products</h1>
<p className="mt-2 text-lg text-muted-foreground">
Print-on-demand merch designed by the community, fulfilled by Printful.
</p>
</div>
{products.length === 0 ? (
<div className="flex flex-col items-center justify-center py-24 text-center">
<div className="rounded-full bg-muted p-6 mb-6">
<svg className="h-12 w-12 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">No products yet</h2>
<p className="text-muted-foreground max-w-md">
New designs are being added. Check back soon or create your own.
</p>
<Link
href="/upload"
className="mt-6 inline-flex items-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
Upload a Design
</Link>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{products.map((product) => (
<Link
key={product.slug}
href={`/products/${product.slug}`}
className="group block"
>
<div className="relative overflow-hidden rounded-xl border bg-card shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
{/* Product image */}
<div className="aspect-square bg-muted relative overflow-hidden">
<img
src={`${API_URL}/designs/${product.slug}/mockup?type=${getMockupType(product.product_type)}`}
alt={product.name}
className="object-cover w-full h-full transition-transform duration-500 group-hover:scale-105"
loading="lazy"
/>
{/* Category badge */}
<div className="absolute top-3 left-3">
<span className="inline-flex items-center rounded-full bg-black/60 backdrop-blur-sm px-3 py-1 text-xs font-medium text-white capitalize">
{product.product_type}
</span>
</div>
</div>
{/* Product info */}
<div className="p-5">
<h3 className="font-semibold text-lg leading-tight group-hover:text-primary transition-colors">
{product.name}
</h3>
<p className="mt-1.5 text-sm text-muted-foreground line-clamp-2">
{product.description}
</p>
<div className="mt-4 flex items-center justify-between">
<span className="text-xl font-bold">
${product.base_price.toFixed(2)}
</span>
<span className="text-xs font-medium text-primary opacity-0 group-hover:opacity-100 transition-opacity">
View Details
</span>
</div>
</div>
</div>
</Link>
))}
</div>
)}
</div>
</div>
);
}