mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
fix(console): read default theme value from local storage (#3515)
This commit is contained in:
parent
44c875270d
commit
ec7e17f2c5
6 changed files with 105 additions and 97 deletions
|
@ -21,6 +21,7 @@ import ErrorBoundary from './containers/ErrorBoundary';
|
|||
import TenantAppContainer from './containers/TenantAppContainer';
|
||||
import AppConfirmModalProvider from './contexts/AppConfirmModalProvider';
|
||||
import AppEndpointsProvider from './contexts/AppEndpointsProvider';
|
||||
import { AppThemeProvider } from './contexts/AppThemeProvider';
|
||||
import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider';
|
||||
|
||||
void initI18n();
|
||||
|
@ -60,17 +61,19 @@ const Content = () => {
|
|||
scopes,
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
{!isCloud || isSettle ? (
|
||||
<AppEndpointsProvider>
|
||||
<AppConfirmModalProvider>
|
||||
<TenantAppContainer />
|
||||
</AppConfirmModalProvider>
|
||||
</AppEndpointsProvider>
|
||||
) : (
|
||||
<CloudApp />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
<AppThemeProvider>
|
||||
<ErrorBoundary>
|
||||
{!isCloud || isSettle ? (
|
||||
<AppEndpointsProvider>
|
||||
<AppConfirmModalProvider>
|
||||
<TenantAppContainer />
|
||||
</AppConfirmModalProvider>
|
||||
</AppEndpointsProvider>
|
||||
) : (
|
||||
<CloudApp />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</AppThemeProvider>
|
||||
</LogtoProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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';
|
||||
|
@ -11,16 +10,14 @@ import { CloudRoute } from './types';
|
|||
const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<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>
|
||||
<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>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { conditionalString, trySafe } from '@silverhand/essentials';
|
||||
import { conditionalString, noop, trySafe } from '@silverhand/essentials';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useMemo, useState, createContext } from 'react';
|
||||
|
||||
|
@ -10,44 +10,55 @@ import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-m
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
fixedTheme?: Theme;
|
||||
appearanceMode?: AppearanceMode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
type AppTheme = {
|
||||
type Context = {
|
||||
theme: Theme;
|
||||
setAppearanceMode: (mode: AppearanceMode) => void;
|
||||
setThemeOverride: React.Dispatch<React.SetStateAction<Theme | undefined>>;
|
||||
};
|
||||
|
||||
const defaultTheme: Theme = Theme.Light;
|
||||
|
||||
const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const getThemeBySystemConfiguration = (): Theme =>
|
||||
darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
|
||||
|
||||
export const getAppearanceModeFromLocalStorage = (): AppearanceMode =>
|
||||
export const buildDefaultAppearanceMode = (): AppearanceMode =>
|
||||
trySafe(() => appearanceModeGuard.parse(localStorage.getItem(appearanceModeStorageKey))) ??
|
||||
DynamicAppearanceMode.System;
|
||||
|
||||
export const AppThemeContext = createContext<AppTheme>({
|
||||
const defaultAppearanceMode = buildDefaultAppearanceMode();
|
||||
|
||||
const defaultTheme =
|
||||
defaultAppearanceMode === DynamicAppearanceMode.System
|
||||
? getThemeBySystemConfiguration()
|
||||
: defaultAppearanceMode;
|
||||
|
||||
export const AppThemeContext = createContext<Context>({
|
||||
theme: defaultTheme,
|
||||
setAppearanceMode: noop,
|
||||
setThemeOverride: noop,
|
||||
});
|
||||
|
||||
export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props) => {
|
||||
export const AppThemeProvider = ({ children }: Props) => {
|
||||
const [theme, setTheme] = useState<Theme>(defaultTheme);
|
||||
const [themeOverride, setThemeOverride] = useState<Theme>();
|
||||
const [mode, setMode] = useState<AppearanceMode>(defaultAppearanceMode);
|
||||
|
||||
const setAppearanceMode = (mode: AppearanceMode) => {
|
||||
setMode(mode);
|
||||
localStorage.setItem(appearanceModeStorageKey, mode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (fixedTheme) {
|
||||
setTheme(fixedTheme);
|
||||
if (themeOverride) {
|
||||
setTheme(themeOverride);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (mode !== DynamicAppearanceMode.System) {
|
||||
setTheme(mode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -63,7 +74,7 @@ export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props
|
|||
return () => {
|
||||
darkThemeWatchMedia.removeEventListener('change', changeTheme);
|
||||
};
|
||||
}, [appearanceMode, fixedTheme]);
|
||||
}, [mode, themeOverride]);
|
||||
|
||||
// Set Theme Mode
|
||||
useEffect(() => {
|
||||
|
@ -71,9 +82,11 @@ export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props
|
|||
document.body.classList.add(conditionalString(styles[theme]));
|
||||
}, [theme]);
|
||||
|
||||
const context = useMemo(
|
||||
const context = useMemo<Context>(
|
||||
() => ({
|
||||
theme,
|
||||
setAppearanceMode,
|
||||
setThemeOverride,
|
||||
}),
|
||||
[theme]
|
||||
);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { builtInLanguages as builtInConsoleLanguages } from '@logto/phrases';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { appearanceModeStorageKey } from '@/consts';
|
||||
import { getAppearanceModeFromLocalStorage } from '@/contexts/AppThemeProvider';
|
||||
import { AppThemeContext, buildDefaultAppearanceMode } from '@/contexts/AppThemeProvider';
|
||||
import { appearanceModeGuard } from '@/types/appearance-mode';
|
||||
|
||||
import useMeCustomData from './use-me-custom-data';
|
||||
|
@ -22,6 +21,7 @@ export type UserPreferences = z.infer<typeof userPreferencesGuard>;
|
|||
|
||||
const useUserPreferences = () => {
|
||||
const { data, error, isLoading, isLoaded, update: updateMeCustomData } = useMeCustomData();
|
||||
const { setAppearanceMode } = useContext(AppThemeContext);
|
||||
|
||||
const userPreferences = useMemo(() => {
|
||||
const parsed = z.object({ [adminConsolePreferencesKey]: userPreferencesGuard }).safeParse(data);
|
||||
|
@ -29,7 +29,7 @@ const useUserPreferences = () => {
|
|||
return parsed.success
|
||||
? parsed.data[adminConsolePreferencesKey]
|
||||
: {
|
||||
appearanceMode: getAppearanceModeFromLocalStorage(),
|
||||
appearanceMode: buildDefaultAppearanceMode(),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
|
@ -43,8 +43,8 @@ const useUserPreferences = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(appearanceModeStorageKey, userPreferences.appearanceMode);
|
||||
}, [userPreferences.appearanceMode]);
|
||||
setAppearanceMode(userPreferences.appearanceMode);
|
||||
}, [setAppearanceMode, userPreferences.appearanceMode]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
|
@ -6,7 +7,7 @@ import AppLoading from '@/components/AppLoading';
|
|||
import Toast from '@/components/Toast';
|
||||
import { getBasename } from '@/consts';
|
||||
import AppBoundary from '@/containers/AppBoundary';
|
||||
import { AppThemeProvider } from '@/contexts/AppThemeProvider';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import useSwrOptions from '@/hooks/use-swr-options';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
|
@ -24,6 +25,15 @@ const welcomePathname = getOnboardingPage(OnboardingPage.Welcome);
|
|||
|
||||
const App = () => {
|
||||
const swrOptions = useSwrOptions();
|
||||
const { setThemeOverride } = useContext(AppThemeContext);
|
||||
|
||||
useEffect(() => {
|
||||
setThemeOverride(Theme.Light);
|
||||
|
||||
return () => {
|
||||
setThemeOverride(undefined);
|
||||
};
|
||||
}, [setThemeOverride]);
|
||||
|
||||
const {
|
||||
data: { questionnaire },
|
||||
|
@ -38,39 +48,31 @@ const App = () => {
|
|||
<BrowserRouter basename={getBasename()}>
|
||||
<div className={styles.app}>
|
||||
<SWRConfig value={swrOptions}>
|
||||
<AppThemeProvider fixedTheme={Theme.Light}>
|
||||
<AppBoundary>
|
||||
<Toast />
|
||||
<Routes>
|
||||
<AppBoundary>
|
||||
<Toast />
|
||||
<Routes>
|
||||
<Route index element={<Navigate replace to={welcomePathname} />} />
|
||||
<Route path={`/${OnboardingRoute.Onboarding}`} element={<AppContent />}>
|
||||
<Route index element={<Navigate replace to={welcomePathname} />} />
|
||||
<Route path={`/${OnboardingRoute.Onboarding}`} element={<AppContent />}>
|
||||
<Route index element={<Navigate replace to={welcomePathname} />} />
|
||||
<Route path={OnboardingPage.Welcome} element={<Welcome />} />
|
||||
<Route
|
||||
path={OnboardingPage.AboutUser}
|
||||
element={questionnaire ? <About /> : <Navigate replace to={welcomePathname} />}
|
||||
/>
|
||||
<Route
|
||||
path={OnboardingPage.SignInExperience}
|
||||
element={
|
||||
questionnaire ? (
|
||||
<SignInExperience />
|
||||
) : (
|
||||
<Navigate replace to={welcomePathname} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={OnboardingPage.Congrats}
|
||||
element={
|
||||
questionnaire ? <Congrats /> : <Navigate replace to={welcomePathname} />
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</AppBoundary>
|
||||
</AppThemeProvider>
|
||||
<Route path={OnboardingPage.Welcome} element={<Welcome />} />
|
||||
<Route
|
||||
path={OnboardingPage.AboutUser}
|
||||
element={questionnaire ? <About /> : <Navigate replace to={welcomePathname} />}
|
||||
/>
|
||||
<Route
|
||||
path={OnboardingPage.SignInExperience}
|
||||
element={
|
||||
questionnaire ? <SignInExperience /> : <Navigate replace to={welcomePathname} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={OnboardingPage.Congrats}
|
||||
element={questionnaire ? <Congrats /> : <Navigate replace to={welcomePathname} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</AppBoundary>
|
||||
</SWRConfig>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
|
|
@ -6,9 +6,7 @@ import { getBasename } from '@/consts';
|
|||
import AppBoundary from '@/containers/AppBoundary';
|
||||
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';
|
||||
|
||||
|
@ -16,26 +14,21 @@ import HandleSocialCallback from '../Profile/containers/HandleSocialCallback';
|
|||
|
||||
const Main = () => {
|
||||
const swrOptions = useSwrOptions();
|
||||
const {
|
||||
data: { appearanceMode },
|
||||
} = useUserPreferences();
|
||||
|
||||
return (
|
||||
<BrowserRouter basename={getBasename()}>
|
||||
<SWRConfig value={swrOptions}>
|
||||
<AppThemeProvider appearanceMode={appearanceMode}>
|
||||
<AppBoundary>
|
||||
<Toast />
|
||||
<Routes>
|
||||
<Route path="callback" element={<Callback />} />
|
||||
<Route path="welcome" element={<Welcome />} />
|
||||
<Route path="handle-social" element={<HandleSocialCallback />} />
|
||||
<Route element={<AppContent />}>
|
||||
<Route path="/*" element={<ConsoleContent />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AppBoundary>
|
||||
</AppThemeProvider>
|
||||
<AppBoundary>
|
||||
<Toast />
|
||||
<Routes>
|
||||
<Route path="callback" element={<Callback />} />
|
||||
<Route path="welcome" element={<Welcome />} />
|
||||
<Route path="handle-social" element={<HandleSocialCallback />} />
|
||||
<Route element={<AppContent />}>
|
||||
<Route path="/*" element={<ConsoleContent />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AppBoundary>
|
||||
</SWRConfig>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue