diff --git a/frontend/src/components/ui/toast.jsx b/frontend/src/components/ui/toast.jsx
index 9ff059c6..35e2b282 100644
--- a/frontend/src/components/ui/toast.jsx
+++ b/frontend/src/components/ui/toast.jsx
@@ -1,7 +1,7 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useRef } from 'react';
import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-react';
-const TOAST_DURATION = 5000;
+const TOAST_DURATION = 5000; // ms
const variants = {
success: 'bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800 text-green-800 dark:text-green-200',
@@ -17,32 +17,33 @@ const icons = {
info: ,
};
-function Toast({ message, type = 'info', onClose }) {
+function Toast({ message, type = 'info', onClose = () => {} }) {
const [visible, setVisible] = useState(true);
+ const dismiss = useCallback(() => {
+ setVisible(false);
+ setTimeout(onClose, 300);
+ }, [onClose]);
+
useEffect(() => {
- const timer = setTimeout(() => {
- setVisible(false);
- setTimeout(onClose, 300);
- }, TOAST_DURATION);
+ const timer = setTimeout(dismiss, TOAST_DURATION);
return () => clearTimeout(timer);
- }, [onClose]);
+ }, [dismiss]);
return (
{icons[type]}
{message}
@@ -53,9 +54,11 @@ function Toast({ message, type = 'info', onClose }) {
export function ToastContainer({ toasts, removeToast }) {
return (
{toasts.map((toast) => (
@@ -70,13 +73,12 @@ export function ToastContainer({ toasts, removeToast }) {
);
}
-let toastId = 0;
-
export function useToast() {
const [toasts, setToasts] = useState([]);
+ const idRef = useRef(0);
const addToast = useCallback((message, type = 'info') => {
- const id = ++toastId;
+ const id = ++idRef.current;
setToasts((prev) => [...prev, { id, message, type }]);
}, []);
diff --git a/frontend/src/context/ThemeContext.jsx b/frontend/src/context/ThemeContext.jsx
index 91650a38..7c0e7fc0 100644
--- a/frontend/src/context/ThemeContext.jsx
+++ b/frontend/src/context/ThemeContext.jsx
@@ -1,32 +1,51 @@
-import { createContext, useContext, useState, useEffect } from 'react';
+import { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
-const ThemeContext = createContext(null);
+export const ThemeContext = createContext(null);
+const STORAGE_KEY = 'tipstream-theme';
+ThemeContext.displayName = 'ThemeContext';
-function getInitialTheme() {
- const stored = localStorage.getItem('tipstream-theme');
- if (stored === 'dark' || stored === 'light') return stored;
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+export function getInitialTheme() {
+ if (typeof window === 'undefined') return 'light';
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored === 'dark' || stored === 'light') return stored;
+ } catch {
+ // localStorage may be unavailable in private browsing or restricted contexts
+ }
+ try {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ } catch {
+ return 'light';
+ }
}
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(getInitialTheme);
useEffect(() => {
+ if (typeof document === 'undefined') return;
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
- localStorage.setItem('tipstream-theme', theme);
+ try {
+ localStorage.setItem(STORAGE_KEY, theme);
+ } catch {
+ // localStorage may be unavailable
+ }
}, [theme]);
- const toggleTheme = () => {
+ const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
- };
+ }, []);
+
+ const isDark = theme === 'dark';
+ const value = useMemo(() => ({ theme, isDark, toggleTheme, setTheme }), [theme, isDark, toggleTheme]);
return (
-
+
{children}
);
@@ -35,7 +54,7 @@ export function ThemeProvider({ children }) {
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
- throw new Error('useTheme must be used within a ThemeProvider');
+ throw new Error('useTheme must be used within a ThemeProvider. Wrap your component tree in .');
}
return context;
}