Implement mobile-responsive design with touch optimizations

This commit is contained in:
2025-11-16 23:57:05 +00:00
parent b0eafebfc4
commit 1a0e3e4e35
9 changed files with 387 additions and 109 deletions

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 ? (
<>

View File

@@ -1,5 +1,19 @@
@import "tailwindcss";
/* Mobile touch optimization */
.touch-manipulation {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Ensure minimum tap target size for accessibility */
@media (max-width: 768px) {
button, a, input[type="checkbox"], input[type="radio"] {
min-width: 44px;
min-height: 44px;
}
}
/* Leaflet CSS overrides */
.leaflet-container {
font-family: inherit;

View File

@@ -14,32 +14,35 @@ export function HomePage() {
<div className="min-h-screen bg-black/40">
{/* Hero Section */}
<div className="relative z-10 max-w-7xl mx-auto py-24 px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl drop-shadow-lg">
<div className="relative z-10 max-w-7xl mx-auto py-12 sm:py-16 md:py-24 px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-extrabold text-white drop-shadow-lg leading-tight">
Track and Clean Ireland's Beaches
</h2>
<p className="mt-6 text-xl text-white max-w-3xl mx-auto drop-shadow-md">
<p className="mt-4 sm:mt-6 text-base sm:text-lg md:text-xl text-white max-w-3xl mx-auto drop-shadow-md px-2">
Join our community in mapping beach litter, organizing clean-up events,
and protecting our coastal environment through data-driven action.
</p>
<SignedOut>
<div className="mt-10 flex flex-wrap justify-center gap-4">
<div className="mt-8 sm:mt-10 flex flex-col sm:flex-row flex-wrap justify-center gap-3 sm:gap-4 px-4">
<Link
to="/sign-up"
className="px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105"
className="px-6 py-3 sm:px-8 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 active:bg-blue-800 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105 touch-manipulation"
style={{ minHeight: '44px' }}
>
Get Started
</Link>
<Link
to="/map"
className="px-8 py-3 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105"
className="px-6 py-3 sm:px-8 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 active:bg-white/30 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105 touch-manipulation"
style={{ minHeight: '44px' }}
>
View Map
</Link>
<Link
to="/why-clean"
className="px-8 py-3 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105"
className="px-6 py-3 sm:px-8 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 active:bg-white/30 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105 touch-manipulation"
style={{ minHeight: '44px' }}
>
Why Clean Our Beaches?
</Link>
@@ -47,22 +50,25 @@ export function HomePage() {
</SignedOut>
<SignedIn>
<div className="mt-10 flex flex-wrap justify-center gap-4">
<div className="mt-8 sm:mt-10 flex flex-col sm:flex-row flex-wrap justify-center gap-3 sm:gap-4 px-4">
<Link
to="/reports/new"
className="px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105"
className="px-6 py-3 sm:px-8 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 active:bg-blue-800 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105 touch-manipulation"
style={{ minHeight: '44px' }}
>
Report Litter
</Link>
<Link
to="/map"
className="px-8 py-3 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105"
className="px-6 py-3 sm:px-8 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 active:bg-white/30 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105 touch-manipulation"
style={{ minHeight: '44px' }}
>
View Map
</Link>
<Link
to="/why-clean"
className="px-8 py-3 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105"
className="px-6 py-3 sm:px-8 border-2 border-white text-base font-medium rounded-md text-white bg-white/10 backdrop-blur-sm hover:bg-white/20 active:bg-white/30 md:py-4 md:text-lg md:px-10 shadow-lg transition-all hover:scale-105 touch-manipulation"
style={{ minHeight: '44px' }}
>
Why Clean Our Beaches?
</Link>
@@ -71,36 +77,36 @@ export function HomePage() {
</div>
{/* Features Section */}
<div className="max-w-7xl mx-auto py-16 px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="bg-white/95 backdrop-blur-sm p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow">
<div className="text-blue-600 text-3xl mb-4">📍</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
<div className="max-w-7xl mx-auto py-8 sm:py-12 md:py-16 px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-3">
<div className="bg-white/95 backdrop-blur-sm p-5 sm:p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow">
<div className="text-blue-600 text-3xl sm:text-4xl mb-3 sm:mb-4">📍</div>
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-2">
Report Litter
</h3>
<p className="text-gray-600">
<p className="text-sm sm:text-base text-gray-600">
Document beach litter with photos and location data to raise awareness
and track pollution patterns.
</p>
</div>
<div className="bg-white/95 backdrop-blur-sm p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow">
<div className="text-blue-600 text-3xl mb-4">🗺️</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
<div className="bg-white/95 backdrop-blur-sm p-5 sm:p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow">
<div className="text-blue-600 text-3xl sm:text-4xl mb-3 sm:mb-4">🗺️</div>
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-2">
Interactive Maps
</h3>
<p className="text-gray-600">
<p className="text-sm sm:text-base text-gray-600">
Visualize litter hotspots, view reports from your area, and identify
areas that need attention.
</p>
</div>
<div className="bg-white/95 backdrop-blur-sm p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow">
<div className="text-blue-600 text-3xl mb-4">🤝</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
<div className="bg-white/95 backdrop-blur-sm p-5 sm:p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow">
<div className="text-blue-600 text-3xl sm:text-4xl mb-3 sm:mb-4">🤝</div>
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-2">
Organize Events
</h3>
<p className="text-gray-600">
<p className="text-sm sm:text-base text-gray-600">
Create and join clean-up events, coordinate with volunteers, and make
a real impact on coastal health.
</p>

View File

@@ -431,20 +431,20 @@ export function MapPage() {
return (
<div className="h-screen flex flex-col">
<div className="bg-white border-b border-gray-200 px-4 py-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-gray-900">Interactive Map</h2>
<p className="mt-1 text-sm text-gray-600">
<div className="bg-white border-b border-gray-200 px-4 py-3 sm:py-4 sm:px-6 lg:px-8">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">Interactive Map</h2>
<p className="mt-1 text-xs sm:text-sm text-gray-600">
View litter reports and cleanup events across Ireland
</p>
</div>
<div className="flex-1 p-4">
<div className="flex-1 p-2 sm:p-4">
<div className="h-full bg-white rounded-lg shadow-lg overflow-hidden relative">
{isLoadingReports || isLoadingEvents ? (
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">
<div className="text-center px-4">
<div className="animate-spin rounded-full h-10 w-10 sm:h-12 sm:w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-sm sm:text-base text-gray-600">
Loading {isLoadingReports && isLoadingEvents ? 'map data' : isLoadingReports ? 'reports' : 'events'}...
</p>
</div>