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
92 lines
3.2 KiB
TypeScript
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;
|