From ce9f64c07afd6056b79b6d8fd9adc13498ff7d29 Mon Sep 17 00:00:00 2001 From: Samuel Dockery Date: Sun, 10 Aug 2025 11:12:44 -0700 Subject: [PATCH] feat: enable manual weather location --- src/app/settings/page.tsx | 186 ++++++++++++++++++++++++++++++- src/components/WeatherWidget.tsx | 93 ++++++++++++---- 2 files changed, 254 insertions(+), 25 deletions(-) diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index c20e634..caa2923 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -28,6 +28,10 @@ interface SettingsType { customOpenaiApiUrl: string; customOpenaiModelName: string; weatherWidgetEnabled?: boolean; + automaticWeatherLocation?: boolean; + weatherLatitude?: string; + weatherLongitude?: string; + weatherLocationName?: string; } interface InputProps extends React.InputHTMLAttributes { @@ -154,6 +158,11 @@ const Page = () => { ); const [weatherWidgetEnabled, setWeatherWidgetEnabled] = useState(true); + const [automaticWeatherLocation, setAutomaticWeatherLocation] = + useState(true); + const [weatherLatitude, setWeatherLatitude] = useState(''); + const [weatherLongitude, setWeatherLongitude] = useState(''); + const [weatherLocationName, setWeatherLocationName] = useState(''); const [savingStates, setSavingStates] = useState>({}); useEffect(() => { @@ -226,6 +235,24 @@ const Page = () => { : 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); }; @@ -388,6 +415,14 @@ const Page = () => { localStorage.setItem('measureUnit', value.toString()); } else if (key === 'weatherWidgetEnabled') { 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) { console.error('Failed to save:', err); @@ -467,7 +502,7 @@ const Page = () => {

- Weather Widget + Weather

@@ -510,6 +545,155 @@ const Page = () => {
+ {weatherWidgetEnabled && ( +
+
+
+
+ +
+
+

+ Automatic Weather Location +

+

+ Use device geolocation or IP lookup to determine your + location +

+
+
+ { + 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', + )} + > + + +
+ + {!automaticWeatherLocation && ( + <> +
+
+

+ Latitude +

+ 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); + } + }} + /> +
+
+

+ Longitude +

+ + 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); + } + }} + /> +
+
+ +
+

+ Location Name +

+ + 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); + } + }} + /> +
+ + )} +
+ )} diff --git a/src/components/WeatherWidget.tsx b/src/components/WeatherWidget.tsx index ad9b2cf..a7da55d 100644 --- a/src/components/WeatherWidget.tsx +++ b/src/components/WeatherWidget.tsx @@ -71,36 +71,81 @@ const WeatherWidget = () => { } }; - getLocation(async (location) => { - const res = await fetch(`/api/weather`, { - method: 'POST', - body: JSON.stringify({ - lat: location.latitude, - lng: location.longitude, - measureUnit: localStorage.getItem('measureUnit') ?? 'Metric', - }), - }); + const fetchWeatherForCoords = async ( + lat: number, + lng: number, + city?: string, + ) => { + try { + 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) { - console.error('Error fetching weather data'); + if (res.status !== 200) { + 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); - 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({ - temperature: data.temperature, - condition: data.condition, - location: location.city, - humidity: data.humidity, - windSpeed: data.windSpeed, - icon: data.icon, - temperatureUnit: data.temperatureUnit, - windSpeedUnit: data.windSpeedUnit, + // Normal behavior: use geolocation or IP-based approximate location + await getLocation(async (location) => { + await fetchWeatherForCoords( + location.latitude, + location.longitude, + location.city, + ); }); - setLoading(false); - }); + })(); }, []); return (