Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"": {
"name": "widgetify-webapp",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2",
"@tanstack/react-query": "5.85.5",
"@wxt-dev/webextension-polyfill": "1.0.0",
"axios": "1.11.0",
"chart.js": "4.5.0",
"dompurify": "^3.2.6",
"dompurify": "3.2.6",
"jalali-moment": "3.3.11",
"moment": "2.30.1",
"moment-hijri": "3.0.0",
Expand Down
31 changes: 16 additions & 15 deletions src/context/widget-visibility.context.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
Expand Down Expand Up @@ -197,7 +199,7 @@ export function WidgetVisibilityProvider({ children }: { children: ReactNode })
setToStorage('activeWidgets', activeWidgets)
}
}, [visibility, widgetOrders])
const toggleWidget = (widgetId: WidgetKeys) => {
const toggleWidget = useCallback((widgetId: WidgetKeys) => {
setVisibility((prev) => {
const isCurrentlyVisible = prev.includes(widgetId)

Expand All @@ -220,9 +222,9 @@ export function WidgetVisibilityProvider({ children }: { children: ReactNode })
widget_id: widgetId,
new_state: !visibility.includes(widgetId),
})
}
}, [isAuthenticated, visibility])

const reorderWidgets = (sourceIndex: number, destinationIndex: number) => {
const reorderWidgets = useCallback((sourceIndex: number, destinationIndex: number) => {
const visibleWidgets = getSortedWidgets()

if (sourceIndex === destinationIndex) return
Expand All @@ -240,27 +242,26 @@ export function WidgetVisibilityProvider({ children }: { children: ReactNode })

return newOrders
})
}
}, [])

const getSortedWidgets = (): WidgetItem[] => {
const getSortedWidgets = useCallback((): WidgetItem[] => {
return widgetItems
.filter((item) => visibility.includes(item.id))
.map((item) => ({
...item,
order: widgetOrders[item.id] ?? item.order,
}))
.sort((a, b) => a.order - b.order)
}
return (
<WidgetVisibilityContext.Provider
value={{
visibility,
toggleWidget,
}, [visibility, widgetOrders])
const contextValue = useMemo(() => ({
visibility,
toggleWidget,
reorderWidgets,
getSortedWidgets,
}), [visibility, toggleWidget, reorderWidgets, getSortedWidgets])

reorderWidgets,
getSortedWidgets,
}}
>
return (
<WidgetVisibilityContext.Provider value={contextValue}>
{children}
</WidgetVisibilityContext.Provider>
)
Expand Down
50 changes: 26 additions & 24 deletions src/services/hooks/timezone/getTimezones.hook.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getMainClient } from '@/services/api'
import { useEffect, useState } from 'react'
import { logError, getUserFriendlyMessage } from '@/utils/error-handler'
import { useCallback, useEffect, useState } from 'react'

const cachedTimezones: Map<string, FetchedTimezone[]> = new Map()

Expand All @@ -8,34 +9,35 @@ export const useTimezones = () => {
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)

useEffect(() => {
const fetchTimezones = async () => {
try {
const cacheKey = 'all-timezones'
if (cachedTimezones.has(cacheKey)) {
setData(cachedTimezones.get(cacheKey) as FetchedTimezone[])
setLoading(false)
return
}

setLoading(true)
const timezones = await getTimezones()
setData(timezones)

cachedTimezones.set(cacheKey, timezones)
} catch (err) {
setError(
err instanceof Error ? err : new Error('An unknown error occurred')
)
} finally {
const fetchTimezones = useCallback(async () => {
try {
const cacheKey = 'all-timezones'
if (cachedTimezones.has(cacheKey)) {
setData(cachedTimezones.get(cacheKey) as FetchedTimezone[])
setLoading(false)
return
}

setLoading(true)
setError(null)
const timezones = await getTimezones()
setData(timezones)

cachedTimezones.set(cacheKey, timezones)
} catch (err) {
logError(err, 'useTimezones')
const userFriendlyMessage = getUserFriendlyMessage(err)
setError(new Error(userFriendlyMessage))
} finally {
setLoading(false)
}
}, [])

useEffect(() => {
fetchTimezones()
}, [])
}, [fetchTimezones])

return { data, loading, error }
return { data, loading, error, refetch: fetchTimezones }
}

export interface FetchedTimezone {
Expand All @@ -50,7 +52,7 @@ export async function getTimezones(): Promise<FetchedTimezone[]> {
const response = await api.get<FetchedTimezone[]>('/date/timezones')
return response.data
} catch (error) {
console.error('Error fetching timezones:', error)
logError(error, 'getTimezones')
return []
}
}
62 changes: 62 additions & 0 deletions src/utils/error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Utility functions for consistent error handling across the application
*/

export interface ErrorInfo {
message: string
source: string
timestamp: Date
userAgent?: string
}

/**
* Creates a standardized error object with additional context
*/
export function createErrorInfo(error: unknown, source: string): ErrorInfo {
return {
message: error instanceof Error ? error.message : 'Unknown error occurred',
source,
timestamp: new Date(),
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
}
}

/**
* Logs error information in a consistent format
*/
export function logError(error: unknown, source: string): void {
const errorInfo = createErrorInfo(error, source)

if (import.meta.env.DEV) {
console.error(`[${source}]`, errorInfo)
}

// In production, you might want to send this to an error reporting service
// Example: errorReportingService.captureException(errorInfo)
}

/**
* Creates a user-friendly error message
*/
export function getUserFriendlyMessage(error: unknown): string {
if (error instanceof Error) {
// Handle common error patterns
if (error.message.includes('Network Error')) {
return 'اتصال به اینترنت برقرار نیست. لطفاً اتصال خود را بررسی کنید.'
}
if (error.message.includes('401')) {
return 'احراز هویت شما منقضی شده است. لطفاً دوباره وارد شوید.'
}
if (error.message.includes('403')) {
return 'شما دسترسی لازم برای این عملیات را ندارید.'
}
if (error.message.includes('404')) {
return 'منبع مورد نظر یافت نشد.'
}
if (error.message.includes('500')) {
return 'خطای سرور. لطفاً بعداً تلاش کنید.'
}
}

return 'خطایی رخ داده است. لطفاً دوباره تلاش کنید.'
}