Edit in product detail and create breadcrumb
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||||||
import { getBlog } from "@/lib/meshkee";
|
import { getBlog } from "@/lib/meshkee";
|
||||||
|
|
||||||
function formatDate(iso?: string) {
|
function formatDate(iso?: string) {
|
||||||
@@ -18,6 +19,14 @@ export default async function BlogDetailPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="mx-auto max-w-3xl px-4 py-10">
|
<article className="mx-auto max-w-3xl px-4 py-10">
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
|
{ label: "خانه", href: "/" },
|
||||||
|
{ label: "وبلاگ", href: "/blog" },
|
||||||
|
{ label: post.title },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mb-6 flex items-center gap-2 text-xs text-muted">
|
<div className="mb-6 flex items-center gap-2 text-xs text-muted">
|
||||||
{post.categoryName && <span>{post.categoryName}</span>}
|
{post.categoryName && <span>{post.categoryName}</span>}
|
||||||
{post.publishedAt && (
|
{post.publishedAt && (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
import { Placeholder } from "@/components/ui/Placeholder";
|
||||||
import { EmptyState } from "@/components/ui/EmptyState";
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { getBlogs } from "@/lib/meshkee";
|
import { getBlogs } from "@/lib/meshkee";
|
||||||
@@ -21,6 +22,8 @@ export default async function BlogListPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-4 py-10">
|
<div className="mx-auto max-w-7xl px-4 py-10">
|
||||||
|
<Breadcrumb items={[{ label: "خانه", href: "/" }, { label: "وبلاگ" }]} />
|
||||||
|
|
||||||
<h1 className="mb-6 text-2xl font-bold text-red-dark">وبلاگ</h1>
|
<h1 className="mb-6 text-2xl font-bold text-red-dark">وبلاگ</h1>
|
||||||
|
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||||||
import { ProductGallery } from "@/components/product/ProductGallery";
|
import { ProductGallery } from "@/components/product/ProductGallery";
|
||||||
|
import { ProductPurchasePanel } from "@/components/product/ProductPurchasePanel";
|
||||||
import { ProductTabs } from "@/components/product/ProductTabs";
|
import { ProductTabs } from "@/components/product/ProductTabs";
|
||||||
import { formatPrice } from "@/data/sample";
|
|
||||||
import {
|
import {
|
||||||
getBusinessInfo,
|
getBusinessInfo,
|
||||||
getProduct,
|
getProduct,
|
||||||
|
getProductStoreVariants,
|
||||||
getProductTechnicalInfo,
|
getProductTechnicalInfo,
|
||||||
getProductVariations,
|
getProductVariations,
|
||||||
} from "@/lib/meshkee";
|
} from "@/lib/meshkee";
|
||||||
@@ -21,108 +23,48 @@ export default async function ProductDetailPage({
|
|||||||
getProductVariations(slug),
|
getProductVariations(slug),
|
||||||
getBusinessInfo(),
|
getBusinessInfo(),
|
||||||
]);
|
]);
|
||||||
const contactPhone = businessInfo?.phoneNumbers[0];
|
|
||||||
|
|
||||||
if (!product) notFound();
|
if (!product) notFound();
|
||||||
|
|
||||||
|
const storeVariants = await getProductStoreVariants(product.id);
|
||||||
|
const contactPhone = businessInfo?.phoneNumbers[0];
|
||||||
const images = product.images.length > 0 ? product.images : product.image ? [product.image] : [];
|
const images = product.images.length > 0 ? product.images : product.image ? [product.image] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-4 py-10">
|
<div className="mx-auto max-w-7xl px-4 py-10">
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
|
{ label: "خانه", href: "/" },
|
||||||
|
...(product.categoryName
|
||||||
|
? [{ label: product.categoryName, href: `/products?categoryId=${product.categoryId}` }]
|
||||||
|
: []),
|
||||||
|
{ label: product.name },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid gap-8 md:grid-cols-12">
|
<div className="grid gap-8 md:grid-cols-12">
|
||||||
<div className="md:col-span-5">
|
<div className="md:col-span-4">
|
||||||
<ProductGallery images={images} alt={product.name} />
|
<ProductGallery images={images} alt={product.name} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-7">
|
<ProductPurchasePanel
|
||||||
{product.brand && (
|
header={
|
||||||
<p className="font-display mb-2 text-xs tracking-wide text-muted uppercase">
|
<>
|
||||||
{product.brand}
|
{product.brand && (
|
||||||
</p>
|
<p className="font-display mb-2 text-xs tracking-wide text-muted uppercase">
|
||||||
)}
|
{product.brand}
|
||||||
<h1 className="mb-3 text-xl font-bold leading-relaxed md:text-2xl">{product.name}</h1>
|
</p>
|
||||||
{product.categoryName && (
|
)}
|
||||||
<a
|
<h1 className="text-xl font-bold leading-relaxed md:text-2xl">{product.name}</h1>
|
||||||
href={`/products?categoryId=${product.categoryId}`}
|
</>
|
||||||
className="text-sm text-red hover:text-red-dark"
|
}
|
||||||
>
|
variations={variations}
|
||||||
{product.categoryName}
|
storeVariants={storeVariants}
|
||||||
</a>
|
fallbackPrice={{ minPrice: product.minPrice, maxPrice: product.maxPrice }}
|
||||||
)}
|
inStock={product.inStock}
|
||||||
|
contactPhone={contactPhone}
|
||||||
<div className="my-5 flex flex-wrap items-center gap-4">
|
storeName={businessInfo?.nameFa}
|
||||||
{product.minPrice != null && (
|
/>
|
||||||
<span className="text-2xl font-bold text-red-dark">
|
|
||||||
{product.minPrice === product.maxPrice
|
|
||||||
? formatPrice(product.minPrice)
|
|
||||||
: `${formatPrice(product.minPrice)} - ${formatPrice(product.maxPrice ?? product.minPrice)}`}
|
|
||||||
<span className="mr-1 text-sm font-normal text-muted">تومان</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="flex items-center gap-2 text-sm">
|
|
||||||
<span className="font-medium text-foreground">موجودی:</span>
|
|
||||||
<span
|
|
||||||
className={`rounded-full px-3 py-1 text-xs font-medium ${
|
|
||||||
product.inStock ? "bg-green-50 text-green-700" : "bg-red-light text-red-dark"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{product.inStock ? "موجود" : "ناموجود"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{variations.length > 0 && (
|
|
||||||
<div className="mb-6 space-y-4">
|
|
||||||
{variations.map((variation) => (
|
|
||||||
<div key={variation.id}>
|
|
||||||
<p className="mb-2 text-sm font-bold">{variation.name}</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{variation.options.map((option) => (
|
|
||||||
<span
|
|
||||||
key={option.id}
|
|
||||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm"
|
|
||||||
>
|
|
||||||
{option.colorHex && (
|
|
||||||
<span
|
|
||||||
className="h-3 w-3 shrink-0 rounded-full border border-border"
|
|
||||||
style={{ backgroundColor: option.colorHex }}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{product.minPrice != null ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark md:w-auto md:px-10"
|
|
||||||
>
|
|
||||||
افزودن به سبد خرید
|
|
||||||
</button>
|
|
||||||
) : contactPhone ? (
|
|
||||||
<a
|
|
||||||
href={`tel:${contactPhone}`}
|
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark md:w-auto md:px-10"
|
|
||||||
>
|
|
||||||
<span aria-hidden>📞</span>
|
|
||||||
تماس با فروشگاه
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled
|
|
||||||
className="w-full cursor-not-allowed rounded-xl bg-red/60 py-3 text-sm font-medium text-white md:w-auto md:px-10"
|
|
||||||
>
|
|
||||||
تماس با فروشگاه
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProductTabs technicalInfo={technicalInfo} descriptionHtml={product.descriptionHtml} />
|
<ProductTabs technicalInfo={technicalInfo} descriptionHtml={product.descriptionHtml} />
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
|
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||||||
import { ProductCard } from "@/components/ui/ProductCard";
|
import { ProductCard } from "@/components/ui/ProductCard";
|
||||||
import { EmptyState } from "@/components/ui/EmptyState";
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { getBusinessInfo, getProducts, productHref } from "@/lib/meshkee";
|
import {
|
||||||
|
type CategoryTreeGroup,
|
||||||
|
getBusinessInfo,
|
||||||
|
getCategoryTree,
|
||||||
|
getProducts,
|
||||||
|
productHref,
|
||||||
|
} from "@/lib/meshkee";
|
||||||
|
|
||||||
const PAGE_SIZE = 24;
|
const PAGE_SIZE = 24;
|
||||||
|
|
||||||
|
function findCategoryLabel(tree: CategoryTreeGroup[], id: string): string | undefined {
|
||||||
|
for (const group of tree) {
|
||||||
|
for (const item of group.items) {
|
||||||
|
if (item.id === id) return item.nameFa;
|
||||||
|
const child = item.children.find((c) => c.id === id);
|
||||||
|
if (child) return child.nameFa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function ProductsPage({
|
export default async function ProductsPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@@ -11,7 +29,7 @@ export default async function ProductsPage({
|
|||||||
}) {
|
}) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const page = Math.max(1, Number(params.page ?? 1) || 1);
|
const page = Math.max(1, Number(params.page ?? 1) || 1);
|
||||||
const [{ items, total }, businessInfo] = await Promise.all([
|
const [{ items, total }, businessInfo, categoryTree] = await Promise.all([
|
||||||
getProducts({
|
getProducts({
|
||||||
categoryId: params.categoryId,
|
categoryId: params.categoryId,
|
||||||
name: params.q,
|
name: params.q,
|
||||||
@@ -19,9 +37,12 @@ export default async function ProductsPage({
|
|||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
}),
|
}),
|
||||||
getBusinessInfo(),
|
getBusinessInfo(),
|
||||||
|
params.categoryId ? getCategoryTree() : Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
const contactPhone = businessInfo?.phoneNumbers[0];
|
const contactPhone = businessInfo?.phoneNumbers[0];
|
||||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||||
|
const isFiltered = Boolean(params.q || params.categoryId);
|
||||||
|
const categoryLabel = params.categoryId ? findCategoryLabel(categoryTree, params.categoryId) : undefined;
|
||||||
|
|
||||||
const pageHref = (p: number) => {
|
const pageHref = (p: number) => {
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
@@ -33,6 +54,15 @@ export default async function ProductsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-4 py-10">
|
<div className="mx-auto max-w-7xl px-4 py-10">
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
|
{ label: "خانه", href: "/" },
|
||||||
|
{ label: "محصولات", href: isFiltered ? "/products" : undefined },
|
||||||
|
...(params.q ? [{ label: `نتایج جستجو برای «${params.q}»` }] : []),
|
||||||
|
...(categoryLabel ? [{ label: categoryLabel }] : []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<h1 className="mb-6 text-2xl font-bold text-red-dark">
|
<h1 className="mb-6 text-2xl font-bold text-red-dark">
|
||||||
{params.q ? `نتایج جستجو برای «${params.q}»` : "محصولات"}
|
{params.q ? `نتایج جستجو برای «${params.q}»` : "محصولات"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type ReactNode, useMemo, useState } from "react";
|
||||||
|
import { formatPrice } from "@/data/sample";
|
||||||
|
import type { ProductStoreVariant, ProductVariation } from "@/lib/meshkee";
|
||||||
|
|
||||||
|
function CheckIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
|
||||||
|
<path
|
||||||
|
d="M5 12.5 9.5 17 19 7.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductPurchasePanel({
|
||||||
|
header,
|
||||||
|
variations,
|
||||||
|
storeVariants,
|
||||||
|
fallbackPrice,
|
||||||
|
inStock,
|
||||||
|
contactPhone,
|
||||||
|
storeName,
|
||||||
|
}: {
|
||||||
|
header: ReactNode;
|
||||||
|
variations: ProductVariation[];
|
||||||
|
storeVariants: ProductStoreVariant[];
|
||||||
|
fallbackPrice: { minPrice: number | null; maxPrice: number | null };
|
||||||
|
inStock: boolean;
|
||||||
|
contactPhone?: string;
|
||||||
|
storeName?: string;
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = useState<Record<string, string>>(() => {
|
||||||
|
const initial: Record<string, string> = {};
|
||||||
|
for (const variation of variations) {
|
||||||
|
if (variation.options[0]) initial[variation.id] = variation.options[0].id;
|
||||||
|
}
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
|
||||||
|
const matchedVariant = useMemo(() => {
|
||||||
|
if (variations.length === 0) return null;
|
||||||
|
return (
|
||||||
|
storeVariants.find((variant) =>
|
||||||
|
variations.every((variation) => {
|
||||||
|
const selection = variant.selections.find((s) => s.variationId === variation.id);
|
||||||
|
return selection?.optionId === selected[variation.id];
|
||||||
|
}),
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}, [variations, storeVariants, selected]);
|
||||||
|
|
||||||
|
const hasVariations = variations.length > 0;
|
||||||
|
const price = hasVariations
|
||||||
|
? (matchedVariant?.discountedPrice ?? matchedVariant?.price ?? null)
|
||||||
|
: fallbackPrice.minPrice;
|
||||||
|
const priceRangeLabel =
|
||||||
|
!hasVariations &&
|
||||||
|
fallbackPrice.minPrice != null &&
|
||||||
|
fallbackPrice.maxPrice != null &&
|
||||||
|
fallbackPrice.minPrice !== fallbackPrice.maxPrice
|
||||||
|
? `${formatPrice(fallbackPrice.minPrice)} - ${formatPrice(fallbackPrice.maxPrice)}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const stockQuantity = hasVariations ? (matchedVariant?.stockQuantity ?? 0) : null;
|
||||||
|
const available = hasVariations ? Boolean(matchedVariant) && stockQuantity! > 0 : inStock;
|
||||||
|
const maxQty = stockQuantity && stockQuantity > 0 ? stockQuantity : 99;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="md:col-span-5">
|
||||||
|
{header}
|
||||||
|
|
||||||
|
{hasVariations && (
|
||||||
|
<div className="mt-6 space-y-5">
|
||||||
|
{variations.map((variation) => (
|
||||||
|
<div key={variation.id}>
|
||||||
|
<p className="mb-2 text-sm font-bold">
|
||||||
|
{variation.type === "color" ? "انتخاب رنگ" : variation.name}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{variation.options.map((option) => {
|
||||||
|
const isSelected = selected[variation.id] === option.id;
|
||||||
|
const swatch = option.colorHex ?? (variation.type === "color" ? option.value : null);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setSelected((prev) => ({ ...prev, [variation.id]: option.id }))
|
||||||
|
}
|
||||||
|
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? "border-red text-red-dark"
|
||||||
|
: "border-border text-foreground hover:border-red-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{swatch && (
|
||||||
|
<span
|
||||||
|
className="h-3 w-3 shrink-0 rounded-full border border-border"
|
||||||
|
style={{ backgroundColor: swatch }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{option.label}
|
||||||
|
{isSelected && <CheckIcon className="h-3.5 w-3.5" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-3">
|
||||||
|
<div className="sticky top-24 rounded-2xl border border-border bg-surface p-5">
|
||||||
|
{storeName && <p className="mb-4 text-sm font-bold">{storeName}</p>}
|
||||||
|
|
||||||
|
<div className="mb-4 flex items-center gap-2 text-sm">
|
||||||
|
<span className="font-medium text-foreground">موجودی:</span>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 text-xs font-medium ${
|
||||||
|
available ? "bg-green-50 text-green-700" : "bg-red-light text-red-dark"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{available ? "موجود" : "ناموجود"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{price != null ? (
|
||||||
|
<>
|
||||||
|
<p className="mb-5 text-2xl font-bold text-red-dark">
|
||||||
|
{priceRangeLabel ?? formatPrice(price)}
|
||||||
|
<span className="mr-1 text-sm font-normal text-muted">تومان</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mb-4 flex items-center gap-3">
|
||||||
|
<span className="text-sm font-medium text-foreground">تعداد:</span>
|
||||||
|
<div className="flex items-center rounded-xl border border-border">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setQuantity((q) => Math.min(maxQty, q + 1))}
|
||||||
|
disabled={quantity >= maxQty}
|
||||||
|
className="px-3 py-2 text-lg text-foreground transition-colors hover:text-red disabled:cursor-not-allowed disabled:text-muted"
|
||||||
|
aria-label="افزایش تعداد"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<span className="min-w-8 text-center text-sm font-medium">{quantity}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
|
||||||
|
disabled={quantity <= 1}
|
||||||
|
className="px-3 py-2 text-lg text-foreground transition-colors hover:text-red disabled:cursor-not-allowed disabled:text-muted"
|
||||||
|
aria-label="کاهش تعداد"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!available}
|
||||||
|
className="w-full rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark disabled:cursor-not-allowed disabled:bg-red/60"
|
||||||
|
>
|
||||||
|
افزودن به سبد خرید
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : contactPhone ? (
|
||||||
|
<a
|
||||||
|
href={`tel:${contactPhone}`}
|
||||||
|
className="flex w-full items-center justify-center gap-2 rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark"
|
||||||
|
>
|
||||||
|
<span aria-hidden>📞</span>
|
||||||
|
تماس با فروشگاه
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
className="w-full cursor-not-allowed rounded-xl bg-red/60 py-3 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
تماس با فروشگاه
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
type BreadcrumbItem = { label: string; href?: string };
|
||||||
|
|
||||||
|
export function Breadcrumb({ items }: { items: BreadcrumbItem[] }) {
|
||||||
|
return (
|
||||||
|
<nav aria-label="مسیر" className="mb-6 flex flex-wrap items-center gap-2 text-sm text-muted">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-2">
|
||||||
|
{i > 0 && <span aria-hidden>/</span>}
|
||||||
|
{item.href ? (
|
||||||
|
<a href={item.href} className="transition-colors hover:text-red">
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-foreground">{item.label}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -252,6 +252,34 @@ export async function getProductVariations(slug: string): Promise<ProductVariati
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One store-item per real purchasable variant combination (e.g. one per color).
|
||||||
|
// `selections` ties it back to the ProductVariation/VariationOption ids above so
|
||||||
|
// the UI can look up price/stock for whatever options the shopper picks.
|
||||||
|
export type ProductStoreVariant = {
|
||||||
|
id: string;
|
||||||
|
price: number | null;
|
||||||
|
discountedPrice: number | null;
|
||||||
|
stockQuantity: number;
|
||||||
|
selections: { variationId: string; optionId: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getProductStoreVariants(productId: string): Promise<ProductStoreVariant[]> {
|
||||||
|
const res = await apiGet<{ items: Record<string, unknown>[] }>(`/tenants/${DOMAIN}/store-items`, {
|
||||||
|
productId,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
return (res?.items ?? []).map((raw) => ({
|
||||||
|
id: String(raw.id ?? ""),
|
||||||
|
price: raw.price != null ? Number(raw.price) : null,
|
||||||
|
discountedPrice: raw.discountedPrice != null ? Number(raw.discountedPrice) : null,
|
||||||
|
stockQuantity: Number(raw.stockQuantity ?? 0),
|
||||||
|
selections: ((raw.selections as Record<string, unknown>[] | undefined) ?? []).map((s) => ({
|
||||||
|
variationId: String(s.variationId ?? ""),
|
||||||
|
optionId: String(s.optionId ?? ""),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export type TechnicalInfoField = { label: string; value: string };
|
export type TechnicalInfoField = { label: string; value: string };
|
||||||
|
|
||||||
export async function getProductTechnicalInfo(slug: string): Promise<TechnicalInfoField[]> {
|
export async function getProductTechnicalInfo(slug: string): Promise<TechnicalInfoField[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user