This commit is contained in:
gpt-engineer-app[bot] 2026-01-29 19:50:13 +00:00
parent ea7828095a
commit 12329094cc
5 changed files with 240 additions and 147 deletions

View file

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

View file

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

View file

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

View file

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