Update default shopify site design

This commit is contained in:
Rami Bitar
2026-06-10 13:20:03 -04:00
parent 1fb1df8cfd
commit e4ecab1875
33 changed files with 527 additions and 2371 deletions

View File

@@ -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>
) : (

View File

@@ -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;

View File

@@ -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} />
))}

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -1,3 +1,3 @@
import ProductDetail from './product-detail/index.tsx';
import ProductDetail from './product-detail/index';
export default ProductDetail;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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'
)}

View File

@@ -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}

View File

@@ -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>
) : (

View 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;

View 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;

View File

@@ -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>&copy; 2025 Store. All rights reserved.</p>
</div>
</span>
<p>&copy; 2025 Store. All rights reserved.</p>
</div>
</footer>
);

View File

@@ -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>
);
};