Fix product gallery to include all Meshkee images and show catalog downloads.
Parse gallery entries as URL objects from the API and surface attached catalog files on the product detail page when present. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
c78abfa1de
commit
69e6261949
@@ -10,6 +10,7 @@ import HomeContactStrip from "@/components/HomeContactStrip";
|
|||||||
import {
|
import {
|
||||||
getProductById,
|
getProductById,
|
||||||
getProducts,
|
getProducts,
|
||||||
|
productCatalog,
|
||||||
productImages,
|
productImages,
|
||||||
productPriceLabel,
|
productPriceLabel,
|
||||||
stripHtml,
|
stripHtml,
|
||||||
@@ -52,6 +53,7 @@ export default async function ProductDetailPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const images = productImages(product);
|
const images = productImages(product);
|
||||||
|
const catalog = productCatalog(product);
|
||||||
const shortDesc = product.summary || stripHtml(product.descriptionHtml, 160);
|
const shortDesc = product.summary || stripHtml(product.descriptionHtml, 160);
|
||||||
|
|
||||||
const related = product.categoryId
|
const related = product.categoryId
|
||||||
@@ -117,6 +119,49 @@ export default async function ProductDetailPage({
|
|||||||
استعلام قیمت
|
استعلام قیمت
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{catalog && (
|
||||||
|
<a
|
||||||
|
href={catalog.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
download={catalog.fileName}
|
||||||
|
className="mt-6 flex items-center gap-3 rounded-2xl border border-primary/15 bg-surface px-4 py-3.5 text-primary transition-colors hover:border-primary/35 hover:bg-primary/5"
|
||||||
|
>
|
||||||
|
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||||
|
<path
|
||||||
|
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14 2v6h6M8 13h8M8 17h5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="block text-sm font-semibold">دانلود فایل پیوست</span>
|
||||||
|
<span className="mt-0.5 block truncate text-xs text-foreground/70" dir="auto">
|
||||||
|
{catalog.fileName}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden className="shrink-0 opacity-70">
|
||||||
|
<path
|
||||||
|
d="M12 3v12m0 0 4-4m-4 4-4-4M5 19h14"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+39
-3
@@ -113,6 +113,9 @@ export type ProductStore = {
|
|||||||
inStock: boolean;
|
inStock: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Gallery entry from Meshkee — list/detail may return a URL string or `{ url }`. */
|
||||||
|
export type ProductImageRef = string | { url: string; mediaId?: string };
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
id: string;
|
id: string;
|
||||||
businessId: string;
|
businessId: string;
|
||||||
@@ -131,7 +134,11 @@ export type Product = {
|
|||||||
thumbnailUrl: string | null;
|
thumbnailUrl: string | null;
|
||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
images: string[];
|
images: ProductImageRef[];
|
||||||
|
/** Attached catalog / datasheet file (PDF etc.), when the owner uploaded one. */
|
||||||
|
catalogUrl: string | null;
|
||||||
|
catalogFileName: string | null;
|
||||||
|
catalogMediaId: string | null;
|
||||||
commentCount: number;
|
commentCount: number;
|
||||||
variantCount: number;
|
variantCount: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -200,11 +207,27 @@ export function productImage(product: Product): string | null {
|
|||||||
return product.thumbnail || product.image || product.thumbnailUrl || null;
|
return product.thumbnail || product.image || product.thumbnailUrl || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unique gallery URLs: `images[]` first, then the primary thumbnail/image. */
|
function imageRefUrl(ref: ProductImageRef | null | undefined): string | null {
|
||||||
|
if (!ref) return null;
|
||||||
|
if (typeof ref === "string") return ref || null;
|
||||||
|
return typeof ref.url === "string" && ref.url ? ref.url : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique gallery URLs for the product detail lightbox.
|
||||||
|
* Primary thumbnail/image first, then every gallery entry (API may return
|
||||||
|
* `images` as `{ url, mediaId }[]` rather than plain strings).
|
||||||
|
*/
|
||||||
export function productImages(product: Product): string[] {
|
export function productImages(product: Product): string[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
for (const url of [...(product.images ?? []), product.thumbnail, product.image, product.thumbnailUrl]) {
|
for (const ref of [
|
||||||
|
product.thumbnail,
|
||||||
|
product.image,
|
||||||
|
product.thumbnailUrl,
|
||||||
|
...(product.images ?? []),
|
||||||
|
]) {
|
||||||
|
const url = imageRefUrl(ref as ProductImageRef);
|
||||||
if (!url || seen.has(url)) continue;
|
if (!url || seen.has(url)) continue;
|
||||||
seen.add(url);
|
seen.add(url);
|
||||||
out.push(url);
|
out.push(url);
|
||||||
@@ -212,6 +235,19 @@ export function productImages(product: Product): string[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Attached catalog/datasheet when the business uploaded one in Meshkee. */
|
||||||
|
export function productCatalog(
|
||||||
|
product: Product
|
||||||
|
): { url: string; fileName: string } | null {
|
||||||
|
const url = product.catalogUrl?.trim();
|
||||||
|
if (!url) return null;
|
||||||
|
const fileName =
|
||||||
|
product.catalogFileName?.trim() ||
|
||||||
|
decodeURIComponent(url.split("/").pop() || "catalog") ||
|
||||||
|
"فایل پیوست";
|
||||||
|
return { url, fileName };
|
||||||
|
}
|
||||||
|
|
||||||
export function productPriceLabel(product: Product): string {
|
export function productPriceLabel(product: Product): string {
|
||||||
if (product.store?.minPrice) {
|
if (product.store?.minPrice) {
|
||||||
const formatted = new Intl.NumberFormat("fa-IR").format(product.store.minPrice);
|
const formatted = new Intl.NumberFormat("fa-IR").format(product.store.minPrice);
|
||||||
|
|||||||
Reference in New Issue
Block a user