CI / test (push) Successful in 49s
Revisions with an IMVU parent store its public info (image, category path, gender) from IMVU's product API, looked up after registering and in a periodic pass. Product views gain parent, image and categories; GET /products takes category and limit. The website shows the image on cards and product pages. Co-Authored-By: Claude Opus 5.5 <[email protected]>
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
// 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" ? <Tag tone="adult">Adult</Tag> : null;
|
|
}
|
|
|
|
/** 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 (
|
|
<img
|
|
src={src}
|
|
alt=""
|
|
referrerPolicy="no-referrer"
|
|
loading="lazy"
|
|
className={`aspect-[5/4] w-full bg-muted object-contain ${className ?? ""}`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function ProductCard({
|
|
product,
|
|
children,
|
|
}: {
|
|
product: Pick<ProductSummary, "id" | "name" | "creator" | "rating" | "latest" | "image" | "categories">;
|
|
children?: React.ReactNode;
|
|
}) {
|
|
const category = product.categories.at(-1)?.name;
|
|
return (
|
|
<Card className="h-full">
|
|
<ProductImage src={product.image} />
|
|
<CardContent className="space-y-2">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<Link href={`/products/${product.id}`} className="font-medium underline-offset-4 hover:underline">
|
|
{product.name}
|
|
</Link>
|
|
<RatingTag rating={product.rating} />
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{[product.creator && `by ${product.creator}`, category, `revision ${product.latest}`].filter(Boolean).join(" · ")}
|
|
</p>
|
|
{children}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function Empty({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<Card>
|
|
<CardContent>
|
|
<p className="py-6 text-sm text-muted-foreground">{children}</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|