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]>
This commit is contained in:
1 parent
d57674d90c
commit
6668b55117
15 files changed
+2026
-29
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { ActionForm, SubmitButton } from "@/components/forms";
|
||||
import { formatSize, RatingTag } from "@/components/products";
|
||||
import { formatSize, ProductImage, RatingTag } from "@/components/products";
|
||||
import { PageHeader, Tag } from "@/components/status";
|
||||
import { DateTime } from "@/components/time";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
@@ -30,6 +30,19 @@ export default async function ProductPage({ params }: { params: Promise<{ id: st
|
||||
</span>
|
||||
</PageHeader>
|
||||
|
||||
{(product.image || product.categories.length > 0) && (
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{product.image && (
|
||||
<div className="w-40 overflow-hidden rounded-xl ring-1 ring-foreground/10">
|
||||
<ProductImage src={product.image} />
|
||||
</div>
|
||||
)}
|
||||
{product.categories.length > 0 && (
|
||||
<p className="text-sm text-muted-foreground">{product.categories.map((c) => c.name).join(" › ")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{me ? (
|
||||
<ActionForm action={libraryAction} fields={{ id }} className="space-y-2">
|
||||
|
||||
@@ -21,15 +21,31 @@ export function formatSize(bytes: number): string {
|
||||
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">;
|
||||
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">
|
||||
@@ -38,7 +54,7 @@ export function ProductCard({
|
||||
<RatingTag rating={product.rating} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{[product.creator && `by ${product.creator}`, `revision ${product.latest}`].filter(Boolean).join(" · ")}
|
||||
{[product.creator && `by ${product.creator}`, category, `revision ${product.latest}`].filter(Boolean).join(" · ")}
|
||||
</p>
|
||||
{children}
|
||||
</CardContent>
|
||||
|
||||
Reference in new issue
Block a user