Edit in product detail and create breadcrumb
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user