mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 04:59:22 +00:00
Consolidate ThemeProvider and AppProvider
This commit is contained in:
parent
bce7af2e76
commit
c3310ab485
33
src/App.tsx
33
src/App.tsx
@ -3,7 +3,6 @@
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import NostrProvider from '@/components/NostrProvider'
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
@ -24,23 +23,21 @@ const queryClient = new QueryClient({
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="light" storageKey="theme">
|
||||
<AppProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NostrLoginProvider storageKey='nostr:login'>
|
||||
<NostrProvider>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<Suspense>
|
||||
<AppRouter />
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
</NostrProvider>
|
||||
</NostrLoginProvider>
|
||||
</QueryClientProvider>
|
||||
</AppProvider>
|
||||
</ThemeProvider>
|
||||
<AppProvider defaultTheme="light">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NostrLoginProvider storageKey='nostr:login'>
|
||||
<NostrProvider>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<Suspense>
|
||||
<AppRouter />
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
</NostrProvider>
|
||||
</NostrLoginProvider>
|
||||
</QueryClientProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, ReactNode, useEffect } from 'react';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
|
||||
export type Theme = "dark" | "light" | "system";
|
||||
|
||||
interface AppConfig {
|
||||
/** Current theme */
|
||||
theme: Theme;
|
||||
/** Selected relay URL */
|
||||
relayUrl: string;
|
||||
}
|
||||
@ -22,64 +27,97 @@ export const RELAY_OPTIONS: RelayInfo[] = [
|
||||
|
||||
// Default application configuration
|
||||
const DEFAULT_CONFIG: AppConfig = {
|
||||
theme: 'system',
|
||||
relayUrl: 'wss://relay.nostr.band',
|
||||
};
|
||||
|
||||
interface AppContextType {
|
||||
/** Current application configuration */
|
||||
config: AppConfig;
|
||||
/** Update any configuration value */
|
||||
updateConfig: <K extends keyof AppConfig>(key: K, value: AppConfig[K]) => void;
|
||||
/** Update configuration using a callback that receives current config and returns new config */
|
||||
updateConfig: (updater: (currentConfig: AppConfig) => AppConfig) => void;
|
||||
/** Available relay options */
|
||||
availableRelays: RelayInfo[];
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextType | undefined>(undefined);
|
||||
|
||||
const STORAGE_KEY = 'nostr:app-config';
|
||||
const APP_CONFIG_STORAGE_KEY = 'nostr:app-config';
|
||||
|
||||
interface AppProviderProps {
|
||||
children: ReactNode;
|
||||
/** Default theme for the application */
|
||||
defaultTheme?: Theme;
|
||||
}
|
||||
|
||||
export function AppProvider({ children }: AppProviderProps) {
|
||||
const [config, setConfig] = useState<AppConfig>(DEFAULT_CONFIG);
|
||||
|
||||
// Load saved config from localStorage on mount
|
||||
export function AppProvider({
|
||||
children,
|
||||
defaultTheme = 'system'
|
||||
}: AppProviderProps) {
|
||||
// App configuration state with localStorage persistence
|
||||
const [config, setConfig] = useLocalStorage(
|
||||
APP_CONFIG_STORAGE_KEY,
|
||||
{ ...DEFAULT_CONFIG, theme: defaultTheme }
|
||||
);
|
||||
/**
|
||||
* Hook to apply theme changes to the document root
|
||||
*/
|
||||
function useThemeEffect(theme: Theme) {
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedConfig = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedConfig) {
|
||||
const parsed = JSON.parse(savedConfig);
|
||||
// Merge with defaults to handle new config options
|
||||
setConfig(prev => ({ ...prev, ...parsed }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load app config from localStorage:', error);
|
||||
}
|
||||
}, []);
|
||||
const root = window.document.documentElement;
|
||||
|
||||
// Save config to localStorage when it changes
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
// Handle system theme changes when theme is set to "system"
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save app config to localStorage:', error);
|
||||
}
|
||||
}, [config]);
|
||||
if (theme !== 'system') return;
|
||||
|
||||
// Generic config updater
|
||||
const updateConfig = <K extends keyof AppConfig>(key: K, value: AppConfig[K]) => {
|
||||
setConfig(prev => ({ ...prev, [key]: value }));
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = () => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
|
||||
root.classList.add(systemTheme);
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, [theme]);
|
||||
}
|
||||
// Apply theme effects to document
|
||||
useThemeEffect(config.theme);
|
||||
|
||||
// Generic config updater with callback pattern
|
||||
const updateConfig = (updater: (currentConfig: AppConfig) => AppConfig) => {
|
||||
setConfig(updater);
|
||||
};
|
||||
|
||||
const contextValue: AppContextType = {
|
||||
const appContextValue: AppContextType = {
|
||||
config,
|
||||
updateConfig,
|
||||
availableRelays: RELAY_OPTIONS,
|
||||
};
|
||||
|
||||
return React.createElement(AppContext.Provider, { value: contextValue }, children);
|
||||
return React.createElement(
|
||||
AppContext.Provider,
|
||||
{ value: appContextValue },
|
||||
children
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,3 +131,59 @@ export function useAppConfig() {
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get and set the active theme
|
||||
* @returns Theme context with theme and setTheme
|
||||
*/
|
||||
export function useTheme() {
|
||||
const context = useContext(AppContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within an AppProvider');
|
||||
}
|
||||
return {
|
||||
theme: context.config.theme,
|
||||
setTheme: (theme: Theme) => context.updateConfig(config => ({ ...config, theme })),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to apply theme changes to the document root
|
||||
*/
|
||||
export function useThemeEffect(theme: Theme) {
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
// Handle system theme changes when theme is set to "system"
|
||||
useEffect(() => {
|
||||
if (theme !== 'system') return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = () => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
|
||||
root.classList.add(systemTheme);
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, [theme]);
|
||||
}
|
@ -46,7 +46,7 @@ export function RelaySelector({ className }: RelaySelectorProps) {
|
||||
const handleAddCustomRelay = (url: string) => {
|
||||
const normalizedUrl = normalizeRelayUrl(url);
|
||||
if (normalizedUrl) {
|
||||
updateConfig("relayUrl", normalizedUrl);
|
||||
updateConfig(config => ({ ...config, relayUrl: normalizedUrl }));
|
||||
setOpen(false);
|
||||
setInputValue("");
|
||||
}
|
||||
@ -130,7 +130,7 @@ export function RelaySelector({ className }: RelaySelectorProps) {
|
||||
key={option.url}
|
||||
value={option.url}
|
||||
onSelect={(currentValue) => {
|
||||
updateConfig("relayUrl", currentValue);
|
||||
updateConfig(config => ({ ...config, relayUrl: currentValue }));
|
||||
setOpen(false);
|
||||
setInputValue("");
|
||||
}}
|
||||
|
@ -1,69 +0,0 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { type Theme, ThemeContext } from "@/lib/ThemeContext"
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme: Theme;
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme,
|
||||
storageKey,
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light"
|
||||
|
||||
root.classList.add(systemTheme)
|
||||
return
|
||||
}
|
||||
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
|
||||
const handleChange = () => {
|
||||
if (theme === "system") {
|
||||
const root = window.document.documentElement
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
const systemTheme = mediaQuery.matches ? "dark" : "light"
|
||||
root.classList.add(systemTheme)
|
||||
}
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange)
|
||||
return () => mediaQuery.removeEventListener("change", handleChange)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme)
|
||||
setTheme(theme)
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
54
src/hooks/useLocalStorage.ts
Normal file
54
src/hooks/useLocalStorage.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Generic hook for managing localStorage state
|
||||
*/
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
serializer?: {
|
||||
serialize: (value: T) => string;
|
||||
deserialize: (value: string) => T;
|
||||
}
|
||||
) {
|
||||
const serialize = serializer?.serialize || JSON.stringify;
|
||||
const deserialize = serializer?.deserialize || JSON.parse;
|
||||
|
||||
const [state, setState] = useState<T>(() => {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? deserialize(item) : defaultValue;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load ${key} from localStorage:`, error);
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = (value: T | ((prev: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(state) : value;
|
||||
setState(valueToStore);
|
||||
localStorage.setItem(key, serialize(valueToStore));
|
||||
} catch (error) {
|
||||
console.warn(`Failed to save ${key} to localStorage:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Sync with localStorage changes from other tabs
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === key && e.newValue !== null) {
|
||||
try {
|
||||
setState(deserialize(e.newValue));
|
||||
} catch (error) {
|
||||
console.warn(`Failed to sync ${key} from localStorage:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => window.removeEventListener('storage', handleStorageChange);
|
||||
}, [key, deserialize]);
|
||||
|
||||
return [state, setValue] as const;
|
||||
}
|
@ -1,13 +1,6 @@
|
||||
import { useContext } from "react"
|
||||
import { ThemeContext, type ThemeContextType } from "@/lib/ThemeContext"
|
||||
import { useTheme as useThemeFromProvider } from "@/components/AppProvider";
|
||||
|
||||
/** Hook to get and set the active theme. */
|
||||
export function useTheme(): ThemeContextType {
|
||||
const context = useContext(ThemeContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
export function useTheme() {
|
||||
return useThemeFromProvider();
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export type Theme = "dark" | "light" | "system";
|
||||
|
||||
export type ThemeContextType = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeContextType = {
|
||||
theme: "system",
|
||||
setTheme: () => undefined,
|
||||
};
|
||||
|
||||
export const ThemeContext = createContext<ThemeContextType>(initialState);
|
Loading…
x
Reference in New Issue
Block a user