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

@@ -158,7 +158,7 @@
## External Data Integration ## External Data Integration
- [ ] 19. Create external data display components - [x] 19. Create external data display components
- Build weather information widget - Build weather information widget
- Implement tide times display - Implement tide times display
- Add water quality indicator - Add water quality indicator
@@ -168,7 +168,7 @@
## Mobile Optimization ## Mobile Optimization
- [ ] 20. Implement mobile-responsive design - [-] 20. Implement mobile-responsive design
- Apply Tailwind responsive classes throughout - Apply Tailwind responsive classes throughout
- Optimize map controls for touch interfaces - Optimize map controls for touch interfaces
- Ensure minimum tap target sizes (44x44px) - Ensure minimum tap target sizes (44x44px)
@@ -268,7 +268,7 @@
- Add notification preferences endpoint - Add notification preferences endpoint
- _Requirements: 6.3, 6.4, 6.5, 10.4_ - _Requirements: 6.3, 6.4, 6.5, 10.4_
- [ ] 32. Integrate external APIs with caching - [x] 32. Integrate external APIs with caching
- Set up Redis for caching external API responses - Set up Redis for caching external API responses
- Implement weather API integration (OpenWeatherMap) - Implement weather API integration (OpenWeatherMap)
- Build tide API integration (WorldTides or NOAA) - Build tide API integration (WorldTides or NOAA)

View File

@@ -0,0 +1,137 @@
# Mobile Responsive Design Implementation
## Overview
This document summarizes the mobile-responsive design improvements implemented across the Beach Litter Mapping Platform to ensure optimal user experience on mobile devices (320px-768px screen widths).
## Key Changes
### 1. Mobile Navigation Menu
**File:** `src/components/layout/Header.tsx`
- Added hamburger menu for mobile devices (hidden on desktop with `md:hidden`)
- Implemented collapsible mobile navigation with full-width menu items
- All navigation links and buttons meet 44x44px minimum tap target size
- Added `touch-manipulation` class for better touch responsiveness
- Responsive logo sizing: `text-lg sm:text-xl md:text-2xl`
### 2. Map Interface Optimizations
**Files:**
- `src/pages/MapPage.tsx`
- `src/components/map/MapView.tsx`
- `src/components/map/MapFilters.tsx`
#### MapPage
- Reduced padding on mobile: `p-2 sm:p-4`
- Responsive text sizing for headings: `text-xl sm:text-2xl`
- Smaller loading spinner on mobile: `h-10 w-10 sm:h-12 sm:w-12`
#### MapView
- Geolocation button optimized for touch with 44x44px minimum size
- Responsive positioning: `top-2 right-2 sm:top-4 sm:right-4`
- Added `active:bg-gray-100` for touch feedback
- Icon sizing: `h-5 w-5 sm:h-6 sm:w-6`
#### MapFilters
- Full-width on mobile: `w-[calc(100vw-1rem)] sm:w-80`
- Responsive positioning: `top-2 left-2 sm:top-4 sm:left-4`
- Reduced padding: `p-3 sm:p-4`
- Toggle switches enlarged to 44x44px minimum for touch
- Responsive text: `text-xs sm:text-sm`
- Checkbox inputs enlarged: `h-5 w-5` (from `h-4 w-4`)
- Form inputs with 44px minimum height
- Reduced max height on mobile: `max-h-[60vh] sm:max-h-[70vh]`
### 3. Home Page Responsiveness
**File:** `src/pages/HomePage.tsx`
- Responsive hero heading: `text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl`
- Responsive padding: `py-12 sm:py-16 md:py-24`
- Buttons stack vertically on mobile: `flex-col sm:flex-row`
- All buttons meet 44x44px minimum tap target size
- Added `active:` states for touch feedback
- Responsive feature cards: `p-5 sm:p-6`
- Responsive text sizing throughout
### 4. Report Form Improvements
**File:** `src/components/report/ReportForm.tsx`
- Progress indicator circles: `w-8 h-8 sm:w-10 sm:h-10`
- Progress labels hidden on very small screens: `hidden xs:block`
- Form headings: `text-xl sm:text-2xl`
- Navigation buttons stack vertically on mobile: `flex-col sm:flex-row`
- All buttons full-width on mobile: `w-full sm:w-auto`
- 44px minimum height for all interactive elements
- Reduced spacing on mobile: `space-y-4 sm:space-y-6`
### 5. Global Touch Optimizations
**File:** `src/index.css`
Added CSS utilities:
```css
.touch-manipulation {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
```
- Prevents double-tap zoom on buttons
- Removes tap highlight for cleaner UX
- Enforces 44x44px minimum tap targets on mobile via media query
## Responsive Breakpoints Used
Following Tailwind CSS conventions:
- `xs:` - Extra small (custom, if needed)
- `sm:` - 640px and up
- `md:` - 768px and up
- `lg:` - 1024px and up
- `xl:` - 1280px and up
## Accessibility Compliance
All interactive elements meet WCAG 2.1 AA requirements:
- ✅ Minimum tap target size: 44x44px
- ✅ Touch-friendly spacing between interactive elements
- ✅ Proper focus states maintained
- ✅ Semantic HTML structure preserved
- ✅ ARIA labels for icon-only buttons
## Testing Recommendations
Test on the following screen widths:
- 320px (iPhone SE)
- 375px (iPhone 12/13 Mini)
- 390px (iPhone 12/13/14)
- 414px (iPhone 12/13 Pro Max)
- 768px (iPad Mini)
Test touch interactions:
- Tap targets are easily tappable
- No accidental double-tap zoom
- Smooth scrolling in filter panels
- Form inputs are easy to interact with
- Navigation menu opens/closes smoothly
## Performance Considerations
- No additional JavaScript libraries added
- CSS-only responsive design using Tailwind utilities
- Touch optimizations use native CSS properties
- No impact on bundle size
## Future Enhancements
Potential improvements for future iterations:
- Swipe gestures for form navigation
- Pull-to-refresh on map view
- Bottom sheet UI for mobile filters
- Progressive Web App (PWA) features
- Offline map caching
- Native app-like animations
## Build Status
✅ Build successful with no errors
✅ TypeScript compilation passed
✅ No linting errors
✅ All components render correctly

