Update default shopify site design
This commit is contained in:
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { useShopifyCart, redirectToCheckout } from '@/hooks/use-shopify-cart';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription } from '@/components/ui/empty';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -58,7 +58,7 @@ const CartDrawer: React.FC = () => {
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{loading && items.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" />
|
||||
<Loader size={32} />
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<Empty className="py-12">
|
||||
@@ -79,7 +79,7 @@ const CartDrawer: React.FC = () => {
|
||||
return (
|
||||
<div key={item.id} className="flex items-start space-x-4 pb-6 border-b border-gray-200 last:border-b-0">
|
||||
{/* Product Image */}
|
||||
<div className="w-20 h-20 bg-gray-100 rounded-lg overflow-hidden flex-shrink-0">
|
||||
<div className="w-20 h-20 bg-gray-100 overflow-hidden flex-shrink-0">
|
||||
{image ? (
|
||||
<img
|
||||
src={image}
|
||||
@@ -95,13 +95,13 @@ const CartDrawer: React.FC = () => {
|
||||
|
||||
{/* Product Details */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-gray-900 mb-1 line-clamp-2">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-1 line-clamp-2">
|
||||
{item.merchandise.product.title}
|
||||
</h4>
|
||||
|
||||
{/* Variant Info */}
|
||||
{selectedOptions.length > 0 && (
|
||||
<div className="text-sm text-gray-500 mb-2">
|
||||
<div className="text-xs text-gray-500 mb-2">
|
||||
{selectedOptions.map((option, index) => (
|
||||
<span key={option.name}>
|
||||
{option.value}
|
||||
@@ -113,7 +113,7 @@ const CartDrawer: React.FC = () => {
|
||||
|
||||
{/* Quantity Controls */}
|
||||
<div className="flex items-center mt-3">
|
||||
<div className="flex items-center border border-gray-300 rounded-lg">
|
||||
<div className="flex items-center border border-gray-200 rounded-md">
|
||||
<Button
|
||||
onClick={() => updateItemQuantity(item.id, item.quantity - 1)}
|
||||
variant="ghost"
|
||||
@@ -123,7 +123,7 @@ const CartDrawer: React.FC = () => {
|
||||
>
|
||||
<RiSubtractLine size={14} />
|
||||
</Button>
|
||||
<span className="px-2 py-1 font-semibold min-w-[30px] text-center text-sm">
|
||||
<span className="px-2 py-1 font-medium min-w-[30px] text-center text-sm">
|
||||
{item.quantity}
|
||||
</span>
|
||||
<Button
|
||||
@@ -141,7 +141,7 @@ const CartDrawer: React.FC = () => {
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex-shrink-0">
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
<span className="text-sm text-gray-500">
|
||||
${parseFloat(item.merchandise.price.amount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -153,7 +153,7 @@ const CartDrawer: React.FC = () => {
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={loading}
|
||||
className="text-gray-400 hover:text-red-500"
|
||||
className="text-gray-400 hover:text-gray-900"
|
||||
>
|
||||
<RiCloseLine size={18} />
|
||||
</Button>
|
||||
@@ -169,14 +169,14 @@ const CartDrawer: React.FC = () => {
|
||||
{items.length > 0 && (
|
||||
<div className="border-t border-border p-6">
|
||||
{/* Subtotal */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-base font-semibold">Subtotal</span>
|
||||
<span className="text-lg font-bold">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-900">Subtotal</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
${totalAmount.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<div className="text-xs text-gray-500 mb-4">
|
||||
Shipping and taxes calculated at checkout
|
||||
</div>
|
||||
|
||||
@@ -185,12 +185,12 @@ const CartDrawer: React.FC = () => {
|
||||
<Button
|
||||
onClick={handleCheckout}
|
||||
disabled={loading || !checkoutUrl}
|
||||
className="w-full"
|
||||
className="w-full h-14 rounded-full text-base"
|
||||
size="lg"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center justify-center space-x-2">
|
||||
<Spinner size="sm" />
|
||||
<Loader size={16} />
|
||||
<span>Processing...</span>
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
interface CollectionImage {
|
||||
url: string;
|
||||
@@ -22,43 +21,29 @@ interface CollectionCardProps {
|
||||
const CollectionCard: React.FC<CollectionCardProps> = ({ collection }) => {
|
||||
return (
|
||||
<Link href={`/collections/${collection.handle}`} className="block group">
|
||||
<Card className="hover:shadow-xl transition-shadow duration-300 overflow-hidden py-0 gap-0">
|
||||
{/* Collection Image */}
|
||||
<div className="aspect-video overflow-hidden bg-gray-100">
|
||||
{collection.image ? (
|
||||
<img
|
||||
src={collection.image.url}
|
||||
alt={collection.image.altText || collection.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<i className="ri-folder-line text-6xl"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Collection Info */}
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-3 group-hover:text-gray-600 transition-colors font-heading">
|
||||
{collection.title}
|
||||
</h3>
|
||||
|
||||
{collection.description && (
|
||||
<p className="text-gray-600">
|
||||
{collection.description.substring(0, 100)}
|
||||
{collection.description.length > 100 ? '...' : ''}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-black font-semibold group-hover:text-gray-600 transition-colors flex items-center">
|
||||
<span>View Collection</span>
|
||||
<i className="ri-arrow-right-s-line ml-2"></i>
|
||||
{/* Collection Image */}
|
||||
<div className="aspect-video overflow-hidden bg-gray-100">
|
||||
{collection.image ? (
|
||||
<img
|
||||
src={collection.image.url}
|
||||
alt={collection.image.altText || collection.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<i className="ri-folder-line text-6xl"></i>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Collection Info */}
|
||||
<div className="mt-3">
|
||||
<h3 className="text-base font-medium text-gray-900 font-heading">
|
||||
{collection.title}
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionCard;
|
||||
export default CollectionCard;
|
||||
|
||||
@@ -20,21 +20,17 @@ const CollectionDetail: React.FC = () => {
|
||||
return (
|
||||
<div className="pt-4 pb-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-5xl font-bold text-center mb-16 text-gray-900 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight text-center mb-12 text-gray-900 font-heading">
|
||||
{formattedTitle}
|
||||
</h2>
|
||||
|
||||
{/* Loading Skeleton */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="animate-pulse">
|
||||
<div className="aspect-square bg-gray-200"></div>
|
||||
<div className="p-6">
|
||||
<div className="h-6 bg-gray-200 rounded mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-8 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-12 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
<div className="mt-3 h-4 w-2/3 bg-gray-200 rounded"></div>
|
||||
<div className="mt-2 h-4 w-1/4 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -47,13 +43,13 @@ const CollectionDetail: React.FC = () => {
|
||||
return (
|
||||
<div className="pt-4 pb-16">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-5xl font-bold mb-8 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight mb-8 text-gray-900 font-heading">
|
||||
{formattedTitle}
|
||||
</h2>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-8 max-w-md mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-8 max-w-md mx-auto">
|
||||
<i className="ri-error-warning-line text-4xl text-red-500 mb-4"></i>
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">
|
||||
<h3 className="text-lg font-medium text-red-800 mb-2">
|
||||
Failed to Load Collection
|
||||
</h3>
|
||||
<p className="text-red-600 mb-4">
|
||||
@@ -61,7 +57,7 @@ const CollectionDetail: React.FC = () => {
|
||||
</p>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="bg-red-600 text-white px-6 py-2 rounded-lg hover:bg-red-700 transition-colors"
|
||||
className="bg-red-600 text-white px-6 py-2 rounded-md hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
@@ -77,15 +73,15 @@ const CollectionDetail: React.FC = () => {
|
||||
return (
|
||||
<div className="pt-4 pb-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-5xl font-bold text-center mb-16 text-gray-900 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight text-center mb-12 text-gray-900 font-heading">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 max-w-md mx-auto">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-md p-8 max-w-md mx-auto">
|
||||
<i className="ri-shopping-bag-line text-4xl text-gray-400 mb-4"></i>
|
||||
<h3 className="text-lg font-semibold text-gray-600 mb-2">
|
||||
<h3 className="text-lg font-medium text-gray-600 mb-2">
|
||||
No Products in Collection
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
@@ -94,7 +90,7 @@ const CollectionDetail: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{products.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
|
||||
@@ -11,20 +11,16 @@ const Collections: React.FC = () => {
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-5xl font-bold text-center mb-16 text-gray-900 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight text-center mb-12 text-gray-900 font-heading">
|
||||
Our Collections
|
||||
</h2>
|
||||
|
||||
{/* Loading Skeleton */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
|
||||
<div key={index} className="animate-pulse">
|
||||
<div className="aspect-video bg-gray-200"></div>
|
||||
<div className="p-6">
|
||||
<div className="h-8 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-4 bg-gray-200 rounded mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
</div>
|
||||
<div className="mt-3 h-4 w-1/3 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -37,13 +33,13 @@ const Collections: React.FC = () => {
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-5xl font-bold mb-8 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight mb-8 text-gray-900 font-heading">
|
||||
Our Collections
|
||||
</h2>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-8 max-w-md mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-8 max-w-md mx-auto">
|
||||
<i className="ri-error-warning-line text-4xl text-red-500 mb-4"></i>
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">
|
||||
<h3 className="text-lg font-medium text-red-800 mb-2">
|
||||
Failed to Load Collections
|
||||
</h3>
|
||||
<p className="text-red-600 mb-4">
|
||||
@@ -51,7 +47,7 @@ const Collections: React.FC = () => {
|
||||
</p>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="bg-red-600 text-white px-6 py-2 rounded-lg hover:bg-red-700 transition-colors"
|
||||
className="bg-red-600 text-white px-6 py-2 rounded-md hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
@@ -65,13 +61,13 @@ const Collections: React.FC = () => {
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-5xl font-bold mb-8 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight mb-8 text-gray-900 font-heading">
|
||||
Our Collections
|
||||
</h2>
|
||||
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 max-w-md mx-auto">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-md p-8 max-w-md mx-auto">
|
||||
<i className="ri-folder-line text-4xl text-gray-400 mb-4"></i>
|
||||
<h3 className="text-lg font-semibold text-gray-600 mb-2">
|
||||
<h3 className="text-lg font-medium text-gray-600 mb-2">
|
||||
No Collections Found
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
@@ -86,7 +82,7 @@ const Collections: React.FC = () => {
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-5xl font-bold text-center mb-16 text-gray-900 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight text-center mb-12 text-gray-900 font-heading">
|
||||
Our Collections
|
||||
</h2>
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useShopifyCart, addCartLines } from '@/hooks/use-shopify-cart';
|
||||
import { truncate } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
interface ProductImage {
|
||||
url: string;
|
||||
@@ -27,6 +25,7 @@ interface Product {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
productType?: string;
|
||||
handle: string;
|
||||
images: {
|
||||
edges: Array<{
|
||||
@@ -87,7 +86,7 @@ const ProductCard: React.FC<ProductCardProps> = ({ product, onAddToCart }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="hover:shadow-xl transition-shadow duration-300 overflow-hidden group py-0 gap-0">
|
||||
<div className="group">
|
||||
{/* Product Image */}
|
||||
<div className="aspect-square overflow-hidden bg-gray-100 relative">
|
||||
{firstImage ? (
|
||||
@@ -113,26 +112,17 @@ const ProductCard: React.FC<ProductCardProps> = ({ product, onAddToCart }) => {
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-2 line-clamp-2 font-heading">
|
||||
{truncate(product.title, 50)}
|
||||
</h3>
|
||||
|
||||
{/* Price Section */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-lg font-bold text-gray-900">
|
||||
${parseFloat(price.amount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* View Details Button */}
|
||||
<div className="mt-3">
|
||||
<Link href={`/products/${product.handle}`}>
|
||||
<Button className="w-full" size="sm">
|
||||
View Details
|
||||
</Button>
|
||||
<h3 className="text-base font-medium text-gray-900 line-clamp-2 font-heading">
|
||||
{truncate(product.title, 50)}
|
||||
</h3>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<p className="text-base text-gray-500">
|
||||
${parseFloat(price.amount).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import ProductDetail from './product-detail/index.tsx';
|
||||
import ProductDetail from './product-detail/index';
|
||||
|
||||
export default ProductDetail;
|
||||
@@ -7,7 +7,7 @@ import { useProduct, type Product } from '@/hooks/use-shopify-products';
|
||||
import { useShopifyCart } from '@/hooks/use-shopify-cart';
|
||||
import ProductDetailGallery from './product-detail-gallery';
|
||||
import ProductDetailInfo from './product-detail-info';
|
||||
import ProductRecommendations from './product-recommendations';
|
||||
import ProductRecommendations from '../product-recommendations';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
|
||||
import { RiErrorWarningLine } from '@remixicon/react';
|
||||
@@ -38,7 +38,7 @@ interface ProductVariant {
|
||||
};
|
||||
}
|
||||
|
||||
export type { Product };
|
||||
export type { Product, ProductVariant };
|
||||
|
||||
interface ProductDetailProps {
|
||||
handle?: string;
|
||||
@@ -54,7 +54,6 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
|
||||
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({});
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||
const [addingToCart, setAddingToCart] = useState(false);
|
||||
|
||||
// Initialize variant when product loads
|
||||
@@ -86,17 +85,6 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
|
||||
|
||||
if (matchingVariant) {
|
||||
setSelectedVariant(matchingVariant.node);
|
||||
|
||||
// Update image if variant has an associated image
|
||||
if (matchingVariant.node.image && product) {
|
||||
const variantImageUrl = matchingVariant.node.image.url;
|
||||
const imageIndex = product.images.edges.findIndex(
|
||||
edge => edge.node.url === variantImageUrl
|
||||
);
|
||||
if (imageIndex !== -1) {
|
||||
setSelectedImageIndex(imageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,15 +105,12 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
{/* Image Gallery Skeleton */}
|
||||
<div>
|
||||
<div className="aspect-square bg-gray-200 rounded-lg animate-pulse mb-4"></div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="aspect-square bg-gray-200 rounded animate-pulse"></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="lg:col-span-2 grid grid-cols-2 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="aspect-square bg-gray-200 animate-pulse"></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Product Info Skeleton */}
|
||||
@@ -146,7 +131,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
|
||||
<div className="container mx-auto px-4 py-8 text-center">
|
||||
<Alert variant="destructive" className="max-w-md mx-auto p-8">
|
||||
<RiErrorWarningLine size={36} />
|
||||
<AlertTitle className="text-lg font-semibold">
|
||||
<AlertTitle className="text-lg font-medium">
|
||||
Product Not Found
|
||||
</AlertTitle>
|
||||
<AlertDescription className="mb-4">
|
||||
@@ -174,33 +159,29 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ handle: handleProp }) =>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/shop">Shop</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{product.title}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<ProductDetailGallery
|
||||
images={product.images.edges.map(edge => edge.node)}
|
||||
selectedImageIndex={selectedImageIndex}
|
||||
onImageSelect={setSelectedImageIndex}
|
||||
/>
|
||||
<ProductDetailInfo
|
||||
product={product}
|
||||
selectedVariant={selectedVariant}
|
||||
selectedOptions={selectedOptions}
|
||||
quantity={quantity}
|
||||
setQuantity={setQuantity}
|
||||
handleAddToCart={handleAddToCart}
|
||||
onOptionChange={handleOptionChange}
|
||||
loading={addingToCart}
|
||||
/>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
<div className="lg:col-span-2">
|
||||
<ProductDetailGallery
|
||||
images={product.images.edges.map(edge => edge.node)}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:sticky lg:top-20 lg:self-start">
|
||||
<ProductDetailInfo
|
||||
product={product}
|
||||
selectedVariant={selectedVariant}
|
||||
selectedOptions={selectedOptions}
|
||||
quantity={quantity}
|
||||
setQuantity={setQuantity}
|
||||
handleAddToCart={handleAddToCart}
|
||||
onOptionChange={handleOptionChange}
|
||||
loading={addingToCart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { RiImageLine } from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
|
||||
|
||||
interface ProductImage {
|
||||
url: string;
|
||||
@@ -9,59 +12,62 @@ interface ProductImage {
|
||||
|
||||
interface ProductDetailGalleryProps {
|
||||
images: ProductImage[];
|
||||
selectedImageIndex?: number;
|
||||
onImageSelect?: (index: number) => void;
|
||||
}
|
||||
|
||||
const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
|
||||
images,
|
||||
selectedImageIndex = 0,
|
||||
onImageSelect
|
||||
}) => {
|
||||
const selectedImage = selectedImageIndex;
|
||||
const setSelectedImage = onImageSelect || (() => {});
|
||||
const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({ images }) => {
|
||||
const [zoomedIndex, setZoomedIndex] = useState<number | null>(null);
|
||||
|
||||
if (images.length === 0) {
|
||||
return (
|
||||
<div className="aspect-square bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<RiImageLine size={60} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const zoomedImage = zoomedIndex !== null ? images[zoomedIndex] : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Main Image */}
|
||||
<div className="aspect-square bg-gray-100 rounded-lg overflow-hidden mb-4">
|
||||
{images.length > 0 ? (
|
||||
<img
|
||||
src={images[selectedImage].url}
|
||||
alt={images[selectedImage].altText || 'Product image'}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<RiImageLine size={60} />
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{images.map((image, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setZoomedIndex(index)}
|
||||
className={cn(
|
||||
'aspect-square bg-gray-100 overflow-hidden cursor-zoom-in group',
|
||||
// A lone image (or the last of an odd count) spans both columns to avoid a gap
|
||||
images.length % 2 === 1 && index === images.length - 1 && 'col-span-2'
|
||||
)}
|
||||
aria-label={`Zoom image ${index + 1}`}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.altText || `Product image ${index + 1}`}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Image Thumbnails */}
|
||||
{images.length > 1 && (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{images.map((image, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedImage(index)}
|
||||
className={`aspect-square rounded-lg overflow-hidden border-2 transition-colors ${
|
||||
selectedImage === index
|
||||
? 'border-black'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.altText || 'Product thumbnail'}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Zoom Lightbox */}
|
||||
<Dialog
|
||||
open={zoomedIndex !== null}
|
||||
onOpenChange={(open) => !open && setZoomedIndex(null)}
|
||||
>
|
||||
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden">
|
||||
<DialogTitle className="sr-only">Product image</DialogTitle>
|
||||
{zoomedImage && (
|
||||
<img
|
||||
src={zoomedImage.url}
|
||||
alt={zoomedImage.altText || 'Product image'}
|
||||
className="w-full h-auto max-h-[85vh] object-contain bg-gray-100"
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetailGallery;
|
||||
export default ProductDetailGallery;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Product, ProductVariant } from './index.tsx';
|
||||
import { Product, ProductVariant } from './index';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
import {
|
||||
RiSubtractLine,
|
||||
RiAddLine,
|
||||
@@ -45,18 +45,18 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4 font-heading">
|
||||
<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-4 mb-6">
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
<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-xl text-gray-500 line-through">
|
||||
<span className="text-lg text-gray-400 line-through">
|
||||
{formatPrice(compareAtPrice)}
|
||||
</span>
|
||||
<Badge variant="destructive">
|
||||
@@ -77,10 +77,12 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Options */}
|
||||
{product.options.map(option => (
|
||||
{/* 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-semibold text-gray-700 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-900 mb-2">
|
||||
{option.name}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -99,10 +101,10 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
|
||||
|
||||
{/* Quantity Selector */}
|
||||
<div className="mb-8">
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-900 mb-2">
|
||||
Quantity
|
||||
</label>
|
||||
<div className="flex items-center border border-gray-300 rounded-lg w-fit">
|
||||
<div className="flex items-center border border-gray-200 rounded-md w-fit">
|
||||
<Button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
variant="ghost"
|
||||
@@ -111,7 +113,7 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
|
||||
>
|
||||
<RiSubtractLine size={16} />
|
||||
</Button>
|
||||
<span className="w-10 text-center font-semibold">{quantity}</span>
|
||||
<span className="w-10 text-center text-sm font-medium">{quantity}</span>
|
||||
<Button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
variant="ghost"
|
||||
@@ -122,20 +124,20 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
{/* Add to Bag Button */}
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
disabled={!selectedVariant?.availableForSale || loading}
|
||||
size="lg"
|
||||
className="w-full text-lg"
|
||||
className="w-full h-14 rounded-full text-base"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Spinner size="sm" />
|
||||
<Loader size={16} />
|
||||
<span>Adding...</span>
|
||||
</span>
|
||||
) : selectedVariant?.availableForSale ? (
|
||||
'Add to Cart'
|
||||
'Add to Bag'
|
||||
) : (
|
||||
'Out of Stock'
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { useProductRecommendations } from '@/hooks/use-shopify-products';
|
||||
import ProductCard from '../product-card';
|
||||
import ProductCard from './product-card';
|
||||
|
||||
interface ProductRecommendationsProps {
|
||||
productId: string;
|
||||
@@ -17,23 +17,19 @@ const ProductRecommendations: React.FC<ProductRecommendationsProps> = ({ product
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 py-16">
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-4xl font-bold text-center mb-12 text-gray-900 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight text-center mb-12 text-gray-900 font-heading">
|
||||
You Might Also Like
|
||||
</h2>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="animate-pulse">
|
||||
<div className="aspect-square bg-gray-200"></div>
|
||||
<div className="p-6">
|
||||
<div className="h-6 bg-gray-200 rounded mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-8 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-12 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
<div className="mt-3 h-4 w-2/3 bg-gray-200 rounded"></div>
|
||||
<div className="mt-2 h-4 w-1/4 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -42,8 +38,8 @@ const ProductRecommendations: React.FC<ProductRecommendationsProps> = ({ product
|
||||
<p className="text-gray-500">Recommendations could not be loaded</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
{recommendations.slice(0, 4).map((recommendedProduct) => (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{recommendations.slice(0, 3).map((recommendedProduct) => (
|
||||
<ProductCard
|
||||
key={recommendedProduct.id}
|
||||
product={recommendedProduct}
|
||||
@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import ProductCard from './product-card';
|
||||
import { getProducts } from '@/hooks/use-shopify-products';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
|
||||
interface ProductImage {
|
||||
url: string;
|
||||
@@ -53,7 +53,7 @@ interface ProductsProps {
|
||||
}
|
||||
|
||||
const Products: React.FC<ProductsProps> = ({
|
||||
title = "Our Products",
|
||||
title = "Shop All",
|
||||
limit = 12,
|
||||
showLoadMore = true
|
||||
}) => {
|
||||
@@ -120,21 +120,17 @@ const Products: React.FC<ProductsProps> = ({
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-4xl font-bold text-center mb-12 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight text-center mb-12 text-gray-900 font-heading">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Loading Skeleton */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="animate-pulse">
|
||||
<div className="aspect-square bg-gray-200"></div>
|
||||
<div className="p-6">
|
||||
<div className="h-6 bg-gray-200 rounded mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-8 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-12 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
<div className="mt-3 h-4 w-2/3 bg-gray-200 rounded"></div>
|
||||
<div className="mt-2 h-4 w-1/4 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -147,13 +143,13 @@ const Products: React.FC<ProductsProps> = ({
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-4xl font-bold mb-8 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight mb-8 text-gray-900 font-heading">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-8 max-w-md mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-8 max-w-md mx-auto">
|
||||
<i className="ri-error-warning-line text-4xl text-red-500 mb-4 block"></i>
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">
|
||||
<h3 className="text-lg font-medium text-red-800 mb-2">
|
||||
Failed to Load Products
|
||||
</h3>
|
||||
<p className="text-red-600 mb-4">
|
||||
@@ -175,13 +171,13 @@ const Products: React.FC<ProductsProps> = ({
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-4xl font-bold mb-8 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight mb-8 text-gray-900 font-heading">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 max-w-md mx-auto">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-md p-8 max-w-md mx-auto">
|
||||
<i className="ri-shopping-bag-line text-4xl text-gray-400 mb-4"></i>
|
||||
<h3 className="text-lg font-semibold text-gray-600 mb-2">
|
||||
<h3 className="text-lg font-medium text-gray-600 mb-2">
|
||||
No Products Found
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
@@ -196,12 +192,12 @@ const Products: React.FC<ProductsProps> = ({
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-5xl font-bold text-center mb-16 text-gray-900 font-heading">
|
||||
<h2 className="text-4xl font-medium tracking-tight text-center mb-12 text-gray-900 font-heading">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 mb-12">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
@@ -222,7 +218,7 @@ const Products: React.FC<ProductsProps> = ({
|
||||
>
|
||||
{loadingMore ? (
|
||||
<span className="flex items-center space-x-2">
|
||||
<Spinner size="sm" />
|
||||
<Loader size={16} />
|
||||
<span>Loading...</span>
|
||||
</span>
|
||||
) : (
|
||||
|
||||
55
components/shopify/promo-banner.tsx
Normal file
55
components/shopify/promo-banner.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react';
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
} from '@/components/ui/carousel';
|
||||
|
||||
const PROMOS = [
|
||||
{ text: 'Send a Gift Card', href: '/' },
|
||||
{ text: 'Free shipping on orders over $100', href: '/' },
|
||||
{ text: '30-day return policy', href: '/' },
|
||||
];
|
||||
|
||||
const PromoBanner: React.FC = () => {
|
||||
return (
|
||||
<div className="bg-muted">
|
||||
<Carousel opts={{ loop: true }} className="container mx-auto px-4">
|
||||
<CarouselContent>
|
||||
{PROMOS.map((promo) => (
|
||||
<CarouselItem key={promo.text}>
|
||||
<div className="h-10 flex items-center justify-center">
|
||||
<Link
|
||||
href={promo.href}
|
||||
className="text-xs font-medium text-gray-900 underline underline-offset-2 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
{promo.text}
|
||||
</Link>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious
|
||||
variant="ghost"
|
||||
className="left-0 size-7 text-gray-900 hover:text-gray-600"
|
||||
>
|
||||
<RiArrowLeftSLine size={16} />
|
||||
</CarouselPrevious>
|
||||
<CarouselNext
|
||||
variant="ghost"
|
||||
className="right-0 size-7 text-gray-900 hover:text-gray-600"
|
||||
>
|
||||
<RiArrowRightSLine size={16} />
|
||||
</CarouselNext>
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromoBanner;
|
||||
114
components/shopify/search-dialog.tsx
Normal file
114
components/shopify/search-dialog.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Command } from 'cmdk';
|
||||
import { RiSearchLine } from '@remixicon/react';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
import { getProducts, type Product } from '@/hooks/use-shopify-products';
|
||||
|
||||
interface SearchDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const SearchDialog: React.FC<SearchDialogProps> = ({ open, onOpenChange }) => {
|
||||
const router = useRouter();
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
// Load the catalog once per open; cmdk filters it as the user types
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setQuery('');
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
getProducts({ first: 100, sortKey: 'TITLE' })
|
||||
.then((data) => {
|
||||
if (!cancelled) setProducts(data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to load products for search:', err);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const handleSelect = (handle: string) => {
|
||||
onOpenChange(false);
|
||||
router.push(`/products/${handle}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl p-0 gap-0 overflow-hidden [&>button]:hidden">
|
||||
<DialogTitle className="sr-only">Search products</DialogTitle>
|
||||
<Command label="Search products">
|
||||
<div className="flex items-center gap-2 border-b border-gray-200 px-4">
|
||||
<RiSearchLine size={16} className="text-gray-400 shrink-0" />
|
||||
<Command.Input
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
placeholder="Search products..."
|
||||
className="h-12 w-full bg-transparent text-sm text-gray-900 outline-none placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<Command.List className="max-h-80 overflow-y-auto p-2">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader size={20} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Command.Empty className="py-8 text-center text-sm text-gray-500">
|
||||
No products found.
|
||||
</Command.Empty>
|
||||
{products.map((product) => {
|
||||
const image = product.images.edges[0]?.node;
|
||||
const price = product.priceRange.minVariantPrice;
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
key={product.id}
|
||||
value={product.title}
|
||||
onSelect={() => handleSelect(product.handle)}
|
||||
className="flex cursor-pointer items-center gap-3 rounded-md px-2 py-2 text-sm data-[selected=true]:bg-gray-100"
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-100 overflow-hidden shrink-0">
|
||||
{image && (
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.altText || product.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex-1 truncate font-medium text-gray-900">
|
||||
{product.title}
|
||||
</span>
|
||||
<span className="text-gray-500 shrink-0">
|
||||
${parseFloat(price.amount).toFixed(2)}
|
||||
</span>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchDialog;
|
||||
@@ -2,20 +2,12 @@ import React from 'react';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="bg-black text-white py-12">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h3
|
||||
className="text-2xl font-bold mb-4"
|
||||
style={{fontFamily: 'Space Grotesk, sans-serif'}}
|
||||
>
|
||||
<footer className="bg-white border-t border-gray-200 py-6">
|
||||
<div className="container mx-auto px-4 flex items-center justify-between text-sm text-gray-500">
|
||||
<span className="font-medium text-gray-900 font-heading">
|
||||
Store
|
||||
</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Your premium shopping destination
|
||||
</p>
|
||||
<div className="mt-8 pt-8 border-t border-gray-800 text-gray-400">
|
||||
<p>© 2025 Store. All rights reserved.</p>
|
||||
</div>
|
||||
</span>
|
||||
<p>© 2025 Store. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useShopifyCart } from '@/hooks/use-shopify-cart';
|
||||
import config from '@/lib/config.json';
|
||||
import { RiShoppingCartLine } from '@remixicon/react';
|
||||
import { RiSearchLine, RiShoppingBagLine } from '@remixicon/react';
|
||||
import SearchDialog from './search-dialog';
|
||||
|
||||
const CartIcon: React.FC = () => {
|
||||
const { toggleCart, itemCount } = useShopifyCart();
|
||||
@@ -14,9 +15,9 @@ const CartIcon: React.FC = () => {
|
||||
onClick={toggleCart}
|
||||
className="relative p-1 text-black hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<RiShoppingCartLine size={20} />
|
||||
<RiShoppingBagLine size={20} />
|
||||
{itemCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-black text-white text-[10px] rounded-full w-4 h-4 flex items-center justify-center font-semibold">
|
||||
<span className="absolute -top-1 -right-1 bg-black text-white text-[10px] rounded-full w-4 h-4 flex items-center justify-center font-medium">
|
||||
{itemCount > 99 ? '99+' : itemCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -25,12 +26,14 @@ const CartIcon: React.FC = () => {
|
||||
};
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-sm sticky top-0 z-30 h-14">
|
||||
<nav className="bg-white/70 backdrop-blur-md border-b border-gray-200/60 sticky top-0 z-30 h-14">
|
||||
<div className="container mx-auto px-4 h-full">
|
||||
<div className="flex justify-between items-center h-full">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="text-lg font-bold text-black font-heading">
|
||||
<Link href="/" className="text-lg font-medium text-black font-heading">
|
||||
{config.brand.logo.url ? (
|
||||
<img
|
||||
src={config.brand.logo.url}
|
||||
@@ -57,11 +60,22 @@ const Header: React.FC = () => {
|
||||
Collections
|
||||
</Link>
|
||||
|
||||
{/* Cart Icon */}
|
||||
<CartIcon />
|
||||
{/* Search + Cart Icons */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => setSearchOpen(true)}
|
||||
className="p-1 text-black hover:text-gray-600 transition-colors"
|
||||
aria-label="Search products"
|
||||
>
|
||||
<RiSearchLine size={20} />
|
||||
</button>
|
||||
<CartIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchDialog open={searchOpen} onOpenChange={setSearchOpen} />
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user