feat: implement external environmental data display components

- Add WeatherWidget for current weather conditions
- Add TideDisplay for tide times and heights
- Add WaterQualityIndicator for EPA water quality data
- Add BeachQualityDisplay for beach ratings and certifications
- Add BiodiversityInfo for marine biodiversity information
- Add EnvironmentalDataPanel composite component
- Create externalDataService for API integration
- Add types for external environmental data
- Create demo page at /demo/environmental-data
- Add comprehensive README documentation

Fulfills requirements 11.1, 11.2, 11.3, 11.4, 11.6, 11.7
Completes task 19 from implementation plan
This commit is contained in:
2025-11-16 23:07:29 +00:00
parent 1e1e54e7ff
commit 2b222b65e1
16 changed files with 1041 additions and 2 deletions

View File

@@ -158,7 +158,7 @@
## External Data Integration ## External Data Integration
- [ ] 19. Create external data display components - [-] 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

View File

@@ -23,7 +23,8 @@ import {
LocationSelectorDemo, LocationSelectorDemo,
ReportDetailDemo, ReportDetailDemo,
WhyCleanPage, WhyCleanPage,
UserProfilePage UserProfilePage,
EnvironmentalDataDemo
} from './pages'; } from './pages';
// Services // Services
@@ -166,6 +167,14 @@ function App() {
</Layout> </Layout>
} }
/> />
<Route
path="/demo/environmental-data"
element={
<Layout>
<EnvironmentalDataDemo />
</Layout>
}
/>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</ApiInitializer> </ApiInitializer>

View File

