This commit is contained in:
2026-01-03 14:16:16 -08:00
commit 1f0e678d47
71 changed files with 16127 additions and 0 deletions

161
frontend/app/page.tsx Normal file
View 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>
)
}