feat: enable manual weather location

This commit is contained in:
Samuel Dockery 2025-08-10 11:12:44 -07:00
parent 44f2e5dbd3
commit ce9f64c07a
2 changed files with 254 additions and 25 deletions

View file

@ -28,6 +28,10 @@ interface SettingsType {
customOpenaiApiUrl: string; customOpenaiApiUrl: string;
customOpenaiModelName: string; customOpenaiModelName: string;
weatherWidgetEnabled?: boolean; weatherWidgetEnabled?: boolean;
automaticWeatherLocation?: boolean;
weatherLatitude?: string;
weatherLongitude?: string;
weatherLocationName?: string;
} }
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@ -154,6 +158,11 @@ const Page = () => {
); );
const [weatherWidgetEnabled, setWeatherWidgetEnabled] = const [weatherWidgetEnabled, setWeatherWidgetEnabled] =
useState<boolean>(true); useState<boolean>(true);
const [automaticWeatherLocation, setAutomaticWeatherLocation] =
useState<boolean>(true);
const [weatherLatitude, setWeatherLatitude] = useState<string>('');
const [weatherLongitude, setWeatherLongitude] = useState<string>('');
const [weatherLocationName, setWeatherLocationName] = useState<string>('');
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({}); const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
useEffect(() => { useEffect(() => {
@ -226,6 +235,24 @@ const Page = () => {
: localStorage.getItem('weatherWidgetEnabled') === 'true', : localStorage.getItem('weatherWidgetEnabled') === 'true',
); );
setAutomaticWeatherLocation(
localStorage.getItem('automaticWeatherLocation') === null
? true
: localStorage.getItem('automaticWeatherLocation') === 'true',
);
setWeatherLatitude(
localStorage.getItem('weatherLatitude') ?? data.weatherLatitude ?? '',
);
setWeatherLongitude(
localStorage.getItem('weatherLongitude') ?? data.weatherLongitude ?? '',
);
setWeatherLocationName(
localStorage.getItem('weatherLocationName') ??
data.weatherLocationName ??
'',
);
setIsLoading(false); setIsLoading(false);
}; };
@ -388,6 +415,14 @@ const Page = () => {
localStorage.setItem('measureUnit', value.toString()); localStorage.setItem('measureUnit', value.toString());
} else if (key === 'weatherWidgetEnabled') { } else if (key === 'weatherWidgetEnabled') {
localStorage.setItem('weatherWidgetEnabled', value.toString()); localStorage.setItem('weatherWidgetEnabled', value.toString());
} else if (key === 'automaticWeatherLocation') {
localStorage.setItem('automaticWeatherLocation', value.toString());
} else if (key === 'weatherLatitude') {
localStorage.setItem('weatherLatitude', value.toString());
} else if (key === 'weatherLongitude') {
localStorage.setItem('weatherLongitude', value.toString());
} else if (key === 'weatherLocationName') {
localStorage.setItem('weatherLocationName', value.toString());
} }
} catch (err) { } catch (err) {
console.error('Failed to save:', err); console.error('Failed to save:', err);
@ -467,7 +502,7 @@ const Page = () => {
</div> </div>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
Weather Widget Weather
</p> </p>
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors"> <div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@ -510,6 +545,155 @@ const Page = () => {
</Switch> </Switch>
</div> </div>
</div> </div>
{weatherWidgetEnabled && (
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
<div className="flex items-center space-x-3">
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
<SettingsIcon
size={18}
className="text-black/70 dark:text-white/70"
/>
</div>
<div>
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
Automatic Weather Location
</p>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
Use device geolocation or IP lookup to determine your
location
</p>
</div>
</div>
<Switch
checked={automaticWeatherLocation}
onChange={(checked) => {
setAutomaticWeatherLocation(checked);
// When enabling automatic mode: clear and persist manual fields.
if (checked) {
setWeatherLatitude('');
setWeatherLongitude('');
setWeatherLocationName('');
saveConfig('weatherLatitude', '');
saveConfig('weatherLongitude', '');
saveConfig('weatherLocationName', '');
saveConfig('automaticWeatherLocation', true);
} else {
const lat = (weatherLatitude ?? '').trim();
const lon = (weatherLongitude ?? '').trim();
const loc = (weatherLocationName ?? '').trim();
// Save manual mode only if all fields are filled
if (lat && lon && loc) {
saveConfig('automaticWeatherLocation', false);
}
}
}}
className={cn(
automaticWeatherLocation
? 'bg-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none',
)}
>
<span
className={cn(
automaticWeatherLocation
? 'translate-x-6'
: 'translate-x-1',
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
)}
/>
</Switch>
</div>
{!automaticWeatherLocation && (
<>
<div className="grid grid-cols-2 gap-3 mt-2">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-xs">
Latitude
</p>
<Input
type="text"
placeholder="e.g. 37.7749"
value={weatherLatitude ?? undefined}
isSaving={savingStates['weatherLatitude']}
onChange={(e) => setWeatherLatitude(e.target.value)}
onSave={(value) => {
const newLat = (value ?? '').trim();
const lon = (weatherLongitude ?? '').trim();
const loc = (weatherLocationName ?? '').trim();
// Save manual location only when all three fields are provided.
if (newLat && lon && loc) {
saveConfig('weatherLatitude', value);
saveConfig('weatherLongitude', lon);
saveConfig('weatherLocationName', loc);
setAutomaticWeatherLocation(false);
saveConfig('automaticWeatherLocation', false);
}
}}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-xs">
Longitude
</p>
<Input
type="text"
placeholder="e.g. -122.4194"
value={weatherLongitude ?? undefined}
isSaving={savingStates['weatherLongitude']}
onChange={(e) =>
setWeatherLongitude(e.target.value)
}
onSave={(value) => {
const lat = (weatherLatitude ?? '').trim();
const newLon = (value ?? '').trim();
const loc = (weatherLocationName ?? '').trim();
// Save manual location only when all three fields are provided.
if (lat && newLon && loc) {
saveConfig('weatherLatitude', lat);
saveConfig('weatherLongitude', value);
saveConfig('weatherLocationName', loc);
setAutomaticWeatherLocation(false);
saveConfig('automaticWeatherLocation', false);
}
}}
/>
</div>
</div>
<div className="flex flex-col space-y-1 mt-2">
<p className="text-black/70 dark:text-white/70 text-xs">
Location Name
</p>
<Input
type="text"
placeholder="e.g. Home, San Francisco"
value={weatherLocationName ?? undefined}
isSaving={savingStates['weatherLocationName']}
onChange={(e) =>
setWeatherLocationName(e.target.value)
}
onSave={(value) => {
const lat = (weatherLatitude ?? '').trim();
const lon = (weatherLongitude ?? '').trim();
const newLoc = (value ?? '').trim();
// Save manual location only when all three fields are provided.
if (lat && lon && newLoc) {
saveConfig('weatherLatitude', lat);
saveConfig('weatherLongitude', lon);
saveConfig('weatherLocationName', value);
setAutomaticWeatherLocation(false);
saveConfig('automaticWeatherLocation', false);
}
}}
/>
</div>
</>
)}
</div>
)}
</SettingsSection> </SettingsSection>
<SettingsSection title="Automatic Search"> <SettingsSection title="Automatic Search">

View file

@ -71,36 +71,81 @@ const WeatherWidget = () => {
} }
}; };
getLocation(async (location) => { const fetchWeatherForCoords = async (
const res = await fetch(`/api/weather`, { lat: number,
method: 'POST', lng: number,
body: JSON.stringify({ city?: string,
lat: location.latitude, ) => {
lng: location.longitude, try {
measureUnit: localStorage.getItem('measureUnit') ?? 'Metric', const res = await fetch(`/api/weather`, {
}), method: 'POST',
}); headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
lat,
lng,
measureUnit: localStorage.getItem('measureUnit') ?? 'Metric',
}),
});
const data = await res.json(); const data = await res.json();
if (res.status !== 200) { if (res.status !== 200) {
console.error('Error fetching weather data'); console.error('Error fetching weather data');
setLoading(false);
return;
}
setData({
temperature: data.temperature,
condition: data.condition,
location: city ?? data.location ?? '',
humidity: data.humidity,
windSpeed: data.windSpeed,
icon: data.icon,
temperatureUnit: data.temperatureUnit,
windSpeedUnit: data.windSpeedUnit,
});
} catch (err) {
console.error('Error fetching weather data', err);
} finally {
setLoading(false); setLoading(false);
return; }
};
(async () => {
// Check automatic setting from localStorage (default true)
const automatic =
localStorage.getItem('automaticWeatherLocation') === null
? true
: localStorage.getItem('automaticWeatherLocation') === 'true';
if (!automatic) {
const latStr = localStorage.getItem('weatherLatitude') ?? '';
const lngStr = localStorage.getItem('weatherLongitude') ?? '';
const name = localStorage.getItem('weatherLocationName') ?? '';
const lat = parseFloat(latStr);
const lng = parseFloat(lngStr);
if (!isNaN(lat) && !isNaN(lng)) {
// Use provided coordinates; prefer user-provided name if available
const locName = name !== '' ? name : `${lat}, ${lng}`;
await fetchWeatherForCoords(lat, lng, locName);
return;
}
// If invalid or missing, fall through to normal location lookup
} }
setData({ // Normal behavior: use geolocation or IP-based approximate location
temperature: data.temperature, await getLocation(async (location) => {
condition: data.condition, await fetchWeatherForCoords(
location: location.city, location.latitude,
humidity: data.humidity, location.longitude,
windSpeed: data.windSpeed, location.city,
icon: data.icon, );
temperatureUnit: data.temperatureUnit,
windSpeedUnit: data.windSpeedUnit,
}); });
setLoading(false); })();
});
}, []); }, []);
return ( return (