167 lines
5.2 KiB
TypeScript
167 lines
5.2 KiB
TypeScript
import React from 'react';
|
|
import { Product, ProductVariant } from './index';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Loader } from '@/components/ui/loader';
|
|
import {
|
|
RiSubtractLine,
|
|
RiAddLine,
|
|
RiTruckLine,
|
|
RiArrowGoBackLine,
|
|
RiSecurePaymentLine,
|
|
} from '@remixicon/react';
|
|
|
|
interface ProductDetailInfoProps {
|
|
product: Product;
|
|
selectedVariant: ProductVariant | null;
|
|
selectedOptions: Record<string, string>;
|
|
quantity: number;
|
|
setQuantity: (quantity: number) => void;
|
|
handleAddToCart: () => void;
|
|
onOptionChange: (optionName: string, value: string) => void;
|
|
loading?: boolean;
|
|
}
|
|
|
|
const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
|
|
product,
|
|
selectedVariant,
|
|
selectedOptions,
|
|
quantity,
|
|
setQuantity,
|
|
handleAddToCart,
|
|
onOptionChange,
|
|
loading = false,
|
|
}) => {
|
|
const formatPrice = (price: { amount: string; currencyCode: string }) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
}).format(parseFloat(price.amount));
|
|
};
|
|
|
|
const price = selectedVariant?.price || product.priceRange.minVariantPrice;
|
|
const compareAtPrice = product.compareAtPriceRange?.minVariantPrice;
|
|
const hasDiscount = compareAtPrice && parseFloat(compareAtPrice.amount) > parseFloat(price.amount);
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="text-4xl font-medium tracking-tight text-gray-900 mb-2 font-heading">
|
|
{product.title}
|
|
</h1>
|
|
|
|
{/* Price */}
|
|
<div className="flex items-center space-x-3 mb-8">
|
|
<span className="text-lg text-gray-500">
|
|
{formatPrice(price)}
|
|
</span>
|
|
{hasDiscount && compareAtPrice && (
|
|
<>
|
|
<span className="text-lg text-gray-400 line-through">
|
|
{formatPrice(compareAtPrice)}
|
|
</span>
|
|
<Badge variant="destructive">
|
|
{Math.round(((parseFloat(compareAtPrice.amount) - parseFloat(price.amount)) / parseFloat(compareAtPrice.amount)) * 100)}% OFF
|
|
</Badge>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Description */}
|
|
{product.description && (
|
|
<div className="text-gray-600 mb-8 text-lg leading-relaxed">
|
|
{product.descriptionHtml ? (
|
|
<div dangerouslySetInnerHTML={{ __html: product.descriptionHtml }} />
|
|
) : (
|
|
<p>{product.description}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Product Options (skip Shopify's placeholder "Title: Default Title" option) */}
|
|
{product.options
|
|
.filter(option => !(option.name === 'Title' && option.values.length === 1 && option.values[0] === 'Default Title'))
|
|
.map(option => (
|
|
<div key={option.id} className="mb-6">
|
|
<label className="block text-sm font-medium text-gray-900 mb-2">
|
|
{option.name}
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{option.values.map(value => (
|
|
<Button
|
|
key={value}
|
|
onClick={() => onOptionChange(option.name, value)}
|
|
variant={selectedOptions[option.name] === value ? 'default' : 'outline'}
|
|
>
|
|
{value}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Quantity Selector */}
|
|
<div className="mb-8">
|
|
<label className="block text-sm font-medium text-gray-900 mb-2">
|
|
Quantity
|
|
</label>
|
|
<div className="flex items-center border border-gray-200 rounded-md w-fit">
|
|
<Button
|
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
disabled={quantity <= 1}
|
|
>
|
|
<RiSubtractLine size={16} />
|
|
</Button>
|
|
<span className="w-10 text-center text-sm font-medium">{quantity}</span>
|
|
<Button
|
|
onClick={() => setQuantity(quantity + 1)}
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
>
|
|
<RiAddLine size={16} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Add to Bag Button */}
|
|
<Button
|
|
onClick={handleAddToCart}
|
|
disabled={!selectedVariant?.availableForSale || loading}
|
|
size="lg"
|
|
className="w-full h-14 rounded-full text-base"
|
|
>
|
|
{loading ? (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<Loader size={16} />
|
|
<span>Adding...</span>
|
|
</span>
|
|
) : selectedVariant?.availableForSale ? (
|
|
'Add to Bag'
|
|
) : (
|
|
'Out of Stock'
|
|
)}
|
|
</Button>
|
|
|
|
{/* Additional Info */}
|
|
<div className="mt-8 pt-8 border-t border-gray-200">
|
|
<div className="space-y-3 text-sm text-gray-600">
|
|
<div className="flex items-center space-x-2">
|
|
<RiTruckLine size={16} />
|
|
<span>Free shipping on orders over $100</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RiArrowGoBackLine size={16} />
|
|
<span>30-day return policy</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RiSecurePaymentLine size={16} />
|
|
<span>Secure payment</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProductDetailInfo; |