diff --git a/app/api/hello/route.ts b/app/api/hello/route.ts
new file mode 100644
index 0000000..3c2ac37
--- /dev/null
+++ b/app/api/hello/route.ts
@@ -0,0 +1,5 @@
+import { NextResponse } from 'next/server';
+
+export async function POST(request: Request) {
+ return NextResponse.json({ message: 'Hello, World' });
+}
diff --git a/app/collections/[handle]/page.tsx b/app/collections/[handle]/page.tsx
new file mode 100644
index 0000000..7a6fc0d
--- /dev/null
+++ b/app/collections/[handle]/page.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import React from 'react';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+import CollectionDetail from '@/components/shopify/collection-detail';
+import { useCollectionProducts } from '@/hooks/use-shopify-collections';
+import {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from '@/components/ui/breadcrumb';
+
+export default function CollectionPage() {
+ const params = useParams();
+ const handle = params?.handle as string;
+ const { collection } = useCollectionProducts(handle);
+
+ // Format title from handle as fallback
+ const formattedTitle = handle
+ ? handle.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
+ : 'Collection';
+
+ const title = collection?.title || formattedTitle;
+
+ return (
+
diff --git a/app/page.tsx b/app/page.tsx
index 54f0b21..96489b0 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,18 +1,11 @@
-import Header from '@/components/Header';
-import Footer from '@/components/Footer';
-import ProductDetail from '@/components/product-detail/ProductDetail';
+import React from 'react';
+import ProductDetail from '@/components/shopify/product-detail';
import config from '@/lib/config.json';
-export default function Page() {
- const productHandle = config.data.product;
-
+const Home: React.FC = () => {
return (
-
+
);
-}
+};
+
+export default Home;
diff --git a/app/products/[handle]/page.tsx b/app/products/[handle]/page.tsx
new file mode 100644
index 0000000..e8fbf8f
--- /dev/null
+++ b/app/products/[handle]/page.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+import ProductDetail from '@/components/shopify/product-detail';
+
+export default function ProductDetailPage() {
+ return
;
+}
diff --git a/app/providers.tsx b/app/providers.tsx
new file mode 100644
index 0000000..94abfec
--- /dev/null
+++ b/app/providers.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import React from 'react';
+import { ShopifyProvider } from '@/contexts/shopify-context';
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components.json b/components.json
deleted file mode 100644
index b7b9791..0000000
--- a/components.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
- "rsc": true,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "app/globals.css",
- "baseColor": "neutral",
- "cssVariables": true,
- "prefix": ""
- },
- "iconLibrary": "lucide",
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- },
- "registries": {}
-}
diff --git a/components/CartDrawer.tsx b/components/CartDrawer.tsx
deleted file mode 100644
index ec4c512..0000000
--- a/components/CartDrawer.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-"use client";
-
-import React, { useState } from 'react';
-import { RiCloseLine, RiImageLine, RiSubtractLine, RiAddLine, RiLoader4Line } from '@remixicon/react';
-import { useShopifyCart, redirectToCheckout } from '@/hooks/use-shopify-cart';
-import {
- Sheet,
- SheetContent,
- SheetHeader,
- SheetTitle,
- SheetBody,
- SheetFooter,
- AnimatePresence,
-} from './ui/sheet';
-import { Button } from './ui/button';
-
-const CartDrawer: React.FC = () => {
- const {
- isOpen,
- items,
- itemCount,
- totalAmount,
- checkoutUrl,
- removeItem,
- updateItemQuantity,
- closeCart
- } = useShopifyCart();
- const [isCheckingOut, setIsCheckingOut] = useState(false);
-
- const handleCheckout = async () => {
- if (!checkoutUrl) return;
-
- setIsCheckingOut(true);
- try {
- redirectToCheckout(checkoutUrl);
- } catch (error) {
- console.error('Error during checkout:', error);
- alert('Failed to proceed to checkout. Please try again.');
- setIsCheckingOut(false);
- }
- };
-
- return (
-
!open && closeCart()}>
-
- {isOpen && (
-
- {/* Header */}
-
-
-
- Shopping Cart ({itemCount})
-
-
-
-
-
- {/* Cart Items */}
-
- {items.length === 0 ? (
-
-
Your cart is empty
-
Add some products to get started!
-
-
- ) : (
-
- {items.map((item) => (
-
- {/* Product Image */}
-
- {item.merchandise.image?.url ? (
-

- ) : (
-
-
-
- )}
-
-
- {/* Product Details */}
-
-
- {item.merchandise.product.title}
-
-
- {/* Variant Info */}
- {item.merchandise.selectedOptions && item.merchandise.selectedOptions.length > 0 && (
-
- {item.merchandise.selectedOptions.map((option, index) => (
-
- {option.value}
- {index < item.merchandise.selectedOptions.length - 1 ? ' / ' : ''}
-
- ))}
-
- )}
-
- {/* Quantity Controls */}
-
-
-
-
- {item.quantity}
-
-
-
-
-
-
- {/* Price */}
-
-
- ${parseFloat(item.merchandise.price.amount).toFixed(2)}
-
-
-
- {/* Remove Button */}
-
-
-
-
- ))}
-
- )}
-
-
- {/* Footer - Checkout Section */}
- {items.length > 0 && (
-
- {/* Subtotal */}
-
- Subtotal
-
- ${totalAmount.toFixed(2)}
-
-
-
-
- Shipping and taxes calculated at checkout
-
-
- {/* Action Buttons */}
-
-
-
-
- )}
-
- )}
-
-
- );
-};
-
-export default CartDrawer;
diff --git a/components/CollectionDetail.tsx b/components/CollectionDetail.tsx
deleted file mode 100644
index 9ef10cf..0000000
--- a/components/CollectionDetail.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-"use client";
-
-import React, { useState, useEffect } from 'react';
-import { getCollectionProducts } from '@/hooks/use-shopify-collections';
-import ProductCard from './ProductCard';
-import ProductModal from './ProductModal';
-import { Button } from './ui/button';
-
-interface ProductImage {
- url: string;
- altText?: string;
-}
-
-interface ProductPrice {
- amount: string;
- currencyCode: string;
-}
-
-interface ProductVariant {
- id: string;
- title: string;
- price: ProductPrice;
- availableForSale: boolean;
-}
-
-interface Product {
- id: string;
- title: string;
- description?: string;
- handle: string;
- images: {
- edges: Array<{
- node: ProductImage;
- }>;
- };
- priceRange: {
- minVariantPrice: ProductPrice;
- };
- compareAtPriceRange?: {
- minVariantPrice: ProductPrice;
- };
- variants: {
- edges: Array<{
- node: ProductVariant;
- }>;
- };
-}
-
-interface Collection {
- id: string;
- title: string;
- handle: string;
- description?: string;
- image?: ProductImage;
-}
-
-interface CollectionDetailProps {
- collectionHandle?: string;
-}
-
-const CollectionDetail: React.FC
= ({ collectionHandle }) => {
- const [collection, setCollection] = useState(null);
- const [products, setProducts] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [selectedProduct, setSelectedProduct] = useState(null);
- const [isModalOpen, setIsModalOpen] = useState(false);
-
- useEffect(() => {
- if (!collectionHandle) return;
-
- const fetchCollectionProducts = async () => {
- try {
- setLoading(true);
- setError(null);
-
- const data = await getCollectionProducts({
- collection: collectionHandle,
- limit: 20,
- sortKey: 'COLLECTION_DEFAULT',
- reverse: false
- });
-
- setCollection(data.collection);
- setProducts(data.products);
- } catch (err) {
- console.error('Error fetching collection products:', err);
- setError(err instanceof Error ? err.message : 'Failed to load collection products');
- } finally {
- setLoading(false);
- }
- };
-
- fetchCollectionProducts();
- }, [collectionHandle]);
-
- if (loading) {
- return (
-
-
-
- Collection
-
-
- {/* Loading Skeleton */}
-
- {Array.from({ length: 8 }).map((_, index) => (
-
- ))}
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
-
-
-
- Collection Not Found
-
-
- {error || "We couldn't find the collection you're looking for. It may have been removed or the link might be incorrect."}
-
-
-
-
- );
- }
-
- return (
-
- {/* Hero Banner */}
- {collection?.image && (
-
-

-
-
- {collection.title}
-
-
-
- )}
-
- {/* Products Section */}
-
-
- {products.length === 0 ? (
-
-
-
-
- No Products in Collection
-
-
- This collection doesn't have any products yet.
-
-
-
- ) : (
-
- {products.map((product) => (
-
{
- setSelectedProduct(product);
- setIsModalOpen(true);
- }}
- />
- ))}
-
- )}
-
-
-
- {/* Product Modal */}
- {selectedProduct && (
-
{
- setIsModalOpen(false);
- setSelectedProduct(null);
- }}
- />
- )}
-
- );
-};
-
-export default CollectionDetail;
\ No newline at end of file
diff --git a/components/Collections.tsx b/components/Collections.tsx
deleted file mode 100644
index 157216a..0000000
--- a/components/Collections.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-"use client";
-
-import React, { useState, useEffect } from 'react';
-import { getCollections } from '@/hooks/use-shopify-collections';
-import CollectionCard from './CollectionCard';
-import { Button } from './ui/button';
-
-interface CollectionImage {
- url: string;
- altText?: string;
-}
-
-interface Collection {
- id: string;
- title: string;
- handle: string;
- description?: string;
- image?: CollectionImage;
-}
-
-const Collections: React.FC = () => {
- const [collections, setCollections] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- const fetchCollections = async () => {
- try {
- setLoading(true);
- setError(null);
- const collectionData = await getCollections(20);
- setCollections(collectionData);
- } catch (err) {
- console.error('Error fetching collections:', err);
- setError(err instanceof Error ? err.message : 'Failed to load collections');
- } finally {
- setLoading(false);
- }
- };
-
- fetchCollections();
- }, []);
-
- if (loading) {
- return (
-
-
-
- Our Collections
-
-
- {/* Loading Skeleton */}
-
- {Array.from({ length: 6 }).map((_, index) => (
-
- ))}
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
- Our Collections
-
-
-
-
-
- Failed to Load Collections
-
-
- {error}
-
-
-
-
-
- );
- }
-
- if (collections.length === 0) {
- return (
-
-
-
- Our Collections
-
-
-
-
-
- No Collections Found
-
-
- Check back later or configure your Shopify store connection.
-
-
-
-
- );
- }
-
- return (
-
-
-
- Our Collections
-
-
- {/* Collections Grid */}
-
- {collections.map((collection) => (
-
- ))}
-
-
-
- );
-};
-
-export default Collections;
\ No newline at end of file
diff --git a/components/Footer.tsx b/components/Footer.tsx
deleted file mode 100644
index 4b8c70b..0000000
--- a/components/Footer.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-
-const Footer: React.FC = () => {
- return (
-
- );
-};
-
-export default Footer;
diff --git a/components/Header.tsx b/components/Header.tsx
deleted file mode 100644
index fe4a795..0000000
--- a/components/Header.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-"use client";
-
-import React from 'react';
-import Link from 'next/link';
-import { RiShoppingBagLine } from '@remixicon/react';
-import { useShopifyCart } from '@/hooks/use-shopify-cart';
-import config from '../lib/config.json';
-
-const CartIcon: React.FC = () => {
- const { itemCount, toggleCart } = useShopifyCart();
-
- return (
-
- );
-};
-
-const Header: React.FC = () => {
- return (
-
- );
-};
-
-export default Header;
diff --git a/components/ProductCard.tsx b/components/ProductCard.tsx
deleted file mode 100644
index eb7db0a..0000000
--- a/components/ProductCard.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-"use client";
-
-import React from 'react';
-import { useShopifyCart } from '@/hooks/use-shopify-cart';
-import { truncate } from '../lib/utils';
-import { Card, CardContent } from './ui/card';
-import { Button } from './ui/button';
-
-interface ProductImage {
- url: string;
- altText?: string;
-}
-
-interface ProductPrice {
- amount: string;
- currencyCode: string;
-}
-
-interface ProductVariant {
- id: string;
- title: string;
- price: ProductPrice;
- availableForSale: boolean;
-}
-
-interface Product {
- id: string;
- title: string;
- description?: string;
- handle: string;
- images: {
- edges: Array<{
- node: ProductImage;
- }>;
- };
- priceRange: {
- minVariantPrice: ProductPrice;
- };
- compareAtPriceRange?: {
- minVariantPrice: ProductPrice;
- };
- variants: {
- edges: Array<{
- node: ProductVariant;
- }>;
- };
-}
-
-interface ProductCardProps {
- product: Product;
- onAddToCart?: (product: Product) => void;
- onClick?: (product: Product) => void;
-}
-
-const ProductCard: React.FC = ({ product, onAddToCart, onClick }) => {
- const { addItem, openCart } = useShopifyCart();
-
- const firstImage = product.images.edges[0]?.node;
- const price = product.priceRange.minVariantPrice;
- const compareAtPrice = product.compareAtPriceRange?.minVariantPrice;
- const hasDiscount = compareAtPrice && parseFloat(compareAtPrice.amount) > parseFloat(price.amount);
- const firstVariant = product.variants.edges[0]?.node;
- const isAvailable = firstVariant?.availableForSale || false;
-
- const handleAddToCart = async (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- if (!firstVariant || !isAvailable) return;
-
- try {
- await addItem(firstVariant.id, 1);
- openCart();
-
- if (onAddToCart) {
- onAddToCart(product);
- }
- } catch (error) {
- console.error('Failed to add item to cart:', error);
- }
- };
-
- return (
-
- {/* Product Image */}
- {
- if (onClick) {
- onClick(product);
- }
- }}
- >
- {firstImage ? (
-

- ) : (
-
-
-
- )}
-
- {/* Discount Badge */}
- {hasDiscount && compareAtPrice && (
-
- {Math.round(((parseFloat(compareAtPrice.amount) - parseFloat(price.amount)) / parseFloat(compareAtPrice.amount)) * 100)}% OFF
-
- )}
-
-
- {/* Product Info */}
-
-
- {truncate(product.title, 60)}
-
-
- {/* Price Section */}
-
-
-
- ${parseFloat(price.amount).toFixed(2)}
-
-
-
-
- {/* View Details Button */}
-
-
-
- );
-};
-
-export default ProductCard;
diff --git a/components/ProductModal.tsx b/components/ProductModal.tsx
deleted file mode 100644
index b3e9e0d..0000000
--- a/components/ProductModal.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-"use client";
-
-import React, { useEffect } from 'react';
-import ProductDetail from './product-detail/ProductDetail';
-import { Button } from './ui/button';
-
-interface ProductImage {
- url: string;
- altText?: string;
-}
-
-interface ProductPrice {
- amount: string;
- currencyCode: string;
-}
-
-interface ProductVariant {
- id: string;
- title: string;
- price: ProductPrice;
- availableForSale: boolean;
-}
-
-interface Product {
- id: string;
- title: string;
- description?: string;
- handle: string;
- images: {
- edges: Array<{
- node: ProductImage;
- }>;
- };
- priceRange: {
- minVariantPrice: ProductPrice;
- };
- compareAtPriceRange?: {
- minVariantPrice: ProductPrice;
- };
- variants: {
- edges: Array<{
- node: ProductVariant;
- }>;
- };
-}
-
-interface ProductModalProps {
- product: Product;
- isOpen: boolean;
- onClose: () => void;
-}
-
-const ProductModal: React.FC = ({ product, isOpen, onClose }) => {
-
- // Close modal on ESC key press
- useEffect(() => {
- const handleEscape = (e: KeyboardEvent) => {
- if (e.key === 'Escape') {
- onClose();
- }
- };
-
- if (isOpen) {
- document.addEventListener('keydown', handleEscape);
- document.body.style.overflow = 'hidden';
- }
-
- return () => {
- document.removeEventListener('keydown', handleEscape);
- document.body.style.overflow = 'unset';
- };
- }, [isOpen, onClose]);
-
- const handleBackdropClick = (e: React.MouseEvent) => {
- if (e.target === e.currentTarget) {
- onClose();
- }
- };
-
- if (!isOpen) return null;
-
- return (
-
-
- {/* Close Button */}
-
-
- {/* Product Detail Component */}
-
-
-
- );
-};
-
-export default ProductModal;
diff --git a/components/Products.tsx b/components/Products.tsx
deleted file mode 100644
index e7e0549..0000000
--- a/components/Products.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-"use client";
-
-import React from 'react';
-import ProductCard from './ProductCard';
-import { useProducts } from '@/hooks/use-shopify-products';
-import { Button } from './ui/button';
-
-interface ProductImage {
- url: string;
- altText?: string;
-}
-
-interface ProductPrice {
- amount: string;
- currencyCode: string;
-}
-
-interface ProductVariant {
- id: string;
- title: string;
- price: ProductPrice;
- availableForSale: boolean;
-}
-
-interface Product {
- id: string;
- title: string;
- description?: string;
- handle: string;
- images: {
- edges: Array<{
- node: ProductImage;
- }>;
- };
- priceRange: {
- minVariantPrice: ProductPrice;
- };
- compareAtPriceRange?: {
- minVariantPrice: ProductPrice;
- };
- variants: {
- edges: Array<{
- node: ProductVariant;
- }>;
- };
-}
-
-interface ProductsProps {
- title?: string;
- limit?: number;
- showLoadMore?: boolean;
- onViewDetails?: (product: Product) => void;
-}
-
-const Products: React.FC = ({
- title = "Our Products",
- limit = 12,
- showLoadMore = true,
- onViewDetails
-}) => {
- const { products, loading, error, refetch } = useProducts({
- first: limit,
- sortKey: 'CREATED_AT',
- reverse: true
- });
-
- const handleAddToCart = async (product: Product) => {
- console.log('Adding to cart:', product);
- };
-
- if (loading) {
- return (
-
-
-
- {title}
-
-
- {/* Loading Skeleton */}
-
- {Array.from({ length: 8 }).map((_, index) => (
-
- ))}
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
- {title}
-
-
-
-
-
- Failed to Load Products
-
-
- {error}
-
-
-
-
-
- );
- }
-
- if (products.length === 0) {
- return (
-
-
-
- {title}
-
-
-
-
-
- No Products Found
-
-
- Check back later or configure your Shopify store connection.
-
-
-
-
- );
- }
-
- return (
-
-
-
- {title}
-
-
- {/* Products Grid */}
-
- {products.map((product) => (
-
- ))}
-
-
- {/* Load More Button */}
- {showLoadMore && products.length >= limit && (
-
-
-
- )}
-
-
- );
-};
-
-export default Products;
diff --git a/components/Theme.tsx b/components/Theme.tsx
deleted file mode 100644
index 1e5c73f..0000000
--- a/components/Theme.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import React from 'react';
-import config from '../lib/config.json';
-
-const Theme: React.FC = () => {
- const headerFont = config.brand.fonts.header;
- const bodyFont = config.brand.fonts.body;
- const primaryColor = config.brand.colors.primary;
- const secondaryColor = config.brand.colors.secondary;
-
- return (
- <>
- {/* Font Imports */}
-
-
-
- {/* Tailwind Browser Script */}
-
-
- {/* Theme Styles */}
-
- >
- );
-};
-
-export default Theme;
diff --git a/components/product-detail/ProductDetail.tsx b/components/product-detail/ProductDetail.tsx
deleted file mode 100644
index f04958b..0000000
--- a/components/product-detail/ProductDetail.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-"use client";
-
-import React, { useState, useEffect } from 'react';
-import { getProduct } from '@/hooks/use-shopify-products';
-import { useShopifyCart } from '@/hooks/use-shopify-cart';
-import ProductDetailGallery from './ProductDetailGallery';
-import ProductDetailInfo from './ProductDetailInfo';
-import { Button } from '../ui/button';
-
-interface ProductImage {
- url: string;
- altText?: string;
-}
-
-interface ProductPrice {
- amount: string;
- currencyCode: string;
-}
-
-export interface ProductVariant {
- id: string;
- title: string;
- price: ProductPrice;
- availableForSale: boolean;
- selectedOptions: Array<{
- name: string;
- value: string;
- }>;
- image?: {
- url: string;
- altText?: string;
- };
-}
-
-interface ProductOption {
- id: string;
- name: string;
- values: string[];
-}
-
-export interface Product {
- id: string;
- title: string;
- description?: string;
- descriptionHtml?: string;
- handle: string;
- images: {
- edges: Array<{
- node: ProductImage;
- }>;
- };
- priceRange: {
- minVariantPrice: ProductPrice;
- };
- compareAtPriceRange?: {
- minVariantPrice: ProductPrice;
- };
- variants: {
- edges: Array<{
- node: ProductVariant;
- }>;
- };
- options: ProductOption[];
-}
-
-type ProductDetailProps = {
- handle: string;
- onAddToCart?: () => void;
-}
-
-const ProductDetail: React.FC = ({ handle, onAddToCart: onAddToCartCallback }) => {
- const { addItem, openCart } = useShopifyCart();
-
- const [product, setProduct] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [selectedVariant, setSelectedVariant] = useState(null);
- const [selectedOptions, setSelectedOptions] = useState>({});
- const [quantity, setQuantity] = useState(1);
- const [selectedImageIndex, setSelectedImageIndex] = useState(0);
- const [isAddingToCart, setIsAddingToCart] = useState(false);
-
- useEffect(() => {
- if (!handle) return;
-
- const fetchProduct = async () => {
- try {
- setLoading(true);
- setError(null);
- const productData = await getProduct(handle);
-
- if (!productData) {
- setError('Product not found');
- return;
- }
-
- setProduct(productData);
-
- // Set default variant
- const firstVariant = productData.variants.edges[0]?.node;
- if (firstVariant) {
- setSelectedVariant(firstVariant);
-
- // Initialize selected options
- const initialOptions: Record = {};
- firstVariant.selectedOptions.forEach(option => {
- initialOptions[option.name] = option.value;
- });
- setSelectedOptions(initialOptions);
- }
- } catch (err) {
- console.error('Error fetching product:', err);
- setError(err instanceof Error ? err.message : 'Failed to load product');
- } finally {
- setLoading(false);
- }
- };
-
- fetchProduct();
- }, [handle]);
-
- 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 imageIndex = product.images.edges.findIndex(
- ({ node }) => node.url === matchingVariant.node.image?.url
- );
- if (imageIndex !== -1) {
- setSelectedImageIndex(imageIndex);
- }
- }
- }
- };
-
- const handleAddToCart = async () => {
- if (!selectedVariant || !product) return;
-
- setIsAddingToCart(true);
- try {
- await addItem(selectedVariant.id, quantity);
- openCart();
-
- if (onAddToCartCallback) {
- onAddToCartCallback();
- }
- } catch (error) {
- console.error('Failed to add item to cart:', error);
- } finally {
- setIsAddingToCart(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 (
-
-
-
-
edge.node)}
- selectedImageIndex={selectedImageIndex}
- onImageChange={setSelectedImageIndex}
- />
-
-
-
-
- );
-};
-
-export default ProductDetail;
diff --git a/components/product-detail/ProductDetailGallery.tsx b/components/product-detail/ProductDetailGallery.tsx
deleted file mode 100644
index 578aa34..0000000
--- a/components/product-detail/ProductDetailGallery.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-
-interface ProductImage {
- url: string;
- altText?: string;
-}
-
-interface ProductDetailGalleryProps {
- images: ProductImage[];
- selectedImageIndex: number;
- onImageChange: (index: number) => void;
-}
-
-const ProductDetailGallery: React.FC = ({ images, selectedImageIndex, onImageChange }) => {
-
- return (
-
- {/* Main Image */}
-
- {images.length > 0 ? (
-
![{images[selectedImageIndex].altText]({images[selectedImageIndex].url})
- ) : (
-
-
-
- )}
-
-
- {/* Image Thumbnails */}
- {images.length > 1 && (
-
- {images.map((image, index) => (
-
- ))}
-
- )}
-
- );
-};
-
-export default ProductDetailGallery;
\ No newline at end of file
diff --git a/components/product-detail/ProductRecommendations.tsx b/components/product-detail/ProductRecommendations.tsx
deleted file mode 100644
index da6845b..0000000
--- a/components/product-detail/ProductRecommendations.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-"use client";
-
-import React, { useState, useEffect } from 'react';
-import { getProductRecommendations } from '@/hooks/use-shopify-products';
-import ProductCard from '../ProductCard';
-
-interface ProductRecommendationsProps {
- productId: string;
-}
-
-const ProductRecommendations: React.FC = ({ productId }) => {
- const [recommendedProducts, setRecommendedProducts] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- const fetchRecommendations = async () => {
- if (!productId) return;
-
- try {
- setLoading(true);
- setError(null);
-
- const recommendations = await getProductRecommendations(productId);
- setRecommendedProducts(recommendations);
- } catch (err) {
- console.error('Error fetching product recommendations:', err);
- setError(err instanceof Error ? err.message : 'Failed to load recommendations');
- } finally {
- setLoading(false);
- }
- };
-
- fetchRecommendations();
- }, [productId]);
-
- // Don't show section if we're not loading and have no recommendations
- if (!loading && (!recommendedProducts || recommendedProducts.length === 0)) {
- return null;
- }
-
- return (
-
-
-
- You Might Also Like
-
-
- {loading ? (
-
- {Array.from({ length: 4 }).map((_, index) => (
-
- ))}
-
- ) : error ? (
-
-
Recommendations could not be loaded
-
- ) : (
-
- {recommendedProducts.slice(0, 4).map((recommendedProduct) => (
-
- ))}
-
- )}
-
-
- );
-};
-
-export default ProductRecommendations;
\ No newline at end of file
diff --git a/components/shopify/cart-drawer.tsx b/components/shopify/cart-drawer.tsx
new file mode 100644
index 0000000..a8b1550
--- /dev/null
+++ b/components/shopify/cart-drawer.tsx
@@ -0,0 +1,216 @@
+'use client';
+
+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 { Loader } from '@/components/ui/loader';
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+} from '@/components/ui/sheet';
+import {
+ RiCloseLine,
+ RiImageLine,
+ RiSubtractLine,
+ RiAddLine,
+} from '@remixicon/react';
+
+const CartDrawer: React.FC = () => {
+ const { isOpen, closeCart, items, itemCount, totalAmount, checkoutUrl, loading, removeItem, updateItemQuantity } = useShopifyCart();
+
+ const handleCheckout = () => {
+ if (checkoutUrl) {
+ redirectToCheckout(checkoutUrl);
+ }
+ };
+
+ const getItemImage = (item: typeof items[0]) => {
+ return item.merchandise.image?.url;
+ };
+
+ const getSelectedOptions = (item: typeof items[0]) => {
+ return item.merchandise.selectedOptions ?? [];
+ };
+
+ return (
+ !open && closeCart()}>
+
+ {/* Header */}
+
+
+
+ Shopping Cart ({itemCount})
+
+
+
+
+
+ {/* Cart Items */}
+
+ {loading && items.length === 0 ? (
+
+
+
+ ) : items.length === 0 ? (
+
+
+ Your cart is empty
+ Add some products to get started!
+
+
+
+ ) : (
+
+ {items.map((item) => {
+ const image = getItemImage(item);
+ const selectedOptions = getSelectedOptions(item);
+
+ return (
+
+ {/* Product Image */}
+
+ {image ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ {/* Product Details */}
+
+
+ {item.merchandise.product.title}
+
+
+ {/* Variant Info */}
+ {selectedOptions.length > 0 && (
+
+ {selectedOptions.map((option, index) => (
+
+ {option.value}
+ {index < selectedOptions.length - 1 ? ' / ' : ''}
+
+ ))}
+
+ )}
+
+ {/* Quantity Controls */}
+
+
+
+
+ {item.quantity}
+
+
+
+
+
+
+ {/* Price */}
+
+
+ ${parseFloat(item.merchandise.price.amount).toFixed(2)}
+
+
+
+ {/* Remove Button */}
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Footer - Checkout Section */}
+ {items.length > 0 && (
+
+ {/* Subtotal */}
+
+ Subtotal
+
+ ${totalAmount.toFixed(2)}
+
+
+
+
+ Shipping and taxes calculated at checkout
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default CartDrawer;
diff --git a/components/CollectionCard.tsx b/components/shopify/collection-card.tsx
similarity index 56%
rename from components/CollectionCard.tsx
rename to components/shopify/collection-card.tsx
index 591a9c7..c9b8a9f 100644
--- a/components/CollectionCard.tsx
+++ b/components/shopify/collection-card.tsx
@@ -20,10 +20,7 @@ interface CollectionCardProps {
const CollectionCard: React.FC = ({ collection }) => {
return (
-
+
{/* Collection Image */}
{collection.image ? (
@@ -40,25 +37,13 @@ const CollectionCard: React.FC = ({ collection }) => {
{/* Collection Info */}
-
-
+
+
{collection.title}
-
- {collection.description && (
-
- {collection.description.substring(0, 100)}
- {collection.description.length > 100 ? '...' : ''}
-
- )}
-
-
- View Collection
-
-
);
};
-export default CollectionCard;
\ No newline at end of file
+export default CollectionCard;
diff --git a/components/shopify/collection-detail.tsx b/components/shopify/collection-detail.tsx
new file mode 100644
index 0000000..56b57f3
--- /dev/null
+++ b/components/shopify/collection-detail.tsx
@@ -0,0 +1,108 @@
+'use client';
+
+import React from 'react';
+import { useParams } from 'next/navigation';
+import { useCollectionProducts } from '@/hooks/use-shopify-collections';
+import ProductCard from './product-card';
+
+interface CollectionDetailProps {
+ handle?: string;
+}
+
+const CollectionDetail: React.FC = ({ handle: handleProp }) => {
+ const params = useParams();
+ const handle = handleProp || (params?.handle as string);
+
+ const { collection, loading, error, refetch } = useCollectionProducts(handle);
+
+ // Format title from handle
+ const formattedTitle = handle
+ ? handle.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
+ : 'Collection';
+
+ if (loading) {
+ return (
+
+
+
+ {formattedTitle}
+
+
+ {/* Loading Skeleton */}
+
+ {Array.from({ length: 6 }).map((_, index) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ {formattedTitle}
+
+
+
+
+
+ Failed to Load Collection
+
+
+ {error}
+
+
+
+
+
+ );
+ }
+
+ const products = collection?.products || [];
+ const title = collection?.title || formattedTitle;
+
+ return (
+
+
+
+ {title}
+
+
+ {products.length === 0 ? (
+
+
+
+
+ No Products in Collection
+
+
+ This collection doesn't have any products yet.
+
+
+
+ ) : (
+
+ {products.map((product) => (
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default CollectionDetail;
diff --git a/components/shopify/collections.tsx b/components/shopify/collections.tsx
new file mode 100644
index 0000000..7d29577
--- /dev/null
+++ b/components/shopify/collections.tsx
@@ -0,0 +1,100 @@
+'use client';
+
+import React from 'react';
+import { useCollections } from '@/hooks/use-shopify-collections';
+import CollectionCard from './collection-card';
+
+const Collections: React.FC = () => {
+ const { collections, loading, error, refetch } = useCollections(20);
+
+ if (loading) {
+ return (
+
+
+
+ Our Collections
+
+
+ {/* Loading Skeleton */}
+
+ {Array.from({ length: 6 }).map((_, index) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ Our Collections
+
+
+
+
+
+ Failed to Load Collections
+
+
+ {error}
+
+
+
+
+
+ );
+ }
+
+ if (collections.length === 0) {
+ return (
+
+
+
+ Our Collections
+
+
+
+
+
+ No Collections Found
+
+
+ Check back later or configure your Shopify store connection.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Our Collections
+
+
+ {/* Collections Grid */}
+
+ {collections.map((collection) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default Collections;
diff --git a/components/shopify/product-card.tsx b/components/shopify/product-card.tsx
new file mode 100644
index 0000000..339a033
--- /dev/null
+++ b/components/shopify/product-card.tsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import Link from 'next/link';
+import { useShopifyCart, addCartLines } from '@/hooks/use-shopify-cart';
+import { truncate } from '@/lib/utils';
+import { Badge } from '@/components/ui/badge';
+
+interface ProductImage {
+ url: string;
+ altText?: string;
+}
+
+interface ProductPrice {
+ amount: string;
+ currencyCode: string;
+}
+
+interface ProductVariant {
+ id: string;
+ title: string;
+ price: ProductPrice;
+ availableForSale: boolean;
+}
+
+interface Product {
+ id: string;
+ title: string;
+ description?: string;
+ productType?: string;
+ handle: string;
+ images: {
+ edges: Array<{
+ node: ProductImage;
+ }>;
+ };
+ priceRange: {
+ minVariantPrice: ProductPrice;
+ };
+ compareAtPriceRange?: {
+ minVariantPrice: ProductPrice;
+ };
+ variants: {
+ edges: Array<{
+ node: ProductVariant;
+ }>;
+ };
+}
+
+interface ProductCardProps {
+ product: Product;
+ onAddToCart?: (product: Product) => void;
+}
+
+const ProductCard: React.FC = ({ product, onAddToCart }) => {
+ const { cartId, openCart } = useShopifyCart();
+
+ const firstImage = product.images.edges[0]?.node;
+ const price = product.priceRange.minVariantPrice;
+ const compareAtPrice = product.compareAtPriceRange?.minVariantPrice;
+ const hasDiscount = compareAtPrice && parseFloat(compareAtPrice.amount) > parseFloat(price.amount);
+ const firstVariant = product.variants.edges[0]?.node;
+ const isAvailable = firstVariant?.availableForSale || false;
+
+ const formatPrice = (price: ProductPrice) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: price.currencyCode,
+ }).format(parseFloat(price.amount));
+ };
+
+ const handleAddToCart = async (e: React.MouseEvent) => {
+ e.preventDefault(); // Prevent navigation when clicking add to cart
+ e.stopPropagation();
+
+ if (!firstVariant || !isAvailable || !cartId) return;
+
+ try {
+ await addCartLines(cartId, [{ merchandiseId: firstVariant.id, quantity: 1 }]);
+ openCart();
+
+ if (onAddToCart) {
+ onAddToCart(product);
+ }
+ } catch (err) {
+ console.error('Failed to add item to cart:', err);
+ }
+ };
+
+ return (
+
+ {/* Product Image */}
+
+ {firstImage ? (
+
+

+
+ ) : (
+
+
+
+ )}
+
+ {/* Discount Badge */}
+ {hasDiscount && compareAtPrice && (
+
+ {Math.round(((parseFloat(compareAtPrice.amount) - parseFloat(price.amount)) / parseFloat(compareAtPrice.amount)) * 100)}% OFF
+
+ )}
+
+
+ {/* Product Info */}
+
+
+
+ {truncate(product.title, 50)}
+
+
+
+ ${parseFloat(price.amount).toFixed(2)}
+
+
+
+ );
+};
+
+export default ProductCard;
\ No newline at end of file
diff --git a/components/shopify/product-detail.tsx b/components/shopify/product-detail.tsx
new file mode 100644
index 0000000..936583f
--- /dev/null
+++ b/components/shopify/product-detail.tsx
@@ -0,0 +1,3 @@
+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
new file mode 100644
index 0000000..ae2d7e0
--- /dev/null
+++ b/components/shopify/product-detail/index.tsx
@@ -0,0 +1,193 @@
+'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 './product-detail-gallery';
+import ProductDetailInfo from './product-detail-info';
+import ProductRecommendations from '../product-recommendations';
+import { Button } from '@/components/ui/button';
+import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
+import { RiErrorWarningLine } from '@remixicon/react';
+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;
+ };
+}
+
+export type { Product, ProductVariant };
+
+interface ProductDetailProps {
+ handle?: string;
+}
+
+const ProductDetail: React.FC = ({ handle: handleProp }) => {
+ const params = useParams();
+ const handle = handleProp || (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 [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);
+ }
+ };
+
+ 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}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ProductDetail;
diff --git a/components/shopify/product-detail/product-detail-gallery.tsx b/components/shopify/product-detail/product-detail-gallery.tsx
new file mode 100644
index 0000000..de248bd
--- /dev/null
+++ b/components/shopify/product-detail/product-detail-gallery.tsx
@@ -0,0 +1,73 @@
+'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;
+ altText?: string;
+}
+
+interface ProductDetailGalleryProps {
+ images: ProductImage[];
+}
+
+const ProductDetailGallery: React.FC = ({ images }) => {
+ const [zoomedIndex, setZoomedIndex] = useState(null);
+
+ if (images.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ const zoomedImage = zoomedIndex !== null ? images[zoomedIndex] : null;
+
+ return (
+ <>
+
+ {images.map((image, index) => (
+
+ ))}
+
+
+ {/* Zoom Lightbox */}
+
+ >
+ );
+};
+
+export default ProductDetailGallery;
diff --git a/components/product-detail/ProductDetailInfo.tsx b/components/shopify/product-detail/product-detail-info.tsx
similarity index 65%
rename from components/product-detail/ProductDetailInfo.tsx
rename to components/shopify/product-detail/product-detail-info.tsx
index 25eaefc..360483a 100644
--- a/components/product-detail/ProductDetailInfo.tsx
+++ b/components/shopify/product-detail/product-detail-info.tsx
@@ -1,13 +1,14 @@
import React from 'react';
-import { Product, ProductVariant } from './ProductDetail.tsx';
-import { Button } from '../ui/button';
+import { Product, ProductVariant } from './index';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Loader } from '@/components/ui/loader';
import {
RiSubtractLine,
RiAddLine,
RiTruckLine,
RiArrowGoBackLine,
RiSecurePaymentLine,
- RiLoader4Line,
} from '@remixicon/react';
interface ProductDetailInfoProps {
@@ -18,7 +19,7 @@ interface ProductDetailInfoProps {
setQuantity: (quantity: number) => void;
handleAddToCart: () => void;
onOptionChange: (optionName: string, value: string) => void;
- isAddingToCart?: boolean;
+ loading?: boolean;
}
const ProductDetailInfo: React.FC = ({
@@ -29,7 +30,7 @@ const ProductDetailInfo: React.FC = ({
setQuantity,
handleAddToCart,
onOptionChange,
- isAddingToCart = false
+ loading = false,
}) => {
const formatPrice = (price: { amount: string; currencyCode: string }) => {
return new Intl.NumberFormat('en-US', {
@@ -44,23 +45,23 @@ const ProductDetailInfo: React.FC = ({
return (
-
+
{product.title}
{/* Price */}
-
-
+
+
{formatPrice(price)}
{hasDiscount && compareAtPrice && (
<>
-
+
{formatPrice(compareAtPrice)}
-
+
{Math.round(((parseFloat(compareAtPrice.amount) - parseFloat(price.amount)) / parseFloat(compareAtPrice.amount)) * 100)}% OFF
-
+
>
)}
@@ -76,34 +77,34 @@ const ProductDetailInfo: React.FC = ({
)}
- {/* Product Options */}
+ {/* Product Options (skip Shopify's placeholder "Title: Default Title" option) */}
{product.options
- .filter(option => option.name !== 'Title')
+ .filter(option => !(option.name === 'Title' && option.values.length === 1 && option.values[0] === 'Default Title'))
.map(option => (
-
-
-
- {option.values.map(value => (
-
- ))}
-
+
+
+
+ {option.values.map(value => (
+
+ ))}
- ))}
+
+ ))}
{/* Quantity Selector */}
-