init
This commit is contained in:
59
frontend/app/globals.css
Normal file
59
frontend/app/globals.css
Normal file
@@ -0,0 +1,59 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
25
frontend/app/layout.tsx
Normal file
25
frontend/app/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import { Providers } from './providers'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Paragliding Weather Forecaster',
|
||||
description: 'Real-time weather analysis for paragliding conditions',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body className={inter.className}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
22
frontend/app/providers.tsx
Normal file
22
frontend/app/providers.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user