🎉 feat: initialization
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { DirectionProvider as RdxDirProvider } from "@radix-ui/react-direction";
|
||||
import { getCookie, removeCookie, setCookie } from "@workspace/ui/lib/cookies";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
export type Direction = "ltr" | "rtl";
|
||||
|
||||
const DEFAULT_DIRECTION = "ltr";
|
||||
const DIRECTION_COOKIE_NAME = "dir";
|
||||
const DIRECTION_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
||||
|
||||
type DirectionContextType = {
|
||||
defaultDir: Direction;
|
||||
dir: Direction;
|
||||
setDir: (dir: Direction) => void;
|
||||
resetDir: () => void;
|
||||
};
|
||||
|
||||
const DirectionContext = createContext<DirectionContextType | null>(null);
|
||||
|
||||
export function DirectionProvider({ children }: { children: React.ReactNode }) {
|
||||
const [dir, _setDir] = useState<Direction>(
|
||||
() => (getCookie(DIRECTION_COOKIE_NAME) as Direction) || DEFAULT_DIRECTION
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const htmlElement = document.documentElement;
|
||||
htmlElement.setAttribute("dir", dir);
|
||||
}, [dir]);
|
||||
|
||||
const setDir = (dir: Direction) => {
|
||||
_setDir(dir);
|
||||
setCookie(DIRECTION_COOKIE_NAME, dir, DIRECTION_COOKIE_MAX_AGE);
|
||||
};
|
||||
|
||||
const resetDir = () => {
|
||||
_setDir(DEFAULT_DIRECTION);
|
||||
removeCookie(DIRECTION_COOKIE_NAME);
|
||||
};
|
||||
|
||||
return (
|
||||
<DirectionContext
|
||||
value={{
|
||||
defaultDir: DEFAULT_DIRECTION,
|
||||
dir,
|
||||
setDir,
|
||||
resetDir,
|
||||
}}
|
||||
>
|
||||
<RdxDirProvider dir={dir}>{children}</RdxDirProvider>
|
||||
</DirectionContext>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useDirection() {
|
||||
const context = useContext(DirectionContext);
|
||||
if (!context) {
|
||||
throw new Error("useDirection must be used within a DirectionProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Language = string;
|
||||
|
||||
interface LanguageContextType {
|
||||
language: Language;
|
||||
changeLanguage: (lng: Language) => void;
|
||||
isLoading: boolean;
|
||||
supportedLanguages: readonly Language[];
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | null>(null);
|
||||
|
||||
interface LanguageProviderProps {
|
||||
children: React.ReactNode;
|
||||
supportedLanguages?: readonly Language[];
|
||||
defaultLanguage?: Language;
|
||||
}
|
||||
|
||||
export function LanguageProvider({
|
||||
children,
|
||||
supportedLanguages = ["en-US", "zh-CN"] as const,
|
||||
defaultLanguage = "en-US",
|
||||
}: LanguageProviderProps) {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
// Initialize with fallback, will be updated when i18n is ready
|
||||
const [language, setLanguage] = useState<Language>(defaultLanguage);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const changeLanguage = async (lng: Language) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// i18n.changeLanguage will automatically handle localStorage caching
|
||||
await i18n.changeLanguage(lng);
|
||||
setLanguage(lng);
|
||||
} catch (error) {
|
||||
console.error("Failed to change language:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle i18n initialization and language changes
|
||||
useEffect(() => {
|
||||
const updateLanguage = () => {
|
||||
const currentLang = i18n.language as Language;
|
||||
if (currentLang && supportedLanguages.includes(currentLang)) {
|
||||
setLanguage(currentLang);
|
||||
// Update HTML lang attribute
|
||||
document.documentElement.lang = currentLang;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChanged = (lng: string) => {
|
||||
const languageCode = lng as Language;
|
||||
setLanguage(languageCode);
|
||||
// Update HTML lang attribute when language changes
|
||||
document.documentElement.lang = languageCode;
|
||||
};
|
||||
|
||||
// Set up language change listener
|
||||
i18n.on("languageChanged", handleLanguageChanged);
|
||||
|
||||
// Initialize language if i18n is already ready
|
||||
if (i18n.isInitialized) {
|
||||
updateLanguage();
|
||||
} else {
|
||||
// Wait for i18n to initialize
|
||||
const handleInitialized = () => {
|
||||
updateLanguage();
|
||||
};
|
||||
i18n.on("initialized", handleInitialized);
|
||||
|
||||
return () => {
|
||||
i18n.off("languageChanged", handleLanguageChanged);
|
||||
i18n.off("initialized", handleInitialized);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
i18n.off("languageChanged", handleLanguageChanged);
|
||||
};
|
||||
}, [i18n, supportedLanguages]);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider
|
||||
value={{ language, changeLanguage, isLoading, supportedLanguages }}
|
||||
>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
const context = useContext(LanguageContext);
|
||||
if (!context) {
|
||||
throw new Error("useLanguage must be used within a LanguageProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools";
|
||||
|
||||
export const TanStackQueryDevtools = {
|
||||
name: "Tanstack Query",
|
||||
render: <ReactQueryDevtoolsPanel />,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
export function TanStackQueryContext() {
|
||||
const queryClient = new QueryClient();
|
||||
return {
|
||||
queryClient,
|
||||
};
|
||||
}
|
||||
|
||||
export function TanStackQueryProvider({
|
||||
children,
|
||||
queryClient,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient: QueryClient;
|
||||
}) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { getCookie, removeCookie, setCookie } from "@workspace/ui/lib/cookies";
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
type ResolvedTheme = Exclude<Theme, "system">;
|
||||
|
||||
const DEFAULT_THEME = "system";
|
||||
const THEME_COOKIE_NAME = "theme";
|
||||
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
defaultTheme: Theme;
|
||||
resolvedTheme: ResolvedTheme;
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
resetTheme: () => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
defaultTheme: DEFAULT_THEME,
|
||||
resolvedTheme: "light",
|
||||
theme: DEFAULT_THEME,
|
||||
setTheme: () => null,
|
||||
resetTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = DEFAULT_THEME,
|
||||
storageKey = THEME_COOKIE_NAME,
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, _setTheme] = useState<Theme>(
|
||||
() => (getCookie(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
// Optimized: Memoize the resolved theme calculation to prevent unnecessary re-computations
|
||||
const resolvedTheme = useMemo((): ResolvedTheme => {
|
||||
if (theme === "system") {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
return theme as ResolvedTheme;
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const applyTheme = (currentResolvedTheme: ResolvedTheme) => {
|
||||
root.classList.remove("light", "dark"); // Remove existing theme classes
|
||||
root.classList.add(currentResolvedTheme); // Add the new theme class
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
if (theme === "system") {
|
||||
const systemTheme = mediaQuery.matches ? "dark" : "light";
|
||||
applyTheme(systemTheme);
|
||||
}
|
||||
};
|
||||
|
||||
applyTheme(resolvedTheme);
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, [theme, resolvedTheme]);
|
||||
|
||||
const setTheme = (theme: Theme) => {
|
||||
setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE);
|
||||
_setTheme(theme);
|
||||
};
|
||||
|
||||
const resetTheme = () => {
|
||||
removeCookie(storageKey);
|
||||
_setTheme(DEFAULT_THEME);
|
||||
};
|
||||
|
||||
const contextValue = {
|
||||
defaultTheme,
|
||||
resolvedTheme,
|
||||
resetTheme,
|
||||
theme,
|
||||
setTheme,
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext value={contextValue} {...props}>
|
||||
{children}
|
||||
</ThemeContext>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
|
||||
if (!context) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user