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>
151 lines
4.0 KiB
TypeScript
151 lines
4.0 KiB
TypeScript
import { PLACEHOLDER, type Post, type Product } from "./data";
|
|
import {
|
|
getBlogs,
|
|
getProducts,
|
|
getStoreSpecials,
|
|
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;
|
|
}
|
|
|
|
function pickVariantPrice(variants: ApiVariant[]) {
|
|
if (!variants.length) {
|
|
return { price: 0, oldPrice: undefined as number | undefined, stock: 0 };
|
|
}
|
|
|
|
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 ?? 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) : 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,
|
|
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): Product {
|
|
const { price, oldPrice, stock } = pickVariantPrice(item.variants ?? []);
|
|
const name = item.productNameFa || item.productTitle;
|
|
return {
|
|
id: item.productId,
|
|
name,
|
|
slug: "",
|
|
href: productHref(item.productId, name),
|
|
price,
|
|
oldPrice,
|
|
stock,
|
|
image: productImage(item.productImage),
|
|
};
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
export async function getSpecialProducts(key: string): Promise<Product[]> {
|
|
try {
|
|
const specials = await getStoreSpecials();
|
|
const special = specials.find((item) => item.isActive && item.key.toLowerCase() === key.toLowerCase());
|
|
if (!special) return [];
|
|
|
|
return special.items
|
|
.filter((item) => item.isActive)
|
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
.map((item) => mapSpecialItem(item));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getLatestPosts(pageSize = 12): Promise<Post[]> {
|
|
try {
|
|
const data = await getBlogs({ page: 1, pageSize });
|
|
return data.items.map(mapBlog);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function listMappedProducts(query: {
|
|
page?: number;
|
|
pageSize?: number;
|
|
name?: string;
|
|
categoryId?: string;
|
|
}) {
|
|
try {
|
|
const data = await getProducts(query);
|
|
return {
|
|
...data,
|
|
items: data.items.map(mapProduct),
|
|
};
|
|
} catch {
|
|
return {
|
|
items: [],
|
|
total: 0,
|
|
page: query.page ?? 1,
|
|
pageSize: query.pageSize ?? 12,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function listMappedProductsForCategory(
|
|
categoryId: string,
|
|
childIds: string[],
|
|
page: number,
|
|
pageSize: number,
|
|
) {
|
|
const ids = [categoryId, ...childIds];
|
|
if (ids.length === 1) {
|
|
return listMappedProducts({ page, pageSize, categoryId });
|
|
}
|
|
|
|
const results = await Promise.all(ids.map((id) => listMappedProducts({ page: 1, pageSize: 50, categoryId: id })));
|
|
const seen = new Set<string>();
|
|
const items = results.flatMap((result) => result.items).filter((item) => {
|
|
if (seen.has(item.id)) return false;
|
|
seen.add(item.id);
|
|
return true;
|
|
});
|
|
|
|
const start = (page - 1) * pageSize;
|
|
return {
|
|
items: items.slice(start, start + pageSize),
|
|
total: items.length,
|
|
page,
|
|
pageSize,
|
|
};
|
|
}
|