7 Commits

8 changed files with 90 additions and 37 deletions
+1 -1
View File
@@ -296,7 +296,7 @@ function buildAssemblyRows(params: {
rows.push({
key: `${groupLabel}-${paths.complete}`,
kind: "category",
label: "Completed Lower (Fastest)",
label: `Completed ${groupLabel.split(" ")[0]} (Fastest)`,
categoryId: paths.complete,
indent: 0,
pill: mode === "complete" ? "Selected" : undefined,
@@ -7,6 +7,7 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
import type { BuilderSlotKey } from "@/types/builderSlots";
import { PART_ROLE_TO_CATEGORY, normalizePartRole } from "@/lib/catalogMappings";
import { trackAffiliateLinkClicked } from "@/lib/analytics";
import { ProductImage } from "@/components/parts/ProductImage";
/**
* API Shapes
@@ -493,12 +494,10 @@ export default function ProductDetailsPage() {
className="h-full w-full"
title="Click to enlarge"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
<ProductImage
src={imageUrl}
alt={product.name}
className="h-full w-full object-contain p-2"
loading="lazy"
/>
</button>
) : (
@@ -771,8 +770,7 @@ export default function ProductDetailsPage() {
Close
</button>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
<ProductImage
src={imageUrl}
alt={product?.name ?? "Product image"}
className="max-h-[90vh] max-w-[90vw] rounded-lg border border-zinc-800 bg-black object-contain"
+6 -12
View File
@@ -1,6 +1,7 @@
// app/builds/[buildId]/page.tsx
import Link from "next/link";
import { notFound } from "next/navigation";
import { ProductImage } from "@/components/parts/ProductImage";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
@@ -169,18 +170,11 @@ export default async function BuildBreakdownPage({
>
{/* Thumb */}
<div className="h-12 w-12 flex-none overflow-hidden rounded-md border border-zinc-800 bg-zinc-950">
{it.productImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={it.productImageUrl}
alt={it.productName ?? "Part image"}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-[10px] text-zinc-600">
IMG
</div>
)}
<ProductImage
src={it.productImageUrl}
alt={it.productName ?? "Part image"}
className="h-full w-full object-cover"
/>
</div>
{/* Info */}
+7 -11
View File
@@ -3,6 +3,7 @@
import Link from "next/link";
import { Plus } from "lucide-react";
import type { UiPart } from "@/types/uiPart";
import { ProductImage } from './ProductImage';
export default function PartsGrid(props: {
viewMode: "card" | "list";
@@ -130,17 +131,12 @@ export default function PartsGrid(props: {
>
{/* Image */}
<div className="hidden md:block justify-self-start">
{part.imageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={part.imageUrl}
alt=""
className="h-10 w-10 rounded object-cover border border-zinc-800 bg-zinc-950"
loading="lazy"
/>
) : (
<div className="h-10 w-10 rounded border border-zinc-800 bg-zinc-950" />
)}
<ProductImage
src={part.imageUrl}
alt=""
className="h-10 w-10 rounded object-cover border border-zinc-800 bg-zinc-950"
loading="lazy"
/>
</div>
{/* Part */}
+6 -8
View File
@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { ProductImage } from './ProductImage';
import PlatformSwitcher from "./PlatformSwitcher";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
@@ -149,14 +150,11 @@ export default function ProductDetailPageClient(props: {
<div className="flex flex-col gap-6 md:flex-row">
<div className="w-full md:w-72">
<div className="aspect-square overflow-hidden rounded-lg border border-zinc-800 bg-black/30">
{/* eslint-disable-next-line @next/next/no-img-element */}
{p.imageUrl ? (
<img src={p.imageUrl} alt={p.name} className="h-full w-full object-cover" />
) : (
<div className="h-full w-full grid place-items-center text-sm text-zinc-600">
No image
</div>
)}
<ProductImage
src={p.imageUrl}
alt={p.name}
className="h-full w-full object-cover"
/>
</div>
</div>
+42
View File
@@ -0,0 +1,42 @@
'use client';
import { useEffect, useRef } from 'react';
const PLACEHOLDER = '/images/product-placeholder.svg';
interface ProductImageProps {
src?: string | null;
alt: string;
className?: string;
width?: number;
height?: number;
loading?: 'lazy' | 'eager';
}
export function ProductImage({ src, alt, className, width, height, loading }: ProductImageProps) {
const hasErrored = useRef(false);
// Reset the loop guard whenever src changes (e.g. navigating between products)
useEffect(() => {
hasErrored.current = false;
}, [src]);
function handleError(e: React.SyntheticEvent<HTMLImageElement>) {
if (hasErrored.current) return;
hasErrored.current = true;
e.currentTarget.src = PLACEHOLDER;
}
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src ?? PLACEHOLDER}
alt={alt}
className={className}
width={width}
height={height}
loading={loading}
onError={handleError}
/>
);
}
+4
View File
@@ -12,6 +12,10 @@ const nextConfig = {
protocol: 'https',
hostname: 'cdn.sanity.io',
},
{
protocol: 'https',
hostname: 'minio.dev.gofwd.group',
},
],
},
+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 3" preserveAspectRatio="xMidYMid meet">
<rect width="4" height="3" fill="#27272a"/>
<text
x="2"
y="1.35"
font-family="system-ui, sans-serif"
font-size="0.28"
fill="#71717a"
text-anchor="middle"
dominant-baseline="middle"
>Image</text>
<text
x="2"
y="1.75"
font-family="system-ui, sans-serif"
font-size="0.28"
fill="#71717a"
text-anchor="middle"
dominant-baseline="middle"
>Coming Soon</text>
</svg>

After

Width:  |  Height:  |  Size: 519 B