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:
@@ -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
|
||||||
|
|||||||
11
src/App.tsx
11
src/App.tsx
@@ -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>
|
||||||
|
|||||||
131
src/components/external/BeachQualityDisplay.tsx
vendored
Normal file
131
src/components/external/BeachQualityDisplay.tsx
vendored
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
src/components/external/BiodiversityInfo.tsx
vendored
Normal file
125
src/components/external/BiodiversityInfo.tsx
vendored
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/components/external/EnvironmentalDataPanel.tsx
vendored
Normal file
35
src/components/external/EnvironmentalDataPanel.tsx
vendored
Normal 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
159
src/components/external/README.md
vendored
Normal 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
107
src/components/external/TideDisplay.tsx
vendored
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/components/external/WaterQualityIndicator.tsx
vendored
Normal file
131
src/components/external/WaterQualityIndicator.tsx
vendored
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/components/external/WeatherWidget.tsx
vendored
Normal file
104
src/components/external/WeatherWidget.tsx
vendored
Normal 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
7
src/components/external/index.ts
vendored
Normal 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';
|
||||||
@@ -12,3 +12,6 @@ export * from './report';
|
|||||||
|
|
||||||
// Event components
|
// Event components
|
||||||
export * from './event';
|
export * from './event';
|
||||||
|
|
||||||
|
// External environmental data components
|
||||||
|
export * from './external';
|
||||||
|
|||||||
53
src/pages/EnvironmentalDataDemo.tsx
Normal file
53
src/pages/EnvironmentalDataDemo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
111
src/services/externalDataService.ts
Normal file
111
src/services/externalDataService.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user