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]>
This commit is contained in:
1 parent
b4f6fce9df
commit
51d5bb5ff3
24 files changed
+1064
-5
No files matched your search
@@ -0,0 +1,57 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user