Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a44ece1
refactor(toast): import useRef from react
Mosas2000 Mar 14, 2026
882eedf
refactor(toast): remove module-level toastId counter
Mosas2000 Mar 14, 2026
d24f66f
refactor(toast): add useRef counter inside useToast
Mosas2000 Mar 14, 2026
41677d3
fix(toast): use idRef.current for toast ID generation
Mosas2000 Mar 14, 2026
5e3f265
style(toast): remove extra blank line from counter removal
Mosas2000 Mar 14, 2026
42e36d8
docs(toast): annotate TOAST_DURATION unit
Mosas2000 Mar 14, 2026
4d584bf
refactor(toast): add default noop for onClose prop
Mosas2000 Mar 14, 2026
899d4a6
a11y(toast): add role=alert to individual toast elements
Mosas2000 Mar 14, 2026
27c7857
test(toast): add data-testid to ToastContainer
Mosas2000 Mar 14, 2026
65d54be
test(toast): add data-testid to individual toast items
Mosas2000 Mar 14, 2026
e848718
a11y(toast): include toast type in dismiss button aria-label
Mosas2000 Mar 14, 2026
c444a97
refactor(toast): extract dismiss handler to reduce duplication
Mosas2000 Mar 14, 2026
279df0a
chore(toast): verify useCallback already imported
Mosas2000 Mar 14, 2026
5427f23
a11y(toast): add tabIndex to toast container for focus management
Mosas2000 Mar 14, 2026
e690f37
fix(toast): prevent container from blocking page clicks
Mosas2000 Mar 14, 2026
1f8d7dc
fix(theme): add SSR guard for window object
Mosas2000 Mar 14, 2026
e8cc023
fix(theme): wrap localStorage and matchMedia in try/catch
Mosas2000 Mar 14, 2026
5fc40a6
fix(theme): guard localStorage.setItem with try/catch
Mosas2000 Mar 14, 2026
587fbaf
fix(theme): guard document access in useEffect
Mosas2000 Mar 14, 2026
25d742a
dx(theme): set displayName on ThemeContext
Mosas2000 Mar 14, 2026
71a5ad5
dx(theme): improve useTheme error message with fix suggestion
Mosas2000 Mar 14, 2026
0afc329
refactor(theme): export ThemeContext for test access
Mosas2000 Mar 14, 2026
aa9bc9b
refactor(theme): import useMemo and useCallback
Mosas2000 Mar 14, 2026
18ec68d
perf(theme): wrap toggleTheme in useCallback
Mosas2000 Mar 14, 2026
ae94094
perf(theme): memoize context value object
Mosas2000 Mar 14, 2026
0fc8b3d
refactor(theme): export getInitialTheme for testability
Mosas2000 Mar 14, 2026
8722a74
feat(theme): add isDark boolean to context value
Mosas2000 Mar 14, 2026
42e8a13
feat(theme): expose setTheme in context value
Mosas2000 Mar 14, 2026
4abe86d
refactor(theme): extract localStorage key to constant
Mosas2000 Mar 14, 2026
92f1799
style: clean up whitespace in toast and theme files
Mosas2000 Mar 14, 2026
c2cebab
style: ensure trailing newlines in modified files
Mosas2000 Mar 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions frontend/src/components/ui/toast.jsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -17,32 +17,33 @@
info: <Info className="w-5 h-5 text-blue-500 dark:text-blue-400" aria-hidden="true" />,
};

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 (
<div
className={`flex items-start gap-3 px-4 py-3 rounded-lg border shadow-lg dark:shadow-black/30 transition-all duration-300 ${
role="alert"
data-testid="toast-item"
className={`pointer-events-auto flex items-start gap-3 px-4 py-3 rounded-lg border shadow-lg dark:shadow-black/30 transition-all duration-300 ${
visible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'
} ${variants[type]}`}
>
<span className="flex-shrink-0 mt-0.5">{icons[type]}</span>
<p className="text-sm font-medium flex-1">{message}</p>
<button
onClick={() => {
setVisible(false);
setTimeout(onClose, 300);
}}
onClick={dismiss}
className="flex-shrink-0 opacity-60 hover:opacity-100 transition-opacity text-current"
aria-label="Dismiss notification"
aria-label={`Dismiss ${type} notification`}
>
<X className="w-4 h-4" aria-hidden="true" />
</button>
Expand All @@ -53,9 +54,11 @@
export function ToastContainer({ toasts, removeToast }) {
return (
<div
className="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full"
data-testid="toast-container"
className="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full pointer-events-none"
aria-live="polite"
aria-atomic="false"
tabIndex={-1}
role="status"
>
{toasts.map((toast) => (
Expand All @@ -70,13 +73,12 @@
);
}

let toastId = 0;

export function useToast() {

Check failure on line 76 in frontend/src/components/ui/toast.jsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
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 }]);
}, []);

Expand Down
41 changes: 30 additions & 11 deletions frontend/src/context/ThemeContext.jsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,60 @@
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);

Check failure on line 3 in frontend/src/context/ThemeContext.jsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

Fast refresh only works when a file only exports components. Move your React context(s) to a separate file
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() {

Check failure on line 7 in frontend/src/context/ThemeContext.jsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
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 (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {

Check failure on line 54 in frontend/src/context/ThemeContext.jsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
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 <ThemeProvider>.');
}
return context;
}
Loading