mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
refactor(console): support offline theme adaptation (#3434)
This commit is contained in:
parent
3404684a61
commit
4a7e00940b
10 changed files with 41 additions and 74 deletions
|
@ -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 (
|
||||
<BrowserRouter>
|
||||
<div className={styles.app}>
|
||||
<Routes>
|
||||
<Route path={`/${CloudRoute.Callback}`} element={<Callback />} />
|
||||
<Route path={`/${CloudRoute.SocialDemoCallback}`} element={<SocialDemoCallback />} />
|
||||
<Route path={`/:tenantId/${CloudRoute.Callback}`} element={<Callback />} />
|
||||
<Route path="/*" element={<Main />} />
|
||||
</Routes>
|
||||
</div>
|
||||
<AppThemeProvider>
|
||||
<div className={styles.app}>
|
||||
<Routes>
|
||||
<Route path={`/${CloudRoute.Callback}`} element={<Callback />} />
|
||||
<Route path={`/${CloudRoute.SocialDemoCallback}`} element={<SocialDemoCallback />} />
|
||||
<Route path={`/:tenantId/${CloudRoute.Callback}`} element={<Callback />} />
|
||||
<Route path="/*" element={<Main />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</AppThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 <div>Forbidden</div>;
|
||||
}
|
||||
|
||||
return <AppLoadingOffline />;
|
||||
return <AppLoading />;
|
||||
};
|
||||
|
||||
export default Redirect;
|
||||
|
|
|
@ -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 <AppLoadingOffline />;
|
||||
return <AppLoading />;
|
||||
};
|
||||
|
||||
export default Tenants;
|
||||
|
|
|
@ -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 <AppLoadingOffline />;
|
||||
return <AppLoading />;
|
||||
};
|
||||
|
||||
const Main = () => {
|
||||
|
@ -55,7 +55,7 @@ const Main = () => {
|
|||
}, [href, isAuthenticated, isLoading, signIn]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <AppLoadingOffline />;
|
||||
return <AppLoading />;
|
||||
}
|
||||
|
||||
return <Protected />;
|
||||
|
|
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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<AppTheme>({
|
||||
theme: defaultTheme,
|
||||
});
|
||||
|
||||
export const AppThemeProvider = ({ fixedTheme, children }: Props) => {
|
||||
export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props) => {
|
||||
const [theme, setTheme] = useState<Theme>(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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<BrowserRouter basename={getBasename()}>
|
||||
<SWRConfig value={swrOptions}>
|
||||
<AppThemeProvider>
|
||||
<AppThemeProvider appearanceMode={appearanceMode}>
|
||||
<AppBoundary>
|
||||
<Toast />
|
||||
<Routes>
|
||||
|
|
|
@ -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;
|
Loading…
Add table
Reference in a new issue