mirror of
https://gitlab.com/davidels1986/ore-cart-checkout.git
synced 2026-11-02 16:57:29 +00:00
Changes
This commit is contained in:
parent
ea7828095a
commit
12329094cc
5 changed files with 240 additions and 147 deletions
|
|
@ -10,6 +10,7 @@ import { Footer } from "@/components/Footer";
|
|||
import { CartDrawer } from "@/components/CartDrawer";
|
||||
import { AuthModal } from "@/components/AuthModal";
|
||||
import Index from "./pages/Index";
|
||||
import Products from "./pages/Products";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
|
@ -27,6 +28,7 @@ const App = () => (
|
|||
<main className="flex-1">
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/products" element={<Products />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { useState } from 'react';
|
||||
import {
|
||||
Drill, Wrench, Cog, HardHat, Zap, Droplet,
|
||||
Flame, Ruler, Package, Mountain, ChevronRight
|
||||
Flame, Ruler, Package, Mountain, ChevronRight, ChevronDown,
|
||||
Wind, Disc, Sparkles
|
||||
} from 'lucide-react';
|
||||
import { categories } from '@/data/products';
|
||||
|
||||
|
|
@ -20,12 +22,80 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|||
Ruler: Ruler,
|
||||
Package: Package,
|
||||
Mountain: Mountain,
|
||||
Wind: Wind,
|
||||
Disc: Disc,
|
||||
Sparkles: Sparkles,
|
||||
};
|
||||
|
||||
export function CategorySidebar({ selectedCategory, onSelectCategory }: CategorySidebarProps) {
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
|
||||
const selectedCategoryName = selectedCategory
|
||||
? categories.find(c => c.id === selectedCategory)?.name
|
||||
: 'All Products';
|
||||
|
||||
return (
|
||||
<aside className="w-full lg:w-72 shrink-0">
|
||||
<div className="bg-card rounded-xl border shadow-industrial overflow-hidden">
|
||||
{/* Mobile: Dropdown Style */}
|
||||
<div className="lg:hidden">
|
||||
<button
|
||||
onClick={() => setIsMobileOpen(!isMobileOpen)}
|
||||
className="w-full flex items-center justify-between gap-3 bg-card border rounded-xl p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<Package className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-xs text-muted-foreground">Category</p>
|
||||
<p className="font-semibold text-foreground">{selectedCategoryName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className={`h-5 w-5 text-muted-foreground transition-transform ${isMobileOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{/* Mobile Dropdown Menu */}
|
||||
{isMobileOpen && (
|
||||
<div className="mt-2 bg-card border rounded-xl shadow-lg overflow-hidden animate-fade-in">
|
||||
<div className="grid grid-cols-2 gap-1 p-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onSelectCategory(null);
|
||||
setIsMobileOpen(false);
|
||||
}}
|
||||
className={`flex flex-col items-center gap-2 p-3 rounded-lg transition-colors ${
|
||||
!selectedCategory ? 'bg-primary text-primary-foreground' : 'hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
<Package className="h-6 w-6" />
|
||||
<span className="text-xs font-medium text-center">All Products</span>
|
||||
</button>
|
||||
{categories.map((category) => {
|
||||
const Icon = iconMap[category.icon] || Package;
|
||||
const isSelected = selectedCategory === category.id;
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => {
|
||||
onSelectCategory(category.id);
|
||||
setIsMobileOpen(false);
|
||||
}}
|
||||
className={`flex flex-col items-center gap-2 p-3 rounded-lg transition-colors ${
|
||||
isSelected ? 'bg-primary text-primary-foreground' : 'hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-6 w-6" />
|
||||
<span className="text-xs font-medium text-center leading-tight">{category.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Full Sidebar */}
|
||||
<div className="hidden lg:block bg-card rounded-xl border shadow-industrial overflow-hidden">
|
||||
<div className="gradient-industrial text-primary-foreground p-4">
|
||||
<h2 className="font-display font-bold text-lg flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
import { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { useState, useMemo, useRef, FormEvent } from 'react';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Product } from '@/types';
|
||||
import { products } from '@/data/products';
|
||||
import { useCart } from '@/context/CartContext';
|
||||
|
||||
interface SearchBarProps {
|
||||
onSearch?: (query: string, results: Product[]) => void;
|
||||
large?: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function SearchBar({ onSearch, large = false }: SearchBarProps) {
|
||||
export function SearchBar({ large = false, compact = false }: SearchBarProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [results, setResults] = useState<Product[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { addItem } = useCart();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fuse = useMemo(() => new Fuse(products, {
|
||||
keys: [
|
||||
|
|
@ -31,130 +27,56 @@ export function SearchBar({ onSearch, large = false }: SearchBarProps) {
|
|||
includeScore: true,
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.length >= 2) {
|
||||
const searchResults = fuse.search(query).map(r => r.item);
|
||||
setResults(searchResults);
|
||||
setIsOpen(true);
|
||||
onSearch?.(query, searchResults);
|
||||
} else {
|
||||
setResults([]);
|
||||
setIsOpen(false);
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (query.trim().length >= 2) {
|
||||
navigate(`/products?search=${encodeURIComponent(query.trim())}`);
|
||||
}
|
||||
}, [query, fuse, onSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setQuery('');
|
||||
setResults([]);
|
||||
setIsOpen(false);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleAddToCart = (product: Product, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
addItem(product);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`relative w-full ${large ? 'max-w-3xl' : 'max-w-xl'}`}>
|
||||
<form onSubmit={handleSubmit} className={`relative w-full ${large ? 'max-w-3xl' : compact ? 'max-w-md' : 'max-w-xl'}`}>
|
||||
<div className={`relative search-glow rounded-xl bg-card ${large ? 'p-1' : ''}`}>
|
||||
<div className="relative flex items-center">
|
||||
<Search className={`absolute left-4 text-muted-foreground ${large ? 'h-6 w-6' : 'h-5 w-5'}`} />
|
||||
<Search className={`absolute left-3 md:left-4 text-muted-foreground ${large ? 'h-5 w-5 md:h-6 md:w-6' : 'h-4 w-4 md:h-5 md:w-5'}`} />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Search products, parts, equipment..."
|
||||
placeholder={compact ? "Search..." : "Search products, parts, equipment..."}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 2 && setIsOpen(true)}
|
||||
className={`pl-12 pr-20 border-0 focus-visible:ring-0 bg-transparent ${
|
||||
className={`border-0 focus-visible:ring-0 bg-transparent ${
|
||||
large
|
||||
? 'h-16 text-lg placeholder:text-base'
|
||||
: 'h-12 text-base'
|
||||
? 'pl-10 md:pl-12 pr-16 md:pr-20 h-12 md:h-14 text-base md:text-lg placeholder:text-sm md:placeholder:text-base'
|
||||
: compact
|
||||
? 'pl-9 pr-16 h-10 text-sm'
|
||||
: 'pl-10 md:pl-12 pr-16 md:pr-20 h-10 md:h-12 text-sm md:text-base'
|
||||
}`}
|
||||
/>
|
||||
{query && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-16 h-8 w-8"
|
||||
className="absolute right-12 md:right-16 h-7 w-7 md:h-8 md:w-8"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<X className="h-3 w-3 md:h-4 md:w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={`absolute right-2 btn-industrial ${large ? 'h-12 px-6' : 'h-9 px-4'}`}
|
||||
type="submit"
|
||||
className={`absolute right-1.5 md:right-2 btn-industrial ${large ? 'h-9 md:h-11 px-3 md:px-5 text-xs md:text-sm' : 'h-7 md:h-9 px-2 md:px-4 text-xs md:text-sm'}`}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
{isOpen && results.length > 0 && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 bg-card rounded-xl shadow-industrial-xl border overflow-hidden z-50 animate-fade-in">
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{results.slice(0, 8).map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="flex items-center gap-4 p-4 hover:bg-secondary/50 cursor-pointer transition-colors border-b last:border-b-0"
|
||||
>
|
||||
<div className="h-16 w-16 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="h-12 w-12 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-foreground truncate">{product.name}</h4>
|
||||
{product.badge && (
|
||||
<span className={product.badge === 'new' ? 'badge-new' : 'badge-hot'}>
|
||||
{product.badge.toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate">{product.description}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">SKU: {product.sku}</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={(e) => handleAddToCart(product, e)}
|
||||
>
|
||||
Add to Quote
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{results.length > 8 && (
|
||||
<div className="p-3 bg-secondary/30 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
+{results.length - 8} more results
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && query.length >= 2 && results.length === 0 && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 bg-card rounded-xl shadow-industrial-xl border p-6 text-center z-50 animate-fade-in">
|
||||
<p className="text-muted-foreground">No products found for "{query}"</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Try different keywords or browse categories</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,87 +8,74 @@ import heroBg from '@/assets/hero-bg.jpg';
|
|||
|
||||
const Index = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<typeof products | null>(null);
|
||||
|
||||
const displayProducts = searchResults ?? (
|
||||
selectedCategory
|
||||
? products.filter(p => p.category === selectedCategory)
|
||||
: products
|
||||
);
|
||||
|
||||
const handleSearch = (query: string, results: typeof products) => {
|
||||
if (query.length >= 2) {
|
||||
setSearchResults(results);
|
||||
setSelectedCategory(null);
|
||||
} else {
|
||||
setSearchResults(null);
|
||||
}
|
||||
};
|
||||
const displayProducts = selectedCategory
|
||||
? products.filter(p => p.category === selectedCategory)
|
||||
: products;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Hero Section with Big Search */}
|
||||
{/* Hero Section */}
|
||||
<section
|
||||
className="relative min-h-[500px] flex items-center justify-center bg-cover bg-center"
|
||||
className="relative min-h-[320px] md:min-h-[400px] flex items-center justify-center bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${heroBg})` }}
|
||||
>
|
||||
<div className="absolute inset-0 gradient-industrial opacity-90" />
|
||||
<div className="relative z-10 container mx-auto px-4 py-16 text-center">
|
||||
<h1 className="font-display text-4xl md:text-5xl lg:text-6xl font-bold text-primary-foreground mb-4 animate-fade-in">
|
||||
<div className="relative z-10 container mx-auto px-4 py-12 text-center">
|
||||
<h1 className="font-display text-3xl md:text-4xl lg:text-5xl font-bold text-primary-foreground mb-3 animate-fade-in">
|
||||
Industrial Supplies for
|
||||
<span className="block text-brand-yellow">Mining & Construction</span>
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-primary-foreground/80 mb-8 max-w-2xl mx-auto animate-fade-in">
|
||||
<p className="text-base md:text-lg text-primary-foreground/80 mb-6 max-w-2xl mx-auto animate-fade-in">
|
||||
Quality fasteners, tools, and equipment. Request a quote today.
|
||||
</p>
|
||||
|
||||
{/* Big Search Bar */}
|
||||
<div className="flex justify-center animate-fade-in" style={{ animationDelay: '0.2s' }}>
|
||||
<SearchBar large onSearch={handleSearch} />
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="flex flex-wrap justify-center gap-8 mt-12 animate-fade-in" style={{ animationDelay: '0.4s' }}>
|
||||
<div className="flex flex-wrap justify-center gap-6 md:gap-8 animate-fade-in" style={{ animationDelay: '0.2s' }}>
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-brand-yellow">10,000+</p>
|
||||
<p className="text-primary-foreground/70 text-sm">Products</p>
|
||||
<p className="text-2xl md:text-3xl font-bold text-brand-yellow">10,000+</p>
|
||||
<p className="text-primary-foreground/70 text-xs md:text-sm">Products</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-brand-yellow">500+</p>
|
||||
<p className="text-primary-foreground/70 text-sm">Mining Clients</p>
|
||||
<p className="text-2xl md:text-3xl font-bold text-brand-yellow">500+</p>
|
||||
<p className="text-primary-foreground/70 text-xs md:text-sm">Mining Clients</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-brand-yellow">24hr</p>
|
||||
<p className="text-primary-foreground/70 text-sm">Quote Response</p>
|
||||
<p className="text-2xl md:text-3xl font-bold text-brand-yellow">24hr</p>
|
||||
<p className="text-primary-foreground/70 text-xs md:text-sm">Quote Response</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Search Bar Section - Below Hero */}
|
||||
<section className="bg-card border-b py-4 md:py-6">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex justify-center">
|
||||
<SearchBar large />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Feature Bar */}
|
||||
<FeatureBar />
|
||||
|
||||
{/* Main Content: Categories + Products */}
|
||||
<section className="container mx-auto px-4 py-12">
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
|
||||
{/* Category Sidebar */}
|
||||
<CategorySidebar
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={(cat) => {
|
||||
setSelectedCategory(cat);
|
||||
setSearchResults(null);
|
||||
}}
|
||||
onSelectCategory={setSelectedCategory}
|
||||
/>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="font-display text-2xl font-bold text-foreground">
|
||||
{searchResults
|
||||
? `Search Results (${searchResults.length})`
|
||||
: selectedCategory
|
||||
? `${selectedCategory.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}`
|
||||
<h2 className="font-display text-xl md:text-2xl font-bold text-foreground">
|
||||
{selectedCategory
|
||||
? `${selectedCategory.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}`
|
||||
: 'Featured Products'}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
|
|
@ -97,7 +84,7 @@ const Index = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4 md:gap-6">
|
||||
{displayProducts.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
|
|
|
|||
112
src/pages/Products.tsx
Normal file
112
src/pages/Products.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { SearchBar } from '@/components/SearchBar';
|
||||
import { CategorySidebar } from '@/components/CategorySidebar';
|
||||
import { ProductCard } from '@/components/ProductCard';
|
||||
import { products } from '@/data/products';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const Products = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
|
||||
const searchQuery = searchParams.get('search') || '';
|
||||
|
||||
const fuse = useMemo(() => new Fuse(products, {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.4 },
|
||||
{ name: 'tags', weight: 0.3 },
|
||||
{ name: 'description', weight: 0.2 },
|
||||
{ name: 'sku', weight: 0.1 },
|
||||
],
|
||||
threshold: 0.4,
|
||||
includeScore: true,
|
||||
}), []);
|
||||
|
||||
const displayProducts = useMemo(() => {
|
||||
let filtered = products;
|
||||
|
||||
// Apply search filter
|
||||
if (searchQuery.length >= 2) {
|
||||
const searchResults = fuse.search(searchQuery).map(r => r.item);
|
||||
filtered = searchResults;
|
||||
}
|
||||
|
||||
// Apply category filter
|
||||
if (selectedCategory) {
|
||||
filtered = filtered.filter(p => p.category === selectedCategory);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [searchQuery, selectedCategory, fuse]);
|
||||
|
||||
const handleCategorySelect = (cat: string | null) => {
|
||||
setSelectedCategory(cat);
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
setSearchParams({});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Search Header */}
|
||||
<section className="bg-card border-b py-6">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-bold text-foreground">
|
||||
{searchQuery ? `Search: "${searchQuery}"` : 'All Products'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
{displayProducts.length} products found
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full md:w-auto md:min-w-[400px]">
|
||||
<SearchBar />
|
||||
</div>
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={clearSearch}
|
||||
className="mt-3 text-sm text-primary hover:underline"
|
||||
>
|
||||
← Clear search and show all products
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<section className="container mx-auto px-4 py-8">
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Category Sidebar */}
|
||||
<CategorySidebar
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={handleCategorySelect}
|
||||
/>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="flex-1">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{displayProducts.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{displayProducts.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground">No products found.</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Try different keywords or browse categories.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Products;
|
||||
Loading…
Reference in a new issue