Phase 1:
- S0: Fix open redirect (safeNavigate), delete AuthContext/legacy auth, encrypt API keys, gitignore .env files
- S1: Split client.ts god object into 5 modules, unify toast system, delete unused Sidebar
- S2: Add glass button variant, migrate 32 z-index to SUMI tokens, fix card dark mode
- S3: Skip nav link, aria-hidden on icons, focus-visible ring fixes, alt attrs, aria-live regions
- S4: React.memo on list items, fix key={index}, loading=lazy on images
- S5: Branded loading screen, page transitions respect reduced-motion, LikeButton micro-interaction, i18n sidebar/header
Phase 2 Sprint 6:
- Wire Tailwind shadow utilities to SUMI tokens in @theme block (fixes 50+ files)
- Define shadow-card/shadow-card-hover tokens
- Remove dark:shadow-none workarounds from card.tsx (SUMI handles per-theme shadows)
Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
2.9 KiB
TypeScript
127 lines
2.9 KiB
TypeScript
import {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useCallback,
|
|
ReactNode,
|
|
} from 'react';
|
|
import { Toast, ToastComponent } from './Toast';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface ToastContextValue {
|
|
toasts: Toast[];
|
|
addToast: (toast: Omit<Toast, 'id'>) => void;
|
|
removeToast: (id: string) => void;
|
|
}
|
|
|
|
const ToastContext = createContext<ToastContextValue | undefined>(undefined);
|
|
|
|
// Export hooks for usage
|
|
export function useToastContext() {
|
|
const context = useContext(ToastContext);
|
|
if (!context) {
|
|
throw new Error('useToastContext must be used within ToastProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* @deprecated S1.2: Use `useToast` from `@/hooks/useToast` or `toast` from `@/utils/toast` instead.
|
|
* Legacy compatibility hook — delegates to react-hot-toast.
|
|
*/
|
|
export function useToast() {
|
|
const context = useToastContext();
|
|
|
|
const addToast = (messageOrToast: string | Omit<Toast, 'id'>, type?: 'success' | 'error' | 'warning' | 'info') => {
|
|
if (typeof messageOrToast === 'string') {
|
|
context.addToast({
|
|
message: messageOrToast,
|
|
type: type || 'info',
|
|
});
|
|
} else {
|
|
context.addToast(messageOrToast);
|
|
}
|
|
};
|
|
|
|
return {
|
|
...context,
|
|
addToast,
|
|
};
|
|
}
|
|
|
|
export interface ToastProviderProps {
|
|
children: ReactNode;
|
|
position?:
|
|
| 'top-right'
|
|
| 'top-left'
|
|
| 'bottom-right'
|
|
| 'bottom-left'
|
|
| 'top-center'
|
|
| 'bottom-center';
|
|
className?: string;
|
|
}
|
|
|
|
const POSITION_CLASSES = {
|
|
'top-right': 'top-4 right-4',
|
|
'top-left': 'top-4 left-4',
|
|
'bottom-right': 'bottom-4 right-4',
|
|
'bottom-left': 'bottom-4 left-4',
|
|
'top-center': 'top-4 left-1/2 -translate-x-1/2',
|
|
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
|
|
};
|
|
|
|
/**
|
|
* Provider pour gérer la queue des toasts.
|
|
*/
|
|
export function ToastProvider({
|
|
children,
|
|
position = 'top-right',
|
|
className,
|
|
}: ToastProviderProps) {
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
|
|
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
|
|
if (import.meta.env.VITE_STORYBOOK === 'true') {
|
|
console.log('[Storybook Toast]', toast.type ?? 'info', toast.message);
|
|
return;
|
|
}
|
|
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
const newToast: Toast = {
|
|
...toast,
|
|
id,
|
|
};
|
|
|
|
setToasts((prev) => [...prev, newToast]);
|
|
}, []);
|
|
|
|
const removeToast = useCallback((id: string) => {
|
|
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
}, []);
|
|
|
|
const value: ToastContextValue = {
|
|
toasts,
|
|
addToast,
|
|
removeToast,
|
|
};
|
|
|
|
return (
|
|
<ToastContext.Provider value={value}>
|
|
{children}
|
|
<div
|
|
className={cn(
|
|
'fixed z-50 flex flex-col gap-2',
|
|
POSITION_CLASSES[position],
|
|
className,
|
|
)}
|
|
>
|
|
{toasts.map((toast) => (
|
|
<ToastComponent
|
|
key={toast.id}
|
|
toast={toast}
|
|
onDismiss={removeToast}
|
|
/>
|
|
))}
|
|
</div>
|
|
</ToastContext.Provider>
|
|
);
|
|
}
|