mirror of
https://gitlab.com/davidels1986/ore-cart-checkout.git
synced 2026-10-30 02:59:13 +00:00
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:
commit
e081a43563
5 changed files with 149 additions and 17 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
70
src/components/MobileBottomNav.tsx
Normal file
70
src/components/MobileBottomNav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue