Add Meshkee SEO, customer auth menu, and footer polish.
Implement canonical product/category/blog URLs, sitemap-config endpoint, page metadata, customer login dropdown with admin handoff, safeteb-style footer bar, backend favicon, and remove the standalone partners strip. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
7a0de3c1f4
commit
b2a0fe73bc
+145
-3
@@ -3,6 +3,52 @@ import { toFa } from "@/lib/format";
|
||||
const API_BASE = "https://api.meshkee.com/api/v1";
|
||||
export const TENANT_DOMAIN = "aradarisman.com";
|
||||
|
||||
export type Tenant = {
|
||||
id: number;
|
||||
name: string;
|
||||
nameFa: string;
|
||||
slug: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export async function getTenant(): Promise<Tenant | null> {
|
||||
try {
|
||||
return await meshkeeFetch<Tenant>(`/tenants/${TENANT_DOMAIN}`, 3600);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBusinessId(): Promise<string> {
|
||||
const tenant = await getTenant();
|
||||
return tenant?.id != null ? String(tenant.id) : "";
|
||||
}
|
||||
|
||||
export type WebsiteFavicon = {
|
||||
faviconUrl: string | null;
|
||||
logoUrl: string | null;
|
||||
logoDarkUrl: string | null;
|
||||
hasDedicatedFavicon: boolean;
|
||||
};
|
||||
|
||||
const defaultFavicon: WebsiteFavicon = {
|
||||
faviconUrl: null,
|
||||
logoUrl: null,
|
||||
logoDarkUrl: null,
|
||||
hasDedicatedFavicon: false,
|
||||
};
|
||||
|
||||
export async function getWebsiteFavicon(): Promise<WebsiteFavicon> {
|
||||
try {
|
||||
return await meshkeeFetch<WebsiteFavicon>(
|
||||
`/tenants/${TENANT_DOMAIN}/website/favicon`,
|
||||
3600
|
||||
);
|
||||
} catch {
|
||||
return defaultFavicon;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Business-managed image overrides (hero slider, category tiles, feature
|
||||
* icons, about photo, brand logos). Each slot has a stable `key`; when a
|
||||
@@ -139,6 +185,17 @@ export async function getProduct(slug: string): Promise<Product | null> {
|
||||
return data.product as Product;
|
||||
}
|
||||
|
||||
export async function getProductById(id: string): Promise<Product | null> {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/tenants/${TENANT_DOMAIN}/products/by-id/${id}`,
|
||||
{ next: { revalidate: 300 } }
|
||||
);
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error(`Meshkee API product id "${id}" failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.product as Product;
|
||||
}
|
||||
|
||||
export function productImage(product: Product): string | null {
|
||||
return product.thumbnail || product.image || product.thumbnailUrl || null;
|
||||
}
|
||||
@@ -151,9 +208,72 @@ export function productPriceLabel(product: Product): string {
|
||||
return "تماس بگیرید";
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
name: string;
|
||||
nameFa: string;
|
||||
slug: string;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export async function getCategories(): Promise<Category[]> {
|
||||
try {
|
||||
const data = await meshkeeFetch<{ items: Category[] }>(
|
||||
`/tenants/${TENANT_DOMAIN}/categories`
|
||||
);
|
||||
return data.items.filter((c) => c.isActive);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCategoryById(id: string): Promise<Category | null> {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/tenants/${TENANT_DOMAIN}/categories/by-id/${id}`,
|
||||
{ next: { revalidate: 300 } }
|
||||
);
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error(`Meshkee API category id "${id}" failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.category as Category;
|
||||
}
|
||||
|
||||
export async function getTopLevelCategories(): Promise<Category[]> {
|
||||
const items = await getCategories();
|
||||
return items
|
||||
.filter((c) => c.parentId === null)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.sortOrder - b.sortOrder ||
|
||||
a.nameFa.localeCompare(b.nameFa, "fa")
|
||||
);
|
||||
}
|
||||
|
||||
/** Includes the category itself and all nested descendants. */
|
||||
export function getCategoryDescendantIds(
|
||||
categoryId: string,
|
||||
categories: Category[]
|
||||
): string[] {
|
||||
const ids = new Set<string>([categoryId]);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const c of categories) {
|
||||
if (c.parentId && ids.has(c.parentId) && !ids.has(c.id)) {
|
||||
ids.add(c.id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(ids);
|
||||
}
|
||||
|
||||
export type CategorySummary = {
|
||||
categoryId: string;
|
||||
name: string;
|
||||
nameEn: string | null;
|
||||
count: number;
|
||||
image: string | null;
|
||||
};
|
||||
@@ -172,7 +292,7 @@ export async function getCategorySummary(limit = 8): Promise<CategorySummary[]>
|
||||
|
||||
const perId = new Map<
|
||||
string,
|
||||
{ id: string; name: string; count: number; image: string | null }
|
||||
{ id: string; name: string; nameEn: string | null; count: number; image: string | null }
|
||||
>();
|
||||
for (const p of items) {
|
||||
if (!p.categoryId || !p.categoryNameFa) continue;
|
||||
@@ -181,8 +301,15 @@ export async function getCategorySummary(limit = 8): Promise<CategorySummary[]>
|
||||
if (entry) {
|
||||
entry.count += 1;
|
||||
if (!entry.image && img) entry.image = img;
|
||||
if (!entry.nameEn && p.categoryName) entry.nameEn = p.categoryName;
|
||||
} else {
|
||||
perId.set(p.categoryId, { id: p.categoryId, name: p.categoryNameFa, count: 1, image: img });
|
||||
perId.set(p.categoryId, {
|
||||
id: p.categoryId,
|
||||
name: p.categoryNameFa,
|
||||
nameEn: p.categoryName ?? null,
|
||||
count: 1,
|
||||
image: img,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +320,7 @@ export async function getCategorySummary(limit = 8): Promise<CategorySummary[]>
|
||||
perName.set(entry.name, {
|
||||
categoryId: entry.id,
|
||||
name: entry.name,
|
||||
nameEn: entry.nameEn,
|
||||
count: entry.count,
|
||||
image: entry.image,
|
||||
});
|
||||
@@ -200,8 +328,12 @@ export async function getCategorySummary(limit = 8): Promise<CategorySummary[]>
|
||||
}
|
||||
existing.count += entry.count;
|
||||
const currentBest = perId.get(existing.categoryId)?.count ?? 0;
|
||||
if (entry.count > currentBest) existing.categoryId = entry.id;
|
||||
if (entry.count > currentBest) {
|
||||
existing.categoryId = entry.id;
|
||||
if (entry.nameEn) existing.nameEn = entry.nameEn;
|
||||
}
|
||||
if (!existing.image && entry.image) existing.image = entry.image;
|
||||
if (!existing.nameEn && entry.nameEn) existing.nameEn = entry.nameEn;
|
||||
}
|
||||
|
||||
return Array.from(perName.values())
|
||||
@@ -290,6 +422,16 @@ export async function getPost(slug: string): Promise<Post | null> {
|
||||
return (data.blog ?? data.post ?? data) as Post;
|
||||
}
|
||||
|
||||
export async function getPostById(id: string): Promise<Post | null> {
|
||||
const res = await fetch(`${API_BASE}/tenants/${TENANT_DOMAIN}/blogs/by-id/${id}`, {
|
||||
next: { revalidate: 300 },
|
||||
});
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error(`Meshkee API blog id "${id}" failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return (data.blog ?? data.post ?? data) as Post;
|
||||
}
|
||||
|
||||
export function postTitle(post: Post): string {
|
||||
return post.nameFa || post.titleFa || post.title || "بدون عنوان";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user