import { getTranslations } from "next-intl/server"; import Link from "next/link"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { excerpt } from "@/lib/format"; import { prisma } from "@/lib/prisma"; export const revalidate = 300; export const metadata = { title: "Shop" }; // Faithful to AtomCMS WebsiteShopArticle::price(): costs are stored in cents, // the displayed price is costs/100 with a floor of 1. function priceLabel(costs: number): string { const price = costs < 100 ? 1 : costs / 100; return `$${price.toFixed(2)}`; } type ShopArticle = { id: bigint; name: string; info: string; iconUrl: string; color: string; costs: number; credits: number | null; duckets: number | null; diamonds: number | null; badges: string | null; position: number; }; type ShopCategory = { id: bigint; name: string; icon: string | null; }; const ARTICLE_SELECT = { id: true, name: true, info: true, iconUrl: true, color: true, costs: true, credits: true, duckets: true, diamonds: true, badges: true, position: true, } as const; export default async function ShopPage({ searchParams, }: { searchParams: Promise<{ category?: string }>; }) { const t = await getTranslations("pages.shop"); const sp = await searchParams; const rawCategory = (sp.category ?? "").trim(); const categoryId = /^\d+$/.test(rawCategory) ? rawCategory : ""; // Categories for the filter bar. let categories: ShopCategory[] = []; try { categories = await prisma.websiteShopCategories.findMany({ select: { id: true, name: true, icon: true }, orderBy: { name: "asc" }, }); } catch { categories = []; } // Articles. The category foreign key (website_shop_category_id) is not modelled // in the typed Prisma client, so when filtering we resolve the matching ids via // a guarded raw query, then load the rows through the typed client. let articles: ShopArticle[] = []; try { if (categoryId) { let ids: bigint[] = []; try { const rows = await prisma.$queryRawUnsafe<{ id: bigint }[]>( "SELECT id FROM website_shop_articles WHERE website_shop_category_id = ?", BigInt(categoryId), ); ids = rows.map((r) => r.id); } catch { ids = []; } articles = ids.length ? await prisma.websiteShopArticles.findMany({ where: { id: { in: ids } }, select: ARTICLE_SELECT, orderBy: { position: "asc" }, }) : []; } else { articles = await prisma.websiteShopArticles.findMany({ select: ARTICLE_SELECT, orderBy: { position: "asc" }, }); } } catch { articles = []; } const activeCategory = categoryId ? categories.find((c) => String(c.id) === categoryId) : undefined; return (
{categories.length > 0 ? (
{t("all")} {categories.map((c) => ( {c.name} ))}
) : null}
0 ? t("packagesAvailable", { count: articles.length }) : undefined } padded={articles.length === 0} > {articles.length === 0 ? ( {t("empty")} ) : (
{articles.map((a) => (
{a.iconUrl ? ( // Article icons are arbitrary external/Imgur URLs from the // emulator data, so a plain is correct here. // eslint-disable-next-line @next/next/no-img-element ) : null}

{a.name}

{priceLabel(a.costs)}

{excerpt(a.info, 160)}

{a.credits ? ( cr {a.credits.toLocaleString()} ) : null} {a.duckets ? ( du {a.duckets.toLocaleString()} ) : null} {a.diamonds ? ( di {a.diamonds.toLocaleString()} ) : null}
{priceLabel(a.costs)} {t("buy")}
))}
)}
); }