From efee1f00584e622d4d6df02bd15a409ab7d51de5 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Sun, 7 Dec 2025 00:27:46 +0330 Subject: [PATCH 1/5] chore: bump version to 1.0.58 in configuration --- wxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxt.config.ts b/wxt.config.ts index f893e06f..09bfb39e 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -48,7 +48,7 @@ export default defineConfig({ '@wxt-dev/module-react', ], manifest: { - version: '1.0.57', + version: '1.0.58', name: 'Widgetify', description: 'Transform your new tab into a smart dashboard with Widgetify! Get currency rates, crypto prices, weather & more.', From 53cf319adee2e974453e3d4ff4e33535d3985984 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Tue, 9 Dec 2025 11:24:13 +0330 Subject: [PATCH 2/5] refactor: migrate weather widget to server-side city selection with authentication - Remove local SelectedCity storage and context - Update city selection to use authenticated user cities from server - Delete unused CitySearchInput and SelectedCityDisplay components - Modify weather hooks to fetch data without lat/lon params - Add new hooks for cities list and user city setting - Update weather components to handle new data structure - Add auth requirement and set-city prompt for weather widget --- src/context/general-setting.context.tsx | 35 +--- .../tabs/general/components/select-city.tsx | 187 ++++++++++++------ .../setting/tabs/weather/CitySearchInput.tsx | 40 ---- .../tabs/weather/SelectedCityDisplay.tsx | 33 ---- .../weather/current/current-box.weather.tsx | 44 +++-- .../widgets/weather/weather-setting.tsx | 1 + .../widgets/weather/weather.layout.tsx | 100 ++++++---- src/services/hooks/cities/getCitiesList.ts | 23 +++ src/services/hooks/user/userService.hook.ts | 19 ++ .../weather/getForecastWeatherByLatLon.ts | 25 +-- .../hooks/weather/getWeatherByLatLon.ts | 18 +- 11 files changed, 277 insertions(+), 248 deletions(-) delete mode 100644 src/layouts/setting/tabs/weather/CitySearchInput.tsx delete mode 100644 src/layouts/setting/tabs/weather/SelectedCityDisplay.tsx create mode 100644 src/services/hooks/cities/getCitiesList.ts diff --git a/src/context/general-setting.context.tsx b/src/context/general-setting.context.tsx index e6c2add8..ea69de04 100644 --- a/src/context/general-setting.context.tsx +++ b/src/context/general-setting.context.tsx @@ -15,21 +15,12 @@ export interface GeneralData { selected_timezone: FetchedTimezone browserBookmarksEnabled: boolean browserTabsEnabled: boolean - selectedCity: SelectedCity -} - -export interface SelectedCity { - name: string - lat: number - lon: number - state?: string | null } interface GeneralSettingContextType extends GeneralData { updateSetting: (key: K, value: GeneralData[K]) => void setAnalyticsEnabled: (value: boolean) => void setTimezone: (value: FetchedTimezone) => void - setSelectedCity: (value: any) => void setBrowserBookmarksEnabled: (value: boolean) => void setBrowserTabsEnabled: (value: boolean, event?: React.MouseEvent) => void } @@ -44,7 +35,6 @@ const DEFAULT_SETTINGS: GeneralData = { }, browserBookmarksEnabled: false, browserTabsEnabled: false, - selectedCity: { name: 'Tehran', lat: 35.6892523, lon: 51.3896004, state: null }, } export const GeneralSettingContext = createContext(null) @@ -59,12 +49,10 @@ export function GeneralSettingProvider({ children }: { children: React.ReactNode async function loadGeneralSettings() { try { const storedSettings = await getFromStorage('generalSettings') - const [browserBookmarksEnabled, browserTabsEnabled, selectedCity] = - await Promise.all([ - browserHasPermission(['bookmarks']), - browserHasPermission(['tabs', 'tabGroups']), - getFromStorage('selectedCity'), - ]) + const [browserBookmarksEnabled, browserTabsEnabled] = await Promise.all([ + browserHasPermission(['bookmarks']), + browserHasPermission(['tabs', 'tabGroups']), + ]) if (storedSettings) { setSettings({ @@ -76,15 +64,6 @@ export function GeneralSettingProvider({ children }: { children: React.ReactNode : storedSettings.selected_timezone, browserBookmarksEnabled: browserBookmarksEnabled, browserTabsEnabled: browserTabsEnabled, - selectedCity: { - lat: selectedCity?.lat || DEFAULT_SETTINGS.selectedCity.lat, - lon: selectedCity?.lon || DEFAULT_SETTINGS.selectedCity.lon, - name: - selectedCity?.name || DEFAULT_SETTINGS.selectedCity.name, - state: - selectedCity?.state || - DEFAULT_SETTINGS.selectedCity.state, - }, }) } } finally { @@ -229,10 +208,6 @@ export function GeneralSettingProvider({ children }: { children: React.ReactNode 'browser_tabs_disabled' ) - const setSelectedCity = (val: SelectedCity) => { - updateSetting('selectedCity', val) - } - if (!isInitialized) { return null } @@ -248,8 +223,6 @@ export function GeneralSettingProvider({ children }: { children: React.ReactNode setBrowserBookmarksEnabled, browserTabsEnabled: settings.browserTabsEnabled, setBrowserTabsEnabled, - selectedCity: settings.selectedCity, - setSelectedCity, } return ( diff --git a/src/layouts/setting/tabs/general/components/select-city.tsx b/src/layouts/setting/tabs/general/components/select-city.tsx index d0b170fc..efbf158c 100644 --- a/src/layouts/setting/tabs/general/components/select-city.tsx +++ b/src/layouts/setting/tabs/general/components/select-city.tsx @@ -1,126 +1,193 @@ -import { useRef, useState } from 'react' +import { useState, useMemo } from 'react' import { CiLocationOn } from 'react-icons/ci' import Analytics from '@/analytics' -import { setToStorage } from '@/common/storage' import { IconLoading } from '@/components/loading/icon-loading' import Modal from '@/components/modal' import { SectionPanel } from '@/components/section-panel' -import { type SelectedCity, useGeneralSetting } from '@/context/general-setting.context' -import { useDebouncedValue } from '@/hooks/useDebouncedValue' -import { useGetRelatedCities } from '@/services/hooks/weather/getRelatedCities' -import { CitySearchInput } from '../../weather/CitySearchInput' -import { SelectedCityDisplay } from '../../weather/SelectedCityDisplay' +import { useGetCitiesList } from '@/services/hooks/cities/getCitiesList' +import { useAuth } from '@/context/auth.context' +import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' +import { useSetCity } from '@/services/hooks/user/userService.hook' +import { TextInput } from '@/components/text-input' +import { showToast } from '@/common/toast' +import { translateError } from '@/utils/translate-error' + +interface SelectedCity { + city: string + cityId: string +} export function SelectCity() { - const [inputValue, setInputValue] = useState('') + const [searchTerm, setSearchTerm] = useState('') const [isModalOpen, setIsModalOpen] = useState(false) - const inputRef = useRef(null) + const [showAuthModal, setShowAuthModal] = useState(false) + const searchInputRef = useRef(null) + const { isAuthenticated, user, isLoadingUser } = useAuth() + const { data: cities, isLoading, error } = useGetCitiesList(isAuthenticated) + const { mutateAsync: setCityToServer, isPending: isSettingCity } = useSetCity() - const debouncedValue = useDebouncedValue( - inputValue.length >= 2 ? inputValue : '', - 500 - ) - const { setSelectedCity, selectedCity } = useGeneralSetting() + const filteredCities = useMemo(() => { + if (!cities || !searchTerm) return cities || [] - const { data: relatedCities, isLoading, error } = useGetRelatedCities(debouncedValue) + const lowerSearchTerm = searchTerm.toLowerCase() - const handleSelectCity = (city: SelectedCity) => { - setSelectedCity(city) - setIsModalOpen(false) - setInputValue('') + const prefixMatches = cities.filter((city) => + city.city.toLowerCase().startsWith(lowerSearchTerm) + ) - Analytics.event('city_selected') + const remainingCities = cities.filter( + (city) => + !city.city.toLowerCase().startsWith(lowerSearchTerm) && + city.city.toLowerCase().includes(lowerSearchTerm) + ) - setToStorage('selectedCity', { - lat: city.lat, - lon: city.lon, - name: city.name, - state: city.state, - }) - } + return [...prefixMatches, ...remainingCities] + }, [cities, searchTerm]) - const handleInputChange = (value: string) => { - setInputValue(value) + const handleSelectCity = async (city: SelectedCity) => { + if (!city.cityId) return + try { + setIsModalOpen(false) + setSearchTerm('') + + Analytics.event('city_selected') + + await setCityToServer(city.cityId) + } catch (error) { + showToast(translateError(error) as any, 'error') + } } + const onModalOpen = () => { + if (!isAuthenticated) { + Analytics.event('open_city_selection_modal_unauthenticated') + setShowAuthModal(true) + return + } + setIsModalOpen(true) Analytics.event('open_city_selection_modal') setTimeout(() => { - inputRef.current?.focus() - }, 100) + searchInputRef.current?.focus() + }, 300) } + const selected = user?.city + ? filteredCities.find((c) => c.cityId === user.city?.id) + : null + return (
- + {error && ( -
-
+
+
خطا در دریافت اطلاعات
-
+
لطفا اتصال اینترنت خود را بررسی کرده و مجدداً تلاش کنید.
)}
+ setShowAuthModal(!showAuthModal)} + message="برای انتخاب شهر، لطفاً وارد حساب کاربری خود شوید." + /> setIsModalOpen(false)} + onClose={() => { + setIsModalOpen(false) + setSearchTerm('') + }} title="انتخاب شهر" size="lg" direction="rtl" > -
- {}} - isLoading={isLoading} - /> -
+
+
+ setSearchTerm(value)} + /> + +
+ +
{isLoading ? (
در حال بارگذاری...
- ) : relatedCities && relatedCities.length > 0 ? ( - relatedCities.map((city) => ( + ) : filteredCities?.length > 0 ? ( + filteredCities.map((city) => (
handleSelectCity(city)} - className="flex items-center w-full p-3 text-right transition-all duration-200 border-b cursor-pointer hover:bg-gradient-to-l hover:from-primary/10 hover:to-transparent border-base-200/30 last:border-b-0 group rounded-2xl" + className="flex items-center w-full p-3 text-right transition-all duration-200 border-b cursor-pointer border-base-200/30 last:border-b-0 group rounded-2xl hover:bg-primary/20 hover:text-primary" > - {city.name} - {city.state ? `, ${city.state}` : ''} + {city.city}
)) - ) : inputValue.length >= 2 ? ( + ) : searchTerm ? (
نتیجه‌ای یافت نشد
+ ) : cities && cities.length === 0 ? ( +
+ هیچ شهری موجود نیست +
) : (
شهر مورد نظر خود را جستجو کنید
)}
+ +
+

+ شهر شما در لیست نیست یا مشکلی مشاهده می‌کنید؟{' '} + + Analytics.event('feedback_link_clicked', { + source: 'city_selection', + }) + } + > + اطلاع دهید + +

+
diff --git a/src/layouts/setting/tabs/weather/CitySearchInput.tsx b/src/layouts/setting/tabs/weather/CitySearchInput.tsx deleted file mode 100644 index 77d4e6ab..00000000 --- a/src/layouts/setting/tabs/weather/CitySearchInput.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { TextInput } from '@/components/text-input' - -interface CitySearchInputProps { - value: string - onChange: (e: string) => void - onFocus: () => void - isLoading: boolean - ref?: React.RefObject -} - -export function CitySearchInput({ - value, - onChange, - onFocus, - isLoading, - ref, -}: CitySearchInputProps) { - const getSpinnerStyle = () => { - return 'border-content border-t-primary/80' - } - - return ( -
- - {isLoading && ( -
-
-
- )} -
- ) -} diff --git a/src/layouts/setting/tabs/weather/SelectedCityDisplay.tsx b/src/layouts/setting/tabs/weather/SelectedCityDisplay.tsx deleted file mode 100644 index 1c299262..00000000 --- a/src/layouts/setting/tabs/weather/SelectedCityDisplay.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { SelectedCity } from '@/context/general-setting.context' - -interface SelectedCityDisplayProps { - city: SelectedCity | null -} - -export function SelectedCityDisplay({ city }: SelectedCityDisplayProps) { - if (!city) return null - return ( -
-
-
-
-
-
- {city.state} -
-
- -
- {city.name} با موفقیت - انتخاب شد و به صورت خودکار در ویجت‌های مربوطه اعمال می‌شود. -
-
-
-
-
- ) -} diff --git a/src/layouts/widgets/weather/current/current-box.weather.tsx b/src/layouts/widgets/weather/current/current-box.weather.tsx index f68643b5..ca953984 100644 --- a/src/layouts/widgets/weather/current/current-box.weather.tsx +++ b/src/layouts/widgets/weather/current/current-box.weather.tsx @@ -5,28 +5,26 @@ import { TbWind } from 'react-icons/tb' import { WiCloudy, WiHumidity } from 'react-icons/wi' interface CurrentWeatherBoxProps { - fetchedWeather: FetchedWeather['weather'] | null + fetchedWeather: FetchedWeather | null enabledShowName: boolean temperatureUnit: keyof typeof unitsFlag - selectedCityName: string } export function CurrentWeatherBox({ fetchedWeather, enabledShowName, - selectedCityName, temperatureUnit, }: CurrentWeatherBoxProps) { return ( <>
- {fetchedWeather?.statusBanner && ( + {fetchedWeather?.weather?.statusBanner && (
)} - {!fetchedWeather?.statusBanner && ( + {!fetchedWeather?.weather?.statusBanner && (
)}
- {enabledShowName ? cleanCityName(selectedCityName) : '🏠'} + {enabledShowName + ? cleanCityName(fetchedWeather?.city?.fa || '') + : '🏠'} - {Math.round(fetchedWeather?.temperature?.temp || 0)} + {Math.round(fetchedWeather?.weather?.temperature?.temp || 0)} {unitsFlag[temperatureUnit || 'metric']} - {fetchedWeather?.description?.text} •{' '} - {fetchedWeather?.temperature?.temp_description} + {fetchedWeather?.weather?.description?.text} •{' '} + {fetchedWeather?.weather?.temperature?.temp_description}
- {fetchedWeather?.description?.text} + {fetchedWeather?.weather?.icon?.url ? ( + {fetchedWeather?.weather?.description?.text} + ) : ( +
+ )}
@@ -72,7 +76,9 @@ export function CurrentWeatherBox({
- {Math.round(fetchedWeather?.temperature?.wind_speed || 0)}{' '} + {Math.round( + fetchedWeather?.weather?.temperature?.wind_speed || 0 + )}{' '} m/s
@@ -82,7 +88,7 @@ export function CurrentWeatherBox({
- {fetchedWeather?.temperature?.humidity || 0}% + {fetchedWeather?.weather?.temperature?.humidity || 0}%
@@ -91,7 +97,7 @@ export function CurrentWeatherBox({
- {fetchedWeather?.temperature?.clouds || 0}% + {fetchedWeather?.weather?.temperature?.clouds || 0}%
diff --git a/src/layouts/widgets/weather/weather-setting.tsx b/src/layouts/widgets/weather/weather-setting.tsx index a61b3dfb..43bb8851 100644 --- a/src/layouts/widgets/weather/weather-setting.tsx +++ b/src/layouts/widgets/weather/weather-setting.tsx @@ -1,3 +1,4 @@ +import { useRef, useState, useEffect } from 'react' import { getFromStorage, setToStorage } from '@/common/storage' import { callEvent } from '@/common/utils/call-event' import { SectionPanel } from '@/components/section-panel' diff --git a/src/layouts/widgets/weather/weather.layout.tsx b/src/layouts/widgets/weather/weather.layout.tsx index 766f3a94..8f64ed2b 100644 --- a/src/layouts/widgets/weather/weather.layout.tsx +++ b/src/layouts/widgets/weather/weather.layout.tsx @@ -1,39 +1,41 @@ import { useEffect, useState } from 'react' import { getFromStorage, setToStorage } from '@/common/storage' -import { listenEvent } from '@/common/utils/call-event' -import { useGeneralSetting } from '@/context/general-setting.context' +import { callEvent, listenEvent } from '@/common/utils/call-event' import type { FetchedForecast, FetchedWeather, WeatherSettings, } from '@/layouts/widgets/weather/weather.interface' -import { useGetForecastWeatherByLatLon } from '@/services/hooks/weather/getForecastWeatherByLatLon' -import { useGetWeatherByLatLon } from '@/services/hooks/weather/getWeatherByLatLon' import { WidgetContainer } from '../widget-container' import { Forecast } from './forecast/forecast' import { CurrentWeatherBox } from './current/current-box.weather' +import { useAuth } from '@/context/auth.context' +import { RequireAuth } from '@/components/auth/require-auth' +import { useGetWeatherByLatLon } from '@/services/hooks/weather/getWeatherByLatLon' +import { useGetForecastWeatherByLatLon } from '@/services/hooks/weather/getForecastWeatherByLatLon' +import type { AxiosError } from 'axios' +import { Button } from '@/components/button/button' +import { WidgetTabKeys } from '@/layouts/widgets-settings/constant/tab-keys' +import Analytics from '@/analytics' export function WeatherLayout() { - const { selectedCity } = useGeneralSetting() + const { isAuthenticated, user } = useAuth() const [weatherSettings, setWeatherSettings] = useState(null) const [weatherState, setWeather] = useState(null) const [forecastWeather, setForecastWeather] = useState(null) - const { data, dataUpdatedAt } = useGetWeatherByLatLon( - selectedCity.lat, - selectedCity.lon, - { - refetchInterval: 0, - units: weatherSettings?.temperatureUnit, - useAI: weatherSettings?.useAI, - enabled: !!weatherSettings, - } - ) + const [showSetCityMessage, setShowSetCityMessage] = useState(false) + const { data, dataUpdatedAt, error } = useGetWeatherByLatLon({ + refetchInterval: 0, + units: weatherSettings?.temperatureUnit, + useAI: weatherSettings?.useAI, + enabled: isAuthenticated, + }) const { data: forecastData, dataUpdatedAt: forecastDataUpdatedAt } = - useGetForecastWeatherByLatLon(selectedCity.lat, selectedCity.lon, { + useGetForecastWeatherByLatLon({ count: 6, units: weatherSettings?.temperatureUnit, - enabled: !!weatherSettings, + enabled: isAuthenticated && user?.city?.id != null, refetchInterval: 0, }) @@ -84,7 +86,13 @@ export function WeatherLayout() { setToStorage('currentWeather', data) setWeather(data) } - }, [data, dataUpdatedAt]) + if (error) { + const axiosError = error as AxiosError + if (axiosError.response?.data?.message === 'USER_CITY_NOT_SET') { + setShowSetCityMessage(true) + } + } + }, [data, dataUpdatedAt, error]) useEffect(() => { if (forecastData) { @@ -93,25 +101,51 @@ export function WeatherLayout() { } }, [forecastDataUpdatedAt]) - if (selectedCity === null || !weatherSettings) return null + const onClickSetCity = () => { + callEvent('openWidgetsSettings', { + tab: WidgetTabKeys.weather_settings, + }) + Analytics.event('weather_set_city_clicked') + } + + if (!weatherSettings) return null return ( -
- + + {showSetCityMessage ? ( +
+
🌤️
+

+ برای نمایش آب و هوا، لطفاً ابتدا در تنظیمات ویجت، شهر خود را + تنظیم کنید. +

+ +
+ ) : ( +
+ -
- -
-
+
+ +
+
+ )} +
) } diff --git a/src/services/hooks/cities/getCitiesList.ts b/src/services/hooks/cities/getCitiesList.ts new file mode 100644 index 00000000..a9807531 --- /dev/null +++ b/src/services/hooks/cities/getCitiesList.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query' +import { getMainClient } from '@/services/api' + +export interface CityResponse { + city: string + cityId: string +} + +async function fetchCitiesList(): Promise { + const client = await getMainClient() + const response = await client.get('/cities/list') + return response.data +} + +export function useGetCitiesList(enabled: boolean) { + return useQuery({ + queryKey: ['getCitiesList'], + queryFn: fetchCitiesList, + enabled, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + }) +} diff --git a/src/services/hooks/user/userService.hook.ts b/src/services/hooks/user/userService.hook.ts index a2db030e..fafa15d1 100644 --- a/src/services/hooks/user/userService.hook.ts +++ b/src/services/hooks/user/userService.hook.ts @@ -30,6 +30,9 @@ interface FetchedProfile { font: FontFamily timeZone: string coins: number + city?: { + id: string + } } export interface UserProfile extends FetchedProfile { @@ -115,3 +118,19 @@ export function useSendVerificationEmail() { mutationKey: ['sendVerificationEmail'], }) } + +async function setCityToServer(cityId: string): Promise { + const client = await getMainClient() + await client.put('/users/@me/city', { cityId }) +} + +export function useSetCity() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (cityId: string) => setCityToServer(cityId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['userProfile'] }) + }, + }) +} diff --git a/src/services/hooks/weather/getForecastWeatherByLatLon.ts b/src/services/hooks/weather/getForecastWeatherByLatLon.ts index f57c9218..374ba96e 100644 --- a/src/services/hooks/weather/getForecastWeatherByLatLon.ts +++ b/src/services/hooks/weather/getForecastWeatherByLatLon.ts @@ -6,8 +6,6 @@ import type { } from '../../../layouts/widgets/weather/weather.interface' async function fetchForecastWeatherByLatLon( - lat: number, - lon: number, count?: number, units?: TemperatureUnit ): Promise { @@ -15,8 +13,6 @@ async function fetchForecastWeatherByLatLon( const response = await client.get('/weather/forecast', { params: { - lat, - lon, ...(count !== null && { count }), ...(units !== null && { units }), }, @@ -24,20 +20,15 @@ async function fetchForecastWeatherByLatLon( return response.data } -export function useGetForecastWeatherByLatLon( - lat: number, - lon: number, - options: { - refetchInterval: number | null - count?: number - units?: TemperatureUnit - enabled: boolean - } -) { +export function useGetForecastWeatherByLatLon(options: { + refetchInterval: number | null + count?: number + units?: TemperatureUnit + enabled: boolean +}) { return useQuery({ - queryKey: ['ForecastGetWeatherByLatLon', lat, lon], - queryFn: () => - fetchForecastWeatherByLatLon(lat, lon, options.count, options.units), + queryKey: ['ForecastGetWeatherByLatLon'], + queryFn: () => fetchForecastWeatherByLatLon(options.count, options.units), refetchInterval: options.refetchInterval || false, enabled: options.enabled, }) diff --git a/src/services/hooks/weather/getWeatherByLatLon.ts b/src/services/hooks/weather/getWeatherByLatLon.ts index 0ff10135..482724f6 100644 --- a/src/services/hooks/weather/getWeatherByLatLon.ts +++ b/src/services/hooks/weather/getWeatherByLatLon.ts @@ -5,21 +5,13 @@ import type { FetchedWeather } from '../../../layouts/widgets/weather/weather.in type units = 'standard' | 'metric' | 'imperial' async function fetchWeatherByLatLon( - lat: number, - lon: number, units?: units, useAI?: boolean ): Promise { - if (lat === 0 && lon === 0) { - throw new Error('Invalid coordinates') - } - const client = await getMainClient() const response = await client.get('/weather/current', { params: { - lat, - lon, units, useAI, }, @@ -33,14 +25,10 @@ type GetWeatherByLatLon = { refetchInterval: number | null enabled: boolean } -export function useGetWeatherByLatLon( - lat: number, - lon: number, - options: GetWeatherByLatLon -) { +export function useGetWeatherByLatLon(options: GetWeatherByLatLon) { return useQuery({ - queryKey: ['getWeatherByLatLon', lat, lon], - queryFn: () => fetchWeatherByLatLon(lat, lon, options.units, options.useAI), + queryKey: ['getWeatherByLatLon'], + queryFn: () => fetchWeatherByLatLon(options.units, options.useAI), refetchInterval: options?.refetchInterval || false, enabled: options.enabled, staleTime: ms('5m'), From cc4c5c29f5b0b77705de1c503600bcc28d809cae Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Tue, 9 Dec 2025 18:52:39 +0330 Subject: [PATCH 3/5] refactor: update event names and enhance RequireAuth component styling --- src/common/utils/call-event.ts | 2 +- src/components/auth/require-auth.tsx | 7 +- src/layouts/navbar/navbar.layout.tsx | 11 +- .../tools/religious/religious-time.tsx | 131 +++++++++++------- .../widgets/weather/weather.layout.tsx | 16 +-- .../hooks/date/getReligiousTime.hook.ts | 76 ++++------ 6 files changed, 123 insertions(+), 120 deletions(-) diff --git a/src/common/utils/call-event.ts b/src/common/utils/call-event.ts index 656b0f27..8b35bae4 100644 --- a/src/common/utils/call-event.ts +++ b/src/common/utils/call-event.ts @@ -14,7 +14,7 @@ import type { FontFamily } from '@/context/appearance.context' export interface EventName { startSync: SyncTarget - openSettings: 'account' | 'wallpapers' | null + openSettings: 'account' | 'wallpapers' | 'general' | null todosChanged: Todo[] wallpaperChanged: StoredWallpaper openWidgetsSettings: { tab: WidgetTabKeys | null } diff --git a/src/components/auth/require-auth.tsx b/src/components/auth/require-auth.tsx index 42684334..24ce4013 100644 --- a/src/components/auth/require-auth.tsx +++ b/src/components/auth/require-auth.tsx @@ -73,7 +73,12 @@ export const RequireAuth = ({ children, fallback, mode = 'block' }: RequireAuthP

برای دسترسی به این بخش، لطفاً وارد حساب کاربری خود شوید.

- diff --git a/src/layouts/navbar/navbar.layout.tsx b/src/layouts/navbar/navbar.layout.tsx index 801ad5c7..f9c57d89 100644 --- a/src/layouts/navbar/navbar.layout.tsx +++ b/src/layouts/navbar/navbar.layout.tsx @@ -40,10 +40,13 @@ export function NavbarLayout(): JSX.Element { const [logoData, setLogoData] = useState(DEFAULT_LOGO_DATA) const { isAuthenticated } = useAuth() - const handleOpenSettings = useCallback((tabName: 'account' | 'wallpapers' | null) => { - if (tabName) setTab(tabName) - setShowSettings(true) - }, []) + const handleOpenSettings = useCallback( + (tabName: 'account' | 'wallpapers' | 'general' | null) => { + if (tabName) setTab(tabName) + setShowSettings(true) + }, + [] + ) const settingsModalCloseHandler = () => setShowSettings(false) diff --git a/src/layouts/widgets/tools/religious/religious-time.tsx b/src/layouts/widgets/tools/religious/religious-time.tsx index 48543de5..571b2ceb 100644 --- a/src/layouts/widgets/tools/religious/religious-time.tsx +++ b/src/layouts/widgets/tools/religious/religious-time.tsx @@ -4,6 +4,12 @@ import { useReligiousTime } from '@/services/hooks/date/getReligiousTime.hook' import type { WidgetifyDate } from '../../calendar/utils' import { DailyZikrBox } from './components/daily-zikr-box' import { PrayerTimeBox } from './components/prayer-time-box' +import { useAuth } from '@/context/auth.context' +import { RequireAuth } from '@/components/auth/require-auth' +import { Button } from '@/components/button/button' +import { callEvent } from '@/common/utils/call-event' +import { WidgetTabKeys } from '@/layouts/widgets-settings/constant/tab-keys' +import Analytics from '@/analytics' interface Prop { currentDate: WidgetifyDate @@ -29,21 +35,17 @@ const DAILY_ZIKRS = [ ] export function ReligiousTime({ currentDate }: Prop) { - const { selectedCity } = useGeneralSetting() - + const { isAuthenticated, user } = useAuth() const day = currentDate.jDate() const month = currentDate.jMonth() + 1 - + 3 const weekDay = currentDate.format('dddd') - const lat = selectedCity?.lat || 35.6892523 - const long = selectedCity?.lon || 51.3896004 - const { data: religiousTimeData, loading, error, - } = useReligiousTime(day, month, lat, long) + } = useReligiousTime(day, month, isAuthenticated && user?.city?.id != null) const dailyZikr = DAILY_ZIKRS.find((item) => item.day === weekDay) const getBoxIconStyle = () => { @@ -67,64 +69,87 @@ export function ReligiousTime({ currentDate }: Prop) { { title: 'نیمه شب', value: religiousTimeData?.nimeshab, icon: FiMoon }, ] + const onClickSetCity = () => { + callEvent('openSettings', 'general') + Analytics.event('religious_time_set_city_clicked') + } + return (
- {loading ? ( -
- {prayerTimeBoxes.map((box, index) => ( - - ))} -
- ) : error ? ( -
-
- + + {!user?.city?.id ? ( +
+
🕋
+

+ برای نمایش اوقات شرعی، لطفاً ابتدا در تنظیمات ویجت، شهر خود را + تنظیم کنید. +

+
-

- مشکلی در دریافت اطلاعات وجود دارد -

-
- ) : ( - <> -
+ ) : loading ? ( +
{prayerTimeBoxes.map((box, index) => ( ))} -
{' '} - {loading ? ( - - ) : ( - dailyZikr && ( - - ) - )} - - )} +
+ ) : error ? ( +
+
+ +
+

+ مشکلی در دریافت اطلاعات وجود دارد +

+
+ ) : ( + <> +
+ {prayerTimeBoxes.map((box, index) => ( + + ))} +
{' '} + {loading ? ( + + ) : ( + dailyZikr && ( + + ) + )} + + )} +
) } diff --git a/src/layouts/widgets/weather/weather.layout.tsx b/src/layouts/widgets/weather/weather.layout.tsx index 8f64ed2b..484db36d 100644 --- a/src/layouts/widgets/weather/weather.layout.tsx +++ b/src/layouts/widgets/weather/weather.layout.tsx @@ -13,7 +13,6 @@ import { useAuth } from '@/context/auth.context' import { RequireAuth } from '@/components/auth/require-auth' import { useGetWeatherByLatLon } from '@/services/hooks/weather/getWeatherByLatLon' import { useGetForecastWeatherByLatLon } from '@/services/hooks/weather/getForecastWeatherByLatLon' -import type { AxiosError } from 'axios' import { Button } from '@/components/button/button' import { WidgetTabKeys } from '@/layouts/widgets-settings/constant/tab-keys' import Analytics from '@/analytics' @@ -23,12 +22,11 @@ export function WeatherLayout() { const [weatherSettings, setWeatherSettings] = useState(null) const [weatherState, setWeather] = useState(null) const [forecastWeather, setForecastWeather] = useState(null) - const [showSetCityMessage, setShowSetCityMessage] = useState(false) - const { data, dataUpdatedAt, error } = useGetWeatherByLatLon({ + const { data, dataUpdatedAt } = useGetWeatherByLatLon({ refetchInterval: 0, units: weatherSettings?.temperatureUnit, useAI: weatherSettings?.useAI, - enabled: isAuthenticated, + enabled: isAuthenticated && user?.city?.id != null, }) const { data: forecastData, dataUpdatedAt: forecastDataUpdatedAt } = @@ -86,13 +84,7 @@ export function WeatherLayout() { setToStorage('currentWeather', data) setWeather(data) } - if (error) { - const axiosError = error as AxiosError - if (axiosError.response?.data?.message === 'USER_CITY_NOT_SET') { - setShowSetCityMessage(true) - } - } - }, [data, dataUpdatedAt, error]) + }, [data, dataUpdatedAt]) useEffect(() => { if (forecastData) { @@ -113,7 +105,7 @@ export function WeatherLayout() { return ( - {showSetCityMessage ? ( + {!user?.city?.id ? (
🌤️

diff --git a/src/services/hooks/date/getReligiousTime.hook.ts b/src/services/hooks/date/getReligiousTime.hook.ts index fc191f59..01089b1d 100644 --- a/src/services/hooks/date/getReligiousTime.hook.ts +++ b/src/services/hooks/date/getReligiousTime.hook.ts @@ -1,5 +1,5 @@ import { getMainClient } from '@/services/api' -import { useEffect, useState } from 'react' +import { useQuery } from '@tanstack/react-query' export interface FetchedReligiousTimeData { azan_sobh: string @@ -9,55 +9,33 @@ export interface FetchedReligiousTimeData { azan_maghreb: string nimeshab: string } -const cachedData: Map = new Map() -export const useReligiousTime = ( - day: number, - month: number, - lat: number, - long: number -) => { - const [data, setData] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) +export interface FetchedReligiousTimeData { + azan_sobh: string + tolu_aftab: string + azan_zohr: string + ghorub_aftab: string + azan_maghreb: string + nimeshab: string +} - useEffect(() => { - const fetchReligiousTime = async () => { - try { - const cacheKey = `${day}-${month}-${lat}-${long}` - if (cachedData.has(cacheKey)) { - setData(cachedData.get(cacheKey) as FetchedReligiousTimeData) - return +export const useReligiousTime = (day: number, month: number, enabled: boolean) => { + const { data, isLoading, error } = useQuery({ + queryKey: ['religiousTime', day, month], + queryFn: async () => { + const client = await getMainClient() + const { data: result } = await client.get( + '/date/owghat', + { + params: { + day, + month, + }, } + ) + return result + }, + enabled, + }) - setLoading(true) - const client = await getMainClient() - - const { data: result } = await client.get( - '/date/owghat', - { - params: { - day, - month, - lat, - long, - }, - } - ) - - setData(result) - - cachedData.set(cacheKey, result) - } catch (err) { - setError( - err instanceof Error ? err : new Error('An unknown error occurred') - ) - } finally { - setLoading(false) - } - } - - fetchReligiousTime() - }, [day, month, lat, long]) - - return { data, loading, error } + return { data, loading: isLoading, error } } From 31c7e459f83203641ae60bd311e55b366e02b72c Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Tue, 9 Dec 2025 20:36:18 +0330 Subject: [PATCH 4/5] refactor: enhance weather and religious time hooks with city-based refetching --- .../tools/religious/religious-time.tsx | 11 +++++-- .../widgets/weather/weather.layout.tsx | 30 ++++++++++++++----- .../hooks/date/getReligiousTime.hook.ts | 4 +-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/layouts/widgets/tools/religious/religious-time.tsx b/src/layouts/widgets/tools/religious/religious-time.tsx index 571b2ceb..3b741317 100644 --- a/src/layouts/widgets/tools/religious/religious-time.tsx +++ b/src/layouts/widgets/tools/religious/religious-time.tsx @@ -1,5 +1,4 @@ import { FiClock, FiMoon, FiSun, FiSunrise, FiSunset } from 'react-icons/fi' -import { useGeneralSetting } from '@/context/general-setting.context' import { useReligiousTime } from '@/services/hooks/date/getReligiousTime.hook' import type { WidgetifyDate } from '../../calendar/utils' import { DailyZikrBox } from './components/daily-zikr-box' @@ -8,7 +7,6 @@ import { useAuth } from '@/context/auth.context' import { RequireAuth } from '@/components/auth/require-auth' import { Button } from '@/components/button/button' import { callEvent } from '@/common/utils/call-event' -import { WidgetTabKeys } from '@/layouts/widgets-settings/constant/tab-keys' import Analytics from '@/analytics' interface Prop { @@ -43,10 +41,17 @@ export function ReligiousTime({ currentDate }: Prop) { const { data: religiousTimeData, - loading, + isLoading: loading, error, + refetch, } = useReligiousTime(day, month, isAuthenticated && user?.city?.id != null) + useEffect(() => { + if (isAuthenticated && user?.city?.id) { + refetch() + } + }, [user?.city?.id, isAuthenticated, refetch]) + const dailyZikr = DAILY_ZIKRS.find((item) => item.day === weekDay) const getBoxIconStyle = () => { return 'text-primary' diff --git a/src/layouts/widgets/weather/weather.layout.tsx b/src/layouts/widgets/weather/weather.layout.tsx index 484db36d..d866a6de 100644 --- a/src/layouts/widgets/weather/weather.layout.tsx +++ b/src/layouts/widgets/weather/weather.layout.tsx @@ -22,20 +22,27 @@ export function WeatherLayout() { const [weatherSettings, setWeatherSettings] = useState(null) const [weatherState, setWeather] = useState(null) const [forecastWeather, setForecastWeather] = useState(null) - const { data, dataUpdatedAt } = useGetWeatherByLatLon({ + const { + data, + dataUpdatedAt, + refetch: refetchWeather, + } = useGetWeatherByLatLon({ refetchInterval: 0, units: weatherSettings?.temperatureUnit, useAI: weatherSettings?.useAI, enabled: isAuthenticated && user?.city?.id != null, }) - const { data: forecastData, dataUpdatedAt: forecastDataUpdatedAt } = - useGetForecastWeatherByLatLon({ - count: 6, - units: weatherSettings?.temperatureUnit, - enabled: isAuthenticated && user?.city?.id != null, - refetchInterval: 0, - }) + const { + data: forecastData, + dataUpdatedAt: forecastDataUpdatedAt, + refetch: refetchForecast, + } = useGetForecastWeatherByLatLon({ + count: 6, + units: weatherSettings?.temperatureUnit, + enabled: isAuthenticated && user?.city?.id != null, + refetchInterval: 0, + }) useEffect(() => { async function load() { @@ -93,6 +100,13 @@ export function WeatherLayout() { } }, [forecastDataUpdatedAt]) + useEffect(() => { + if (isAuthenticated && user?.city?.id) { + refetchWeather() + refetchForecast() + } + }, [user?.city?.id, isAuthenticated, refetchWeather, refetchForecast]) + const onClickSetCity = () => { callEvent('openWidgetsSettings', { tab: WidgetTabKeys.weather_settings, diff --git a/src/services/hooks/date/getReligiousTime.hook.ts b/src/services/hooks/date/getReligiousTime.hook.ts index 01089b1d..9db48fef 100644 --- a/src/services/hooks/date/getReligiousTime.hook.ts +++ b/src/services/hooks/date/getReligiousTime.hook.ts @@ -19,7 +19,7 @@ export interface FetchedReligiousTimeData { } export const useReligiousTime = (day: number, month: number, enabled: boolean) => { - const { data, isLoading, error } = useQuery({ + return useQuery({ queryKey: ['religiousTime', day, month], queryFn: async () => { const client = await getMainClient() @@ -36,6 +36,4 @@ export const useReligiousTime = (day: number, month: number, enabled: boolean) = }, enabled, }) - - return { data, loading: isLoading, error } } From 9b8a19377399795f280dd03c18602505bd877130 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Tue, 9 Dec 2025 20:40:59 +0330 Subject: [PATCH 5/5] refactor: remove selectedCity from StorageKV interface --- src/common/constant/store.key.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/constant/store.key.ts b/src/common/constant/store.key.ts index 7137da23..da1cb029 100644 --- a/src/common/constant/store.key.ts +++ b/src/common/constant/store.key.ts @@ -1,5 +1,4 @@ import type { CurrencyColorMode } from '@/context/currency.context' -import type { SelectedCity } from '@/context/general-setting.context' import type { Theme } from '@/context/theme.context' import type { TodoOptions } from '@/context/todo.context' import type { WidgetItem } from '@/context/widget-visibility.context' @@ -31,7 +30,6 @@ export interface StorageKV { currencies: string[] currencyColorMode: CurrencyColorMode hasShownPwaModal: boolean - selectedCity: SelectedCity | null currentWeather: FetchedWeather todos: Todo[] wallpaper: StoredWallpaper