// Pieces shared by the product pages. import Link from "next/link"; import { Tag } from "@/components/status"; import { Card, CardContent } from "@/components/ui/card"; import type { ProductSummary } from "@/lib/api"; export function RatingTag({ rating }: { rating: string }) { return rating === "adult" ? Adult : null; } /** A product's state, where it isn't plainly published and listed: draft, unlisted, deleted, removed, locked. */ export function StateTags({ product: p, }: { product: Pick; }) { return ( <> {p.status === "draft" && Draft} {p.status === "published" && !p.listed && Unlisted} {p.deleted && Deleted} {p.removed && Removed} {p.locked.length > 0 && Locked: {p.locked.join(", ")}} ); } /** 1536 → "1.5 KB". */ export function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; const units = ["KB", "MB", "GB"]; let n = bytes / 1024; let i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`; } /** The image of the IMVU product a product derives from (IMVU's, from IMVU), if it has one. */ export function ProductImage({ src, className }: { src: string | null | undefined; className?: string }) { if (!src) return null; return ( ); } export function ProductCard({ product, children, }: { product: Pick; children?: React.ReactNode; }) { const category = product.categories.at(-1)?.name; return (
{product.name} {!product.listed && Unlisted}

{[product.creator && `by ${product.creator}`, category, `revision ${product.latest}`].filter(Boolean).join(" · ")}

{children}
); } export function Empty({ children }: { children: React.ReactNode }) { return (

{children}

); }