Compare commits
14 Commits
version_4_
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d167fbcf0 | |||
| 8a779380e5 | |||
| b8b47b9c8d | |||
| b721108cf0 | |||
| 854b37d63c | |||
| e010d7f7da | |||
| 9197db6e03 | |||
| b402be8284 | |||
| da71c01821 | |||
| d4bc4d2272 | |||
| 6738d73c72 | |||
| 233e9a9801 | |||
| 24f365deb7 | |||
| 353cbcd574 |
@@ -1,209 +0,0 @@
|
|||||||
import { useState, useMemo, useCallback } from "react";
|
|
||||||
import useProduct from "./useProduct";
|
|
||||||
import type { ExtendedCartItem } from "./useCart";
|
|
||||||
|
|
||||||
type ProductImage = {
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProductMeta = {
|
|
||||||
salePrice?: string;
|
|
||||||
ribbon?: string;
|
|
||||||
inventoryStatus?: string;
|
|
||||||
inventoryQuantity?: number;
|
|
||||||
sku?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProductVariant = {
|
|
||||||
label: string;
|
|
||||||
options: string[];
|
|
||||||
selected: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useProductDetail = (productId: string) => {
|
|
||||||
const { product, isLoading, error } = useProduct(productId);
|
|
||||||
const [selectedQuantity, setSelectedQuantity] = useState(1);
|
|
||||||
const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
const images = useMemo<ProductImage[]>(() => {
|
|
||||||
if (!product) return [];
|
|
||||||
|
|
||||||
if (product.images && product.images.length > 0) {
|
|
||||||
return product.images.map((src, index) => ({
|
|
||||||
src,
|
|
||||||
alt: product.imageAlt || `${product.name} - Image ${index + 1}`,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
src: product.imageSrc,
|
|
||||||
alt: product.imageAlt || product.name,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [product]);
|
|
||||||
|
|
||||||
const meta = useMemo<ProductMeta>(() => {
|
|
||||||
if (!product?.metadata) return {};
|
|
||||||
|
|
||||||
const metadata = product.metadata;
|
|
||||||
|
|
||||||
let salePrice: string | undefined;
|
|
||||||
const onSaleValue = metadata.onSale;
|
|
||||||
const onSale =
|
|
||||||
String(onSaleValue) === "true" || onSaleValue === 1 || String(onSaleValue) === "1";
|
|
||||||
const salePriceValue = metadata.salePrice;
|
|
||||||
|
|
||||||
if (onSale && salePriceValue !== undefined && salePriceValue !== null) {
|
|
||||||
if (typeof salePriceValue === "number") {
|
|
||||||
salePrice = `$${salePriceValue.toFixed(2)}`;
|
|
||||||
} else {
|
|
||||||
const salePriceStr = String(salePriceValue);
|
|
||||||
salePrice = salePriceStr.startsWith("$") ? salePriceStr : `$${salePriceStr}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inventoryQuantity: number | undefined;
|
|
||||||
if (metadata.inventoryQuantity !== undefined) {
|
|
||||||
const qty = metadata.inventoryQuantity;
|
|
||||||
inventoryQuantity = typeof qty === "number" ? qty : parseInt(String(qty), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
salePrice,
|
|
||||||
ribbon: metadata.ribbon ? String(metadata.ribbon) : undefined,
|
|
||||||
inventoryStatus: metadata.inventoryStatus ? String(metadata.inventoryStatus) : undefined,
|
|
||||||
inventoryQuantity,
|
|
||||||
sku: metadata.sku ? String(metadata.sku) : undefined,
|
|
||||||
};
|
|
||||||
}, [product]);
|
|
||||||
|
|
||||||
const variants = useMemo<ProductVariant[]>(() => {
|
|
||||||
if (!product) return [];
|
|
||||||
|
|
||||||
const variantList: ProductVariant[] = [];
|
|
||||||
|
|
||||||
if (product.metadata?.variantOptions) {
|
|
||||||
try {
|
|
||||||
const variantOptionsStr = String(product.metadata.variantOptions);
|
|
||||||
const parsedOptions = JSON.parse(variantOptionsStr);
|
|
||||||
|
|
||||||
if (Array.isArray(parsedOptions)) {
|
|
||||||
parsedOptions.forEach((option: { name?: string; values?: string | string[] }) => {
|
|
||||||
if (option.name && option.values) {
|
|
||||||
const values =
|
|
||||||
typeof option.values === "string"
|
|
||||||
? option.values.split(",").map((v: string) => v.trim())
|
|
||||||
: Array.isArray(option.values)
|
|
||||||
? option.values.map((v) => String(v).trim())
|
|
||||||
: [String(option.values)];
|
|
||||||
|
|
||||||
if (values.length > 0) {
|
|
||||||
const optionLabel = option.name;
|
|
||||||
const currentSelected = selectedVariants[optionLabel] || values[0];
|
|
||||||
|
|
||||||
variantList.push({
|
|
||||||
label: optionLabel,
|
|
||||||
options: values,
|
|
||||||
selected: currentSelected,
|
|
||||||
onChange: (value) => {
|
|
||||||
setSelectedVariants((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[optionLabel]: value,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Failed to parse variantOptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variantList.length === 0 && product.brand) {
|
|
||||||
variantList.push({
|
|
||||||
label: "Brand",
|
|
||||||
options: [product.brand],
|
|
||||||
selected: product.brand,
|
|
||||||
onChange: () => {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variantList.length === 0 && product.variant) {
|
|
||||||
const variantOptions = product.variant.includes("/")
|
|
||||||
? product.variant.split("/").map((v) => v.trim())
|
|
||||||
: [product.variant];
|
|
||||||
|
|
||||||
const variantLabel = "Variant";
|
|
||||||
const currentSelected = selectedVariants[variantLabel] || variantOptions[0];
|
|
||||||
|
|
||||||
variantList.push({
|
|
||||||
label: variantLabel,
|
|
||||||
options: variantOptions,
|
|
||||||
selected: currentSelected,
|
|
||||||
onChange: (value) => {
|
|
||||||
setSelectedVariants((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[variantLabel]: value,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return variantList;
|
|
||||||
}, [product, selectedVariants]);
|
|
||||||
|
|
||||||
const quantityVariant = useMemo<ProductVariant>(
|
|
||||||
() => ({
|
|
||||||
label: "Quantity",
|
|
||||||
options: Array.from({ length: 10 }, (_, i) => String(i + 1)),
|
|
||||||
selected: String(selectedQuantity),
|
|
||||||
onChange: (value) => setSelectedQuantity(parseInt(value, 10)),
|
|
||||||
}),
|
|
||||||
[selectedQuantity]
|
|
||||||
);
|
|
||||||
|
|
||||||
const createCartItem = useCallback((): ExtendedCartItem | null => {
|
|
||||||
if (!product) return null;
|
|
||||||
|
|
||||||
const variantStrings = Object.entries(selectedVariants).map(
|
|
||||||
([label, value]) => `${label}: ${value}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (variantStrings.length === 0 && product.variant) {
|
|
||||||
variantStrings.push(`Variant: ${product.variant}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantId = Object.values(selectedVariants).join("-") || "default";
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `${product.id}-${variantId}-${selectedQuantity}`,
|
|
||||||
productId: product.id,
|
|
||||||
name: product.name,
|
|
||||||
variants: variantStrings,
|
|
||||||
price: product.price,
|
|
||||||
quantity: selectedQuantity,
|
|
||||||
imageSrc: product.imageSrc,
|
|
||||||
imageAlt: product.imageAlt || product.name,
|
|
||||||
};
|
|
||||||
}, [product, selectedVariants, selectedQuantity]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
product,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
images,
|
|
||||||
meta,
|
|
||||||
variants,
|
|
||||||
quantityVariant,
|
|
||||||
selectedQuantity,
|
|
||||||
selectedVariants,
|
|
||||||
createCartItem,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useProductDetail;
|
|
||||||
export type { ProductImage, ProductMeta, ProductVariant };
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import BlogSimpleCards from "@/components/sections/blog/BlogSimpleCards";
|
|
||||||
|
|
||||||
const BlogPage = () => {
|
|
||||||
return (
|
|
||||||
<BlogSimpleCards
|
|
||||||
tag="Blog"
|
|
||||||
title="Latest Articles"
|
|
||||||
description="Stay updated with our latest insights and news"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlogPage;
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { ReactLenis } from "lenis/react";
|
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import ProductDetailCard from "@/components/ecommerce/ProductDetailCard";
|
|
||||||
import ProductCart from "@/components/ecommerce/ProductCart";
|
|
||||||
import useProductDetail from "@/hooks/useProductDetail";
|
|
||||||
import useCart from "@/hooks/useCart";
|
|
||||||
import useCheckout from "@/hooks/useCheckout";
|
|
||||||
|
|
||||||
const ProductPage = () => {
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { product, isLoading, images, createCartItem, selectedQuantity } = useProductDetail(id || "");
|
|
||||||
const { items: cartItems, isOpen: cartOpen, setIsOpen: setCartOpen, addItem, updateQuantity, removeItem, total: cartTotal, getCheckoutItems } = useCart();
|
|
||||||
const { buyNow, checkout } = useCheckout();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<section className="w-content-width mx-auto py-20">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Loader2 className="size-8 animate-spin text-foreground" strokeWidth={1.5} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!product) {
|
|
||||||
return (
|
|
||||||
<section className="w-content-width mx-auto py-20 text-center">
|
|
||||||
<p className="text-foreground mb-4">Product not found</p>
|
|
||||||
<button onClick={() => navigate("/shop")} className="primary-button px-6 py-2 rounded-theme text-primary-cta-text">
|
|
||||||
Back to Shop
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
|
||||||
const item = createCartItem();
|
|
||||||
if (item) addItem(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBuyNow = () => {
|
|
||||||
buyNow(product, selectedQuantity);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckout = async () => {
|
|
||||||
if (cartItems.length === 0) return;
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set("success", "true");
|
|
||||||
await checkout(getCheckoutItems(), { successUrl: url.toString() });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactLenis root>
|
|
||||||
<ProductDetailCard
|
|
||||||
name={product.name}
|
|
||||||
price={product.price}
|
|
||||||
description={product.description}
|
|
||||||
images={images.map((img) => img.src)}
|
|
||||||
rating={product.rating}
|
|
||||||
onAddToCart={handleAddToCart}
|
|
||||||
onBuyNow={handleBuyNow}
|
|
||||||
/>
|
|
||||||
<ProductCart
|
|
||||||
isOpen={cartOpen}
|
|
||||||
onClose={() => setCartOpen(false)}
|
|
||||||
items={cartItems}
|
|
||||||
total={`$${cartTotal}`}
|
|
||||||
onQuantityChange={updateQuantity}
|
|
||||||
onRemove={removeItem}
|
|
||||||
onCheckout={handleCheckout}
|
|
||||||
/>
|
|
||||||
</ReactLenis>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProductPage;
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import ProductCatalog from "@/components/ecommerce/ProductCatalog";
|
|
||||||
import useProductCatalog from "@/hooks/useProductCatalog";
|
|
||||||
|
|
||||||
const ShopPage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { products, isLoading, search, setSearch } = useProductCatalog({
|
|
||||||
onProductClick: (productId) => navigate(`/shop/${productId}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<section className="w-content-width mx-auto py-20">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Loader2 className="size-8 animate-spin text-foreground" strokeWidth={1.5} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProductCatalog
|
|
||||||
products={products}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShopPage;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user