Add Meshkee sitemap config, technical-info labels, and footer credit.
Wire specs to /technical-info for field labels, match Safeteb footer credit, and publish build-time /meshkee/sitemap-config.json for SEO sync. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
386787b4b5
commit
d249d958eb
@@ -1,47 +0,0 @@
|
||||
import { apiGet } from "./client";
|
||||
import { findCategoryById, listCategories } from "./categories";
|
||||
|
||||
export type CategoryVariationField = {
|
||||
id: string;
|
||||
name: string;
|
||||
nameFa: string | null;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
function variationFieldLabel(
|
||||
field: Pick<CategoryVariationField, "nameFa" | "name">,
|
||||
) {
|
||||
return field.nameFa || field.name;
|
||||
}
|
||||
|
||||
export async function listCategoryVariationFields(categoryId: string) {
|
||||
try {
|
||||
const data = await apiGet<{ items: CategoryVariationField[] }>(
|
||||
"/product-category-variation-fields",
|
||||
{ categoryId },
|
||||
);
|
||||
return data.items ?? [];
|
||||
} catch (error) {
|
||||
console.error("listCategoryVariationFields failed", error);
|
||||
return [] as CategoryVariationField[];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCategoryVariationFieldMap(categoryId?: string | null) {
|
||||
if (!categoryId) return {};
|
||||
|
||||
const categories = await listCategories("product");
|
||||
const category = findCategoryById(categories, categoryId);
|
||||
const categoryIds = [categoryId, category?.parentId].filter(Boolean) as string[];
|
||||
|
||||
for (const id of categoryIds) {
|
||||
const fields = await listCategoryVariationFields(id);
|
||||
if (fields.length > 0) {
|
||||
return Object.fromEntries(
|
||||
fields.map((field) => [field.id, variationFieldLabel(field)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -18,6 +18,42 @@ export type ListUserProductsParams = {
|
||||
promoted?: boolean;
|
||||
};
|
||||
|
||||
export type TechnicalFormFieldOption = {
|
||||
id: string;
|
||||
label?: string | null;
|
||||
value?: string | null;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
export type TechnicalFormField = {
|
||||
id: string;
|
||||
label: string;
|
||||
key?: string | null;
|
||||
type?: string | null;
|
||||
isRequired?: boolean;
|
||||
sortOrder?: number;
|
||||
options?: TechnicalFormFieldOption[];
|
||||
};
|
||||
|
||||
export type TechnicalForm = {
|
||||
id?: string;
|
||||
categoryId?: string;
|
||||
fields?: TechnicalFormField[];
|
||||
} | null;
|
||||
|
||||
export type TechnicalInfoValue = {
|
||||
fieldId: string;
|
||||
textValue?: string | null;
|
||||
numberValue?: number | string | null;
|
||||
optionId?: string | null;
|
||||
optionIds?: string[] | null;
|
||||
};
|
||||
|
||||
export type UserProductTechnicalInfo = {
|
||||
form: TechnicalForm;
|
||||
values: TechnicalInfoValue[];
|
||||
};
|
||||
|
||||
const emptyPage = <T,>(page = 1, pageSize = 12): Paginated<T> => ({
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -61,6 +97,32 @@ export async function getUserProductBySlugOrNull(slug: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Labels + values for specs table. Join form.fields[].id ↔ values[].fieldId. */
|
||||
export async function getUserProductTechnicalInfo(slug: string) {
|
||||
try {
|
||||
return await apiGet<UserProductTechnicalInfo>(
|
||||
`/user-products/${encodeURIComponent(slug)}/technical-info`,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 404) {
|
||||
return { form: null, values: [] } satisfies UserProductTechnicalInfo;
|
||||
}
|
||||
console.error("getUserProductTechnicalInfo failed", error);
|
||||
return { form: null, values: [] } satisfies UserProductTechnicalInfo;
|
||||
}
|
||||
}
|
||||
|
||||
export function technicalFieldLabelMap(
|
||||
info: UserProductTechnicalInfo | null | undefined,
|
||||
): Record<string, string> {
|
||||
const fields = info?.form?.fields ?? [];
|
||||
return Object.fromEntries(
|
||||
fields
|
||||
.filter((field) => field.id && field.label)
|
||||
.map((field) => [String(field.id), field.label]),
|
||||
);
|
||||
}
|
||||
|
||||
export const conditionLabels: Record<string, string> = {
|
||||
new: "نو",
|
||||
stock: "کارکرده",
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type {
|
||||
TechnicalFormField,
|
||||
TechnicalInfoValue,
|
||||
UserProductTechnicalInfo,
|
||||
} from "@/lib/api/user-products";
|
||||
|
||||
export type TechnicalValueItem = {
|
||||
fieldId?: string | number | null;
|
||||
textValue?: string | null;
|
||||
numberValue?: number | string | null;
|
||||
optionId?: string | null;
|
||||
optionIds?: string[] | null;
|
||||
value?: string | number | null;
|
||||
fieldName?: string | null;
|
||||
fieldNameFa?: string | null;
|
||||
@@ -20,6 +28,7 @@ export type TechnicalValueRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
sortOrder: number;
|
||||
};
|
||||
|
||||
function pickString(...values: Array<string | number | null | undefined>) {
|
||||
@@ -48,6 +57,71 @@ function parseJsonObject(value: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOptionLabel(
|
||||
field: TechnicalFormField | undefined,
|
||||
optionId: string | null | undefined,
|
||||
) {
|
||||
if (!field || !optionId) return "";
|
||||
const option = field.options?.find((item) => String(item.id) === String(optionId));
|
||||
return pickString(option?.label, option?.value, optionId);
|
||||
}
|
||||
|
||||
function resolveValueText(
|
||||
value: TechnicalInfoValue | TechnicalValueItem,
|
||||
field?: TechnicalFormField,
|
||||
) {
|
||||
const optionIds =
|
||||
value.optionIds?.filter(Boolean).map(String) ??
|
||||
(value.optionId ? [String(value.optionId)] : []);
|
||||
|
||||
if (optionIds.length > 0) {
|
||||
const labels = optionIds
|
||||
.map((id) => resolveOptionLabel(field, id))
|
||||
.filter(Boolean);
|
||||
if (labels.length > 0) return labels.join("، ");
|
||||
}
|
||||
|
||||
return pickString(
|
||||
formatValue(value.textValue),
|
||||
formatValue(value.numberValue),
|
||||
formatValue("value" in value ? value.value : undefined),
|
||||
);
|
||||
}
|
||||
|
||||
/** Preferred path: join technical-info form fields with submitted values. */
|
||||
export function rowsFromTechnicalInfo(
|
||||
info: UserProductTechnicalInfo | null | undefined,
|
||||
): TechnicalValueRow[] {
|
||||
const fields = info?.form?.fields ?? [];
|
||||
const values = info?.values ?? [];
|
||||
if (fields.length === 0 || values.length === 0) return [];
|
||||
|
||||
const fieldById = new Map(fields.map((field) => [String(field.id), field]));
|
||||
const sortById = new Map(
|
||||
fields.map((field, index) => [
|
||||
String(field.id),
|
||||
field.sortOrder ?? index,
|
||||
]),
|
||||
);
|
||||
|
||||
return values
|
||||
.map((value, index) => {
|
||||
const id = String(value.fieldId ?? `item-${index}`);
|
||||
const field = fieldById.get(id);
|
||||
const rowValue = resolveValueText(value, field);
|
||||
if (!rowValue) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
label: pickString(field?.label) || "",
|
||||
value: rowValue,
|
||||
sortOrder: sortById.get(id) ?? index,
|
||||
};
|
||||
})
|
||||
.filter((row): row is TechnicalValueRow => Boolean(row))
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder || a.label.localeCompare(b.label, "fa"));
|
||||
}
|
||||
|
||||
export function normalizeTechnicalValueRows(
|
||||
value: unknown,
|
||||
fieldLabels: Record<string, string> = {},
|
||||
@@ -59,7 +133,7 @@ export function normalizeTechnicalValueRows(
|
||||
if (parsed !== null) return normalizeTechnicalValueRows(parsed, fieldLabels);
|
||||
|
||||
const text = value.trim();
|
||||
return text ? [{ id: "text", label: "", value: text }] : [];
|
||||
return text ? [{ id: "text", label: "", value: text, sortOrder: 0 }] : [];
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
@@ -75,6 +149,7 @@ export function normalizeTechnicalValueRows(
|
||||
id: `item-${index}`,
|
||||
label: "",
|
||||
value: item.trim(),
|
||||
sortOrder: index,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,29 +171,26 @@ export function normalizeTechnicalValueRows(
|
||||
id ? fieldLabels[id] : undefined,
|
||||
) || "";
|
||||
|
||||
const rowValue = pickString(
|
||||
formatValue(field.textValue),
|
||||
formatValue(field.numberValue),
|
||||
formatValue(field.value),
|
||||
);
|
||||
const rowValue = resolveValueText(field);
|
||||
|
||||
if (!rowValue) return null;
|
||||
|
||||
return { id, label, value: rowValue };
|
||||
return { id, label, value: rowValue, sortOrder: index };
|
||||
})
|
||||
.filter((row): row is TechnicalValueRow => Boolean(row));
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
return Object.entries(value as Record<string, unknown>)
|
||||
.map(([key, val]) => ({
|
||||
.map(([key, val], index) => ({
|
||||
id: key,
|
||||
label: fieldLabels[key] || key,
|
||||
value: formatValue(val),
|
||||
sortOrder: index,
|
||||
}))
|
||||
.filter((row) => row.value);
|
||||
}
|
||||
|
||||
const text = formatValue(value);
|
||||
return text ? [{ id: "value", label: "", value: text }] : [];
|
||||
return text ? [{ id: "value", label: "", value: text, sortOrder: 0 }] : [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"baseUrl": "https://www.mashinify.com",
|
||||
"staticPages": [
|
||||
{
|
||||
"path": "/",
|
||||
"changefreq": "daily",
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"path": "/about",
|
||||
"changefreq": "monthly",
|
||||
"priority": 0.6
|
||||
},
|
||||
{
|
||||
"path": "/blog",
|
||||
"changefreq": "weekly",
|
||||
"priority": 0.8
|
||||
},
|
||||
{
|
||||
"path": "/categories",
|
||||
"changefreq": "weekly",
|
||||
"priority": 0.8
|
||||
},
|
||||
{
|
||||
"path": "/contact",
|
||||
"changefreq": "monthly",
|
||||
"priority": 0.5
|
||||
},
|
||||
{
|
||||
"path": "/user-products",
|
||||
"changefreq": "daily",
|
||||
"priority": 0.9
|
||||
}
|
||||
],
|
||||
"templates": {
|
||||
"userProduct": "/user-products/{slug}",
|
||||
"blog": "/blog/{slug}"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user