SoftTraderFrontend/components/widgets/WeatherWidget.tsx
2026-03-20 11:10:13 -04:00

191 lines
5.6 KiB
TypeScript

'use client'
import { useEffect, useState, useCallback } from 'react'
import { MapPin } from 'lucide-react'
import {
getCurrentWeather,
setWeatherLocation,
setLocationHint,
getTempUnit,
formatTemperature,
type CurrentWeather,
type TempUnit,
} from '@/lib/weatherApi'
interface WeatherWidgetProps {
/** Compact mode for inline display (e.g., next to time) */
compact?: boolean
/** Callback when location is successfully set */
onLocationSet?: () => void
}
export function WeatherWidget({ compact = false, onLocationSet }: WeatherWidgetProps) {
const [weather, setWeather] = useState<CurrentWeather | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [needsLocation, setNeedsLocation] = useState(false)
const [tempUnit, setTempUnitState] = useState<TempUnit>('F')
// Load temp unit from localStorage on mount
useEffect(() => {
setTempUnitState(getTempUnit())
}, [])
// City input state
const [cityInput, setCityInput] = useState('')
const [saving, setSaving] = useState(false)
const [saveError, setSaveError] = useState<string | null>(null)
const fetchWeather = useCallback(async () => {
setLoading(true)
setError(null)
try {
const data = await getCurrentWeather()
if (data === null) {
// No location set or API issue - prompt for location
setNeedsLocation(true)
setWeather(null)
} else {
setWeather(data)
setNeedsLocation(false)
setLocationHint(true)
}
} catch {
// On any error, show location input instead of error message
setNeedsLocation(true)
setWeather(null)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchWeather()
}, [fetchWeather])
const handleSetLocation = async (e: React.FormEvent) => {
e.preventDefault()
if (!cityInput.trim()) return
setSaving(true)
setSaveError(null)
try {
await setWeatherLocation(cityInput.trim())
setLocationHint(true)
setCityInput('')
await fetchWeather()
onLocationSet?.()
} catch (err) {
setSaveError(err instanceof Error ? err.message : 'Failed to set location')
} finally {
setSaving(false)
}
}
// Loading state
if (loading) {
if (compact) {
return (
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<div className="skeleton h-4 w-16" />
</div>
)
}
return (
<div className="border border-border bg-card p-4 space-y-2">
<div className="skeleton h-4 w-20" />
<div className="skeleton h-6 w-16" />
<div className="skeleton h-3 w-24" />
</div>
)
}
// Needs location input
if (needsLocation) {
if (compact) {
return (
<form onSubmit={handleSetLocation} className="flex items-center gap-2">
<MapPin size={14} className="text-muted-foreground" />
<input
type="text"
value={cityInput}
onChange={(e) => setCityInput(e.target.value)}
placeholder="Enter city"
disabled={saving}
className="border border-border bg-input text-foreground text-xs px-2 py-1 w-24"
/>
<button
type="submit"
disabled={saving || !cityInput.trim()}
className="text-xs bg-primary text-primary-foreground px-2 py-1 disabled:opacity-50"
>
{saving ? '...' : 'Set'}
</button>
{saveError && <span className="text-xs text-negative">{saveError}</span>}
</form>
)
}
return (
<div className="border border-border bg-card p-4 space-y-3">
<div className="flex items-center gap-2">
<MapPin size={16} className="text-muted-foreground" />
<span className="text-sm font-medium">Set your weather location</span>
</div>
<form onSubmit={handleSetLocation} className="flex gap-2">
<input
type="text"
value={cityInput}
onChange={(e) => setCityInput(e.target.value)}
placeholder="Enter your city"
disabled={saving}
className="flex-1 border border-border bg-input text-foreground text-sm px-3 py-1.5"
/>
<button
type="submit"
disabled={saving || !cityInput.trim()}
className="bg-primary text-primary-foreground text-sm px-3 py-1.5 disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</form>
{saveError && (
<div className="text-xs text-negative">{saveError}</div>
)}
</div>
)
}
// Weather display
if (weather) {
if (compact) {
// Two-line display: white text above (temp + city), grey text below (description)
return (
<div className="text-right">
<div className="text-5xl font-bold text-foreground">
{formatTemperature(weather.temperatureF, tempUnit)}
</div>
<div className="text-sm text-muted-foreground mt-1">
{weather.city} - {weather.description}
</div>
</div>
)
}
return (
<div className="border border-border bg-card p-4 space-y-1">
<div className="flex items-center gap-2">
<MapPin size={14} className="text-muted-foreground" />
<span className="text-sm font-medium">{weather.city}</span>
</div>
<div className="text-2xl font-bold">{formatTemperature(weather.temperatureF, tempUnit)}</div>
<div className="text-xs text-muted-foreground">{weather.description}</div>
</div>
)
}
return null
}