diff --git a/app/globals.css b/app/globals.css index 0a218b6..abaed60 100644 --- a/app/globals.css +++ b/app/globals.css @@ -18,16 +18,16 @@ --color-popover-foreground: hsl(222.2 84% 4.9%); /* Primary */ - --color-primary: hsl(222.2 47.4% 11.2%); - --color-primary-foreground: hsl(210 40% 98%); + --color-primary: hsl(0 0% 0%); + --color-primary-foreground: hsl(0 0% 100%); /* Secondary */ --color-secondary: hsl(210 40% 96.1%); --color-secondary-foreground: hsl(222.2 47.4% 11.2%); /* Muted */ - --color-muted: hsl(210 40% 96.1%); - --color-muted-foreground: hsl(215.4 16.3% 46.9%); + --color-muted: hsl(0 0% 96.1%); + --color-muted-foreground: hsl(0 0% 45%); /* Accent */ --color-accent: hsl(210 40% 96.1%); @@ -59,7 +59,7 @@ --radius-xl: 0.75rem; /* Fonts */ - --font-heading: 'Space Grotesk', sans-serif; + --font-heading: var(--font-poppins), 'Poppins', sans-serif; --font-body: 'Inter', sans-serif; } diff --git a/app/layout.tsx b/app/layout.tsx index 0b75872..7ea8014 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,11 +1,19 @@ import React from 'react'; import type { Metadata } from 'next'; +import { Poppins } from 'next/font/google'; import './globals.css'; import { Providers } from './providers'; import ShopifyCart from '@/components/shopify/cart-drawer'; import ShopHeader from '@/components/shopify/shop-header'; +import PromoBanner from '@/components/shopify/promo-banner'; import ShopFooter from '@/components/shopify/shop-footer'; +const poppins = Poppins({ + subsets: ['latin'], + weight: ['400', '500', '600', '700', '800'], + variable: '--font-poppins', +}); + export const metadata: Metadata = { title: 'Frontend | Shopify', description: 'Frontend Shopify Storefront', @@ -17,10 +25,11 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + +
{children} diff --git a/app/products/[handle]/page.tsx b/app/products/[handle]/page.tsx index a9ce14f..e8fbf8f 100644 --- a/app/products/[handle]/page.tsx +++ b/app/products/[handle]/page.tsx @@ -1,197 +1,7 @@ 'use client'; -import React, { useState, useEffect } from 'react'; -import { useParams } from 'next/navigation'; -import Link from 'next/link'; -import { useProduct, type Product } from '@/hooks/use-shopify-products'; -import { useShopifyCart } from '@/hooks/use-shopify-cart'; -import ProductDetailGallery from '@/components/shopify/product-detail/product-detail-gallery'; -import ProductDetailInfo from '@/components/shopify/product-detail/product-detail-info'; -import ProductRecommendations from '@/components/shopify/product-detail/product-recommendations'; -import { Button } from '@/components/ui/button'; -import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; -import { - Breadcrumb, - BreadcrumbList, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbPage, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb'; - -interface ProductVariant { - id: string; - title: string; - price: { - amount: string; - currencyCode: string; - }; - availableForSale: boolean; - selectedOptions: Array<{ - name: string; - value: string; - }>; - image?: { - url: string; - altText?: string; - }; -} +import ProductDetail from '@/components/shopify/product-detail'; export default function ProductDetailPage() { - const params = useParams(); - const handle = params.handle as string; - const { addItem, openCart } = useShopifyCart(); - - const { product, loading, error } = useProduct(handle); - - const [selectedVariant, setSelectedVariant] = useState(null); - const [selectedOptions, setSelectedOptions] = useState>({}); - const [quantity, setQuantity] = useState(1); - const [selectedImageIndex, setSelectedImageIndex] = useState(0); - const [addingToCart, setAddingToCart] = useState(false); - - // Initialize variant when product loads - useEffect(() => { - if (product) { - const firstVariant = product.variants.edges[0]?.node; - if (firstVariant) { - setSelectedVariant(firstVariant); - - const initialOptions: Record = {}; - firstVariant.selectedOptions.forEach((option: { name: string; value: string }) => { - initialOptions[option.name] = option.value; - }); - setSelectedOptions(initialOptions); - } - } - }, [product]); - - const handleOptionChange = (optionName: string, value: string) => { - const newOptions = { ...selectedOptions, [optionName]: value }; - setSelectedOptions(newOptions); - - // Find matching variant - const matchingVariant = product?.variants.edges.find(({ node }) => { - return node.selectedOptions.every(option => - newOptions[option.name] === option.value - ); - }); - - 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); - } - } - } - }; - - const handleAddToCart = async () => { - if (!selectedVariant || !product) return; - - try { - setAddingToCart(true); - await addItem(selectedVariant.id, quantity); - openCart(); - } catch (err) { - console.error('Failed to add item to cart:', err); - } finally { - setAddingToCart(false); - } - }; - - if (loading) { - return ( -
-
- {/* Image Gallery Skeleton */} -
-
-
- {Array.from({ length: 4 }).map((_, i) => ( -
- ))} -
-
- - {/* Product Info Skeleton */} -
-
-
-
-
-
-
-
-
- ); - } - - if (error || !product) { - return ( -
- - - - Product Not Found - - - {error || 'The requested product could not be found.'} - - - -
- ); - } - - return ( -
-
- - - - - Home - - - - - {product.title} - - - -
- edge.node)} - selectedImageIndex={selectedImageIndex} - onImageSelect={setSelectedImageIndex} - /> - -
-
- - -
- ); + return ; } diff --git a/components/shopify/cart-drawer.tsx b/components/shopify/cart-drawer.tsx index 1327d8f..a8b1550 100644 --- a/components/shopify/cart-drawer.tsx +++ b/components/shopify/cart-drawer.tsx @@ -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 = () => {
{loading && items.length === 0 ? (
- +
) : items.length === 0 ? ( @@ -79,7 +79,7 @@ const CartDrawer: React.FC = () => { return (
{/* Product Image */} -
+
{image ? ( { {/* Product Details */}
-

+

{item.merchandise.product.title}

{/* Variant Info */} {selectedOptions.length > 0 && ( -
+
{selectedOptions.map((option, index) => ( {option.value} @@ -113,7 +113,7 @@ const CartDrawer: React.FC = () => { {/* Quantity Controls */}
-
+
- + {item.quantity} @@ -169,14 +169,14 @@ const CartDrawer: React.FC = () => { {items.length > 0 && (
{/* Subtotal */} -
- Subtotal - +
+ Subtotal + ${totalAmount.toFixed(2)}
-
+
Shipping and taxes calculated at checkout
@@ -185,12 +185,12 @@ const CartDrawer: React.FC = () => { @@ -77,15 +73,15 @@ const CollectionDetail: React.FC = () => { return (
-

+

{title}

{products.length === 0 ? (
-
+
-

+

No Products in Collection

@@ -94,7 +90,7 @@ const CollectionDetail: React.FC = () => {

) : ( -
+
{products.map((product) => ( ))} diff --git a/components/shopify/collections.tsx b/components/shopify/collections.tsx index 232724d..7d29577 100644 --- a/components/shopify/collections.tsx +++ b/components/shopify/collections.tsx @@ -11,20 +11,16 @@ const Collections: React.FC = () => { return (
-

+

Our Collections

{/* Loading Skeleton */}
{Array.from({ length: 6 }).map((_, index) => ( -
+
-
-
-
-
-
+
))}
@@ -37,13 +33,13 @@ const Collections: React.FC = () => { return (
-

+

Our Collections

-
+
-

+

Failed to Load Collections

@@ -51,7 +47,7 @@ const Collections: React.FC = () => {

@@ -65,13 +61,13 @@ const Collections: React.FC = () => { return (
-

+

Our Collections

-
+
-

+

No Collections Found

@@ -86,7 +82,7 @@ const Collections: React.FC = () => { return (

-

+

Our Collections

diff --git a/components/shopify/product-card.tsx b/components/shopify/product-card.tsx index 01eafdf..339a033 100644 --- a/components/shopify/product-card.tsx +++ b/components/shopify/product-card.tsx @@ -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 = ({ product, onAddToCart }) => { }; return ( - +
{/* Product Image */}
{firstImage ? ( @@ -113,26 +112,17 @@ const ProductCard: React.FC = ({ product, onAddToCart }) => {
{/* Product Info */} - -

- {truncate(product.title, 50)} -

- - {/* Price Section */} -
- - ${parseFloat(price.amount).toFixed(2)} - -
- - {/* View Details Button */} +
- +

+ {truncate(product.title, 50)} +

- - +

+ ${parseFloat(price.amount).toFixed(2)} +

+
+
); }; diff --git a/components/shopify/product-detail.tsx b/components/shopify/product-detail.tsx index 5f51fed..936583f 100644 --- a/components/shopify/product-detail.tsx +++ b/components/shopify/product-detail.tsx @@ -1,3 +1,3 @@ -import ProductDetail from './product-detail/index.tsx'; +import ProductDetail from './product-detail/index'; export default ProductDetail; \ No newline at end of file diff --git a/components/shopify/product-detail/index.tsx b/components/shopify/product-detail/index.tsx index d176aa1..ae2d7e0 100644 --- a/components/shopify/product-detail/index.tsx +++ b/components/shopify/product-detail/index.tsx @@ -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 = ({ handle: handleProp }) => const [selectedVariant, setSelectedVariant] = useState(null); const [selectedOptions, setSelectedOptions] = useState>({}); 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 = ({ 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 = ({ handle: handleProp }) => if (loading) { return (
-
+
{/* Image Gallery Skeleton */} -
-
-
- {Array.from({ length: 4 }).map((_, i) => ( -
- ))} -
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))}
{/* Product Info Skeleton */} @@ -146,7 +131,7 @@ const ProductDetail: React.FC = ({ handle: handleProp }) =>
- + Product Not Found @@ -174,33 +159,29 @@ const ProductDetail: React.FC = ({ handle: handleProp }) => - - - Shop - - - {product.title} -
- edge.node)} - selectedImageIndex={selectedImageIndex} - onImageSelect={setSelectedImageIndex} - /> - +
+
+ edge.node)} + /> +
+
+ +
diff --git a/components/shopify/product-detail/product-detail-gallery.tsx b/components/shopify/product-detail/product-detail-gallery.tsx index a74f55e..de248bd 100644 --- a/components/shopify/product-detail/product-detail-gallery.tsx +++ b/components/shopify/product-detail/product-detail-gallery.tsx @@ -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 = ({ - images, - selectedImageIndex = 0, - onImageSelect -}) => { - const selectedImage = selectedImageIndex; - const setSelectedImage = onImageSelect || (() => {}); +const ProductDetailGallery: React.FC = ({ images }) => { + const [zoomedIndex, setZoomedIndex] = useState(null); + + if (images.length === 0) { + return ( +
+ +
+ ); + } + + const zoomedImage = zoomedIndex !== null ? images[zoomedIndex] : null; return ( -
- {/* Main Image */} -
- {images.length > 0 ? ( - {images[selectedImage].altText - ) : ( -
- -
- )} + <> +
+ {images.map((image, index) => ( + + ))}
- {/* Image Thumbnails */} - {images.length > 1 && ( -
- {images.map((image, index) => ( - - ))} -
- )} -
+ {/* Zoom Lightbox */} + !open && setZoomedIndex(null)} + > + + Product image + {zoomedImage && ( + {zoomedImage.altText + )} + + + ); }; -export default ProductDetailGallery; \ No newline at end of file +export default ProductDetailGallery; diff --git a/components/shopify/product-detail/product-detail-info.tsx b/components/shopify/product-detail/product-detail-info.tsx index 3fd5808..360483a 100644 --- a/components/shopify/product-detail/product-detail-info.tsx +++ b/components/shopify/product-detail/product-detail-info.tsx @@ -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 = ({ return (
-

+

{product.title}

{/* Price */} -
- +
+ {formatPrice(price)} {hasDiscount && compareAtPrice && ( <> - + {formatPrice(compareAtPrice)} @@ -77,10 +77,12 @@ const ProductDetailInfo: React.FC = ({
)} - {/* 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 => (
-
+ + ); }; diff --git a/components/ui/carousel.tsx b/components/ui/carousel.tsx index 0e05a77..392f4aa 100644 --- a/components/ui/carousel.tsx +++ b/components/ui/carousel.tsx @@ -175,6 +175,7 @@ function CarouselPrevious({ className, variant = "outline", size = "icon", + children, ...props }: React.ComponentProps) { const { orientation, scrollPrev, canScrollPrev } = useCarousel() @@ -195,7 +196,7 @@ function CarouselPrevious({ onClick={scrollPrev} {...props} > - + {children ?? } Previous slide ) @@ -205,6 +206,7 @@ function CarouselNext({ className, variant = "outline", size = "icon", + children, ...props }: React.ComponentProps) { const { orientation, scrollNext, canScrollNext } = useCarousel() @@ -225,7 +227,7 @@ function CarouselNext({ onClick={scrollNext} {...props} > - + {children ?? } Next slide ) diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx deleted file mode 100644 index 8b42f21..0000000 --- a/components/ui/chart.tsx +++ /dev/null @@ -1,357 +0,0 @@ -"use client" - -import * as React from "react" -import * as RechartsPrimitive from "recharts" - -import { cn } from "@/lib/utils" - -// Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const - -export type ChartConfig = { - [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ) -} - -type ChartContextProps = { - config: ChartConfig -} - -const ChartContext = React.createContext(null) - -function useChart() { - const context = React.useContext(ChartContext) - - if (!context) { - throw new Error("useChart must be used within a ") - } - - return context -} - -function ChartContainer({ - id, - className, - children, - config, - ...props -}: React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] -}) { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` - - return ( - -
- - - {children} - -
-
- ) -} - -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme || config.color - ) - - if (!colorConfig.length) { - return null - } - - return ( -