Files
shopify-template/components/shopify/product-detail/product-detail-info.tsx
T
Rami BitarandClaude Opus 5 e3d5e75299 Initial commit: Shopify storefront Next.js template
Next.js 16 + React 19 storefront template with Shopify Storefront API
integration, Tailwind v4, and shadcn/ui components.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111qSr3KopyGRsJ6LznR1xZ
2026-07-31 22:12:19 -04:00

241 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Loader } from '@/app/components/ui/loader';
import RemixIcon from '@/components/ui/remix-icon';
interface ProductPrice {
amount: string;
currencyCode: string;
}
interface ProductVariant {
id: string;
title: string;
price: ProductPrice;
availableForSale: boolean;
selectedOptions: Array<{
name: string;
value: string;
}>;
}
interface ProductOption {
id: string;
name: string;
values: string[];
}
interface Product {
id: string;
title: string;
description?: string;
descriptionHtml?: string;
handle: string;
priceRange: {
minVariantPrice: ProductPrice;
};
compareAtPriceRange?: {
minVariantPrice: ProductPrice;
};
options: ProductOption[];
}
export interface ProductFeature {
icon: string;
label: string;
}
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;
addToCartLabel?: string;
features?: ProductFeature[];
}
const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
product,
selectedVariant,
selectedOptions,
quantity,
setQuantity,
handleAddToCart,
onOptionChange,
loading = false,
addToCartLabel = 'Add to Cart',
features = [
{ icon: 'RiTruckLine', label: 'Free shipping on orders over $100' },
{ icon: 'RiArrowGoBackLine', label: '30-day return policy' },
{ icon: 'RiSecurePaymentLine', label: 'Secure payment' },
],
}) => {
const formatPrice = (amount: string) => {
return `$${parseFloat(amount).toFixed(2)}`;
};
const price = selectedVariant?.price || product.priceRange.minVariantPrice;
const compareAtPrice = product.compareAtPriceRange?.minVariantPrice;
const hasDiscount =
compareAtPrice &&
parseFloat(compareAtPrice.amount) > parseFloat(price.amount);
return (
<div className="pt-2">
<div className="mb-2">
<div className="uppercase tracking-[3px] text-xs font-mono text-muted-foreground mb-1">
LUMINA COLLECTION
</div>
<h1 className="text-5xl md:text-6xl font-bold tracking-tighter font-heading text-foreground leading-none mb-6">
{product.title}
</h1>
</div>
{/* Price */}
<div className="flex items-center gap-x-4 mb-10">
<span className="text-4xl font-semibold tracking-tighter text-foreground price-display">
{formatPrice(price.amount)}
</span>
{hasDiscount && compareAtPrice && (
<>
<span className="text-2xl text-muted-foreground line-through tracking-tight">
{formatPrice(compareAtPrice.amount)}
</span>
<Badge
variant="destructive"
className="text-xs font-mono px-4 py-1"
>
SAVE{' '}
{Math.round(
((parseFloat(compareAtPrice.amount) -
parseFloat(price.amount)) /
parseFloat(compareAtPrice.amount)) *
100
)}
%
</Badge>
</>
)}
</div>
{/* Description */}
{product.description && (
<div className="prose text-muted-foreground mb-10 text-[15px] leading-relaxed max-w-prose">
{product.descriptionHtml ? (
<div
dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}
/>
) : (
<p>{product.description}</p>
)}
</div>
)}
{/* Product Options */}
{product.options &&
product.options.map((option) => (
<div key={option.id} className="mb-8">
<div className="text-xs uppercase font-mono tracking-widest text-muted-foreground mb-3">
{option.name}
</div>
<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'
}
className={`rounded-2xl px-6 py-2.5 text-sm font-medium transition-all ${
selectedOptions[option.name] === value ? 'shadow-md' : ''
}`}
>
{value}
</Button>
))}
</div>
</div>
))}
{/* Quantity Selector */}
<div className="mb-8">
<div className="text-xs uppercase font-mono tracking-widest text-muted-foreground mb-3">
QUANTITY
</div>
<div className="inline-flex items-center border border-border rounded-2xl">
<Button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
variant="ghost"
size="icon-sm"
className="rounded-l-2xl h-12 w-12"
disabled={quantity <= 1}
>
</Button>
<div className="px-8 font-mono text-lg font-semibold tabular-nums">
{quantity}
</div>
<Button
onClick={() => setQuantity(quantity + 1)}
variant="ghost"
size="icon-sm"
className="rounded-r-2xl h-12 w-12"
>
+
</Button>
</div>
</div>
{/* Add to Cart Button */}
<Button
onClick={handleAddToCart}
disabled={!selectedVariant?.availableForSale || loading}
size="lg"
className="w-full h-16 text-base font-medium tracking-wider rounded-3xl btn-modern"
>
{loading ? (
<>
<Loader size={18} className="mr-3" />
ADDING TO BAG...
</>
) : selectedVariant?.availableForSale ? (
String(addToCartLabel ?? 'Add to Cart').toUpperCase()
) : (
'OUT OF STOCK'
)}
</Button>
{/* Features */}
{features.length > 0 && (
<div className="mt-12 pt-10 border-t border-border">
<div className="space-y-6">
{features.map((feature, index) => (
<div key={index} className="flex gap-x-4 text-sm">
<div className="mt-0.5 text-primary">
<RemixIcon name={feature.icon} size={18} />
</div>
<div className="text-muted-foreground leading-snug">
{feature.label}
</div>
</div>
))}
</div>
</div>
)}
<div className="mt-12 text-[10px] font-mono text-center text-muted-foreground tracking-widest">
FREE SHIPPING EASY RETURNS SECURE CHECKOUT
</div>
</div>
);
};
export default ProductDetailInfo;