init
This commit is contained in:
161
frontend/app/page.tsx
Normal file
161
frontend/app/page.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client'
|
||||
|
||||
import { useCurrentWeather, useForecast, useHistorical } from '@/hooks/use-weather'
|
||||
import { AssessmentBadge } from '@/components/weather/assessment-badge'
|
||||
import { WindSpeedChart } from '@/components/weather/wind-speed-chart'
|
||||
import { WindDirectionChart } from '@/components/weather/wind-direction-chart'
|
||||
import { ThresholdControls } from '@/components/weather/threshold-controls'
|
||||
import { RefreshCountdown } from '@/components/weather/refresh-countdown'
|
||||
import { StaleDataBanner } from '@/components/weather/stale-data-banner'
|
||||
import { Collapsible } from '@/components/ui/collapsible'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Loader2, AlertCircle } from 'lucide-react'
|
||||
import { subDays, format } from 'date-fns'
|
||||
|
||||
export default function DashboardPage() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Fetch current weather and forecast
|
||||
const {
|
||||
data: currentWeather,
|
||||
isLoading: isLoadingCurrent,
|
||||
error: currentError,
|
||||
} = useCurrentWeather()
|
||||
|
||||
const {
|
||||
data: forecast,
|
||||
isLoading: isLoadingForecast,
|
||||
error: forecastError,
|
||||
} = useForecast()
|
||||
|
||||
// Fetch yesterday's data for comparison
|
||||
const yesterday = format(subDays(new Date(), 1), 'yyyy-MM-dd')
|
||||
const {
|
||||
data: historicalData,
|
||||
isLoading: isLoadingHistorical,
|
||||
} = useHistorical(yesterday)
|
||||
|
||||
// Handle refresh
|
||||
const handleRefresh = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['weather'] })
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (isLoadingCurrent || isLoadingForecast) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center space-y-4">
|
||||
<Loader2 className="h-12 w-12 animate-spin mx-auto text-primary" />
|
||||
<p className="text-muted-foreground">Loading weather data...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (currentError || forecastError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-red-500 bg-red-50 dark:bg-red-950 p-6">
|
||||
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400 flex-shrink-0" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-red-800 dark:text-red-200 mb-1">
|
||||
Failed to load weather data
|
||||
</h2>
|
||||
<p className="text-sm text-red-700 dark:text-red-300">
|
||||
{(currentError as Error)?.message || (forecastError as Error)?.message || 'An unexpected error occurred'}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="mt-3 text-sm font-medium text-red-600 dark:text-red-400 hover:underline"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// No data state
|
||||
if (!currentWeather || !forecast) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center space-y-4">
|
||||
<AlertCircle className="h-12 w-12 mx-auto text-muted-foreground" />
|
||||
<p className="text-muted-foreground">No weather data available</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Get best flyable window
|
||||
const bestWindow = forecast.flyable_windows && forecast.flyable_windows.length > 0
|
||||
? forecast.flyable_windows[0]
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="container mx-auto px-4 py-6 md:py-8 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl md:text-4xl font-bold">
|
||||
#ShouldIFly TPG?
|
||||
</h1>
|
||||
{currentWeather.location.name && (
|
||||
<p className="text-muted-foreground">
|
||||
{currentWeather.location.name} ({currentWeather.location.lat.toFixed(2)}, {currentWeather.location.lon.toFixed(2)})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stale Data Warning */}
|
||||
<StaleDataBanner lastUpdated={currentWeather.last_updated} />
|
||||
|
||||
{/* Assessment Badge */}
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div></div>
|
||||
<AssessmentBadge
|
||||
assessment={currentWeather.assessment}
|
||||
currentWeather={currentWeather.current}
|
||||
bestWindow={bestWindow}
|
||||
/>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid gap-6 lg:grid-cols-1">
|
||||
<WindSpeedChart
|
||||
data={forecast.forecast}
|
||||
yesterdayData={historicalData?.data}
|
||||
/>
|
||||
<WindDirectionChart
|
||||
data={forecast.forecast}
|
||||
yesterdayData={historicalData?.data}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Threshold Controls - Collapsible */}
|
||||
<Collapsible title="Threshold Controls" defaultOpen={false}>
|
||||
<ThresholdControls />
|
||||
</Collapsible>
|
||||
|
||||
{/* Refresh Countdown */}
|
||||
<RefreshCountdown onRefresh={handleRefresh} />
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="text-center text-xs text-muted-foreground pt-8 border-t">
|
||||
<p>
|
||||
Data updates every 5 minutes. Forecast generated at{' '}
|
||||
{format(new Date(forecast.generated_at), 'PPpp')}
|
||||
</p>
|
||||
{isLoadingHistorical && (
|
||||
<p className="mt-1">Loading historical comparison data...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user