191 lines
5.6 KiB
TypeScript
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
|
|
}
|