@@ -0,0 +1,131 @@
// Beach quality rating display component
import { useEffect, useState } from 'react';
import { format } from 'date-fns';
import type { BeachQualityData } from '@/types';
import { externalDataService } from '@/services/externalDataService';
interface BeachQualityDisplayProps {
latitude: number;
longitude: number;
}
export function BeachQualityDisplay({ latitude, longitude }: BeachQualityDisplayProps) {
const [beachQuality, setBeachQuality] = useState<BeachQualityData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
const fetchBeachQuality = async () => {
try {
setLoading(true);
setError(null);
const data = await externalDataService.getBeachQuality(latitude, longitude);
if (mounted) {
setBeachQuality(data);
}
} catch (err) {
if (mounted) {
setError('Unable to load beach quality data');
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchBeachQuality();
return () => {
mounted = false;
};
}, [latitude, longitude]);
if (loading) {
return (
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-600">Loading beach quality...</span>
</div>
</div>
);
}
if (error || !beachQuality) {
return (
<div className="bg-white rounded-lg shadow p-4">
<p className="text-sm text-gray-500">Beach quality data unavailable</p>
</div>
);
}
const ratingConfig = {
'blue-flag': { color: 'text-blue-600', icon: '🏴', label: 'Blue Flag Award' },
'green-coast': { color: 'text-green-600', icon: '🌿', label: 'Green Coast Award' },
'excellent': { color: 'text-green-600', icon: '⭐', label: 'Excellent' },
'good': { color: 'text-blue-600', icon: '✓', label: 'Good' },
'adequate': { color: 'text-yellow-600', icon: '○', label: 'Adequate' },
'poor': { color: 'text-red-600', icon: '✗', label: 'Poor' },
'unknown': { color: 'text-gray-500', icon: '?', label: 'Unknown' },
};
const config = ratingConfig[beachQuality.rating];
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-3 flex items-center">
<span className="mr-2">🏖️</span>
Beach Quality
</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3">
<span className="text-3xl">{config.icon}</span>
<div>
<div className={`font-semibold text-lg ${config.color}`}>
{config.label}
</div>
{beachQuality.lastInspection && (
<div className="text-xs text-gray-500">
Last inspection: {format(new Date(beachQuality.lastInspection), 'MMM d, yyyy')}
</div>
)}
</div>
</div>
{beachQuality.awards && beachQuality.awards.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-sm font-medium text-gray-700 mb-2">Awards & Certifications</div>
<div className="flex flex-wrap gap-2">
{beachQuality.awards.map((award, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{award}
</span>
))}
</div>
</div>
)}
{beachQuality.facilities && beachQuality.facilities.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-sm font-medium text-gray-700 mb-2">Facilities</div>
<div className="grid grid-cols-2 gap-2 text-sm">
{beachQuality.facilities.map((facility, index) => (
<div key={index} className="flex items-center space-x-1">
<span className="text-green-500">✓</span>
<span className="text-gray-700">{facility}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,125 @@
// Marine biodiversity information component
import { useEffect, useState } from 'react';
import type { BiodiversityData } from '@/types';
import { externalDataService } from '@/services/externalDataService';
interface BiodiversityInfoProps {
latitude: number;
longitude: number;
}
export function BiodiversityInfo({ latitude, longitude }: BiodiversityInfoProps) {
const [biodiversity, setBiodiversity] = useState<BiodiversityData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
const fetchBiodiversity = async () => {
try {
setLoading(true);
setError(null);
const data = await externalDataService.getBiodiversity(latitude, longitude);
if (mounted) {
setBiodiversity(data);
}
} catch (err) {
if (mounted) {
setError('Unable to load biodiversity data');
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchBiodiversity();
return () => {
mounted = false;
};
}, [latitude, longitude]);
if (loading) {
return (
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-600">Loading biodiversity data...</span>
</div>
</div>
);
}
if (error || !biodiversity) {
return (
<div className="bg-white rounded-lg shadow p-4">
<p className="text-sm text-gray-500">Biodiversity data unavailable</p>
</div>
);
}
// Don't render if there's no meaningful data
if (!biodiversity.species?.length && !biodiversity.protectedArea && !biodiversity.habitatType) {
return null;
}
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-3 flex items-center">
<span className="mr-2">🐚</span>
Marine Biodiversity
</h3>
<div className="space-y-3">
{biodiversity.protectedArea && (
<div className="p-3 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-center space-x-2">
<span className="text-green-600 text-xl">🛡️</span>
<span className="text-sm font-medium text-green-800">Protected Area</span>
</div>
</div>
)}
{biodiversity.habitatType && (
<div>
<div className="text-sm font-medium text-gray-700 mb-1">Habitat Type</div>
<div className="text-sm text-gray-600">{biodiversity.habitatType}</div>
</div>
)}
{biodiversity.conservationStatus && (
<div>
<div className="text-sm font-medium text-gray-700 mb-1">Conservation Status</div>
<div className="text-sm text-gray-600">{biodiversity.conservationStatus}</div>
</div>
)}
{biodiversity.species && biodiversity.species.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-sm font-medium text-gray-700 mb-2">
Notable Species ({biodiversity.species.length})
</div>
<div className="space-y-2 max-h-48 overflow-y-auto">
{biodiversity.species.map((species, index) => (
<div key={index} className="p-2 bg-gray-50 rounded text-sm">
<div className="font-medium text-gray-800">{species.name}</div>
{species.scientificName && (
<div className="text-xs text-gray-500 italic">{species.scientificName}</div>
)}
{species.status && (
<div className="text-xs text-gray-600 mt-1">
Status: {species.status}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
// Composite panel displaying all external environmental data
import { WeatherWidget } from './WeatherWidget';
import { TideDisplay } from './TideDisplay';
import { WaterQualityIndicator } from './WaterQualityIndicator';
import { BeachQualityDisplay } from './BeachQualityDisplay';
import { BiodiversityInfo } from './BiodiversityInfo';
interface EnvironmentalDataPanelProps {
latitude: number;
longitude: number;
className?: string;
}
export function EnvironmentalDataPanel({
latitude,
longitude,
className = '',
}: EnvironmentalDataPanelProps) {
return (
<div className={`space-y-4 ${className}`}>
<div className="mb-4">
<h2 className="text-xl font-bold text-gray-800 mb-2">Environmental Context</h2>
<p className="text-sm text-gray-600">
Real-time environmental data for this location
</p>
</div>
<WeatherWidget latitude={latitude} longitude={longitude} />
<TideDisplay latitude={latitude} longitude={longitude} />
<WaterQualityIndicator latitude={latitude} longitude={longitude} />
<BeachQualityDisplay latitude={latitude} longitude={longitude} />
<BiodiversityInfo latitude={latitude} longitude={longitude} />
</div>
);
}

159
src/components/external/README.md vendored Normal file
View File

@@ -0,0 +1,159 @@
# External Environmental Data Components
This directory contains components for displaying external environmental data from various public APIs and data sources.
## Components
### WeatherWidget
Displays current weather conditions for a location including:
- Temperature and "feels like" temperature
- Humidity percentage
- Wind speed and direction
- Weather description
**Props:**
- `latitude: number` - Location latitude
- `longitude: number` - Location longitude
**API Endpoint:** `GET /api/external/weather/:lat/:lng`
### TideDisplay
Shows tide information for coastal locations including:
- Next tide time and type (high/low)
- Today's tide schedule with times and heights
- Tide heights in meters
**Props:**
- `latitude: number` - Location latitude
- `longitude: number` - Location longitude
**API Endpoint:** `GET /api/external/tides/:lat/:lng`
### WaterQualityIndicator
Displays water quality status from EPA data:
- Overall quality rating (excellent, good, sufficient, poor)
- Water quality parameters (bacteria, pH, temperature)
- Last update date
- Data source information
**Props:**
- `latitude: number` - Location latitude
- `longitude: number` - Location longitude
**API Endpoint:** `GET /api/external/water-quality/:lat/:lng`
### BeachQualityDisplay
Shows beach quality ratings and certifications:
- Quality rating (Blue Flag, Green Coast, etc.)
- Awards and certifications
- Available facilities
- Last inspection date
**Props:**
- `latitude: number` - Location latitude
- `longitude: number` - Location longitude
**API Endpoint:** `GET /api/external/beach-quality/:lat/:lng`
### BiodiversityInfo
Displays marine biodiversity information:
- Protected area status
- Habitat type
- Conservation status
- Notable species list with scientific names
**Props:**
- `latitude: number` - Location latitude
- `longitude: number` - Location longitude
**API Endpoint:** `GET /api/external/biodiversity/:lat/:lng`
**Note:** This component only renders if meaningful data is available.
### EnvironmentalDataPanel
Composite component that displays all external environmental data widgets together.
**Props:**
- `latitude: number` - Location latitude
- `longitude: number` - Location longitude
- `className?: string` - Optional CSS classes
## Usage
### Individual Components
```tsx
import { WeatherWidget, TideDisplay } from '@/components/external';
function MyComponent() {
return (
<div>
<WeatherWidget latitude={53.3498} longitude={-6.2603} />
<TideDisplay latitude={53.3498} longitude={-6.2603} />
</div>
);
}
```
### Composite Panel
```tsx
import { EnvironmentalDataPanel } from '@/components/external';
function ReportDetails({ report }) {
return (
<div>
<h2>Report Details</h2>
{/* ... report info ... */}
<EnvironmentalDataPanel
latitude={report.latitude}
longitude={report.longitude}
/>
</div>
);
}
```
## Error Handling
All components handle loading and error states gracefully:
- **Loading state:** Shows a spinner with loading message
- **Error state:** Displays "data unavailable" message
- **No data:** Components hide themselves or show appropriate message
Components use `try-catch` blocks and never throw errors to parent components.
## Data Caching
External API responses are cached on the backend for up to 6 hours to:
- Minimize API calls to external services
- Improve performance
- Reduce costs
- Ensure data availability
## Backend Requirements
These components require the following backend API endpoints to be implemented:
1. `GET /api/external/weather/:lat/:lng` - Weather data from OpenWeatherMap
2. `GET /api/external/tides/:lat/:lng` - Tide data from WorldTides or NOAA
3. `GET /api/external/water-quality/:lat/:lng` - EPA water quality data
4. `GET /api/external/beach-quality/:lat/:lng` - Beach quality ratings
5. `GET /api/external/biodiversity/:lat/:lng` - Marine biodiversity information
See the design document for detailed API specifications and data formats.
## Demo
Visit `/demo/environmental-data` to see all components in action with sample locations.
## Requirements Fulfilled
These components fulfill the following requirements:
- **11.1** - Weather data integration
- **11.2** - Tide information display
- **11.3** - Water quality data from EPA
- **11.4** - Beach quality ratings
- **11.6** - Beach quality from EU databases
- **11.7** - Marine biodiversity information

107
src/components/external/TideDisplay.tsx vendored Normal file
View File

@@ -0,0 +1,107 @@
// Tide times display component
import { useEffect, useState } from 'react';
import { format } from 'date-fns';
import type { TideData } from '@/types';
import { externalDataService } from '@/services/externalDataService';
interface TideDisplayProps {
latitude: number;
longitude: number;
}
export function TideDisplay({ latitude, longitude }: TideDisplayProps) {
const [tideData, setTideData] = useState<TideData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
const fetchTides = async () => {
try {
setLoading(true);
setError(null);
const data = await externalDataService.getTides(latitude, longitude);
if (mounted) {
setTideData(data);
}
} catch (err) {
if (mounted) {
setError('Unable to load tide data');
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchTides();
return () => {
mounted = false;
};
}, [latitude, longitude]);
if (loading) {
return (
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-600">Loading tide data...</span>
</div>
</div>
);
}
if (error || !tideData) {
return (
<div className="bg-white rounded-lg shadow p-4">
<p className="text-sm text-gray-500">Tide data unavailable</p>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-3 flex items-center">
<span className="mr-2">🌊</span>
Tide Times
</h3>
{tideData.nextTide && (
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
<div className="text-sm text-gray-600 mb-1">Next Tide</div>
<div className="flex items-center justify-between">
<span className="font-semibold capitalize">{tideData.nextTide.type} Tide</span>
<span className="text-sm">{format(new Date(tideData.nextTide.time), 'HH:mm')}</span>
</div>
<div className="text-sm text-gray-600 mt-1">
Height: {tideData.nextTide.height.toFixed(2)}m
</div>
</div>
)}
<div className="space-y-2">
<div className="text-sm font-medium text-gray-700 mb-2">Today's Tides</div>
{tideData.tides.slice(0, 4).map((tide, index) => (
<div
key={index}
className="flex items-center justify-between py-2 border-b border-gray-100 last:border-0"
>
<div className="flex items-center space-x-3">
<span className="text-lg">{tide.type === 'high' ? '⬆️' : '⬇️'}</span>
<div>
<div className="font-medium capitalize">{tide.type} Tide</div>
<div className="text-xs text-gray-500">
{format(new Date(tide.time), 'HH:mm')}
</div>
</div>
</div>
<div className="text-sm text-gray-600">{tide.height.toFixed(2)}m</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,131 @@
// Water quality indicator component
import { useEffect, useState } from 'react';
import { format } from 'date-fns';
import type { WaterQualityData } from '@/types';
import { externalDataService } from '@/services/externalDataService';
interface WaterQualityIndicatorProps {
latitude: number;
longitude: number;
}
export function WaterQualityIndicator({ latitude, longitude }: WaterQualityIndicatorProps) {
const [waterQuality, setWaterQuality] = useState<WaterQualityData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
const fetchWaterQuality = async () => {
try {
setLoading(true);
setError(null);
const data = await externalDataService.getWaterQuality(latitude, longitude);
if (mounted) {
setWaterQuality(data);
}
} catch (err) {
if (mounted) {
setError('Unable to load water quality data');
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchWaterQuality();
return () => {
mounted = false;
};
}, [latitude, longitude]);
if (loading) {
return (
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-600">Loading water quality...</span>
</div>
</div>
);
}
if (error || !waterQuality) {
return (
<div className="bg-white rounded-lg shadow p-4">
<p className="text-sm text-gray-500">Water quality data unavailable</p>
</div>
);
}
const statusConfig = {
excellent: { color: 'bg-green-500', text: 'Excellent', icon: '✓' },
good: { color: 'bg-blue-500', text: 'Good', icon: '✓' },
sufficient: { color: 'bg-yellow-500', text: 'Sufficient', icon: '!' },
poor: { color: 'bg-red-500', text: 'Poor', icon: '✗' },
unknown: { color: 'bg-gray-400', text: 'Unknown', icon: '?' },
};
const config = statusConfig[waterQuality.status];
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-3 flex items-center">
<span className="mr-2">💧</span>
Water Quality
</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3">
<div className={`w-12 h-12 ${config.color} rounded-full flex items-center justify-center text-white text-xl font-bold`}>
{config.icon}
</div>
<div>
<div className="font-semibold text-lg">{config.text}</div>
{waterQuality.lastUpdated && (
<div className="text-xs text-gray-500">
Updated: {format(new Date(waterQuality.lastUpdated), 'MMM d, yyyy')}
</div>
)}
</div>
</div>
{waterQuality.parameters && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-sm font-medium text-gray-700 mb-2">Parameters</div>
<div className="grid grid-cols-2 gap-2 text-sm">
{waterQuality.parameters.bacteria !== undefined && (
<div>
<span className="text-gray-600">Bacteria:</span>
<span className="ml-2 font-medium">{waterQuality.parameters.bacteria} CFU/100ml</span>
</div>
)}
{waterQuality.parameters.ph !== undefined && (
<div>
<span className="text-gray-600">pH:</span>
<span className="ml-2 font-medium">{waterQuality.parameters.ph.toFixed(1)}</span>
</div>
)}
{waterQuality.parameters.temperature !== undefined && (
<div>
<span className="text-gray-600">Temp:</span>
<span className="ml-2 font-medium">{waterQuality.parameters.temperature}°C</span>
</div>
)}
</div>
</div>
)}
{waterQuality.source && (
<div className="text-xs text-gray-500 mt-2">
Source: {waterQuality.source}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,104 @@
// Weather information widget component
import { useEffect, useState } from 'react';
import type { WeatherData } from '@/types';
import { externalDataService } from '@/services/externalDataService';
interface WeatherWidgetProps {
latitude: number;
longitude: number;
}
export function WeatherWidget({ latitude, longitude }: WeatherWidgetProps) {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
const fetchWeather = async () => {
try {
setLoading(true);
setError(null);
const data = await externalDataService.getWeather(latitude, longitude);
if (mounted) {
setWeather(data);
}
} catch (err) {
if (mounted) {
setError('Unable to load weather data');
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchWeather();
return () => {
mounted = false;
};
}, [latitude, longitude]);
if (loading) {
return (
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-600">Loading weather...</span>
</div>
</div>
);
}
if (error || !weather) {
return (
<div className="bg-white rounded-lg shadow p-4">
<p className="text-sm text-gray-500">Weather data unavailable</p>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-3 flex items-center">
<span className="mr-2">🌤️</span>
Weather Conditions
</h3>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-2xl font-bold">{Math.round(weather.temperature)}°C</span>
<span className="text-gray-600 capitalize">{weather.description}</span>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-600">Feels like:</span>
<span className="ml-2 font-medium">{Math.round(weather.feelsLike)}°C</span>
</div>
<div>
<span className="text-gray-600">Humidity:</span>
<span className="ml-2 font-medium">{weather.humidity}%</span>
</div>
<div>
<span className="text-gray-600">Wind:</span>
<span className="ml-2 font-medium">{Math.round(weather.windSpeed * 3.6)} km/h</span>
</div>
<div>
<span className="text-gray-600">Direction:</span>
<span className="ml-2 font-medium">{getWindDirection(weather.windDirection)}</span>
</div>
</div>
</div>
</div>
);
}
function getWindDirection(degrees: number): string {
const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const index = Math.round(degrees / 45) % 8;
return directions[index] || 'N';
}

7
src/components/external/index.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
// External environmental data components
export { WeatherWidget } from './WeatherWidget';
export { TideDisplay } from './TideDisplay';
export { WaterQualityIndicator } from './WaterQualityIndicator';
export { BeachQualityDisplay } from './BeachQualityDisplay';
export { BiodiversityInfo } from './BiodiversityInfo';
export { EnvironmentalDataPanel } from './EnvironmentalDataPanel';

View File

@@ -12,3 +12,6 @@ export * from './report';
// Event components // Event components
export * from './event'; export * from './event';
// External environmental data components
export * from './external';

View File

@@ -0,0 +1,53 @@
// Demo page for external environmental data components
import { useState } from 'react';
import { EnvironmentalDataPanel } from '@/components/external';
export function EnvironmentalDataDemo() {
// Default to a location in Ireland (Dublin Bay)
const [latitude, setLatitude] = useState(53.3498);
const [longitude, setLongitude] = useState(-6.2603);
const handleLocationChange = () => {
// Example: Switch to another Irish coastal location (Galway Bay)
if (latitude === 53.3498) {
setLatitude(53.2707);
setLongitude(-9.0568);
} else {
setLatitude(53.3498);
setLongitude(-6.2603);
}
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Environmental Data Display
</h1>
<p className="text-gray-600">
View real-time environmental context for coastal locations
</p>
</div>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Current Location</h2>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Latitude: {latitude.toFixed(4)}</p>
<p className="text-sm text-gray-600">Longitude: {longitude.toFixed(4)}</p>
</div>
<button
onClick={handleLocationChange}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Switch Location
</button>
</div>
</div>
<EnvironmentalDataPanel latitude={latitude} longitude={longitude} />
</div>
</div>
);
}

View File

@@ -8,3 +8,4 @@ export { default as LocationSelectorDemo } from './LocationSelectorDemo';
export { ReportDetailDemo } from './ReportDetailDemo'; export { ReportDetailDemo } from './ReportDetailDemo';
export { WhyCleanPage } from './WhyCleanPage'; export { WhyCleanPage } from './WhyCleanPage';
export { UserProfilePage } from './UserProfilePage'; export { UserProfilePage } from './UserProfilePage';
export { EnvironmentalDataDemo } from './EnvironmentalDataDemo';

View File

@@ -0,0 +1,111 @@
// External environmental data service
import { apiClient } from './api';
import type {
WeatherData,
TideData,
WaterQualityData,
BeachQualityData,
BiodiversityData,
ExternalEnvironmentalData,
} from '@/types';
export const externalDataService = {
/**
* Fetch weather data for a location
*/
async getWeather(latitude: number, longitude: number): Promise<WeatherData | null> {
try {
const data = await apiClient.get<WeatherData>(
`/external/weather/${latitude}/${longitude}`
);
return data;
} catch (error) {
console.error('Failed to fetch weather data:', error);
return null;
}
},
/**
* Fetch tide information for a location
*/
async getTides(latitude: number, longitude: number): Promise<TideData | null> {
try {
const data = await apiClient.get<TideData>(
`/external/tides/${latitude}/${longitude}`
);
return data;
} catch (error) {
console.error('Failed to fetch tide data:', error);
return null;
}
},
/**
* Fetch water quality data for a location
*/
async getWaterQuality(latitude: number, longitude: number): Promise<WaterQualityData | null> {
try {
const data = await apiClient.get<WaterQualityData>(
`/external/water-quality/${latitude}/${longitude}`
);
return data;
} catch (error) {
console.error('Failed to fetch water quality data:', error);
return null;
}
},
/**
* Fetch beach quality rating for a location
*/
async getBeachQuality(latitude: number, longitude: number): Promise<BeachQualityData | null> {
try {
const data = await apiClient.get<BeachQualityData>(
`/external/beach-quality/${latitude}/${longitude}`
);
return data;
} catch (error) {
console.error('Failed to fetch beach quality data:', error);
return null;
}
},
/**
* Fetch marine biodiversity information for a location
*/
async getBiodiversity(latitude: number, longitude: number): Promise<BiodiversityData | null> {
try {
const data = await apiClient.get<BiodiversityData>(
`/external/biodiversity/${latitude}/${longitude}`
);
return data;
} catch (error) {
console.error('Failed to fetch biodiversity data:', error);
return null;
}
},
/**
* Fetch all available external environmental data for a location
*/
async getAllEnvironmentalData(
latitude: number,
longitude: number
): Promise<ExternalEnvironmentalData> {
const [weather, tides, waterQuality, beachQuality, biodiversity] = await Promise.all([
this.getWeather(latitude, longitude),
this.getTides(latitude, longitude),
this.getWaterQuality(latitude, longitude),
this.getBeachQuality(latitude, longitude),
this.getBiodiversity(latitude, longitude),
]);
return {
weather: weather || undefined,
tides: tides || undefined,
waterQuality: waterQuality || undefined,
beachQuality: beachQuality || undefined,
biodiversity: biodiversity || undefined,
};
},
};

View File

@@ -4,6 +4,7 @@ export { reportService } from './reportService';
export { analyticsService } from './analyticsService'; export { analyticsService } from './analyticsService';
export { eventService } from './eventService'; export { eventService } from './eventService';
export { userService } from './userService'; export { userService } from './userService';
export { externalDataService } from './externalDataService';
export type { ApiError } from './api'; export type { ApiError } from './api';
export type { CreateReportPayload, UploadPhotoResponse } from './reportService'; export type { CreateReportPayload, UploadPhotoResponse } from './reportService';
export type { CreateEventDTO, UpdateEventDTO, EventFilters, EventRegistration } from './eventService'; export type { CreateEventDTO, UpdateEventDTO, EventFilters, EventRegistration } from './eventService';

View File

@@ -179,3 +179,65 @@ export interface QuantityAggregation {
count: number; count: number;
percentage: number; percentage: number;
} }
// External environmental data types
export interface WeatherData {
temperature: number; // Celsius
feelsLike: number;
humidity: number; // percentage
windSpeed: number; // m/s
windDirection: number; // degrees
description: string;
icon: string;
timestamp: Date;
}
export interface TideData {
tides: Array<{
time: Date;
height: number; // meters
type: 'high' | 'low';
}>;
nextTide?: {
time: Date;
height: number;
type: 'high' | 'low';
};
}
export interface WaterQualityData {
status: 'excellent' | 'good' | 'sufficient' | 'poor' | 'unknown';
lastUpdated?: Date;
parameters?: {
bacteria?: number;
ph?: number;
temperature?: number;
};
source?: string;
}
export interface BeachQualityData {
rating: 'blue-flag' | 'green-coast' | 'excellent' | 'good' | 'adequate' | 'poor' | 'unknown';
awards?: string[];
facilities?: string[];
lastInspection?: Date;
}
export interface BiodiversityData {
species?: Array<{
name: string;
scientificName?: string;
status?: string;
}>;
protectedArea?: boolean;
habitatType?: string;
conservationStatus?: string;
}
export interface ExternalEnvironmentalData {
weather?: WeatherData;
tides?: TideData;
waterQuality?: WaterQualityData;
beachQuality?: BeachQualityData;
biodiversity?: BiodiversityData;
}