/products (search), product pages with revisions, getting and staying on a revision, retracting; /products/:id/edit; /create and new revisions upload from the browser straight to the catalog with a token from the hub; /library; /catalogs (directory, registering with a once-shown key); /console/catalogs to approve or reject. Every page now sends a nonce-based CSP (proxy.ts), so product descriptions can't run scripts. buttonVariants now merges classes, so outline links get their border. Co-Authored-By: Claude Opus 5.5 <[email protected]>
58 lines
1.7 KiB
TypeScript
58 lines
1.7 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]}`;
|
|
}
|
|
|
|
export function ProductCard({
|
|
product,
|
|
children,
|
|
}: {
|
|
product: Pick<ProductSummary, "id" | "name" | "creator" | "rating" | "latest">;
|
|
children?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<Card className="h-full">
|
|
<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}`, `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>
|
|
);
|
|
}
|