0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

refactor(console): support offline theme adaptation (#3434)

This commit is contained in:
Xiao Yijun 2023-03-17 14:31:29 +08:00 committed by GitHub
parent 3404684a61
commit 4a7e00940b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 41 additions and 74 deletions

View file

@ -1,5 +1,6 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { AppThemeProvider } from '@/contexts/AppThemeProvider';
import Callback from '@cloud/pages/Callback'; import Callback from '@cloud/pages/Callback';
import * as styles from './App.module.scss'; import * as styles from './App.module.scss';
@ -10,14 +11,16 @@ import { CloudRoute } from './types';
const App = () => { const App = () => {
return ( return (
<BrowserRouter> <BrowserRouter>
<div className={styles.app}> <AppThemeProvider>
<Routes> <div className={styles.app}>
<Route path={`/${CloudRoute.Callback}`} element={<Callback />} /> <Routes>
<Route path={`/${CloudRoute.SocialDemoCallback}`} element={<SocialDemoCallback />} /> <Route path={`/${CloudRoute.Callback}`} element={<Callback />} />
<Route path={`/:tenantId/${CloudRoute.Callback}`} element={<Callback />} /> <Route path={`/${CloudRoute.SocialDemoCallback}`} element={<SocialDemoCallback />} />
<Route path="/*" element={<Main />} /> <Route path={`/:tenantId/${CloudRoute.Callback}`} element={<Callback />} />
</Routes> <Route path="/*" element={<Main />} />
</div> </Routes>
</div>
</AppThemeProvider>
</BrowserRouter> </BrowserRouter>
); );
}; };

View file

@ -4,7 +4,7 @@ import { trySafe } from '@silverhand/essentials';
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { useHref } from 'react-router-dom'; import { useHref } from 'react-router-dom';
import { AppLoadingOffline } from '@/components/AppLoading/Offline'; import AppLoading from '@/components/AppLoading';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
type Props = { type Props = {
@ -39,7 +39,7 @@ const Redirect = ({ tenants, toTenantId }: Props) => {
return <div>Forbidden</div>; return <div>Forbidden</div>;
} }
return <AppLoadingOffline />; return <AppLoading />;
}; };
export default Redirect; export default Redirect;

View file

@ -2,7 +2,7 @@ import type { TenantInfo } from '@logto/schemas';
import { useCallback, useContext, useEffect } from 'react'; import { useCallback, useContext, useEffect } from 'react';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; 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 Button from '@/components/Button';
import DangerousRaw from '@/components/DangerousRaw'; import DangerousRaw from '@/components/DangerousRaw';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
@ -56,7 +56,7 @@ const Tenants = ({ data, onAdd }: Props) => {
); );
} }
return <AppLoadingOffline />; return <AppLoading />;
}; };
export default Tenants; export default Tenants;

View file

@ -4,7 +4,7 @@ import { useContext, useEffect } from 'react';
import { useHref } from 'react-router-dom'; import { useHref } from 'react-router-dom';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; 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 { TenantsContext } from '@/contexts/TenantsProvider';
import Redirect from './Redirect'; import Redirect from './Redirect';
@ -40,7 +40,7 @@ const Protected = () => {
); );
} }
return <AppLoadingOffline />; return <AppLoading />;
}; };
const Main = () => { const Main = () => {
@ -55,7 +55,7 @@ const Main = () => {
}, [href, isAuthenticated, isLoading, signIn]); }, [href, isAuthenticated, isLoading, signIn]);
if (!isAuthenticated) { if (!isAuthenticated) {
return <AppLoadingOffline />; return <AppLoading />;
} }
return <Protected />; return <Protected />;

View file

