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:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user