Files
paragliding/frontend/components/weather/assessment-badge.tsx
2026-01-03 14:16:16 -08:00

164 lines
5.4 KiB
TypeScript

'use client'
import { Card, CardContent } from '@/components/ui/card'
import { Check, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { Assessment, FlyableWindow, WeatherPoint } from '@/lib/types'
import { format, parseISO } from 'date-fns'
import { useThresholdStore } from '@/store/threshold-store'
interface AssessmentBadgeProps {
assessment: Assessment
currentWeather: WeatherPoint
bestWindow?: FlyableWindow
className?: string
}
// Transform direction to offset from West (270°)
function calculateOffset(direction: number): number {
return ((direction - 270 + 180) % 360) - 180
}
type Rating = 'Great' | 'Good' | 'Okay' | 'Bad'
interface RatingInfo {
text: Rating
color: string
}
export function AssessmentBadge({ assessment, currentWeather, bestWindow, className }: AssessmentBadgeProps) {
const { speedMin, speedMax, dirCenter, dirRange } = useThresholdStore()
// Evaluate wind speed
const evaluateWindSpeed = (speed: number): RatingInfo => {
if (speed < speedMin || speed > speedMax) {
return { text: 'Bad', color: 'text-red-600 dark:text-red-400' }
}
const range = speedMax - speedMin
const distanceFromMin = speed - speedMin
const distanceFromMax = speedMax - speed
const minDistance = Math.min(distanceFromMin, distanceFromMax)
if (minDistance < range * 0.15) {
return { text: 'Okay', color: 'text-yellow-600 dark:text-yellow-400' }
} else if (minDistance < range * 0.35) {
return { text: 'Good', color: 'text-green-600 dark:text-green-400' }
} else {
return { text: 'Great', color: 'text-green-700 dark:text-green-300' }
}
}
// Evaluate wind direction
const evaluateWindDirection = (direction: number): RatingInfo => {
const offset = calculateOffset(direction)
const centerOffset = calculateOffset(dirCenter)
const minOffset = centerOffset - dirRange
const maxOffset = centerOffset + dirRange
if (offset < minOffset || offset > maxOffset) {
return { text: 'Bad', color: 'text-red-600 dark:text-red-400' }
}
const distanceFromCenter = Math.abs(offset - centerOffset)
if (distanceFromCenter > dirRange * 0.7) {
return { text: 'Okay', color: 'text-yellow-600 dark:text-yellow-400' }
} else if (distanceFromCenter > dirRange * 0.4) {
return { text: 'Good', color: 'text-green-600 dark:text-green-400' }
} else {
return { text: 'Great', color: 'text-green-700 dark:text-green-300' }
}
}
const speedRating = evaluateWindSpeed(currentWeather.wind_speed)
const directionRating = evaluateWindDirection(currentWeather.wind_direction)
// Overall assessment is based on the worse of the two metrics
const getOverallAssessment = (): boolean => {
const ratingValues: Record<Rating, number> = {
'Great': 4,
'Good': 3,
'Okay': 2,
'Bad': 1
}
const worstRating = Math.min(
ratingValues[speedRating.text],
ratingValues[directionRating.text]
)
// Only GOOD if both metrics are at least "Good"
return worstRating >= 3
}
const isGood = getOverallAssessment()
return (
<Card
className={cn(
'border-2',
isGood
? 'border-green-500 bg-green-50 dark:bg-green-950'
: 'border-red-500 bg-red-50 dark:bg-red-950',
className
)}
>
<CardContent className="p-6">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div
className={cn(
'flex h-12 w-12 items-center justify-center rounded-full',
isGood ? 'bg-green-500' : 'bg-red-500'
)}
aria-hidden="true"
>
{isGood ? (
<Check className="h-8 w-8 text-white" strokeWidth={3} />
) : (
<X className="h-8 w-8 text-white" strokeWidth={3} />
)}
</div>
<div>
<div
className={cn(
'text-3xl font-bold',
isGood ? 'text-green-700 dark:text-green-300' : 'text-red-700 dark:text-red-300'
)}
>
{isGood ? 'GOOD' : 'BAD'}
</div>
<div className="text-sm text-muted-foreground">
Flyability Assessment
</div>
</div>
</div>
{bestWindow && isGood && (
<div className="text-right">
<div className="text-sm font-medium text-muted-foreground">
Best window
</div>
<div className="text-lg font-semibold">
{format(parseISO(bestWindow.start), 'ha')} -{' '}
{format(parseISO(bestWindow.end), 'ha')}
</div>
<div className="text-xs text-muted-foreground">
{bestWindow.duration_hours.toFixed(1)}h duration
</div>
</div>
)}
</div>
{/* Individual metric ratings */}
<div className="mt-4 pt-4 border-t space-y-2">
<div className="text-lg font-semibold">
Wind direction is <span className={directionRating.color}>{directionRating.text}</span>
</div>
<div className="text-lg font-semibold">
Wind speed is <span className={speedRating.color}>{speedRating.text}</span>
</div>
</div>
</CardContent>
</Card>
)
}