diff --git a/src/App.tsx b/src/App.tsx
index 7e18712..f8d0a19 100644
--- a/src/App.tsx
+++ b/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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/AppProvider.ts b/src/components/AppProvider.ts
index ee74f9e..a765c88 100644
--- a/src/components/AppProvider.ts
+++ b/src/components/AppProvider.ts
@@ -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: (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(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(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 = (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]);
+}
\ No newline at end of file
diff --git a/src/components/RelaySelector.tsx b/src/components/RelaySelector.tsx
index ffe07c3..ae7f4f5 100644
--- a/src/components/RelaySelector.tsx
+++ b/src/components/RelaySelector.tsx
@@ -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("");
}}
diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx
deleted file mode 100644
index 04c778c..0000000
--- a/src/components/ThemeProvider.tsx
+++ /dev/null
@@ -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(
- () => (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 (
-
- {children}
-
- )
-}
-
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 0000000..73c2959
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,54 @@
+import { useState, useEffect } from 'react';
+
+/**
+ * Generic hook for managing localStorage state
+ */
+export function useLocalStorage(
+ 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(() => {
+ 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;
+}
\ No newline at end of file
diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts
index be0c18d..31c692a 100644
--- a/src/hooks/useTheme.ts
+++ b/src/hooks/useTheme.ts
@@ -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();
}
\ No newline at end of file
diff --git a/src/lib/ThemeContext.ts b/src/lib/ThemeContext.ts
deleted file mode 100644
index 902edf9..0000000
--- a/src/lib/ThemeContext.ts
+++ /dev/null
@@ -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(initialState);
\ No newline at end of file