Implement mobile-responsive design with touch optimizations
This commit is contained in:
@@ -1,46 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { SignedIn, SignedOut, SignInButton, SignUpButton } from '@clerk/clerk-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { UserProfile } from '../auth/UserProfile';
|
||||
|
||||
/**
|
||||
* Application header with navigation and authentication controls
|
||||
* Responsive design with mobile hamburger menu
|
||||
*/
|
||||
export function Header() {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const toggleMobileMenu = () => {
|
||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||
};
|
||||
|
||||
const closeMobileMenu = () => {
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-4">
|
||||
<div className="flex justify-between items-center py-3 sm:py-4">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center">
|
||||
<Link to="/" className="flex items-center">
|
||||
<h1 className="text-2xl font-bold text-blue-600">
|
||||
<Link to="/" className="flex items-center" onClick={closeMobileMenu}>
|
||||
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-blue-600">
|
||||
Beach Litter Mapping
|
||||
</h1>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center space-x-4">
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-2 lg:space-x-4">
|
||||
<SignedIn>
|
||||
<Link
|
||||
to="/map"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Map
|
||||
</Link>
|
||||
<Link
|
||||
to="/reports/new"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Report Litter
|
||||
Report
|
||||
</Link>
|
||||
<Link
|
||||
to="/events"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Events
|
||||
</Link>
|
||||
<Link
|
||||
to="/analytics"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
@@ -50,23 +64,116 @@ export function Header() {
|
||||
<SignedOut>
|
||||
<Link
|
||||
to="/map"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium"
|
||||
className="text-gray-700 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
View Map
|
||||
</Link>
|
||||
<SignInButton mode="modal">
|
||||
<button className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<button className="px-3 lg:px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
|
||||
Sign In
|
||||
</button>
|
||||
</SignInButton>
|
||||
<SignUpButton mode="modal">
|
||||
<button className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<button className="px-3 lg:px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
|
||||
Sign Up
|
||||
</button>
|
||||
</SignUpButton>
|
||||
</SignedOut>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
onClick={toggleMobileMenu}
|
||||
className="md:hidden inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-blue-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 transition-colors"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
aria-label="Toggle navigation menu"
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
{isMobileMenuOpen ? (
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation Menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-gray-200">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
<SignedIn>
|
||||
<Link
|
||||
to="/map"
|
||||
onClick={closeMobileMenu}
|
||||
className="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Map
|
||||
</Link>
|
||||
<Link
|
||||
to="/reports/new"
|
||||
onClick={closeMobileMenu}
|
||||
className="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Report Litter
|
||||
</Link>
|
||||
<Link
|
||||
to="/events"
|
||||
onClick={closeMobileMenu}
|
||||
className="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Events
|
||||
</Link>
|
||||
<Link
|
||||
to="/analytics"
|
||||
onClick={closeMobileMenu}
|
||||
className="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
<div className="px-3 py-2">
|
||||
<UserProfile />
|
||||
</div>
|
||||
</SignedIn>
|
||||
|
||||
<SignedOut>
|
||||
<Link
|
||||
to="/map"
|
||||
onClick={closeMobileMenu}
|
||||
className="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
View Map
|
||||
</Link>
|
||||
<div className="px-3 py-2 space-y-2">
|
||||
<SignInButton mode="modal">
|
||||
<button
|
||||
className="w-full px-4 py-3 text-base font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</SignInButton>
|
||||
<SignUpButton mode="modal">
|
||||
<button
|
||||
className="w-full px-4 py-3 text-base font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
</SignUpButton>
|
||||
</div>
|
||||
</SignedOut>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -100,14 +100,15 @@ export function MapFilters({
|
||||
filters.dateRange;
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 left-4 z-[1000] bg-white rounded-lg shadow-lg max-w-sm">
|
||||
<div className="absolute top-2 left-2 sm:top-4 sm:left-4 z-[1000] bg-white rounded-lg shadow-lg w-[calc(100vw-1rem)] sm:w-80 max-w-sm">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">Filters</h3>
|
||||
<div className="flex items-center justify-between p-3 sm:p-4 border-b border-gray-200">
|
||||
<h3 className="text-sm sm:text-base font-semibold text-gray-900">Filters</h3>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
className="text-gray-500 hover:text-gray-700 p-2 touch-manipulation"
|
||||
aria-label={isExpanded ? 'Collapse filters' : 'Expand filters'}
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<svg
|
||||
className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||
@@ -122,23 +123,24 @@ export function MapFilters({
|
||||
|
||||
{/* Filter Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-4 max-h-[70vh] overflow-y-auto">
|
||||
<div className="p-3 sm:p-4 space-y-3 sm:space-y-4 max-h-[60vh] sm:max-h-[70vh] overflow-y-auto">
|
||||
{/* Cluster Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="cluster-toggle" className="text-sm font-medium text-gray-700">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<label htmlFor="cluster-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
|
||||
Cluster Markers
|
||||
</label>
|
||||
<button
|
||||
id="cluster-toggle"
|
||||
onClick={onToggleClusters}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors touch-manipulation ${
|
||||
showClusters ? 'bg-blue-600' : 'bg-gray-200'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showClusters}
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
showClusters ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
@@ -146,21 +148,22 @@ export function MapFilters({
|
||||
</div>
|
||||
|
||||
{/* Heat Map Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="heatmap-toggle" className="text-sm font-medium text-gray-700">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<label htmlFor="heatmap-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
|
||||
Heat Map
|
||||
</label>
|
||||
<button
|
||||
id="heatmap-toggle"
|
||||
onClick={onToggleHeatmap}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors touch-manipulation ${
|
||||
showHeatmap ? 'bg-blue-600' : 'bg-gray-200'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showHeatmap}
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
showHeatmap ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
@@ -169,21 +172,22 @@ export function MapFilters({
|
||||
|
||||
{/* Events Toggle */}
|
||||
{onToggleEvents && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="events-toggle" className="text-sm font-medium text-gray-700">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<label htmlFor="events-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
|
||||
Show Events
|
||||
</label>
|
||||
<button
|
||||
id="events-toggle"
|
||||
onClick={onToggleEvents}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors touch-manipulation ${
|
||||
showEvents ? 'bg-green-600' : 'bg-gray-200'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showEvents}
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
showEvents ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
@@ -193,21 +197,22 @@ export function MapFilters({
|
||||
|
||||
{/* Water Quality Toggle */}
|
||||
{onToggleWaterQuality && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="water-quality-toggle" className="text-sm font-medium text-gray-700">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<label htmlFor="water-quality-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
|
||||
Water Quality
|
||||
</label>
|
||||
<button
|
||||
id="water-quality-toggle"
|
||||
onClick={onToggleWaterQuality}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors touch-manipulation ${
|
||||
showWaterQuality ? 'bg-blue-600' : 'bg-gray-200'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showWaterQuality}
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
showWaterQuality ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
@@ -217,11 +222,11 @@ export function MapFilters({
|
||||
|
||||
{/* Hotspots Toggle */}
|
||||
{onToggleHotspots && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="hotspots-toggle" className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<label htmlFor="hotspots-toggle" className="text-xs sm:text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
Hotspots
|
||||
{isLoadingHotspots && (
|
||||
<svg className="animate-spin h-4 w-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<svg className="animate-spin h-3 w-3 sm:h-4 sm:w-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
@@ -231,14 +236,15 @@ export function MapFilters({
|
||||
id="hotspots-toggle"
|
||||
onClick={onToggleHotspots}
|
||||
disabled={isLoadingHotspots}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors touch-manipulation ${
|
||||
showHotspots ? 'bg-orange-600' : 'bg-gray-200'
|
||||
} ${isLoadingHotspots ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
role="switch"
|
||||
aria-checked={showHotspots}
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
showHotspots ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
@@ -247,21 +253,22 @@ export function MapFilters({
|
||||
)}
|
||||
|
||||
{/* Show Cleaned Reports Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="cleaned-toggle" className="text-sm font-medium text-gray-700">
|
||||
Show Cleaned Reports
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<label htmlFor="cleaned-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
|
||||
Show Cleaned
|
||||
</label>
|
||||
<button
|
||||
id="cleaned-toggle"
|
||||
onClick={() => onFiltersChange({ ...filters, showCleaned: filters.showCleaned === false ? true : false })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors touch-manipulation ${
|
||||
filters.showCleaned !== false ? 'bg-green-600' : 'bg-gray-200'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={filters.showCleaned !== false}
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
filters.showCleaned !== false ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
@@ -295,7 +302,7 @@ export function MapFilters({
|
||||
|
||||
{/* Date Range */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||
Date Range
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
@@ -303,34 +310,36 @@ export function MapFilters({
|
||||
type="date"
|
||||
value={filters.dateRange?.start ? filters.dateRange.start.toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => handleDateRangeChange('start', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-3 py-2 sm:py-3 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 touch-manipulation"
|
||||
placeholder="Start date"
|
||||
style={{ minHeight: '44px' }}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.dateRange?.end ? filters.dateRange.end.toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => handleDateRangeChange('end', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-3 py-2 sm:py-3 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 touch-manipulation"
|
||||
placeholder="End date"
|
||||
style={{ minHeight: '44px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Litter Type */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||
Litter Type
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{litterTypes.map((type) => (
|
||||
<label key={type} className="flex items-center">
|
||||
<label key={type} className="flex items-center py-1 touch-manipulation" style={{ minHeight: '44px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.litterTypes?.includes(type) || false}
|
||||
onChange={() => handleLitterTypeToggle(type)}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
className="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="ml-2 text-sm text-gray-700 capitalize">{type}</span>
|
||||
<span className="ml-3 text-xs sm:text-sm text-gray-700 capitalize">{type}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -338,19 +347,19 @@ export function MapFilters({
|
||||
|
||||
{/* Quantity Level */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||
Quantity Level
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{quantities.map((quantity) => (
|
||||
<label key={quantity} className="flex items-center">
|
||||
<label key={quantity} className="flex items-center py-1 touch-manipulation" style={{ minHeight: '44px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.quantities?.includes(quantity) || false}
|
||||
onChange={() => handleQuantityToggle(quantity)}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
className="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="ml-2 text-sm text-gray-700 capitalize">{quantity}</span>
|
||||
<span className="ml-3 text-xs sm:text-sm text-gray-700 capitalize">{quantity}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -360,7 +369,8 @@ export function MapFilters({
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
className="w-full px-4 py-3 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 rounded-md transition-colors touch-manipulation"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Clear All Filters
|
||||
</button>
|
||||
|
||||
@@ -564,16 +564,17 @@ export function MapView({
|
||||
<div className="relative w-full h-full">
|
||||
<div ref={mapRef} className="w-full h-full rounded-lg" />
|
||||
|
||||
{/* Geolocation button */}
|
||||
{/* Geolocation button - optimized for touch with 44x44px minimum */}
|
||||
<button
|
||||
onClick={handleGeolocation}
|
||||
className="absolute top-4 right-4 z-[1000] bg-white hover:bg-gray-50 text-gray-700 p-3 rounded-lg shadow-lg transition-colors"
|
||||
className="absolute top-2 right-2 sm:top-4 sm:right-4 z-[1000] bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 p-3 rounded-lg shadow-lg transition-colors touch-manipulation"
|
||||
title="Center map on your location"
|
||||
aria-label="Center map on your location"
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
className="h-5 w-5 sm:h-6 sm:w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -260,14 +260,14 @@ export function ReportForm() {
|
||||
)}
|
||||
|
||||
{/* Progress Indicator */}
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
{[1, 2, 3, 4].map((step) => (
|
||||
<div key={step} className="flex items-center flex-1">
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<div
|
||||
className={`
|
||||
w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm
|
||||
w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center font-semibold text-xs sm:text-sm
|
||||
${step < currentStep
|
||||
? 'bg-green-500 text-white'
|
||||
: step === currentStep
|
||||
@@ -277,14 +277,14 @@ export function ReportForm() {
|
||||
`}
|
||||
>
|
||||
{step < currentStep ? (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="w-4 h-4 sm:w-6 sm:h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
step
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-xs font-medium text-gray-600 text-center">
|
||||
<div className="mt-1 sm:mt-2 text-[10px] sm:text-xs font-medium text-gray-600 text-center hidden xs:block">
|
||||
{step === 1 && 'Photos'}
|
||||
{step === 2 && 'Location'}
|
||||
{step === 3 && 'Details'}
|
||||
@@ -294,7 +294,7 @@ export function ReportForm() {
|
||||
{step < totalSteps && (
|
||||
<div
|
||||
className={`
|
||||
h-1 flex-1 mx-2
|
||||
h-0.5 sm:h-1 flex-1 mx-1 sm:mx-2
|
||||
${step < currentStep ? 'bg-green-500' : 'bg-gray-200'}
|
||||
`}
|
||||
/>
|
||||
@@ -305,13 +305,13 @@ export function ReportForm() {
|
||||
</div>
|
||||
|
||||
{/* Form Content */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 md:p-8">
|
||||
<div className="bg-white rounded-lg shadow-lg p-4 sm:p-6 md:p-8">
|
||||
{/* Step 1: Photos */}
|
||||
{currentStep === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Upload Photos</h2>
|
||||
<p className="text-gray-600">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-2">Upload Photos</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Take or upload photos of the litter. Photos with GPS data will auto-populate the location.
|
||||
</p>
|
||||
</div>
|
||||
@@ -326,10 +326,10 @@ export function ReportForm() {
|
||||
|
||||
{/* Step 2: Location */}
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Select Location</h2>
|
||||
<p className="text-gray-600">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-2">Select Location</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Confirm or select the location where the litter was found.
|
||||
</p>
|
||||
</div>
|
||||
@@ -680,12 +680,13 @@ export function ReportForm() {
|
||||
)}
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-between mt-8 pt-6 border-t border-gray-200">
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-0 mt-6 sm:mt-8 pt-4 sm:pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
disabled={currentStep === 1}
|
||||
className="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="w-full sm:w-auto px-6 py-3 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50 active:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors touch-manipulation"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
@@ -694,7 +695,8 @@ export function ReportForm() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
className="w-full sm:w-auto px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 active:bg-blue-800 transition-colors touch-manipulation"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -703,7 +705,8 @@ export function ReportForm() {
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
|
||||
className="w-full sm:w-auto px-6 py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 active:bg-green-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2 touch-manipulation"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user