localgreenchain/components/mobile/SwipeableCard.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

131 lines
3.5 KiB
TypeScript

import * as React from 'react';
import classNames from 'classnames';
interface SwipeableCardProps {
children: React.ReactNode;
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
leftAction?: React.ReactNode;
rightAction?: React.ReactNode;
className?: string;
}
export function SwipeableCard({
children,
onSwipeLeft,
onSwipeRight,
leftAction,
rightAction,
className,
}: SwipeableCardProps) {
const cardRef = React.useRef<HTMLDivElement>(null);
const [startX, setStartX] = React.useState(0);
const [currentX, setCurrentX] = React.useState(0);
const [isSwiping, setIsSwiping] = React.useState(false);
const threshold = 100;
const maxSwipe = 150;
const handleTouchStart = (e: React.TouchEvent) => {
setStartX(e.touches[0].clientX);
setIsSwiping(true);
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!isSwiping) return;
const diff = e.touches[0].clientX - startX;
const clampedDiff = Math.max(-maxSwipe, Math.min(maxSwipe, diff));
// Only allow swiping in directions that have actions
if (diff > 0 && !onSwipeRight) return;
if (diff < 0 && !onSwipeLeft) return;
setCurrentX(clampedDiff);
};
const handleTouchEnd = () => {
setIsSwiping(false);
if (currentX > threshold && onSwipeRight) {
onSwipeRight();
} else if (currentX < -threshold && onSwipeLeft) {
onSwipeLeft();
}
setCurrentX(0);
};
const swipeProgress = Math.abs(currentX) / threshold;
const direction = currentX > 0 ? 'right' : 'left';
return (
<div className={classNames('relative overflow-hidden rounded-lg', className)}>
{/* Left action background */}
{rightAction && (
<div
className={classNames(
'absolute inset-y-0 left-0 flex items-center justify-start pl-4 bg-green-500 transition-opacity',
{
'opacity-100': currentX > 0,
'opacity-0': currentX <= 0,
}
)}
style={{ width: Math.abs(currentX) }}
>
{rightAction}
</div>
)}
{/* Right action background */}
{leftAction && (
<div
className={classNames(
'absolute inset-y-0 right-0 flex items-center justify-end pr-4 bg-red-500 transition-opacity',
{
'opacity-100': currentX < 0,
'opacity-0': currentX >= 0,
}
)}
style={{ width: Math.abs(currentX) }}
>
{leftAction}
</div>
)}
{/* Main card content */}
<div
ref={cardRef}
className="relative bg-white transition-transform touch-pan-y"
style={{
transform: `translateX(${currentX}px)`,
transitionDuration: isSwiping ? '0ms' : '200ms',
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{children}
</div>
{/* Swipe indicator */}
{isSwiping && Math.abs(currentX) > 20 && (
<div
className={classNames(
'absolute top-1/2 transform -translate-y-1/2 text-white text-sm font-medium',
{
'left-4': direction === 'right',
'right-4': direction === 'left',
}
)}
style={{ opacity: swipeProgress }}
>
{direction === 'right' && onSwipeRight && 'Release to confirm'}
{direction === 'left' && onSwipeLeft && 'Release to delete'}
</div>
)}
</div>
);
}
export default SwipeableCard;