Add Torob tags, Meshkee SEO routes, and updated static-image slots.
Product pages emit crawler-readable meta, public URLs use id plus Farsi slugs, and the homepage catalog matches the dashboard slot keys. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f42c30a4b7
commit
c84147e765
+17
-17
@@ -3,13 +3,13 @@ import {
|
||||
getBlogs,
|
||||
getProducts,
|
||||
getStoreSpecials,
|
||||
resolveProductSlug,
|
||||
type ApiBlog,
|
||||
type ApiProduct,
|
||||
type ApiSpecialItem,
|
||||
type ApiVariant,
|
||||
} from "./api";
|
||||
import { formatDate } from "./format";
|
||||
import { blogHref, productHref } from "./paths";
|
||||
|
||||
function productImage(url?: string | null) {
|
||||
return url || PLACEHOLDER.product;
|
||||
@@ -23,35 +23,39 @@ function pickVariantPrice(variants: ApiVariant[]) {
|
||||
const inStock = variants.filter((variant) => (variant.stockQuantity ?? 0) > 0);
|
||||
const pool = inStock.length ? inStock : variants;
|
||||
const best = pool.reduce((current, next) => {
|
||||
const currentPrice = current.discountedPrice ?? current.price;
|
||||
const nextPrice = next.discountedPrice ?? next.price;
|
||||
const currentPrice = current.discountedPrice ?? current.price ?? 0;
|
||||
const nextPrice = next.discountedPrice ?? next.price ?? 0;
|
||||
return nextPrice < currentPrice ? next : current;
|
||||
});
|
||||
|
||||
const price = best.discountedPrice ?? best.price ?? 0;
|
||||
const oldPrice = best.discountedPrice ? best.price : undefined;
|
||||
const oldPrice = best.discountedPrice ? (best.price ?? undefined) : undefined;
|
||||
const stock = variants.reduce((sum, variant) => sum + (variant.stockQuantity ?? 0), 0);
|
||||
|
||||
return { price, oldPrice, stock };
|
||||
}
|
||||
|
||||
export function mapProduct(item: ApiProduct): Product {
|
||||
const name = item.nameFa || item.title;
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.nameFa || item.title,
|
||||
name,
|
||||
slug: item.slug,
|
||||
href: productHref(item),
|
||||
price: item.store?.minPrice ?? 0,
|
||||
stock: item.store?.inStock ? item.store.variantCount || 1 : 0,
|
||||
image: productImage(item.thumbnailUrl || item.image || item.thumbnail),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapSpecialItem(item: ApiSpecialItem, slug: string): Product {
|
||||
export function mapSpecialItem(item: ApiSpecialItem): Product {
|
||||
const { price, oldPrice, stock } = pickVariantPrice(item.variants ?? []);
|
||||
const name = item.productNameFa || item.productTitle;
|
||||
return {
|
||||
id: item.productId,
|
||||
name: item.productNameFa || item.productTitle,
|
||||
slug,
|
||||
name,
|
||||
slug: "",
|
||||
href: productHref(item.productId, name),
|
||||
price,
|
||||
oldPrice,
|
||||
stock,
|
||||
@@ -61,8 +65,10 @@ export function mapSpecialItem(item: ApiSpecialItem, slug: string): Product {
|
||||
|
||||
export function mapBlog(item: ApiBlog): Post {
|
||||
return {
|
||||
id: item.id,
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
href: blogHref(item),
|
||||
date: formatDate(item.publishedAt || item.createdAt || ""),
|
||||
image: item.titleImageUrl || PLACEHOLDER.blog,
|
||||
abstract: item.abstract?.trim() || undefined,
|
||||
@@ -75,16 +81,10 @@ export async function getSpecialProducts(key: string): Promise<Product[]> {
|
||||
const special = specials.find((item) => item.isActive && item.key.toLowerCase() === key.toLowerCase());
|
||||
if (!special) return [];
|
||||
|
||||
const items = special.items
|
||||
return special.items
|
||||
.filter((item) => item.isActive)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
return Promise.all(
|
||||
items.map(async (item) => {
|
||||
const slug = await resolveProductSlug(item.productId, item.productTitle || item.productNameFa || "");
|
||||
return mapSpecialItem(item, slug);
|
||||
}),
|
||||
);
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((item) => mapSpecialItem(item));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user