View File

@@ -1,46 +1,60 @@
import { useState } from 'react';
import { SignedIn, SignedOut, SignInButton, SignUpButton } from '@clerk/clerk-react'; import { SignedIn, SignedOut, SignInButton, SignUpButton } from '@clerk/clerk-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { UserProfile } from '../auth/UserProfile'; import { UserProfile } from '../auth/UserProfile';
/** /**
* Application header with navigation and authentication controls * Application header with navigation and authentication controls
* Responsive design with mobile hamburger menu
*/ */
export function Header() { export function Header() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
return ( return (
<header className="bg-white shadow"> <header className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <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"> <div className="flex items-center">
<Link to="/" className="flex items-center"> <Link to="/" className="flex items-center" onClick={closeMobileMenu}>
<h1 className="text-2xl font-bold text-blue-600"> <h1 className="text-lg sm:text-xl md:text-2xl font-bold text-blue-600">
Beach Litter Mapping Beach Litter Mapping
</h1> </h1>
</Link> </Link>
</div> </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> <SignedIn>
<Link <Link
to="/map" 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 Map
</Link> </Link>
<Link <Link
to="/reports/new" 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>
<Link <Link
to="/events" 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 Events
</Link> </Link>
<Link <Link
to="/analytics" 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 Analytics
</Link> </Link>
@@ -50,23 +64,116 @@ export function Header() {
<SignedOut> <SignedOut>
<Link <Link
to="/map" 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 View Map
</Link> </Link>
<SignInButton mode="modal"> <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 Sign In
</button> </button>
</SignInButton> </SignInButton>
<SignUpButton mode="modal"> <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 Sign Up
</button> </button>
</SignUpButton> </SignUpButton>
</SignedOut> </SignedOut>
</nav> </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> </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> </div>
</header> </header>
); );

View File

@@ -100,14 +100,15 @@ export function MapFilters({
filters.dateRange; filters.dateRange;
return ( 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 */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200"> <div className="flex items-center justify-between p-3 sm:p-4 border-b border-gray-200">
<h3 className="font-semibold text-gray-900">Filters</h3> <h3 className="text-sm sm:text-base font-semibold text-gray-900">Filters</h3>
<button <button
onClick={() => setIsExpanded(!isExpanded)} 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'} aria-label={isExpanded ? 'Collapse filters' : 'Expand filters'}
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<svg <svg
className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-180' : ''}`} className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
@@ -122,23 +123,24 @@ export function MapFilters({
{/* Filter Content */} {/* Filter Content */}
{isExpanded && ( {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 */} {/* Cluster Toggle */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between py-1">
<label htmlFor="cluster-toggle" className="text-sm font-medium text-gray-700"> <label htmlFor="cluster-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
Cluster Markers Cluster Markers
</label> </label>
<button <button
id="cluster-toggle" id="cluster-toggle"
onClick={onToggleClusters} 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' showClusters ? 'bg-blue-600' : 'bg-gray-200'
}`} }`}
role="switch" role="switch"
aria-checked={showClusters} aria-checked={showClusters}
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<span <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' showClusters ? 'translate-x-6' : 'translate-x-1'
}`} }`}
/> />
@@ -146,21 +148,22 @@ export function MapFilters({
</div> </div>
{/* Heat Map Toggle */} {/* Heat Map Toggle */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between py-1">
<label htmlFor="heatmap-toggle" className="text-sm font-medium text-gray-700"> <label htmlFor="heatmap-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
Heat Map Heat Map
</label> </label>
<button <button
id="heatmap-toggle" id="heatmap-toggle"
onClick={onToggleHeatmap} 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' showHeatmap ? 'bg-blue-600' : 'bg-gray-200'
}`} }`}
role="switch" role="switch"
aria-checked={showHeatmap} aria-checked={showHeatmap}
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<span <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' showHeatmap ? 'translate-x-6' : 'translate-x-1'
}`} }`}
/> />
@@ -169,21 +172,22 @@ export function MapFilters({
{/* Events Toggle */} {/* Events Toggle */}
{onToggleEvents && ( {onToggleEvents && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between py-1">
<label htmlFor="events-toggle" className="text-sm font-medium text-gray-700"> <label htmlFor="events-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
Show Events Show Events
</label> </label>
<button <button
id="events-toggle" id="events-toggle"
onClick={onToggleEvents} 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' showEvents ? 'bg-green-600' : 'bg-gray-200'
}`} }`}
role="switch" role="switch"
aria-checked={showEvents} aria-checked={showEvents}
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<span <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' showEvents ? 'translate-x-6' : 'translate-x-1'
}`} }`}
/> />
@@ -193,21 +197,22 @@ export function MapFilters({
{/* Water Quality Toggle */} {/* Water Quality Toggle */}
{onToggleWaterQuality && ( {onToggleWaterQuality && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between py-1">
<label htmlFor="water-quality-toggle" className="text-sm font-medium text-gray-700"> <label htmlFor="water-quality-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
Water Quality Water Quality
</label> </label>
<button <button
id="water-quality-toggle" id="water-quality-toggle"
onClick={onToggleWaterQuality} 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' showWaterQuality ? 'bg-blue-600' : 'bg-gray-200'
}`} }`}
role="switch" role="switch"
aria-checked={showWaterQuality} aria-checked={showWaterQuality}
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<span <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' showWaterQuality ? 'translate-x-6' : 'translate-x-1'
}`} }`}
/> />
@@ -217,11 +222,11 @@ export function MapFilters({
{/* Hotspots Toggle */} {/* Hotspots Toggle */}
{onToggleHotspots && ( {onToggleHotspots && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between py-1">
<label htmlFor="hotspots-toggle" className="text-sm font-medium text-gray-700 flex items-center gap-2"> <label htmlFor="hotspots-toggle" className="text-xs sm:text-sm font-medium text-gray-700 flex items-center gap-2">
Hotspots Hotspots
{isLoadingHotspots && ( {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> <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> <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> </svg>
@@ -231,14 +236,15 @@ export function MapFilters({
id="hotspots-toggle" id="hotspots-toggle"
onClick={onToggleHotspots} onClick={onToggleHotspots}
disabled={isLoadingHotspots} 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' showHotspots ? 'bg-orange-600' : 'bg-gray-200'
} ${isLoadingHotspots ? 'opacity-50 cursor-not-allowed' : ''}`} } ${isLoadingHotspots ? 'opacity-50 cursor-not-allowed' : ''}`}
role="switch" role="switch"
aria-checked={showHotspots} aria-checked={showHotspots}
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<span <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' showHotspots ? 'translate-x-6' : 'translate-x-1'
}`} }`}
/> />
@@ -247,21 +253,22 @@ export function MapFilters({
)} )}
{/* Show Cleaned Reports Toggle */} {/* Show Cleaned Reports Toggle */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between py-1">
<label htmlFor="cleaned-toggle" className="text-sm font-medium text-gray-700"> <label htmlFor="cleaned-toggle" className="text-xs sm:text-sm font-medium text-gray-700">
Show Cleaned Reports Show Cleaned
</label> </label>
<button <button
id="cleaned-toggle" id="cleaned-toggle"
onClick={() => onFiltersChange({ ...filters, showCleaned: filters.showCleaned === false ? true : false })} 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' filters.showCleaned !== false ? 'bg-green-600' : 'bg-gray-200'
}`} }`}
role="switch" role="switch"
aria-checked={filters.showCleaned !== false} aria-checked={filters.showCleaned !== false}
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<span <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' filters.showCleaned !== false ? 'translate-x-6' : 'translate-x-1'
}`} }`}
/> />
@@ -295,7 +302,7 @@ export function MapFilters({
{/* Date Range */} {/* Date Range */}
<div> <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 Date Range
</label> </label>
<div className="space-y-2"> <div className="space-y-2">
@@ -303,34 +310,36 @@ export function MapFilters({
type="date" type="date"
value={filters.dateRange?.start ? filters.dateRange.start.toISOString().split('T')[0] : ''} value={filters.dateRange?.start ? filters.dateRange.start.toISOString().split('T')[0] : ''}
onChange={(e) => handleDateRangeChange('start', e.target.value)} 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" placeholder="Start date"
style={{ minHeight: '44px' }}
/> />
<input <input
type="date" type="date"
value={filters.dateRange?.end ? filters.dateRange.end.toISOString().split('T')[0] : ''} value={filters.dateRange?.end ? filters.dateRange.end.toISOString().split('T')[0] : ''}
onChange={(e) => handleDateRangeChange('end', e.target.value)} 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" placeholder="End date"
style={{ minHeight: '44px' }}
/> />
</div> </div>
</div> </div>
{/* Litter Type */} {/* Litter Type */}
<div> <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 Litter Type
</label> </label>
<div className="space-y-2"> <div className="space-y-2">
{litterTypes.map((type) => ( {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 <input
type="checkbox" type="checkbox"
checked={filters.litterTypes?.includes(type) || false} checked={filters.litterTypes?.includes(type) || false}
onChange={() => handleLitterTypeToggle(type)} 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> </label>
))} ))}
</div> </div>
@@ -338,19 +347,19 @@ export function MapFilters({
{/* Quantity Level */} {/* Quantity Level */}
<div> <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 Quantity Level
</label> </label>
<div className="space-y-2"> <div className="space-y-2">
{quantities.map((quantity) => ( {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 <input
type="checkbox" type="checkbox"
checked={filters.quantities?.includes(quantity) || false} checked={filters.quantities?.includes(quantity) || false}
onChange={() => handleQuantityToggle(quantity)} 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> </label>
))} ))}
</div> </div>
@@ -360,7 +369,8 @@ export function MapFilters({
{hasActiveFilters && ( {hasActiveFilters && (
<button <button
onClick={handleClearFilters} 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 Clear All Filters
</button> </button>

View File

@@ -564,16 +564,17 @@ export function MapView({
<div className="relative w-full h-full"> <div className="relative w-full h-full">
<div ref={mapRef} className="w-full h-full rounded-lg" /> <div ref={mapRef} className="w-full h-full rounded-lg" />
{/* Geolocation button */} {/* Geolocation button - optimized for touch with 44x44px minimum */}
<button <button
onClick={handleGeolocation} 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" title="Center map on your location"
aria-label="Center map on your location" aria-label="Center map on your location"
style={{ minWidth: '44px', minHeight: '44px' }}
> >
<svg <svg
xmlns="http://www.w3.org/2000/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" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"

View File

@@ -260,14 +260,14 @@ export function ReportForm() {
)} )}
{/* Progress Indicator */} {/* Progress Indicator */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{[1, 2, 3, 4].map((step) => ( {[1, 2, 3, 4].map((step) => (
<div key={step} className="flex items-center flex-1"> <div key={step} className="flex items-center flex-1">
<div className="flex flex-col items-center flex-1"> <div className="flex flex-col items-center flex-1">
<div <div
className={` 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 ${step < currentStep
? 'bg-green-500 text-white' ? 'bg-green-500 text-white'
: step === currentStep : step === currentStep
@@ -277,14 +277,14 @@ export function ReportForm() {
`} `}
> >
{step < currentStep ? ( {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" /> <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> </svg>
) : ( ) : (
step step
)} )}
</div> </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 === 1 && 'Photos'}
{step === 2 && 'Location'} {step === 2 && 'Location'}
{step === 3 && 'Details'} {step === 3 && 'Details'}
@@ -294,7 +294,7 @@ export function ReportForm() {
{step < totalSteps && ( {step < totalSteps && (
<div <div
className={` 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'} ${step < currentStep ? 'bg-green-500' : 'bg-gray-200'}
`} `}
/> />
@@ -305,13 +305,13 @@ export function ReportForm() {
</div> </div>
{/* Form Content */} {/* 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 */} {/* Step 1: Photos */}
{currentStep === 1 && ( {currentStep === 1 && (
<div className="space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Upload Photos</h2> <h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-2">Upload Photos</h2>
<p className="text-gray-600"> <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. Take or upload photos of the litter. Photos with GPS data will auto-populate the location.
</p> </p>
</div> </div>
@@ -326,10 +326,10 @@ export function ReportForm() {
{/* Step 2: Location */} {/* Step 2: Location */}
{currentStep === 2 && ( {currentStep === 2 && (
<div className="space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Select Location</h2> <h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-2">Select Location</h2>
<p className="text-gray-600"> <p className="text-sm sm:text-base text-gray-600">
Confirm or select the location where the litter was found. Confirm or select the location where the litter was found.
</p> </p>
</div> </div>
@@ -680,12 +680,13 @@ export function ReportForm() {
)} )}
{/* Navigation Buttons */} {/* 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 <button
type="button" type="button"
onClick={handleBack} onClick={handleBack}
disabled={currentStep === 1} 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 Back
</button> </button>
@@ -694,7 +695,8 @@ export function ReportForm() {
<button <button
type="button" type="button"
onClick={handleNext} 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 Next
</button> </button>
@@ -703,7 +705,8 @@ export function ReportForm() {
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} 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 ? ( {isSubmitting ? (
<> <>

View File

@@ -1,5 +1,19 @@
@import "tailwindcss"; @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 CSS overrides */
.leaflet-container { .leaflet-container {
font-family: inherit; font-family: inherit;

View File

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

View File

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