@ -6,8 +6,8 @@ import ErrorDark from '@/assets/images/error-dark.svg';
import Error from '@/assets/images/error.svg'; import Error from '@/assets/images/error.svg';
import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg'; import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg';
import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg'; import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg';
import useTheme from '@/hooks/use-theme';
import { onKeyDownHandler } from '@/utils/a11y'; import { onKeyDownHandler } from '@/utils/a11y';
import { getThemeFromLocalStorage } from '@/utils/theme';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -22,7 +22,7 @@ type Props = {
const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props) => { const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isDetailsOpen, setIsDetailsOpen] = useState(false); const [isDetailsOpen, setIsDetailsOpen] = useState(false);
const theme = getThemeFromLocalStorage(); // Should be able to use the component in an offline environment const theme = useTheme();
return ( return (
<div className={styles.container}> <div className={styles.container}>

View file

@ -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 (
<div className={styles.container}>
{theme === Theme.Light ? <Illustration /> : <IllustrationDark />}
<Spinner />
</div>
);
};

View file

@ -1,15 +1,17 @@
import { Theme } from '@logto/schemas'; import { Theme } from '@logto/schemas';
import { conditionalString } from '@silverhand/essentials'; import { conditionalString, trySafe } from '@silverhand/essentials';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useEffect, useMemo, useState, createContext } from 'react'; import { useEffect, useMemo, useState, createContext } from 'react';
import useUserPreferences from '@/hooks/use-user-preferences'; import { appearanceModeStorageKey } from '@/consts';
import { DynamicAppearanceMode } from '@/types/appearance-mode'; import type { AppearanceMode } from '@/types/appearance-mode';
import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-mode';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
type Props = { type Props = {
fixedTheme?: Theme; fixedTheme?: Theme;
appearanceMode?: AppearanceMode;
children: ReactNode; children: ReactNode;
}; };
@ -23,17 +25,17 @@ const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
const getThemeBySystemConfiguration = (): Theme => const getThemeBySystemConfiguration = (): Theme =>
darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light; darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
export const getAppearanceModeFromLocalStorage = (): AppearanceMode =>
trySafe(() => appearanceModeGuard.parse(localStorage.getItem(appearanceModeStorageKey))) ??
DynamicAppearanceMode.System;
export const AppThemeContext = createContext<AppTheme>({ export const AppThemeContext = createContext<AppTheme>({
theme: defaultTheme, theme: defaultTheme,
}); });
export const AppThemeProvider = ({ fixedTheme, children }: Props) => { export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props) => {
const [theme, setTheme] = useState<Theme>(defaultTheme); const [theme, setTheme] = useState<Theme>(defaultTheme);
const {
data: { appearanceMode },
} = useUserPreferences();
useEffect(() => { useEffect(() => {
if (fixedTheme) { if (fixedTheme) {
setTheme(fixedTheme); setTheme(fixedTheme);
@ -41,8 +43,11 @@ export const AppThemeProvider = ({ fixedTheme, children }: Props) => {
return; return;
} }
if (appearanceMode !== DynamicAppearanceMode.System) { // Note: if the appearanceMode is not available, attempt to retrieve the last saved value from localStorage.
setTheme(appearanceMode); const appliedAppearanceMode = appearanceMode ?? getAppearanceModeFromLocalStorage();
if (appliedAppearanceMode !== DynamicAppearanceMode.System) {
setTheme(appliedAppearanceMode);
return; return;
} }

View file

@ -3,8 +3,8 @@ import { useEffect, useMemo } from 'react';
import { z } from 'zod'; import { z } from 'zod';
import { appearanceModeStorageKey } from '@/consts'; import { appearanceModeStorageKey } from '@/consts';
import { getAppearanceModeFromLocalStorage } from '@/contexts/AppThemeProvider';
import { appearanceModeGuard } from '@/types/appearance-mode'; import { appearanceModeGuard } from '@/types/appearance-mode';
import { getAppearanceModeFromLocalStorage } from '@/utils/theme';
import useMeCustomData from './use-me-custom-data'; import useMeCustomData from './use-me-custom-data';

View file

@ -8,6 +8,7 @@ import AppContent from '@/containers/AppContent';
import ConsoleContent from '@/containers/ConsoleContent'; import ConsoleContent from '@/containers/ConsoleContent';
import { AppThemeProvider } from '@/contexts/AppThemeProvider'; import { AppThemeProvider } from '@/contexts/AppThemeProvider';
import useSwrOptions from '@/hooks/use-swr-options'; import useSwrOptions from '@/hooks/use-swr-options';
import useUserPreferences from '@/hooks/use-user-preferences';
import Callback from '@/pages/Callback'; import Callback from '@/pages/Callback';
import Welcome from '@/pages/Welcome'; import Welcome from '@/pages/Welcome';
@ -15,11 +16,14 @@ import HandleSocialCallback from '../Profile/containers/HandleSocialCallback';
const Main = () => { const Main = () => {
const swrOptions = useSwrOptions(); const swrOptions = useSwrOptions();
const {
data: { appearanceMode },
} = useUserPreferences();
return ( return (
<BrowserRouter basename={getBasename()}> <BrowserRouter basename={getBasename()}>
<SWRConfig value={swrOptions}> <SWRConfig value={swrOptions}>
<AppThemeProvider> <AppThemeProvider appearanceMode={appearanceMode}>
<AppBoundary> <AppBoundary>
<Toast /> <Toast />
<Routes> <Routes>

View file

@ -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;