localgreenchain/components/mobile/BottomNav.tsx
Claude c2a1b05677
Implement Agent 10: Mobile Optimization with PWA capabilities
This implements the mobile optimization agent (P3 - Enhancement) with:

PWA Configuration:
- Add next-pwa integration with offline caching strategies
- Create web app manifest for installability
- Add service worker with background sync support
- Create offline fallback page

Mobile Components:
- BottomNav: Touch-friendly bottom navigation bar
- MobileHeader: Responsive header with back navigation
- InstallPrompt: Smart PWA install prompt (iOS & Android)
- SwipeableCard: Gesture-based swipeable cards
- PullToRefresh: Native-like pull to refresh
- QRScanner: Camera-based QR code scanning

Mobile Library:
- camera.ts: Camera access and photo capture utilities
- offline.ts: IndexedDB-based offline storage and sync
- gestures.ts: Touch gesture detection (swipe, pinch, tap)
- pwa.ts: PWA status, install prompts, service worker management

Mobile Pages:
- /m: Mobile dashboard with quick actions and stats
- /m/scan: QR code scanner for plant lookup
- /m/quick-add: Streamlined plant registration form
- /m/profile: User profile with offline status

Dependencies added: next-pwa, idb
2025-11-23 03:56:30 +00:00

92 lines
3.2 KiB
TypeScript

import * as React from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import classNames from 'classnames';
interface NavItem {
href: string;
icon: React.ReactNode;
label: string;
}
const navItems: NavItem[] = [
{
href: '/m',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
label: 'Home',
},
{
href: '/m/scan',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
),
label: 'Scan',
},
{
href: '/m/quick-add',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
),
label: 'Add',
},
{
href: '/plants/explore',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
),
label: 'Explore',
},
{
href: '/m/profile',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
),
label: 'Profile',
},
];
export function BottomNav() {
const router = useRouter();
const pathname = router.pathname;
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 pb-safe md:hidden">
<div className="flex items-center justify-around h-16">
{navItems.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
return (
<Link key={item.href} href={item.href}>
<a
className={classNames(
'flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors',
{
'text-green-600': isActive,
'text-gray-500 hover:text-gray-700': !isActive,
}
)}
>
{item.icon}
<span className="text-xs font-medium">{item.label}</span>
</a>
</Link>
);
})}
</div>
</nav>
);
}
export default BottomNav;