122 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
}
|