🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
@@ -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;
}
+103
View File
@@ -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>
);
}
+110
View File
@@ -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;
};