0
Fork 0
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:
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 { 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>
);
};

View file

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

View file

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

View file

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

View file

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

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 { 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;
}

View file

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

View file

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

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;