From 4a7e00940b6c350b381242c756886bde90758360 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 17 Mar 2023 14:31:29 +0800 Subject: [PATCH] refactor(console): support offline theme adaptation (#3434) --- packages/console/src/cloud/App.tsx | 19 ++++++++------ .../console/src/cloud/pages/Main/Redirect.tsx | 4 +-- .../console/src/cloud/pages/Main/Tenants.tsx | 4 +-- .../console/src/cloud/pages/Main/index.tsx | 6 ++--- .../console/src/components/AppError/index.tsx | 4 +-- .../src/components/AppLoading/Offline.tsx | 22 ---------------- .../src/contexts/AppThemeProvider/index.tsx | 25 +++++++++++-------- .../console/src/hooks/use-user-preferences.ts | 2 +- packages/console/src/pages/Main/index.tsx | 6 ++++- packages/console/src/utils/theme.ts | 23 ----------------- 10 files changed, 41 insertions(+), 74 deletions(-) delete mode 100644 packages/console/src/components/AppLoading/Offline.tsx delete mode 100644 packages/console/src/utils/theme.ts diff --git a/packages/console/src/cloud/App.tsx b/packages/console/src/cloud/App.tsx index be16c4c6a..b09f47a34 100644 --- a/packages/console/src/cloud/App.tsx +++ b/packages/console/src/cloud/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { AppThemeProvider } from '@/contexts/AppThemeProvider'; import Callback from '@cloud/pages/Callback'; import * as styles from './App.module.scss'; @@ -10,14 +11,16 @@ import { CloudRoute } from './types'; const App = () => { return ( -
- - } /> - } /> - } /> - } /> - -
+ +
+ + } /> + } /> + } /> + } /> + +
+
); }; diff --git a/packages/console/src/cloud/pages/Main/Redirect.tsx b/packages/console/src/cloud/pages/Main/Redirect.tsx index c1f897d29..7d1f25f84 100644 --- a/packages/console/src/cloud/pages/Main/Redirect.tsx +++ b/packages/console/src/cloud/pages/Main/Redirect.tsx @@ -4,7 +4,7 @@ import { trySafe } from '@silverhand/essentials'; import { useContext, useEffect } from 'react'; import { useHref } from 'react-router-dom'; -import { AppLoadingOffline } from '@/components/AppLoading/Offline'; +import AppLoading from '@/components/AppLoading'; import { TenantsContext } from '@/contexts/TenantsProvider'; type Props = { @@ -39,7 +39,7 @@ const Redirect = ({ tenants, toTenantId }: Props) => { return
Forbidden
; } - return ; + return ; }; export default Redirect; diff --git a/packages/console/src/cloud/pages/Main/Tenants.tsx b/packages/console/src/cloud/pages/Main/Tenants.tsx index 64f75ee69..d2ce72d8a 100644 --- a/packages/console/src/cloud/pages/Main/Tenants.tsx +++ b/packages/console/src/cloud/pages/Main/Tenants.tsx @@ -2,7 +2,7 @@ import type { TenantInfo } from '@logto/schemas'; import { useCallback, useContext, useEffect } from 'react'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; -import { AppLoadingOffline } from '@/components/AppLoading/Offline'; +import AppLoading from '@/components/AppLoading'; import Button from '@/components/Button'; import DangerousRaw from '@/components/DangerousRaw'; import { TenantsContext } from '@/contexts/TenantsProvider'; @@ -56,7 +56,7 @@ const Tenants = ({ data, onAdd }: Props) => { ); } - return ; + return ; }; export default Tenants; diff --git a/packages/console/src/cloud/pages/Main/index.tsx b/packages/console/src/cloud/pages/Main/index.tsx index 4a1689399..a31114f80 100644 --- a/packages/console/src/cloud/pages/Main/index.tsx +++ b/packages/console/src/cloud/pages/Main/index.tsx @@ -4,7 +4,7 @@ import { useContext, useEffect } from 'react'; import { useHref } from 'react-router-dom'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; -import { AppLoadingOffline } from '@/components/AppLoading/Offline'; +import AppLoading from '@/components/AppLoading'; import { TenantsContext } from '@/contexts/TenantsProvider'; import Redirect from './Redirect'; @@ -40,7 +40,7 @@ const Protected = () => { ); } - return ; + return ; }; const Main = () => { @@ -55,7 +55,7 @@ const Main = () => { }, [href, isAuthenticated, isLoading, signIn]); if (!isAuthenticated) { - return ; + return ; } return ; diff --git a/packages/console/src/components/AppError/index.tsx b/packages/console/src/components/AppError/index.tsx index 1eed1eb3c..953aa5d9e 100644 --- a/packages/console/src/components/AppError/index.tsx +++ b/packages/console/src/components/AppError/index.tsx @@ -6,8 +6,8 @@ import ErrorDark from '@/assets/images/error-dark.svg'; import Error from '@/assets/images/error.svg'; import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg'; import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg'; +import useTheme from '@/hooks/use-theme'; import { onKeyDownHandler } from '@/utils/a11y'; -import { getThemeFromLocalStorage } from '@/utils/theme'; import * as styles from './index.module.scss'; @@ -22,7 +22,7 @@ type Props = { const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [isDetailsOpen, setIsDetailsOpen] = useState(false); - const theme = getThemeFromLocalStorage(); // Should be able to use the component in an offline environment + const theme = useTheme(); return (
diff --git a/packages/console/src/components/AppLoading/Offline.tsx b/packages/console/src/components/AppLoading/Offline.tsx deleted file mode 100644 index 89c5b5600..000000000 --- a/packages/console/src/components/AppLoading/Offline.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Theme } from '@logto/schemas'; - -import IllustrationDark from '@/assets/images/loading-illustration-dark.svg'; -import Illustration from '@/assets/images/loading-illustration.svg'; -import { Daisy as Spinner } from '@/components/Spinner'; -import { getThemeFromLocalStorage } from '@/utils/theme'; - -import * as styles from './index.module.scss'; - -/** - * An fullscreen loading component fetches local stored theme without sending request. - */ -export const AppLoadingOffline = () => { - const theme = getThemeFromLocalStorage(); - - return ( -
- {theme === Theme.Light ? : } - -
- ); -}; diff --git a/packages/console/src/contexts/AppThemeProvider/index.tsx b/packages/console/src/contexts/AppThemeProvider/index.tsx index 244e1a496..6a79ca0d4 100644 --- a/packages/console/src/contexts/AppThemeProvider/index.tsx +++ b/packages/console/src/contexts/AppThemeProvider/index.tsx @@ -1,15 +1,17 @@ import { Theme } from '@logto/schemas'; -import { conditionalString } from '@silverhand/essentials'; +import { conditionalString, trySafe } from '@silverhand/essentials'; import type { ReactNode } from 'react'; import { useEffect, useMemo, useState, createContext } from 'react'; -import useUserPreferences from '@/hooks/use-user-preferences'; -import { DynamicAppearanceMode } from '@/types/appearance-mode'; +import { appearanceModeStorageKey } from '@/consts'; +import type { AppearanceMode } from '@/types/appearance-mode'; +import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-mode'; import * as styles from './index.module.scss'; type Props = { fixedTheme?: Theme; + appearanceMode?: AppearanceMode; children: ReactNode; }; @@ -23,17 +25,17 @@ const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)'); const getThemeBySystemConfiguration = (): Theme => darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light; +export const getAppearanceModeFromLocalStorage = (): AppearanceMode => + trySafe(() => appearanceModeGuard.parse(localStorage.getItem(appearanceModeStorageKey))) ?? + DynamicAppearanceMode.System; + export const AppThemeContext = createContext({ theme: defaultTheme, }); -export const AppThemeProvider = ({ fixedTheme, children }: Props) => { +export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props) => { const [theme, setTheme] = useState(defaultTheme); - const { - data: { appearanceMode }, - } = useUserPreferences(); - useEffect(() => { if (fixedTheme) { setTheme(fixedTheme); @@ -41,8 +43,11 @@ export const AppThemeProvider = ({ fixedTheme, children }: Props) => { return; } - if (appearanceMode !== DynamicAppearanceMode.System) { - setTheme(appearanceMode); + // Note: if the appearanceMode is not available, attempt to retrieve the last saved value from localStorage. + const appliedAppearanceMode = appearanceMode ?? getAppearanceModeFromLocalStorage(); + + if (appliedAppearanceMode !== DynamicAppearanceMode.System) { + setTheme(appliedAppearanceMode); return; } diff --git a/packages/console/src/hooks/use-user-preferences.ts b/packages/console/src/hooks/use-user-preferences.ts index 8e8f49e91..bf9dbd046 100644 --- a/packages/console/src/hooks/use-user-preferences.ts +++ b/packages/console/src/hooks/use-user-preferences.ts @@ -3,8 +3,8 @@ import { useEffect, useMemo } from 'react'; import { z } from 'zod'; import { appearanceModeStorageKey } from '@/consts'; +import { getAppearanceModeFromLocalStorage } from '@/contexts/AppThemeProvider'; import { appearanceModeGuard } from '@/types/appearance-mode'; -import { getAppearanceModeFromLocalStorage } from '@/utils/theme'; import useMeCustomData from './use-me-custom-data'; diff --git a/packages/console/src/pages/Main/index.tsx b/packages/console/src/pages/Main/index.tsx index 445e705a0..50af7fd41 100644 --- a/packages/console/src/pages/Main/index.tsx +++ b/packages/console/src/pages/Main/index.tsx @@ -8,6 +8,7 @@ import AppContent from '@/containers/AppContent'; import ConsoleContent from '@/containers/ConsoleContent'; import { AppThemeProvider } from '@/contexts/AppThemeProvider'; import useSwrOptions from '@/hooks/use-swr-options'; +import useUserPreferences from '@/hooks/use-user-preferences'; import Callback from '@/pages/Callback'; import Welcome from '@/pages/Welcome'; @@ -15,11 +16,14 @@ import HandleSocialCallback from '../Profile/containers/HandleSocialCallback'; const Main = () => { const swrOptions = useSwrOptions(); + const { + data: { appearanceMode }, + } = useUserPreferences(); return ( - + diff --git a/packages/console/src/utils/theme.ts b/packages/console/src/utils/theme.ts deleted file mode 100644 index 4a6423405..000000000 --- a/packages/console/src/utils/theme.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Theme } from '@logto/schemas'; -import { trySafe } from '@silverhand/essentials'; - -import { appearanceModeStorageKey } from '@/consts'; -import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-mode'; -import type { AppearanceMode } from '@/types/appearance-mode'; - -export const getTheme = (appearanceMode: AppearanceMode): Theme => { - if (appearanceMode !== DynamicAppearanceMode.System) { - return appearanceMode; - } - - const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)'); - const theme = darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light; - - return theme; -}; - -export const getThemeFromLocalStorage = () => getTheme(getAppearanceModeFromLocalStorage()); - -export const getAppearanceModeFromLocalStorage = (): AppearanceMode => - trySafe(() => appearanceModeGuard.parse(localStorage.getItem(appearanceModeStorageKey))) ?? - DynamicAppearanceMode.System;