Add mobile bottom nav and tweaks

Implemented a new MobileBottomNav component, integrated into App with padding adjustments, and enabled cart access via a bottom nav button. Updated CartDrawer to listen for open-cart events. Refined Footer to accept custom className. Updated Products page to support mobile full-screen product view with a mobile filter sheet. Also wired up mobile bottom nav to trigger cart drawer and adjusted layout padding accordingly.

X-Lovable-Edit-ID: edt-21bfcd61-e800-4ace-9973-66d0ac4860a5
This commit is contained in:
gpt-engineer-app[bot] 2026-01-29 21:52:15 +00:00
commit e081a43563
5 changed files with 149 additions and 17 deletions

View file

@ -9,6 +9,7 @@ import { Header } from "@/components/Header";
import { Footer } from "@/components/Footer";
import { CartDrawer } from "@/components/CartDrawer";
import { AuthModal } from "@/components/AuthModal";
import { MobileBottomNav } from "@/components/MobileBottomNav";
import Index from "./pages/Index";
import Products from "./pages/Products";
import About from "./pages/About";
@ -25,7 +26,7 @@ const App = () => (
<TooltipProvider>
<Toaster />
<Sonner />
<div className="flex flex-col min-h-screen">
<div className="flex flex-col min-h-screen pb-16 md:pb-0">
<Header />
<main className="flex-1">
<Routes>
@ -37,8 +38,9 @@ const App = () => (
<Route path="*" element={<NotFound />} />
</Routes>
</main>
<Footer />
<Footer className="hidden md:block" />
</div>
<MobileBottomNav />
<CartDrawer />
<AuthModal />
</TooltipProvider>

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { X, Minus, Plus, Trash2, Send, MessageSquare } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -16,6 +16,12 @@ import { useCart } from '@/context/CartContext';
import { useToast } from '@/hooks/use-toast';
export function CartDrawer() {
// Listen for custom event from mobile nav
useEffect(() => {
const handleOpenCart = () => setIsCartOpen(true);
window.addEventListener('open-cart', handleOpenCart);
return () => window.removeEventListener('open-cart', handleOpenCart);
}, []);
const { items, isCartOpen, setIsCartOpen, removeItem, updateQuantity, updateNotes, clearCart } = useCart();
const { toast } = useToast();
const [quoteMessage, setQuoteMessage] = useState('');

View file

@ -1,8 +1,14 @@
import { Link } from 'react-router-dom';
import { Mail, Phone, MapPin, Facebook, Linkedin, Instagram } from 'lucide-react';
import logo from '@/assets/logo.jpeg';
export function Footer() {
return <footer className="bg-foreground text-background">
import { cn } from '@/lib/utils';
interface FooterProps {
className?: string;
}
export function Footer({ className }: FooterProps) {
return <footer className={cn("bg-foreground text-background", className)}>
<div className="container mx-auto px-4 py-12 bg-zinc-700">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{/* Brand */}

View file

@ -0,0 +1,70 @@
import { Home, Package, ShoppingCart } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
import { useCart } from '@/context/CartContext';
import { cn } from '@/lib/utils';
export function MobileBottomNav() {
const location = useLocation();
const { items } = useCart();
const cartCount = items.reduce((sum, item) => sum + item.quantity, 0);
const navItems = [
{ to: '/', icon: Home, label: 'Home' },
{ to: '/products', icon: Package, label: 'Products' },
{ to: '#cart', icon: ShoppingCart, label: 'Cart', badge: cartCount },
];
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-background border-t border-border md:hidden">
<div className="flex justify-around items-center h-16 px-2 safe-area-inset-bottom">
{navItems.map((item) => {
const isActive = item.to === '/'
? location.pathname === '/'
: location.pathname.startsWith(item.to.replace('#', ''));
if (item.to === '#cart') {
return (
<button
key={item.label}
onClick={() => {
// Trigger cart drawer - we'll dispatch a custom event
window.dispatchEvent(new CustomEvent('open-cart'));
}}
className={cn(
"flex flex-col items-center justify-center gap-1 px-4 py-2 relative transition-colors",
"text-muted-foreground hover:text-foreground"
)}
>
<div className="relative">
<item.icon className="h-5 w-5" />
{item.badge ? (
<span className="absolute -top-1.5 -right-1.5 bg-brand-yellow text-brand-yellow-foreground text-xs font-bold rounded-full h-4 w-4 flex items-center justify-center">
{item.badge > 9 ? '9+' : item.badge}
</span>
) : null}
</div>
<span className="text-xs font-medium">{item.label}</span>
</button>
);
}
return (
<Link
key={item.label}
to={item.to}
className={cn(
"flex flex-col items-center justify-center gap-1 px-4 py-2 relative transition-colors",
isActive
? "text-brand-yellow"
: "text-muted-foreground hover:text-foreground"
)}
>
<item.icon className="h-5 w-5" />
<span className="text-xs font-medium">{item.label}</span>
</Link>
);
})}
</div>
</nav>
);
}

View file

@ -3,11 +3,15 @@ import { SearchBar } from '@/components/SearchBar';
import { CategorySidebar } from '@/components/CategorySidebar';
import { ProductCard } from '@/components/ProductCard';
import { products } from '@/data/products';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { Filter } from 'lucide-react';
import Fuse from 'fuse.js';
const Products = () => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [filterOpen, setFilterOpen] = useState(false);
const fuse = useMemo(() => new Fuse(products, {
keys: [
@ -37,22 +41,66 @@ const Products = () => {
return filtered;
}, [searchQuery, selectedCategory, fuse]);
const handleCategorySelect = (category: string | null) => {
setSelectedCategory(category);
setFilterOpen(false);
};
return (
<div className="min-h-screen bg-background">
{/* Search Header */}
<section className="bg-card border-b py-6">
{/* Mobile Header with Search and Filter */}
<section className="bg-card border-b py-4 md:py-6">
<div className="container mx-auto px-4">
<h1 className="font-display text-2xl md:text-3xl font-bold text-foreground mb-4">
<h1 className="font-display text-xl md:text-3xl font-bold text-foreground mb-3 md:mb-4">
All Products
</h1>
{/* Mobile: Search + Filter Button Row */}
<div className="flex gap-2 md:hidden">
<div className="flex-1">
<SearchBar value={searchQuery} onChange={setSearchQuery} />
</div>
<Sheet open={filterOpen} onOpenChange={setFilterOpen}>
<SheetTrigger asChild>
<Button variant="outline" size="icon" className="shrink-0">
<Filter className="h-4 w-4" />
</Button>
</SheetTrigger>
<SheetContent side="bottom" className="h-[70vh]">
<SheetHeader>
<SheetTitle>Filter by Category</SheetTitle>
</SheetHeader>
<div className="mt-4">
<CategorySidebar
selectedCategory={selectedCategory}
onSelectCategory={handleCategorySelect}
/>
</div>
</SheetContent>
</Sheet>
</div>
{/* Active filter badge on mobile */}
{selectedCategory && (
<div className="mt-2 md:hidden">
<Button
variant="secondary"
size="sm"
onClick={() => setSelectedCategory(null)}
className="text-xs"
>
{selectedCategory.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} ✕
</Button>
</div>
)}
</div>
</section>
{/* Main Content */}
<section className="container mx-auto px-4 py-8">
<section className="container mx-auto px-4 py-4 md:py-8">
<div className="flex flex-col lg:flex-row gap-6">
{/* Category Sidebar - Sticky */}
<div className="lg:sticky lg:top-4 lg:self-start">
{/* Category Sidebar - Desktop Only */}
<div className="hidden lg:block lg:sticky lg:top-4 lg:self-start">
<CategorySidebar
selectedCategory={selectedCategory}
onSelectCategory={setSelectedCategory}
@ -61,21 +109,21 @@ const Products = () => {
{/* Products Section */}
<div className="flex-1">
{/* Search Bar */}
<div className="mb-6">
{/* Desktop Search Bar */}
<div className="hidden md:block mb-6">
<SearchBar value={searchQuery} onChange={setSearchQuery} />
</div>
{/* Results Info */}
<div className="mb-4">
<p className="text-muted-foreground text-sm">
<div className="mb-3 md:mb-4">
<p className="text-muted-foreground text-xs md:text-sm">
{displayProducts.length} products found
{searchQuery.length >= 2 && ` for "${searchQuery}"`}
</p>
</div>
{/* Scrollable Products Grid */}
<div className="max-h-[70vh] overflow-y-auto pr-2 scrollbar-thin">
{/* Products Grid - Full screen on mobile */}
<div className="md:max-h-[70vh] md:overflow-y-auto md:pr-2 scrollbar-thin">
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3 md:gap-4">
{displayProducts.map((product) => (
<ProductCard key={product.id} product={product} />