0
Fork 0
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:
Xiao Yijun 2023-03-20 00:15:00 +08:00 committed by GitHub
parent 44c875270d
commit ec7e17f2c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 97 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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