From 2b222b65e11b364985bd73665ae33e5e15aead40 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Sun, 16 Nov 2025 23:07:29 +0000 Subject: [PATCH] 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 --- .kiro/specs/beach-litter-mapping/tasks.md | 2 +- src/App.tsx | 11 +- .../external/BeachQualityDisplay.tsx | 131 +++++++++++++++ src/components/external/BiodiversityInfo.tsx | 125 ++++++++++++++ .../external/EnvironmentalDataPanel.tsx | 35 ++++ src/components/external/README.md | 159 ++++++++++++++++++ src/components/external/TideDisplay.tsx | 107 ++++++++++++ .../external/WaterQualityIndicator.tsx | 131 +++++++++++++++ src/components/external/WeatherWidget.tsx | 104 ++++++++++++ src/components/external/index.ts | 7 + src/components/index.ts | 3 + src/pages/EnvironmentalDataDemo.tsx | 53 ++++++ src/pages/index.ts | 1 + src/services/externalDataService.ts | 111 ++++++++++++ src/services/index.ts | 1 + src/types/index.ts | 62 +++++++ 16 files changed, 1041 insertions(+), 2 deletions(-) create mode 100644 src/components/external/BeachQualityDisplay.tsx create mode 100644 src/components/external/BiodiversityInfo.tsx create mode 100644 src/components/external/EnvironmentalDataPanel.tsx create mode 100644 src/components/external/README.md create mode 100644 src/components/external/TideDisplay.tsx create mode 100644 src/components/external/WaterQualityIndicator.tsx create mode 100644 src/components/external/WeatherWidget.tsx create mode 100644 src/components/external/index.ts create mode 100644 src/pages/EnvironmentalDataDemo.tsx create mode 100644 src/services/externalDataService.ts diff --git a/.kiro/specs/beach-litter-mapping/tasks.md b/.kiro/specs/beach-litter-mapping/tasks.md index 495ddd1..7e9a7b1 100644 --- a/.kiro/specs/beach-litter-mapping/tasks.md +++ b/.kiro/specs/beach-litter-mapping/tasks.md @@ -158,7 +158,7 @@ ## External Data Integration -- [ ] 19. Create external data display components +- [-] 19. Create external data display components - Build weather information widget - Implement tide times display - Add water quality indicator diff --git a/src/App.tsx b/src/App.tsx index b61aa40..791e865 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,8 @@ import { LocationSelectorDemo, ReportDetailDemo, WhyCleanPage, - UserProfilePage + UserProfilePage, + EnvironmentalDataDemo } from './pages'; // Services @@ -166,6 +167,14 @@ function App() { } /> + + + + } + /> diff --git a/src/components/external/BeachQualityDisplay.tsx b/src/components/external/BeachQualityDisplay.tsx new file mode 100644 index 0000000..9334ae0 --- /dev/null +++ b/src/components/external/BeachQualityDisplay.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+
+ Loading beach quality... +
+
+ ); + } + + if (error || !beachQuality) { + return ( +
+

Beach quality data unavailable

+
+ ); + } + + 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 ( +
+

+ 🏖️ + Beach Quality +

+ +
+
+ {config.icon} +
+
+ {config.label} +
+ {beachQuality.lastInspection && ( +
+ Last inspection: {format(new Date(beachQuality.lastInspection), 'MMM d, yyyy')} +
+ )} +
+
+ + {beachQuality.awards && beachQuality.awards.length > 0 && ( +
+
Awards & Certifications
+
+ {beachQuality.awards.map((award, index) => ( + + {award} + + ))} +
+
+ )} + + {beachQuality.facilities && beachQuality.facilities.length > 0 && ( +
+
Facilities
+
+ {beachQuality.facilities.map((facility, index) => ( +
+ ✓ + {facility} +
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/src/components/external/BiodiversityInfo.tsx b/src/components/external/BiodiversityInfo.tsx new file mode 100644 index 0000000..9eaf1eb --- /dev/null +++ b/src/components/external/BiodiversityInfo.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+
+ Loading biodiversity data... +
+
+ ); + } + + if (error || !biodiversity) { + return ( +
+

Biodiversity data unavailable

+
+ ); + } + + // Don't render if there's no meaningful data + if (!biodiversity.species?.length && !biodiversity.protectedArea && !biodiversity.habitatType) { + return null; + } + + return ( +
+

+ 🐚 + Marine Biodiversity +

+ +
+ {biodiversity.protectedArea && ( +
+
+ 🛡️ + Protected Area +
+
+ )} + + {biodiversity.habitatType && ( +
+
Habitat Type
+
{biodiversity.habitatType}
+
+ )} + + {biodiversity.conservationStatus && ( +
+
Conservation Status
+
{biodiversity.conservationStatus}
+
+ )} + + {biodiversity.species && biodiversity.species.length > 0 && ( +
+
+ Notable Species ({biodiversity.species.length}) +
+
+ {biodiversity.species.map((species, index) => ( +
+
{species.name}
+ {species.scientificName && ( +
{species.scientificName}
+ )} + {species.status && ( +
+ Status: {species.status} +
+ )} +
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/src/components/external/EnvironmentalDataPanel.tsx b/src/components/external/EnvironmentalDataPanel.tsx new file mode 100644 index 0000000..201736f --- /dev/null +++ b/src/components/external/EnvironmentalDataPanel.tsx @@ -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 ( +
+
+

Environmental Context

+

+ Real-time environmental data for this location +

+
+ + + + + + +
+ ); +} diff --git a/src/components/external/README.md b/src/components/external/README.md new file mode 100644 index 0000000..8e32437 --- /dev/null +++ b/src/components/external/README.md @@ -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 ( +
+ + +
+ ); +} +``` + +### Composite Panel + +```tsx +import { EnvironmentalDataPanel } from '@/components/external'; + +function ReportDetails({ report }) { + return ( +
+

Report Details

+ {/* ... report info ... */} + + +
+ ); +} +``` + +## 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 diff --git a/src/components/external/TideDisplay.tsx b/src/components/external/TideDisplay.tsx new file mode 100644 index 0000000..ba2f0a7 --- /dev/null +++ b/src/components/external/TideDisplay.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+
+ Loading tide data... +
+
+ ); + } + + if (error || !tideData) { + return ( +
+

Tide data unavailable

+
+ ); + } + + return ( +
+

+ 🌊 + Tide Times +

+ + {tideData.nextTide && ( +
+
Next Tide
+
+ {tideData.nextTide.type} Tide + {format(new Date(tideData.nextTide.time), 'HH:mm')} +
+
+ Height: {tideData.nextTide.height.toFixed(2)}m +
+
+ )} + +
+
Today's Tides
+ {tideData.tides.slice(0, 4).map((tide, index) => ( +
+
+ {tide.type === 'high' ? '⬆️' : '⬇️'} +
+
{tide.type} Tide
+
+ {format(new Date(tide.time), 'HH:mm')} +
+
+
+
{tide.height.toFixed(2)}m
+
+ ))} +
+
+ ); +} diff --git a/src/components/external/WaterQualityIndicator.tsx b/src/components/external/WaterQualityIndicator.tsx new file mode 100644 index 0000000..b39cd5f --- /dev/null +++ b/src/components/external/WaterQualityIndicator.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+
+ Loading water quality... +
+
+ ); + } + + if (error || !waterQuality) { + return ( +
+

Water quality data unavailable

+
+ ); + } + + 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 ( +
+

+ 💧 + Water Quality +

+ +
+
+
+ {config.icon} +
+
+
{config.text}
+ {waterQuality.lastUpdated && ( +
+ Updated: {format(new Date(waterQuality.lastUpdated), 'MMM d, yyyy')} +
+ )} +
+
+ + {waterQuality.parameters && ( +
+
Parameters
+
+ {waterQuality.parameters.bacteria !== undefined && ( +
+ Bacteria: + {waterQuality.parameters.bacteria} CFU/100ml +
+ )} + {waterQuality.parameters.ph !== undefined && ( +
+ pH: + {waterQuality.parameters.ph.toFixed(1)} +
+ )} + {waterQuality.parameters.temperature !== undefined && ( +
+ Temp: + {waterQuality.parameters.temperature}°C +
+ )} +
+
+ )} + + {waterQuality.source && ( +
+ Source: {waterQuality.source} +
+ )} +
+
+ ); +} diff --git a/src/components/external/WeatherWidget.tsx b/src/components/external/WeatherWidget.tsx new file mode 100644 index 0000000..e438c6c --- /dev/null +++ b/src/components/external/WeatherWidget.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+
+ Loading weather... +
+
+ ); + } + + if (error || !weather) { + return ( +
+

Weather data unavailable

+
+ ); + } + + return ( +
+

+ 🌤️ + Weather Conditions +

+ +
+
+ {Math.round(weather.temperature)}°C + {weather.description} +
+ +
+
+ Feels like: + {Math.round(weather.feelsLike)}°C +
+
+ Humidity: + {weather.humidity}% +
+
+ Wind: + {Math.round(weather.windSpeed * 3.6)} km/h +
+
+ Direction: + {getWindDirection(weather.windDirection)} +
+
+
+
+ ); +} + +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'; +} diff --git a/src/components/external/index.ts b/src/components/external/index.ts new file mode 100644 index 0000000..55fd093 --- /dev/null +++ b/src/components/external/index.ts @@ -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'; diff --git a/src/components/index.ts b/src/components/index.ts index 9b228d1..add77c7 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -12,3 +12,6 @@ export * from './report'; // Event components export * from './event'; + +// External environmental data components +export * from './external'; diff --git a/src/pages/EnvironmentalDataDemo.tsx b/src/pages/EnvironmentalDataDemo.tsx new file mode 100644 index 0000000..2854ae3 --- /dev/null +++ b/src/pages/EnvironmentalDataDemo.tsx @@ -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 ( +
+
+
+

+ Environmental Data Display +

+

+ View real-time environmental context for coastal locations +

+
+ +
+

Current Location

+
+
+

Latitude: {latitude.toFixed(4)}

+

Longitude: {longitude.toFixed(4)}

+
+ +
+
+ + +
+
+ ); +} diff --git a/src/pages/index.ts b/src/pages/index.ts index fa1bbf4..6eeafc3 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -8,3 +8,4 @@ export { default as LocationSelectorDemo } from './LocationSelectorDemo'; export { ReportDetailDemo } from './ReportDetailDemo'; export { WhyCleanPage } from './WhyCleanPage'; export { UserProfilePage } from './UserProfilePage'; +export { EnvironmentalDataDemo } from './EnvironmentalDataDemo'; diff --git a/src/services/externalDataService.ts b/src/services/externalDataService.ts new file mode 100644 index 0000000..8b643e3 --- /dev/null +++ b/src/services/externalDataService.ts @@ -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 { + try { + const data = await apiClient.get( + `/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 { + try { + const data = await apiClient.get( + `/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 { + try { + const data = await apiClient.get( + `/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 { + try { + const data = await apiClient.get( + `/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 { + try { + const data = await apiClient.get( + `/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 { + 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, + }; + }, +}; diff --git a/src/services/index.ts b/src/services/index.ts index 8ad9a2b..f381384 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -4,6 +4,7 @@ export { reportService } from './reportService'; export { analyticsService } from './analyticsService'; export { eventService } from './eventService'; export { userService } from './userService'; +export { externalDataService } from './externalDataService'; export type { ApiError } from './api'; export type { CreateReportPayload, UploadPhotoResponse } from './reportService'; export type { CreateEventDTO, UpdateEventDTO, EventFilters, EventRegistration } from './eventService'; diff --git a/src/types/index.ts b/src/types/index.ts index 3c4a119..30d86e3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -179,3 +179,65 @@ export interface QuantityAggregation { count: 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; +}