diff --git a/src/common/constant/store.key.ts b/src/common/constant/store.key.ts index 6ed0af69..bc853293 100644 --- a/src/common/constant/store.key.ts +++ b/src/common/constant/store.key.ts @@ -87,4 +87,5 @@ export interface StorageKV { selected_engine: string | null widget_tab: string yadkar_tab: string + notifications: any } diff --git a/src/common/motion.tsx b/src/common/motion.tsx new file mode 100644 index 00000000..bb8cf7c4 --- /dev/null +++ b/src/common/motion.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { motion, type HTMLMotionProps, type SVGMotionProps } from 'framer-motion' +import { useGeneralSetting } from '@/context/general-setting.context' + +function cleanMotionProps(props: T): T { + const { + initial, + animate, + exit, + variants, + transition, + whileHover, + whileTap, + whileDrag, + whileFocus, + whileInView, + layout, + layoutId, + drag, + dragConstraints, + dragElastic, + dragMomentum, + onAnimationStart, + onAnimationComplete, + ...rest + } = props as any + + return { + ...rest, + initial: false, + transition: { duration: 0 }, + } as T +} + +function createMotionComponent( + Component: React.ComponentType

+) { + return React.forwardRef((props, ref) => { + const { isOptimalMode } = useGeneralSetting() + + return ( + + ) + }) +} + +export const Motion = { + div: createMotionComponent<'div', HTMLMotionProps<'div'>>(motion.div), + + span: createMotionComponent<'span', HTMLMotionProps<'span'>>(motion.span), + + button: createMotionComponent<'button', HTMLMotionProps<'button'>>(motion.button), + + img: createMotionComponent<'img', HTMLMotionProps<'img'>>(motion.img), + + ul: createMotionComponent<'ul', HTMLMotionProps<'ul'>>(motion.ul), + + li: createMotionComponent<'li', HTMLMotionProps<'li'>>(motion.li), + + svg: createMotionComponent<'svg', SVGMotionProps>(motion.svg), + + path: createMotionComponent<'path', SVGMotionProps>(motion.path), +} + +import { AnimatePresence } from 'framer-motion' + +export function Presence({ + children, + ...props +}: React.ComponentProps) { + const { isOptimalMode } = useGeneralSetting() + + if (isOptimalMode) { + return <>{children} + } + + return {children} +} diff --git a/src/components/auth/require-auth.tsx b/src/components/auth/require-auth.tsx index 24ce4013..6165b69e 100644 --- a/src/components/auth/require-auth.tsx +++ b/src/components/auth/require-auth.tsx @@ -1,4 +1,4 @@ -import { motion } from 'framer-motion' +import { Motion as motion } from "@/common/motion"; import type { ReactNode } from 'react' import { callEvent } from '@/common/utils/call-event' import { useAuth } from '@/context/auth.context' diff --git a/src/components/auth/require-verification.tsx b/src/components/auth/require-verification.tsx index e1f72a35..0c6e4264 100644 --- a/src/components/auth/require-verification.tsx +++ b/src/components/auth/require-verification.tsx @@ -1,4 +1,4 @@ -import { motion } from 'framer-motion' +import { Motion as motion } from "@/common/motion"; import type { ReactNode } from 'react' import { callEvent } from '@/common/utils/call-event' import { useAuth } from '@/context/auth.context' diff --git a/src/components/bottom-sheet/bottom-sheet.tsx b/src/components/bottom-sheet/bottom-sheet.tsx index 417b7389..d1787262 100644 --- a/src/components/bottom-sheet/bottom-sheet.tsx +++ b/src/components/bottom-sheet/bottom-sheet.tsx @@ -1,5 +1,5 @@ import { useState, type ReactNode } from 'react' -import { motion, AnimatePresence, type PanInfo } from 'framer-motion' +import { Presence, Motion as motion } from '@/common/motion' import { Portal } from '../portal/Portal' import { Icon } from '@/src/icons' @@ -40,7 +40,7 @@ export function BottomSheet({ screen: '98vh', } - const handleDragEnd = (_: any, info: PanInfo) => { + const handleDragEnd = (_: any, info: any) => { setIsDragging(false) if (info.offset.y > dragThreshold) { @@ -50,7 +50,7 @@ export function BottomSheet({ return ( - + {isOpen && ( <> )} - + ) } diff --git a/src/components/clickableTooltip.tsx b/src/components/clickableTooltip.tsx index ac3b191c..d47be85a 100644 --- a/src/components/clickableTooltip.tsx +++ b/src/components/clickableTooltip.tsx @@ -1,4 +1,5 @@ -import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion' +import { Motion as motion, Presence } from '@/common/motion' + import { type ReactNode, useEffect, useRef, useState, type RefObject } from 'react' import ReactDOM from 'react-dom' @@ -208,28 +209,26 @@ const ClickableTooltip = ({ return ( <> {ReactDOM.createPortal( - - - {isOpen && ( - - {content} - - )} - - , + + {isOpen && ( + + {content} + + )} + , document.body )} diff --git a/src/components/tab-navigation.tsx b/src/components/tab-navigation.tsx index 114a8250..9c08d596 100644 --- a/src/components/tab-navigation.tsx +++ b/src/components/tab-navigation.tsx @@ -1,5 +1,5 @@ import React, { useId } from 'react' -import { motion } from 'framer-motion' +import { Motion as motion } from "@/common/motion"; interface TabItem { id: T diff --git a/src/components/toolTip.tsx b/src/components/toolTip.tsx index d547bbfc..4212ab9f 100644 --- a/src/components/toolTip.tsx +++ b/src/components/toolTip.tsx @@ -1,4 +1,4 @@ -import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion' +import { Motion as motion, Presence } from '@/common/motion' import { type ReactNode, useEffect, useRef, useState } from 'react' import ReactDOM from 'react-dom' @@ -218,28 +218,26 @@ const Tooltip = ({ {(alwaysShow || isVisible) && ReactDOM.createPortal( - - - - {content} -

- - - , + + + {content} +
+ + , document.body )} diff --git a/src/context/general-setting.context.tsx b/src/context/general-setting.context.tsx index ea69de04..6d4bd5c3 100644 --- a/src/context/general-setting.context.tsx +++ b/src/context/general-setting.context.tsx @@ -11,6 +11,7 @@ import { useAuth } from './auth.context' export interface GeneralData { blurMode: boolean + isOptimalMode: boolean analyticsEnabled: boolean selected_timezone: FetchedTimezone browserBookmarksEnabled: boolean @@ -35,6 +36,7 @@ const DEFAULT_SETTINGS: GeneralData = { }, browserBookmarksEnabled: false, browserTabsEnabled: false, + isOptimalMode: false, } export const GeneralSettingContext = createContext(null) @@ -223,6 +225,7 @@ export function GeneralSettingProvider({ children }: { children: React.ReactNode setBrowserBookmarksEnabled, browserTabsEnabled: settings.browserTabsEnabled, setBrowserTabsEnabled, + isOptimalMode: settings.isOptimalMode, } return ( diff --git a/src/layouts/setting/tabs/account/account.tsx b/src/layouts/setting/tabs/account/account.tsx index f3f3241f..1b4c9a84 100644 --- a/src/layouts/setting/tabs/account/account.tsx +++ b/src/layouts/setting/tabs/account/account.tsx @@ -1,4 +1,4 @@ -import { AnimatePresence, motion } from 'framer-motion' +import { Motion as motion, Presence } from '@/common/motion' import { useAuth } from '@/context/auth.context' import AuthForm from './auth-form/auth-form' import { UserProfile } from './tabs/user-profile/user-profile' @@ -8,7 +8,7 @@ export const AccountTab = () => { return (
- + {isAuthenticated ? ( { )} - +
) } diff --git a/src/layouts/setting/tabs/general/components/timezone-settings.tsx b/src/layouts/setting/tabs/general/components/timezone-settings.tsx index ded034c8..412934f1 100644 --- a/src/layouts/setting/tabs/general/components/timezone-settings.tsx +++ b/src/layouts/setting/tabs/general/components/timezone-settings.tsx @@ -12,7 +12,7 @@ export function TimezoneSettings() { } return ( - +

منطقه‌ی زمانی مورد نظر خود را انتخاب کنید. diff --git a/src/layouts/setting/tabs/general/general.tsx b/src/layouts/setting/tabs/general/general.tsx index ed8130ed..3d9b1de9 100644 --- a/src/layouts/setting/tabs/general/general.tsx +++ b/src/layouts/setting/tabs/general/general.tsx @@ -1,11 +1,37 @@ +import { ToggleSwitch } from '@/components/toggle-switch.component' import { SelectCity } from './components/select-city' import { TimezoneSettings } from './components/timezone-settings' +import { useGeneralSetting } from '@/context/general-setting.context' +import { SectionPanel } from '@/components/section-panel' export function GeneralSettingTab() { + const { isOptimalMode, updateSetting } = useGeneralSetting() return (

+ +

حالت بهینه

+ + جدید + +
+ } + size="sm" + > +
+

+ برای کاهش مصرف منابع، انیمیشن‌ها، حیوان خانگی، ثانیه‌شمار ساعت و + برخی افکت‌های بصری غیرفعال می‌شوند. +

+ updateSetting('isOptimalMode', !isOptimalMode)} + /> +
+
) } diff --git a/src/layouts/widgetify-card/pets/pet.tsx b/src/layouts/widgetify-card/pets/pet.tsx index 48620284..9e99b467 100644 --- a/src/layouts/widgetify-card/pets/pet.tsx +++ b/src/layouts/widgetify-card/pets/pet.tsx @@ -1,10 +1,12 @@ import { usePetContext } from './pet.context' import { PetFactory } from './pet-factory' +import { useGeneralSetting } from '@/context/general-setting.context' export function Pet() { + const { isOptimalMode } = useGeneralSetting() const { isEnabled } = usePetContext() - if (!isEnabled) return null + if (!isEnabled || isOptimalMode) return null return } diff --git a/src/layouts/widgets/habit/components/habit-form.modal.tsx b/src/layouts/widgets/habit/components/habit-form.modal.tsx index 8605215b..fa3bae2e 100644 --- a/src/layouts/widgets/habit/components/habit-form.modal.tsx +++ b/src/layouts/widgets/habit/components/habit-form.modal.tsx @@ -25,7 +25,8 @@ import { useUpdateHabit } from '@/services/hooks/habit/update-habit.hook' import { translateError } from '@/utils/translate-error' import Tooltip from '@/components/toolTip' import type { HabitIcon } from '@/services/hooks/habit/get-habits.hook' -import { AnimatePresence, motion } from 'framer-motion' +import { Motion as motion, Presence } from '@/common/motion' + import { Icon } from '@/src/icons' interface HabitFormModalProps { @@ -64,8 +65,13 @@ export function HabitFormModal({ const isPending = isAdding || isUpdating useEffect(() => { - if (!isOpen) return + if (!isOpen) { + if (showAdvanced) setShowAdvanced(false) + + return + } + if (habit) { setForm({ title: habit.title, @@ -199,7 +205,7 @@ export function HabitFormModal({ - + {showAdvanced && ( )} - +
- + {isExpanded && ( )} - + ) diff --git a/src/layouts/widgets/tools/currency/currency-converter.tsx b/src/layouts/widgets/tools/currency/currency-converter.tsx index b1477eca..4d3e6235 100644 --- a/src/layouts/widgets/tools/currency/currency-converter.tsx +++ b/src/layouts/widgets/tools/currency/currency-converter.tsx @@ -1,4 +1,4 @@ -import { motion } from 'framer-motion' +import { Motion as motion } from "@/common/motion"; import type React from 'react' import { useEffect, useState } from 'react' import { SelectBox } from '@/components/selectbox/selectbox' diff --git a/src/layouts/widgets/tools/tools.layout.tsx b/src/layouts/widgets/tools/tools.layout.tsx index 6d8c546f..5181dbef 100644 --- a/src/layouts/widgets/tools/tools.layout.tsx +++ b/src/layouts/widgets/tools/tools.layout.tsx @@ -1,4 +1,4 @@ -import { motion } from 'framer-motion' +import { Motion as motion } from "@/common/motion"; import React, { Suspense, useEffect, useState } from 'react' import Analytics from '@/analytics' import { getFromStorage, setToStorage } from '@/common/storage' diff --git a/src/layouts/widgets/wigiPad/clock-display/clocks/analog.clock.tsx b/src/layouts/widgets/wigiPad/clock-display/clocks/analog.clock.tsx index 7e81291e..7e1fa08e 100644 --- a/src/layouts/widgets/wigiPad/clock-display/clocks/analog.clock.tsx +++ b/src/layouts/widgets/wigiPad/clock-display/clocks/analog.clock.tsx @@ -1,5 +1,6 @@ import type { FetchedTimezone } from '@/services/hooks/timezone/getTimezones.hook' import type { ClockSettings } from '../clock-setting.interface' +import { useGeneralSetting } from '@/context/general-setting.context' interface AnalogClockProps { timezone: FetchedTimezone @@ -10,15 +11,29 @@ export function AnalogClock({ timezone, setting }: AnalogClockProps) { const [time, setTime] = useState( new Date(new Date().toLocaleString('en-US', { timeZone: timezone.value })) ) + const { isOptimalMode } = useGeneralSetting() + + const showSeconds = setting.showSeconds && !isOptimalMode useEffect(() => { - const timer = setInterval(() => { + const updateTime = () => { setTime( - new Date(new Date().toLocaleString('en-US', { timeZone: timezone.value })) + new Date( + new Date().toLocaleString('en-US', { + timeZone: timezone.value, + }) + ) ) - }, 1000) + } + + updateTime() + + const interval = showSeconds ? 1000 : 60_000 + + const timer = setInterval(updateTime, interval) + return () => clearInterval(timer) - }, [timezone]) + }, [timezone.value, showSeconds]) const hours = time.getHours() % 12 const minutes = time.getMinutes() diff --git a/src/layouts/widgets/wigiPad/clock-display/clocks/digital.clock.tsx b/src/layouts/widgets/wigiPad/clock-display/clocks/digital.clock.tsx index b4235a01..f30e2513 100644 --- a/src/layouts/widgets/wigiPad/clock-display/clocks/digital.clock.tsx +++ b/src/layouts/widgets/wigiPad/clock-display/clocks/digital.clock.tsx @@ -1,23 +1,38 @@ import type { FetchedTimezone } from '@/services/hooks/timezone/getTimezones.hook' import type { ClockSettings } from '../clock-setting.interface' +import { useGeneralSetting } from '@/context/general-setting.context' interface DigitalClockProps { timezone: FetchedTimezone setting: ClockSettings } export function DigitalClock({ timezone, setting }: DigitalClockProps) { + const { isOptimalMode } = useGeneralSetting() const [time, setTime] = useState( new Date(new Date().toLocaleString('en-US', { timeZone: timezone.value })) ) + const showSeconds = setting.showSeconds && !isOptimalMode + useEffect(() => { - const timer = setInterval(() => { + const updateTime = () => { setTime( - new Date(new Date().toLocaleString('en-US', { timeZone: timezone.value })) + new Date( + new Date().toLocaleString('en-US', { + timeZone: timezone.value, + }) + ) ) - }, 1000) + } + + updateTime() + + const interval = showSeconds ? 1000 : 60_000 + + const timer = setInterval(updateTime, interval) + return () => clearInterval(timer) - }, [timezone]) + }, [timezone.value, showSeconds]) const hours = time.getHours().toString().padStart(2, '0') const minutes = time.getMinutes().toString().padStart(2, '0') @@ -31,7 +46,7 @@ export function DigitalClock({ timezone, setting }: DigitalClockProps) { : '-translate-y-[calc(50%+0.9rem)]' } - const digitalClockStyles = setting.showSeconds + const digitalClockStyles = showSeconds ? `inset-x-5 ${ setting.showTimeZone ? getFontTranslateClass() @@ -43,11 +58,9 @@ export function DigitalClock({ timezone, setting }: DigitalClockProps) { : '-translate-y-[calc(50%)]' } text-[3.4rem]` - const clockWidth = setting.showSeconds - ? 'w-[calc(100%-2.8rem)]' - : 'w-[calc(100%-2.5rem)]' + const clockWidth = showSeconds ? 'w-[calc(100%-2.8rem)]' : 'w-[calc(100%-2.5rem)]' - const timeZoneStyles = setting.showSeconds + const timeZoneStyles = showSeconds ? 'inset-x-5 w-[calc(100%-2.5rem)] text-xs' : 'translate-y-20 text-sm' @@ -114,7 +127,7 @@ export function DigitalClock({ timezone, setting }: DigitalClockProps) { return (
- {setting.showSeconds && ( + {showSeconds ? ( ))} - )} + ) : null}
- -
-
- -
+
+
+
+ +
-
-
- - - - - -
-
- - - -
+
+
+ + + + + +
+
+ + + +
-
- - - -
+
+ + +
- - +
+
) } diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 953df47b..1f21f763 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -2,13 +2,16 @@ import { useState } from 'react' import { Toaster } from 'react-hot-toast' import Analytics from '@/analytics' import { listenEvent } from '@/common/utils/call-event' -import { GeneralSettingProvider } from '@/context/general-setting.context' +import { + GeneralSettingProvider, + useGeneralSetting, +} from '@/context/general-setting.context' import { WidgetVisibilityProvider } from '@/context/widget-visibility.context' import { NavbarLayout } from '@/layouts/navbar/navbar.layout' import type { WidgetTabKeys } from '@/layouts/widgets-settings/constant/tab-keys' import { WidgetSettingsModal } from '@/layouts/widgets-settings/widget-settings-modal' import { Page, usePage } from '@/context/page.context' -import { AnimatePresence, motion } from 'framer-motion' +import { AnimatePresence, motion, MotionConfig } from 'framer-motion' import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' import { MiniAppPage } from './mini-apps/mini-app.page' import { ExplorerPage } from './explorer/explorer.pge' @@ -19,12 +22,45 @@ import { WallpaperProvider } from '@/context/wallpaper.context' import { IconProvider } from '../icons/icons.context' export function RootLayout() { + useWallpaperApply() + + return ( +
+ + + +
+
+
+ + {/* */} +
+
+ ) +} + +function Main() { const [showWidgetSettings, setShowWidgetSettings] = useState(false) const [showAuthRequired, setAuthRequired] = useState(false) const [tab, setTab] = useState(null) const { page } = usePage() - - useWallpaperApply() + const { isOptimalMode } = useGeneralSetting() useEffect(() => { const openWidgetsSettingsEvent = listenEvent( @@ -48,71 +84,47 @@ export function RootLayout() { }, []) return ( -
- - - - - + + + - - - {page === Page.Home ? ( - - ) : page === Page.Explorer ? ( - - ) : ( - - )} - - - { - setShowWidgetSettings(false) - setTab(null) - }} - selectedTab={tab} - /> - - - - + + {page === Page.Home ? ( + + ) : page === Page.Explorer ? ( + + ) : ( + + )} + + + { + setShowWidgetSettings(false) + setTab(null) }} + selectedTab={tab} /> - {/* */} + - {showAuthRequired && ( - setAuthRequired(false)} - /> - )} - -
+ {showAuthRequired && ( + setAuthRequired(false)} + /> + )} + ) } diff --git a/src/services/hooks/extension/getNotifications.hook.ts b/src/services/hooks/extension/getNotifications.hook.ts index d0e579f5..763b05f7 100644 --- a/src/services/hooks/extension/getNotifications.hook.ts +++ b/src/services/hooks/extension/getNotifications.hook.ts @@ -1,6 +1,6 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { getMainClient } from '@/services/api' -import { CacheName, type SwEvent, SwEventType } from '@/common/types/sw-events' +import { getFromStorage, setToStorage } from '@/common/storage' export type NotificationType = 'text' | 'url' | 'action' | 'page' | 'banner' export enum NotificationGoTo { @@ -60,10 +60,25 @@ async function fetchNotifications(): Promise { } export function useGetNotifications() { + const [cachedData, setCachedData] = useState( + undefined + ) + + useEffect(() => { + getFromStorage('notifications').then((cached) => { + if (cached) setCachedData(cached) + }) + }, []) + return useQuery({ queryKey: ['notifications'], retry: 1, - queryFn: () => fetchNotifications(), + queryFn: async () => { + const response = await fetchNotifications() + await setToStorage('notifications', response) + return response + }, + placeholderData: cachedData, staleTime: 5 * 60 * 1000, // 5 minutes }) } @@ -74,13 +89,6 @@ export function useNotifyAsSeen() { const client = getMainClient() await client.put(`/notifications/${id}/seen`) }, - onSuccess: () => { - browser.runtime.sendMessage({ - cacheName: CacheName.API, - type: SwEventType.DeleteCache, - path: '/extension/notifications', - }) - }, mutationKey: ['seen_notification'], }) }