localgreenchain/pages/m/scan.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

225 lines
8 KiB
TypeScript

import * as React from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { MobileHeader, QRScanner } from 'components/mobile';
interface ScanResult {
type: 'plant' | 'transport' | 'unknown';
id: string;
data?: any;
}
function parseScanResult(result: string): ScanResult {
// Expected formats:
// plant:{id}-{species}-{variety}
// transport:{id}
// Or just a raw ID
if (result.startsWith('plant:')) {
const parts = result.slice(6).split('-');
return {
type: 'plant',
id: parts[0],
data: {
species: parts[1],
variety: parts[2],
},
};
}
if (result.startsWith('transport:')) {
return {
type: 'transport',
id: result.slice(10),
};
}
// Assume it's a plant ID
return {
type: 'unknown',
id: result,
};
}
export default function ScanPage() {
const router = useRouter();
const [scanResult, setScanResult] = React.useState<ScanResult | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const handleScan = async (result: string) => {
setIsLoading(true);
setError(null);
try {
const parsed = parseScanResult(result);
setScanResult(parsed);
// Try to fetch the plant/transport data
if (parsed.type === 'plant' || parsed.type === 'unknown') {
const response = await fetch(`/api/plants/${parsed.id}`);
if (response.ok) {
const data = await response.json();
setScanResult({ ...parsed, type: 'plant', data });
} else if (response.status === 404) {
setError('Plant not found. Would you like to register it?');
} else {
setError('Failed to fetch plant data');
}
}
} catch (err) {
setError('An error occurred while processing the scan');
} finally {
setIsLoading(false);
}
};
const handleScanError = (err: Error) => {
setError(err.message);
};
const handleViewPlant = () => {
if (scanResult) {
router.push(`/plants/${scanResult.id}`);
}
};
const handleRegisterNew = () => {
router.push('/m/quick-add');
};
const handleRescan = () => {
setScanResult(null);
setError(null);
};
return (
<>
<Head>
<title>Scan QR Code - LocalGreenChain</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
</Head>
<div className="min-h-screen bg-black md:hidden">
<MobileHeader
title="Scan QR Code"
showBack
rightAction={
<button
className="text-sm text-green-500 font-medium"
onClick={() => router.push('/m/quick-add')}
>
Manual
</button>
}
/>
<div className="pt-14 h-screen">
{!scanResult && !error && (
<QRScanner
onScan={handleScan}
onError={handleScanError}
className="h-full"
/>
)}
{/* Loading overlay */}
{isLoading && (
<div className="absolute inset-0 bg-black/80 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl p-6 text-center">
<div className="animate-spin w-10 h-10 border-4 border-green-500 border-t-transparent rounded-full mx-auto"></div>
<p className="mt-4 text-gray-600">Looking up plant...</p>
</div>
</div>
)}
{/* Error state */}
{error && !scanResult && (
<div className="absolute inset-0 bg-black flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-sm text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Scan Error</h3>
<p className="text-gray-600 mb-6">{error}</p>
<div className="flex space-x-3">
<button
onClick={handleRescan}
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200"
>
Try Again
</button>
<button
onClick={handleRegisterNew}
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700"
>
Register New
</button>
</div>
</div>
</div>
)}
{/* Success result */}
{scanResult && scanResult.data && (
<div className="absolute inset-0 bg-black flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-sm">
<div className="text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-1">Plant Found!</h3>
<p className="text-gray-500 text-sm">ID: {scanResult.id}</p>
</div>
{/* Plant details */}
<div className="mt-6 space-y-3">
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-500">Name</span>
<span className="font-medium">{scanResult.data.name || 'Unknown'}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-500">Species</span>
<span className="font-medium">{scanResult.data.species || 'Unknown'}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-500">Generation</span>
<span className="font-medium">{scanResult.data.generation || 1}</span>
</div>
{scanResult.data.registeredAt && (
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-500">Registered</span>
<span className="font-medium">
{new Date(scanResult.data.registeredAt).toLocaleDateString()}
</span>
</div>
)}
</div>
{/* Actions */}
<div className="mt-6 flex space-x-3">
<button
onClick={handleRescan}
className="flex-1 px-4 py-3 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200"
>
Scan Another
</button>
<button
onClick={handleViewPlant}
className="flex-1 px-4 py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700"
>
View Details
</button>
</div>
</div>
</div>
)}
</div>
</div>
</>
);
}