fitaiProto/apps/mobile/src/contexts/ThemeContext.tsx
2026-03-11 08:22:48 +01:00

130 lines
3.2 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from "react";
import { useColorScheme } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { ColorScheme, lightColors, darkColors } from "../styles/colors";
import {
TypographyPresets,
createTypographyPresets,
} from "../styles/typography";
type ThemeMode = "light" | "dark" | "system";
type ActiveTheme = "light" | "dark";
interface ThemeContextType {
// Current active theme
theme: ActiveTheme;
// User's theme preference
themeMode: ThemeMode;
// Active color scheme
colors: ColorScheme;
// Typography presets
typography: TypographyPresets;
// Theme actions
setTheme: (mode: ThemeMode) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const THEME_STORAGE_KEY = "@fitai:theme";
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const systemColorScheme = useColorScheme();
const [themeMode, setThemeMode] = useState<ThemeMode>("system");
const [isLoading, setIsLoading] = useState(true);
// Determine active theme based on mode and system preference
const getActiveTheme = (): ActiveTheme => {
if (themeMode === "system") {
return systemColorScheme === "dark" ? "dark" : "light";
}
return themeMode;
};
const activeTheme = getActiveTheme();
const colors = activeTheme === "dark" ? darkColors : lightColors;
const typography = createTypographyPresets(
colors.textPrimary,
colors.textSecondary,
colors.textTertiary,
);
// Load saved theme preference on mount
useEffect(() => {
const loadTheme = async () => {
try {
const savedTheme = await AsyncStorage.getItem(THEME_STORAGE_KEY);
if (savedTheme && ["light", "dark", "system"].includes(savedTheme)) {
setThemeMode(savedTheme as ThemeMode);
}
} catch (error) {
console.error("Failed to load theme preference:", error);
} finally {
setIsLoading(false);
}
};
loadTheme();
}, []);
// Save theme preference when it changes
const setTheme = async (mode: ThemeMode) => {
try {
setThemeMode(mode);
await AsyncStorage.setItem(THEME_STORAGE_KEY, mode);
} catch (error) {
console.error("Failed to save theme preference:", error);
}
};
// Toggle between light and dark (sets explicit mode, not system)
const toggleTheme = () => {
const newMode = activeTheme === "dark" ? "light" : "dark";
setTheme(newMode);
};
// Don't render children until theme is loaded
if (isLoading) {
return null;
}
const value: ThemeContextType = {
theme: activeTheme,
themeMode,
colors,
typography,
setTheme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
/**
* Hook to access theme context
* @throws Error if used outside ThemeProvider
*/
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}