Files
t5de-hub/apps/web/components/products.tsx
T
dhkatzandClaude Opus 5.5 51d5bb5ff3 Website: products, library, catalogs and their console page
/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]>
2026-10-29 19:06:08 -07:00

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>
);
}