- Create LoadingSpinner component with spinning circle - Wrap products grid with Suspense boundary - Shows spinner while products are loading - Improves UX for slower connections Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
import { prisma } from '@/lib/prisma';
|
|
import { toProductDTO } from '@/lib/product';
|
|
import { fetchActivePromotions } from '@/lib/promotions';
|
|
import ProductCard from '@/components/ProductCard';
|
|
import LoadingSpinner from '@/components/LoadingSpinner';
|
|
import { Suspense } from 'react';
|
|
|
|
type SearchParams = {
|
|
page?: string;
|
|
};
|
|
|
|
const PRODUCTS_PER_PAGE = 20;
|
|
|
|
async function ProductsGrid({ currentPage }: { currentPage: number }) {
|
|
const activePromotions = await fetchActivePromotions();
|
|
|
|
const [rows, totalCount] = await Promise.all([
|
|
prisma.product.findMany({
|
|
where: { showOnPersonalised: true },
|
|
orderBy: { createdAt: 'asc' },
|
|
take: PRODUCTS_PER_PAGE,
|
|
skip: (currentPage - 1) * PRODUCTS_PER_PAGE,
|
|
}),
|
|
prisma.product.count({ where: { showOnPersonalised: true } }),
|
|
]);
|
|
|
|
const products = rows.map((p) => toProductDTO(p, activePromotions, 'personalised'));
|
|
const totalPages = Math.ceil(totalCount / PRODUCTS_PER_PAGE);
|
|
|
|
return { products, totalPages, totalCount, currentPage };
|
|
}
|
|
|
|
export default async function ProductsPage({ searchParams }: { searchParams: SearchParams }) {
|
|
const currentPage = Math.max(1, Number(searchParams.page) || 1);
|
|
const totalCount = await prisma.product.count({ where: { showOnPersonalised: true } });
|
|
|
|
return (
|
|
<div className="mx-auto max-w-6xl px-6 py-12">
|
|
<div className="mb-12 flex items-baseline justify-between">
|
|
<h1 className="font-display text-4xl">Shop all</h1>
|
|
<p className="tag-label">
|
|
{totalCount} template{totalCount === 1 ? '' : 's'}
|
|
</p>
|
|
</div>
|
|
|
|
<Suspense fallback={<LoadingSpinner />}>
|
|
<ProductsGridContent currentPage={currentPage} totalCount={totalCount} />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
async function ProductsGridContent({ currentPage, totalCount }: { currentPage: number; totalCount: number }) {
|
|
const { products, totalPages } = await ProductsGrid({ currentPage });
|
|
|
|
return (
|
|
<div>
|
|
{products.length === 0 ? (
|
|
<div className="border border-line bg-surface p-12 text-center">
|
|
<p className="font-display text-xl">Nothing here yet</p>
|
|
<p className="mt-2 text-sm text-muted">Templates will show up here once created.</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div className="grid gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3">
|
|
{products.map((p) => (
|
|
<ProductCard key={p.id} product={p} />
|
|
))}
|
|
</div>
|
|
|
|
{totalPages > 1 && (
|
|
<div className="mt-12 flex items-center justify-center gap-4 border-t border-line pt-8">
|
|
{currentPage > 1 && (
|
|
<a
|
|
href={`/made-to-order?page=${currentPage - 1}`}
|
|
className="border border-ink px-4 py-2 text-sm hover:border-clay hover:bg-clay hover:text-paper"
|
|
>
|
|
← Previous
|
|
</a>
|
|
)}
|
|
<p className="text-sm text-muted">
|
|
Page {currentPage} of {totalPages}
|
|
</p>
|
|
{currentPage < totalPages && (
|
|
<a
|
|
href={`/made-to-order?page=${currentPage + 1}`}
|
|
className="border border-ink px-4 py-2 text-sm hover:border-clay hover:bg-clay hover:text-paper"
|
|
>
|
|
Next →
|
|
</a>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|