From 30df8344a5ddbe110ee74a603cfb99c114f55a09 Mon Sep 17 00:00:00 2001 From: Mojtaba-NA Date: Tue, 25 Feb 2025 18:40:51 +0330 Subject: [PATCH] refactor: storage types --- biome.json | 88 ++--- package.json | 109 +++--- src/common/constant/store.key.ts | 23 +- src/common/storage.ts | 22 +- src/context/todo.context.tsx | 91 +++-- src/layouts/arzLive/arzLive.layout.tsx | 66 ++-- .../arzLive/components/currency-box.tsx | 337 +++++++++--------- .../components/options-modal.component.tsx | 189 +++++----- src/layouts/weather/weather.layout.tsx | 110 +++--- src/pages/home.tsx | 186 +++++----- .../getMethodHooks/getCurrencyByCode.hook.ts | 48 +-- src/services/getMethodHooks/getEvents.hook.ts | 42 +-- .../getSupportCurrencies.hook.ts | 36 +- .../weather/getForecastWeatherByLatLon.ts | 27 +- 14 files changed, 670 insertions(+), 704 deletions(-) diff --git a/biome.json b/biome.json index dd95da8..1cb5189 100644 --- a/biome.json +++ b/biome.json @@ -1,46 +1,46 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "ignore": [] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab", - "lineWidth": 90 - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "a11y": { - "useAltText": "off", - "useButtonType": "off", - "useKeyWithClickEvents": "off", - "noSvgWithoutTitle": "off", - "noLabelWithoutControl": "off" - }, - "style": { - "useSelfClosingElements": "off" - }, - "suspicious": { - "noExplicitAny": "off", - "noArrayIndexKey": "off" - } - } - }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "semicolons": "asNeeded" - } - } + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 90 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "a11y": { + "useAltText": "off", + "useButtonType": "off", + "useKeyWithClickEvents": "off", + "noSvgWithoutTitle": "off", + "noLabelWithoutControl": "off" + }, + "style": { + "useSelfClosingElements": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noArrayIndexKey": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + } + } } diff --git a/package.json b/package.json index a32063e..ebf5ecc 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,57 @@ { - "name": "widgetify-webapp", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "tsbuild": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview", - "test": "npx tailwindcss init" - }, - "dependencies": { - "@hello-pangea/dnd": "^18.0.1", - "@tanstack/react-query": "5.66.0", - "add": "2.0.6", - "axios": "1.7.9", - "chart.js": "^4.4.7", - "jalali-moment": "3.3.11", - "moment": "^2.30.1", - "moment-hijri": "^3.0.0", - "motion": "12.3.1", - "ms": "2.1.3", - "react": "18.3.1", - "react-chartjs-2": "^5.3.0", - "react-daisyui": "5.0.5", - "react-dom": "18.3.1", - "react-hot-toast": "2.5.1", - "react-icons": "5.4.0", - "react-router-dom": "7.1.5", - "react-select": "^5.10.0", - "vite-plugin-pwa": "0.21.1" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@eslint/js": "9.17.0", - "@tailwindcss/vite": "4.0.0", - "@types/moment-hijri": "^2.1.4", - "@types/ms": "2.1.0", - "@types/node": "22.13.1", - "@types/react": "18.2.19", - "@types/react-dom": "18.3.5", - "@vitejs/plugin-react": "4.3.4", - "autoprefixer": "10.4.20", - "eslint": "9.17.0", - "eslint-plugin-react-hooks": "5.0.0", - "eslint-plugin-react-refresh": "0.4.16", - "globals": "15.14.0", - "postcss": "8.5.1", - "tailwindcss": "4.0.0", - "typescript": "~5.6.3", - "typescript-eslint": "8.18.2", - "vite": "6.0.5" - } + "name": "widgetify-webapp", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "tsbuild": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "npx tailwindcss init", + "tcw": "tsc --project tsconfig.app.json -w" + }, + "dependencies": { + "@hello-pangea/dnd": "^18.0.1", + "@tanstack/react-query": "5.66.0", + "add": "2.0.6", + "axios": "1.7.9", + "chart.js": "^4.4.7", + "jalali-moment": "3.3.11", + "moment": "^2.30.1", + "moment-hijri": "^3.0.0", + "motion": "12.3.1", + "ms": "2.1.3", + "react": "18.3.1", + "react-chartjs-2": "^5.3.0", + "react-daisyui": "5.0.5", + "react-dom": "18.3.1", + "react-hot-toast": "2.5.1", + "react-icons": "5.4.0", + "react-router-dom": "7.1.5", + "react-select": "^5.10.0", + "vite-plugin-pwa": "0.21.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@eslint/js": "9.17.0", + "@tailwindcss/vite": "4.0.0", + "@types/moment-hijri": "^2.1.4", + "@types/ms": "2.1.0", + "@types/node": "22.13.1", + "@types/react": "18.2.19", + "@types/react-dom": "18.3.5", + "@vitejs/plugin-react": "4.3.4", + "autoprefixer": "10.4.20", + "eslint": "9.17.0", + "eslint-plugin-react-hooks": "5.0.0", + "eslint-plugin-react-refresh": "0.4.16", + "globals": "15.14.0", + "postcss": "8.5.1", + "tailwindcss": "4.0.0", + "typescript": "~5.6.3", + "typescript-eslint": "8.18.2", + "vite": "6.0.5" + } } diff --git a/src/common/constant/store.key.ts b/src/common/constant/store.key.ts index 7c06fe2..b714254 100644 --- a/src/common/constant/store.key.ts +++ b/src/common/constant/store.key.ts @@ -1,10 +1,15 @@ -export enum StoreKey { - CURRENCIES = 'CURRENCIES', - hasShownPwaModal = 'hasShownPwaModal', - CURRENCY_UPDATED_AT = 'CURRENCY_UPDATED_AT', - SELECTED_CITY = 'SELECTED_CITY', - CURRENT_WEATHER = 'CURRENT_WEATHER', - LAYOUT_ORDER = 'LAYOUT_ORDER', - Todos = 'Todos', +import type { SelectedCity } from '../../context/setting.context' +import type { Todo } from '../../layouts/calendar/interface/todo.interface' +import type { FetchedCurrency } from '../../services/getMethodHooks/getCurrencyByCode.hook' +import type { FetchedWeather } from '../../services/getMethodHooks/weather/weather.interface' + +export interface StorageKV { + CURRENCIES: string[] + hasShownPwaModal: boolean + CURRENCY_UPDATED_AT: string + SELECTED_CITY: SelectedCity + CURRENT_WEATHER: FetchedWeather + LAYOUT_ORDER: string[] + Todos: Todo[] + [key: `currency:${string}`]: FetchedCurrency } -export type StoreKeyType = StoreKey | `currency:${string}` diff --git a/src/common/storage.ts b/src/common/storage.ts index 3b72501..a5ba12b 100644 --- a/src/common/storage.ts +++ b/src/common/storage.ts @@ -1,15 +1,15 @@ -import type { StoreKeyType } from './constant/store.key' +import type { StorageKV } from './constant/store.key' -export function setToStorage(key: StoreKeyType, value: T) { - localStorage.setItem(key, JSON.stringify(value)) +export function setToStorage(key: K, value: StorageKV[K]) { + localStorage.setItem(key, JSON.stringify(value)) } -export function getFromStorage(key: StoreKeyType): T | null { - const value = localStorage.getItem(key) - if (!value) return null - try { - return JSON.parse(value) as T - } catch { - return value as T - } +export function getFromStorage(key: K): StorageKV[K] | null { + const value = localStorage.getItem(key) + if (!value) return null + try { + return JSON.parse(value) + } catch { + return null + } } diff --git a/src/context/todo.context.tsx b/src/context/todo.context.tsx index 79dd9bf..0626e52 100644 --- a/src/context/todo.context.tsx +++ b/src/context/todo.context.tsx @@ -1,61 +1,56 @@ import { type ReactNode, createContext, useContext, useEffect, useState } from 'react' -import { StoreKey } from '../common/constant/store.key' import { getFromStorage, setToStorage } from '../common/storage' import type { Todo } from '../layouts/calendar/interface/todo.interface' interface TodoContextType { - todos: Todo[] - addTodo: (text: string, date: string) => void - removeTodo: (id: string) => void - toggleTodo: (id: string) => void - setTodos: (todos: Todo[]) => void + todos: Todo[] + addTodo: (text: string, date: string) => void + removeTodo: (id: string) => void + toggleTodo: (id: string) => void + setTodos: (todos: Todo[]) => void } const TodoContext = createContext(undefined) export function TodoProvider({ children }: { children: ReactNode }) { - const [todos, setTodos] = useState([]) - - useEffect(() => { - const todosFromStorage = getFromStorage(StoreKey.Todos) - if (todosFromStorage) { - setTodos(todosFromStorage as Todo[]) - } - }, []) - - const addTodo = (text: string, date: string) => { - const todoList = [ - ...todos, - { id: Math.random().toString(36).slice(2), text, completed: false, date }, - ] - - setTodos(todoList) - - setToStorage(StoreKey.Todos, todoList) - } - - const removeTodo = (id: string) => { - setTodos(todos.filter((todo) => todo.id !== id)) - } - - const toggleTodo = (id: string) => { - setTodos( - todos.map((todo) => - todo.id === id ? { ...todo, completed: !todo.completed } : todo, - ), - ) - } - - return ( - - {children} - - ) + const [todos, setTodos] = useState([]) + + useEffect(() => { + const todosFromStorage = getFromStorage('Todos') + if (todosFromStorage) { + setTodos(todosFromStorage) + } + }, []) + + const addTodo = (text: string, date: string) => { + const todoList = [ + ...todos, + { id: Math.random().toString(36).slice(2), text, completed: false, date } + ] + + setTodos(todoList) + + setToStorage('Todos', todoList) + } + + const removeTodo = (id: string) => { + setTodos(todos.filter(todo => todo.id !== id)) + } + + const toggleTodo = (id: string) => { + setTodos(todos.map(todo => (todo.id === id ? { ...todo, completed: !todo.completed } : todo))) + } + + return ( + + {children} + + ) } export function useTodo() { - const context = useContext(TodoContext) - if (context === undefined) { - throw new Error('useTodo must be used within a TodoProvider') - } - return context + const context = useContext(TodoContext) + if (context === undefined) { + throw new Error('useTodo must be used within a TodoProvider') + } + return context } diff --git a/src/layouts/arzLive/arzLive.layout.tsx b/src/layouts/arzLive/arzLive.layout.tsx index 40866d6..1da698d 100644 --- a/src/layouts/arzLive/arzLive.layout.tsx +++ b/src/layouts/arzLive/arzLive.layout.tsx @@ -1,47 +1,45 @@ import jalaliMoment from 'jalali-moment' import { useContext, useEffect, useState } from 'react' -import { StoreKey } from '../../common/constant/store.key' +import { getFromStorage, setToStorage } from '../../common/storage' import { storeContext } from '../../context/setting.context' import { useGetSupportCurrencies } from '../../services/getMethodHooks/getSupportCurrencies.hook' import { AddCurrencyBox } from './components/addCurrency-box' import { CurrencyBox } from './components/currency-box' export function ArzLiveLayout() { - const { isLoading, data } = useGetSupportCurrencies() - const { selectedCurrencies } = useContext(storeContext) - const [updatedAt, setUpdatedAt] = useState( - localStorage.getItem(StoreKey.CURRENCY_UPDATED_AT) || new Date(), - ) + const { isLoading, data } = useGetSupportCurrencies() + const { selectedCurrencies } = useContext(storeContext) + const [updatedAt, setUpdatedAt] = useState(getFromStorage('CURRENCY_UPDATED_AT') || new Date()) - useEffect(() => { - function handleUpdatedAt() { - setUpdatedAt(new Date()) - localStorage.setItem(StoreKey.CURRENCY_UPDATED_AT, new Date().toString()) - } + useEffect(() => { + function handleUpdatedAt() { + setUpdatedAt(new Date()) + setToStorage('CURRENCY_UPDATED_AT', new Date().toString()) + } - window.addEventListener('fetched-data', handleUpdatedAt) + window.addEventListener('fetched-data', handleUpdatedAt) - return () => { - window.removeEventListener('fetched-data', handleUpdatedAt) - } - }, []) + return () => { + window.removeEventListener('fetched-data', handleUpdatedAt) + } + }, []) - return ( -
-
-

- 🪙 ArzLive -

- - {jalaliMoment(updatedAt).format('jYYYY/jM/jD, HH:mm A')} - -
-
- {selectedCurrencies.map((currency, index) => ( - - ))} + return ( +
+
+

+ 🪙 ArzLive +

+ + {jalaliMoment(updatedAt).format('jYYYY/jM/jD, HH:mm A')} + +
+
+ {selectedCurrencies.map((currency, index) => ( + + ))} - -
-
- ) + +
+
+ ) } diff --git a/src/layouts/arzLive/components/currency-box.tsx b/src/layouts/arzLive/components/currency-box.tsx index 26968c7..1aefbcc 100644 --- a/src/layouts/arzLive/components/currency-box.tsx +++ b/src/layouts/arzLive/components/currency-box.tsx @@ -4,181 +4,176 @@ import { useEffect, useRef, useState } from 'react' import { FaArrowDownLong, FaArrowUpLong } from 'react-icons/fa6' import { getMainColorFromImage } from '../../../common/color' import { getFromStorage, setToStorage } from '../../../common/storage' -import { - type FetchedCurrency, - useGetCurrencyByCode, -} from '../../../services/getMethodHooks/getCurrencyByCode.hook' +import { useGetCurrencyByCode } from '../../../services/getMethodHooks/getCurrencyByCode.hook' import { CurrencyModalComponent } from './currency-modal' interface CurrencyBoxProps { - code: string + code: string } export const CurrencyBox = ({ code }: CurrencyBoxProps) => { - const { data, dataUpdatedAt } = useGetCurrencyByCode(code, { - refetchInterval: ms('3m'), - }) - const [currency, setCurrency] = useState( - getFromStorage(`currency:${code}`) || null, - ) - - const [imgColor, setImgColor] = useState() - const [displayPrice, setDisplayPrice] = useState(0) - const [priceChange, setPriceChange] = useState(0) - const [isModalOpen, setIsModalOpen] = useState(false) - - const prevPriceRef = useRef(null) - - const priceMotion = useMotionValue(0) - const defaultDamping = 20 - const [damping, setDamping] = useState(defaultDamping) - - const animatedPrice = useSpring(priceMotion, { - stiffness: 100, - damping, - }) - - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (data) { - setCurrency(data) - setToStorage(`currency:${code}`, data) - } - const event = new Event('fetched-data') - window.dispatchEvent(event) - }, [dataUpdatedAt]) - - useEffect(() => { - if (currency?.icon) { - getMainColorFromImage(currency.icon).then((color) => { - setImgColor(color) - }) - } - }, [currency?.icon]) - - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (currency?.price) { - if (prevPriceRef.current !== currency.price) { - priceMotion.set(currency.rialPrice) - prevPriceRef.current = currency.price - if (currency.changePercentage) { - const changeAmount = (currency.changePercentage / 100) * currency.price - setPriceChange(changeAmount) - } - } - } - }, [currency?.price, priceMotion]) - - useEffect(() => { - const unsubscribe = animatedPrice.on('change', (v) => { - setDisplayPrice(Math.round(v)) - - const diff = Math.abs(v - (currency?.rialPrice || 0)) - setDamping(diff < 5 ? 50 : defaultDamping) - }) - return () => unsubscribe() - }, [animatedPrice, currency?.rialPrice]) - - function toggleCurrencyModal() { - if (!isModalOpen === true) { - if (!data) return - // vibration - if ('vibrate' in navigator) { - navigator.vibrate(100) - } - } - setIsModalOpen(!isModalOpen) - } - - const longPressTimeout = useRef(null) - - const handleMouseDown = () => { - longPressTimeout.current = setTimeout(() => { - toggleCurrencyModal() - }, 500) // 500ms for long press - } - - const handleMouseUp = () => { - if (longPressTimeout.current) { - clearTimeout(longPressTimeout.current) - longPressTimeout.current = null - } - } - - return ( - <> - toggleCurrencyModal()} - onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} - onTouchStart={handleMouseDown} - onTouchEnd={handleMouseUp} - > -
-
- {currency?.name?.en} -
-
-
- -
-

- {currency?.name.en} -

-

- {code.toUpperCase()} -

-
-
- -
- {/* {currency?.changePercentage ? ( */} - 0 ? 'text-red-500' : 'text-green-500' - } ${currency?.changePercentage ? 'opacity-100' : 'invisible'}`} - > - {priceChange > 0 ? : } - {Number(priceChange.toFixed()).toLocaleString()} - - {/* // ) : null} */} - - {displayPrice !== 0 ? displayPrice.toLocaleString() : ''} - -
- - {currency && ( - - )} - - ) + const { data, dataUpdatedAt } = useGetCurrencyByCode(code, { + refetchInterval: ms('3m') + }) + const [currency, setCurrency] = useState(getFromStorage(`currency:${code}`) || null) + + const [imgColor, setImgColor] = useState() + const [displayPrice, setDisplayPrice] = useState(0) + const [priceChange, setPriceChange] = useState(0) + const [isModalOpen, setIsModalOpen] = useState(false) + + const prevPriceRef = useRef(null) + + const priceMotion = useMotionValue(0) + const defaultDamping = 20 + const [damping, setDamping] = useState(defaultDamping) + + const animatedPrice = useSpring(priceMotion, { + stiffness: 100, + damping + }) + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (data) { + setCurrency(data) + setToStorage(`currency:${code}`, data) + } + const event = new Event('fetched-data') + window.dispatchEvent(event) + }, [dataUpdatedAt]) + + useEffect(() => { + if (currency?.icon) { + getMainColorFromImage(currency.icon).then(color => { + setImgColor(color) + }) + } + }, [currency?.icon]) + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (currency?.price) { + if (prevPriceRef.current !== currency.price) { + priceMotion.set(currency.rialPrice) + prevPriceRef.current = currency.price + if (currency.changePercentage) { + const changeAmount = (currency.changePercentage / 100) * currency.price + setPriceChange(changeAmount) + } + } + } + }, [currency?.price, priceMotion]) + + useEffect(() => { + const unsubscribe = animatedPrice.on('change', v => { + setDisplayPrice(Math.round(v)) + + const diff = Math.abs(v - (currency?.rialPrice || 0)) + setDamping(diff < 5 ? 50 : defaultDamping) + }) + return () => unsubscribe() + }, [animatedPrice, currency?.rialPrice]) + + function toggleCurrencyModal() { + if (!isModalOpen === true) { + if (!data) return + // vibration + if ('vibrate' in navigator) { + navigator.vibrate(100) + } + } + setIsModalOpen(!isModalOpen) + } + + const longPressTimeout = useRef(null) + + const handleMouseDown = () => { + longPressTimeout.current = setTimeout(() => { + toggleCurrencyModal() + }, 500) // 500ms for long press + } + + const handleMouseUp = () => { + if (longPressTimeout.current) { + clearTimeout(longPressTimeout.current) + longPressTimeout.current = null + } + } + + return ( + <> + toggleCurrencyModal()} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} + onTouchStart={handleMouseDown} + onTouchEnd={handleMouseUp} + > +
+
+ {currency?.name?.en} +
+
+
+ +
+

+ {currency?.name.en} +

+

+ {code.toUpperCase()} +

+
+
+ +
+ {/* {currency?.changePercentage ? ( */} + 0 ? 'text-red-500' : 'text-green-500' + } ${currency?.changePercentage ? 'opacity-100' : 'invisible'}`} + > + {priceChange > 0 ? : } + {Number(priceChange.toFixed()).toLocaleString()} + + {/* // ) : null} */} + + {displayPrice !== 0 ? displayPrice.toLocaleString() : ''} + +
+ + {currency && ( + + )} + + ) } diff --git a/src/layouts/weather/components/options-modal.component.tsx b/src/layouts/weather/components/options-modal.component.tsx index 1349d1c..16ac5b1 100644 --- a/src/layouts/weather/components/options-modal.component.tsx +++ b/src/layouts/weather/components/options-modal.component.tsx @@ -1,122 +1,117 @@ import { useContext, useState } from 'react' -import { StoreKey } from '../../../common/constant/store.key' import { setToStorage } from '../../../common/storage' import Modal from '../../../components/modal' import { storeContext } from '../../../context/setting.context' import { useGetRelatedCities } from '../../../services/getMethodHooks/weather/getRelatedCities' interface WeatherOptionsModalProps { - show: boolean - onClose: () => void + show: boolean + onClose: () => void } export function WeatherOptionsModal({ onClose, show }: WeatherOptionsModalProps) { - const { setSelectedCity, selectedCity } = useContext(storeContext) + const { setSelectedCity, selectedCity } = useContext(storeContext) - const [inputValue, setInputValue] = useState('') + const [inputValue, setInputValue] = useState('') - const { - data: relatedCities, - isSuccess, - isLoading, - } = useGetRelatedCities(inputValue || '') + const { data: relatedCities, isSuccess, isLoading } = useGetRelatedCities(inputValue || '') - const handleInputChange = (value: string) => { - if (value === '') { - return - } - if (value.length < 2) return + const handleInputChange = (value: string) => { + if (value === '') { + return + } + if (value.length < 2) return - // delay the request to prevent too many requests - setTimeout(() => { - setInputValue(value) - }, 1000) // 1 seconds - } + // delay the request to prevent too many requests + setTimeout(() => { + setInputValue(value) + }, 1000) // 1 seconds + } - function handleSelect(selected: string) { - if (!selected) return - const [name, lat, lon] = selected.split(':') + function handleSelect(selected: string) { + if (!selected) return + const [name, lat, lon] = selected.split(':') - const city = { - city: name, - lat: Number.parseFloat(lat), - lon: Number.parseFloat(lon), - } + const city = { + city: name, + lat: Number.parseFloat(lat), + lon: Number.parseFloat(lon) + } - if (city.city === selectedCity.city) return + if (city.city === selectedCity.city) return - if (!city.lat || !city.lon) return + if (!city.lat || !city.lon) return - setSelectedCity(city) - setToStorage(StoreKey.SELECTED_CITY, city) + setSelectedCity(city) + setToStorage('SELECTED_CITY', city) - setInputValue(null) - onClose() - } + setInputValue(null) + onClose() + } - return ( - -
-
- handleInputChange(e.target.value)} - /> - {selectedCity?.city && ( -
-
- - - + placeholder-gray-500 focus:placeholder-gray-500' + onChange={e => handleInputChange(e.target.value)} + /> + {selectedCity?.city && ( +
+
+ + + - - {selectedCity.city} - -
-
- )} -
- {isLoading && ( -
-
-
- )} - {isSuccess && relatedCities && relatedCities.length > 0 && ( -
- {relatedCities.map((city) => ( -
handleSelect(`${city.name}:${city.lat}:${city.lon}`)} - > - {city.name} {city.state && `(${city.state})`} -
- ))} -
- )} -
- - ) + + {selectedCity.city} + +
+
+ )} +
+ {isLoading && ( +
+
+
+ )} + {isSuccess && relatedCities && relatedCities.length > 0 && ( +
+ {relatedCities.map(city => ( +
handleSelect(`${city.name}:${city.lat}:${city.lon}`)} + > + {city.name} {city.state && `(${city.state})`} +
+ ))} +
+ )} +
+ + ) } diff --git a/src/layouts/weather/weather.layout.tsx b/src/layouts/weather/weather.layout.tsx index 8a80dcb..c55d92e 100644 --- a/src/layouts/weather/weather.layout.tsx +++ b/src/layouts/weather/weather.layout.tsx @@ -1,7 +1,6 @@ import ms from 'ms' import { useContext, useEffect, useState } from 'react' import { FaGears } from 'react-icons/fa6' -import { StoreKey } from '../../common/constant/store.key' import { getFromStorage, setToStorage } from '../../common/storage' import { storeContext } from '../../context/setting.context' import { useGetWeatherByLatLon } from '../../services/getMethodHooks/weather/getWeatherByLatLon' @@ -13,68 +12,61 @@ import { ForecastComponent } from './components/forecast.component' import { WeatherOptionsModal } from './components/options-modal.component' export function WeatherLayout() { - const { selectedCity } = useContext(storeContext) - const [cityWeather, setCityWeather] = useState( - getFromStorage(StoreKey.CURRENT_WEATHER) || null, - ) + const { selectedCity } = useContext(storeContext) + const [cityWeather, setCityWeather] = useState(getFromStorage('CURRENT_WEATHER') || null) - const [forecast, setForecast] = useState([]) + const [forecast, setForecast] = useState([]) - const { data, dataUpdatedAt } = useGetWeatherByLatLon( - selectedCity.lat, - selectedCity.lon, - { - refetchInterval: ms('10m'), // 10 minutes - }, - ) - const { data: forecastData, dataUpdatedAt: forecastUpdatedAt } = - useGetForecastWeatherByLatLon(selectedCity.lat, selectedCity.lon, { - refetchInterval: ms('2m'), - }) + const { data, dataUpdatedAt } = useGetWeatherByLatLon(selectedCity.lat, selectedCity.lon, { + refetchInterval: ms('10m') // 10 minutes + }) + const { data: forecastData, dataUpdatedAt: forecastUpdatedAt } = useGetForecastWeatherByLatLon( + selectedCity.lat, + selectedCity.lon, + { + refetchInterval: ms('2m') + } + ) - const [showModal, setShowModal] = useState(false) + const [showModal, setShowModal] = useState(false) - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (forecastData) { - setForecast([...forecastData]) - } - }, [forecastUpdatedAt]) + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (forecastData) { + setForecast([...forecastData]) + } + }, [forecastUpdatedAt]) - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (data) { - setCityWeather(data) - setToStorage(StoreKey.CURRENT_WEATHER, data) - } - }, [dataUpdatedAt]) + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (data) { + setCityWeather(data) + setToStorage('CURRENT_WEATHER', data) + } + }, [dataUpdatedAt]) - return ( - <> -
-
-

- ☂️ Weather -

-
setShowModal(true)} - > - - {cityWeather?.city?.en || 'Options'} -
-
-
- {cityWeather ? : null} - {forecast?.length - ? forecast.map((item) => ( - - )) - : null} -
-
- setShowModal(false)} /> - - ) + return ( + <> +
+
+

☂️ Weather

+
setShowModal(true)} + > + + {cityWeather?.city?.en || 'Options'} +
+
+
+ {cityWeather ? : null} + {forecast?.length + ? forecast.map(item => ) + : null} +
+
+ setShowModal(false)} /> + + ) } diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 35940ce..527be14 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -2,120 +2,110 @@ import { DragDropContext, Draggable, type DropResult, Droppable } from '@hello-p import { useEffect, useState } from 'react' import { RxDragHandleDots2 } from 'react-icons/rx' -import { StoreKey } from '../common/constant/store.key' import { getFromStorage, setToStorage } from '../common/storage' -import { type SelectedCity, storeContext } from '../context/setting.context' +import { storeContext } from '../context/setting.context' import { ArzLiveLayout } from '../layouts/arzLive/arzLive.layout' import CalendarLayout from '../layouts/calendar/calendar' import { WeatherLayout } from '../layouts/weather/weather.layout' type LayoutItem = { - id: string - component: React.ReactNode + id: string + component: React.ReactNode } export function HomePage() { - const defaultCurrencies = ['USD', 'EUR', 'GRAM'] - const storedCurrencies = getFromStorage(StoreKey.CURRENCIES) as string[] | null - const [selectedCurrencies, setSelectedCurrencies] = useState>( - storedCurrencies && storedCurrencies.length > 0 - ? storedCurrencies - : defaultCurrencies, - ) + const defaultCurrencies = ['USD', 'EUR', 'GRAM'] + const storedCurrencies = getFromStorage('CURRENCIES') + const [selectedCurrencies, setSelectedCurrencies] = useState( + storedCurrencies && storedCurrencies.length > 0 ? storedCurrencies : defaultCurrencies + ) - const city = getFromStorage(StoreKey.SELECTED_CITY) - const [selectedCity, setSelectedCity] = useState( - city || { - city: 'Tehran', - lat: 35.6892523, - lon: 51.3896004, - }, - ) + const city = getFromStorage('SELECTED_CITY') + const [selectedCity, setSelectedCity] = useState( + city || { + city: 'Tehran', + lat: 35.6892523, + lon: 51.3896004 + } + ) - const initialLayouts: LayoutItem[] = [ - { - id: 'arz-live', - component: , - }, - { - id: 'weather', - component: , - }, - { - id: 'calendar', - component: , - }, - ] + const initialLayouts: LayoutItem[] = [ + { + id: 'arz-live', + component: + }, + { + id: 'weather', + component: + }, + { + id: 'calendar', + component: + } + ] - const storedOrder = getFromStorage(StoreKey.LAYOUT_ORDER) - const [layouts, setLayouts] = useState(() => { - if (storedOrder) { - return storedOrder - .map((id) => initialLayouts.find((layout) => layout.id === id)) - .filter((layout): layout is LayoutItem => layout !== undefined) - } - return initialLayouts - }) + const storedOrder = getFromStorage('LAYOUT_ORDER') + const [layouts, setLayouts] = useState(() => { + if (storedOrder) { + return storedOrder + .map(id => initialLayouts.find(layout => layout.id === id)) + .filter((layout): layout is LayoutItem => layout !== undefined) + } + return initialLayouts + }) - const onDragEnd = (result: DropResult) => { - const { destination, source } = result + const onDragEnd = (result: DropResult) => { + const { destination, source } = result - if (!destination) return + if (!destination) return - const items = Array.from(layouts) - const [reorderedItem] = items.splice(source.index, 1) - items.splice(destination.index, 0, reorderedItem) + const items = Array.from(layouts) + const [reorderedItem] = items.splice(source.index, 1) + items.splice(destination.index, 0, reorderedItem) - setLayouts(items) - setToStorage( - StoreKey.LAYOUT_ORDER, - items.map((item) => item.id), - ) - } + setLayouts(items) + setToStorage( + 'LAYOUT_ORDER', + items.map(item => item.id) + ) + } - useEffect(() => { - setToStorage(StoreKey.CURRENCIES, selectedCurrencies) - }, [selectedCurrencies]) + useEffect(() => { + setToStorage('CURRENCIES', selectedCurrencies) + }, [selectedCurrencies]) - return ( - - - - {(provided) => ( -
- {layouts.map((layout, index) => ( - - {(provided) => ( -
-
- -
-
-
{layout.component}
-
-
- )} -
- ))} - {provided.placeholder} -
- )} -
-
-
- ) + return ( + + + + {provided => ( +
+ {layouts.map((layout, index) => ( + + {provided => ( +
+
+ +
+
+
{layout.component}
+
+
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+
+ ) } diff --git a/src/services/getMethodHooks/getCurrencyByCode.hook.ts b/src/services/getMethodHooks/getCurrencyByCode.hook.ts index c78c89a..6a415b9 100644 --- a/src/services/getMethodHooks/getCurrencyByCode.hook.ts +++ b/src/services/getMethodHooks/getCurrencyByCode.hook.ts @@ -2,37 +2,37 @@ import { useQuery } from '@tanstack/react-query' import { getMainClient } from '../api' export interface FetchedCurrency { - name: { - fa: string - en: string - } - icon: string - price: number - rialPrice: number - changePercentage: number - priceHistory: PriceHistory[] - type: 'coin' | 'crypto' | 'currency' + name: { + fa: string + en: string + } + icon: string + price: number + rialPrice: number + changePercentage: number + priceHistory: PriceHistory[] + type: 'coin' | 'crypto' | 'currency' } export interface PriceHistory { - price: number - createdAt: string + price: number + createdAt: string } export const useGetCurrencyByCode = ( - currency: string, - options: { refetchInterval: number | null }, + currency: string, + options: { refetchInterval: number | null } ) => { - return useQuery({ - queryKey: [`currency-${currency}`], - queryFn: async () => getSupportCurrencies(currency), - retry: 0, - refetchInterval: options.refetchInterval || false, - }) + return useQuery({ + queryKey: [`currency-${currency}`], + queryFn: () => getSupportCurrencies(currency), + retry: 0, + refetchInterval: options.refetchInterval || false + }) } -async function getSupportCurrencies(currency: string): Promise { - const client = await getMainClient() - const { data } = await client.get(`/v2/arz/${currency}`) - return data +async function getSupportCurrencies(currency: string) { + const client = await getMainClient() + const { data } = await client.get(`/v2/arz/${currency}`) + return data } diff --git a/src/services/getMethodHooks/getEvents.hook.ts b/src/services/getMethodHooks/getEvents.hook.ts index 49b9892..b2f582e 100644 --- a/src/services/getMethodHooks/getEvents.hook.ts +++ b/src/services/getMethodHooks/getEvents.hook.ts @@ -2,32 +2,32 @@ import { useQuery } from '@tanstack/react-query' import { getMainClient } from '../api' export interface FetchedEvent { - isHoliday: boolean - title: string - day: number - month: number + isHoliday: boolean + title: string + day: number + month: number } export interface FetchedAllEvents { - shamsiEvents: FetchedEvent[] - gregorianEvents: FetchedEvent[] - hijriEvents: FetchedEvent[] + shamsiEvents: FetchedEvent[] + gregorianEvents: FetchedEvent[] + hijriEvents: FetchedEvent[] } export const useGetEvents = () => { - return useQuery({ - queryKey: ['get-events'], - queryFn: async () => getEvents(), - retry: 0, - initialData: { - shamsiEvents: [], - gregorianEvents: [], - hijriEvents: [], - }, - }) + return useQuery({ + queryKey: ['get-events'], + queryFn: () => getEvents(), + retry: 0, + initialData: { + shamsiEvents: [], + gregorianEvents: [], + hijriEvents: [] + } + }) } -async function getEvents(): Promise { - const client = await getMainClient() - const { data } = await client.get('/date/events') - return data ?? [] +async function getEvents() { + const client = await getMainClient() + const { data } = await client.get('/date/events') + return data ?? [] } diff --git a/src/services/getMethodHooks/getSupportCurrencies.hook.ts b/src/services/getMethodHooks/getSupportCurrencies.hook.ts index 0678484..6a95400 100644 --- a/src/services/getMethodHooks/getSupportCurrencies.hook.ts +++ b/src/services/getMethodHooks/getSupportCurrencies.hook.ts @@ -2,28 +2,26 @@ import { useQuery } from '@tanstack/react-query' import { getMainClient } from '../api' export type SupportedCurrencies = { - key: string - type: 'coin' | 'crypto' | 'currency' - country?: string - label: { - fa: string - en: string - } - changePercentage: number + key: string + type: 'coin' | 'crypto' | 'currency' + country?: string + label: { + fa: string + en: string + } + changePercentage: number }[] export const useGetSupportCurrencies = () => { - return useQuery({ - queryKey: ['supportedCurrencies'], - queryFn: async () => getSupportCurrencies(), - retry: 0, - }) + return useQuery({ + queryKey: ['supportedCurrencies'], + queryFn: () => getSupportCurrencies(), + retry: 0 + }) } -async function getSupportCurrencies(): Promise { - const client = await getMainClient() - const { data } = await client.get<{ currencies: SupportedCurrencies }>( - '/v2/supported-currencies', - ) - return data.currencies +async function getSupportCurrencies() { + const client = await getMainClient() + const { data } = await client.get<{ currencies: SupportedCurrencies }>('/v2/supported-currencies') + return data.currencies } diff --git a/src/services/getMethodHooks/weather/getForecastWeatherByLatLon.ts b/src/services/getMethodHooks/weather/getForecastWeatherByLatLon.ts index 65c9ae3..2a23fb9 100644 --- a/src/services/getMethodHooks/weather/getForecastWeatherByLatLon.ts +++ b/src/services/getMethodHooks/weather/getForecastWeatherByLatLon.ts @@ -2,24 +2,21 @@ import { useQuery } from '@tanstack/react-query' import { getMainClient } from '../../api' import type { FetchedForecast } from './weather.interface' -async function fetchForecastWeatherByLatLon( - lat: number, - lon: number, -): Promise { - const client = await getMainClient() +async function fetchForecastWeatherByLatLon(lat: number, lon: number) { + const client = await getMainClient() - const response = await client.get(`/weather/forecast?lat=${lat}&lon=${lon}`) - return response.data + const response = await client.get(`/weather/forecast?lat=${lat}&lon=${lon}`) + return response.data } export function useGetForecastWeatherByLatLon( - lat: number, - lon: number, - options: { refetchInterval: number | null }, + lat: number, + lon: number, + options: { refetchInterval: number | null } ) { - return useQuery({ - queryKey: ['ForecastGetWeatherByLatLon', lat, lon], - queryFn: () => fetchForecastWeatherByLatLon(lat, lon), - refetchInterval: options.refetchInterval || false, - }) + return useQuery({ + queryKey: ['ForecastGetWeatherByLatLon', lat, lon], + queryFn: () => fetchForecastWeatherByLatLon(lat, lon), + refetchInterval: options.refetchInterval || false + }) }