veza/apps/web/src/components/feedback/ToastProvider.tsx
senke 559cfbee3e refactor(web): zero out 3 ESLint warning buckets (storybook + react-refresh + non-null-assertion)
Three rules cleaned in parallel passes — 187 fewer warnings, 0 TS
errors, 0 behaviour change beyond one incidental auth bugfix
flagged below.

storybook/no-redundant-story-name (23 → 0) — 14 stories files
  Storybook v7+ infers the story name from the variable name, so
  `name: 'Default'` next to `export const Default: Story = …` is
  pure noise. Removed only when the name was redundant ;
  preserved when the label was a French translation
  ('Par défaut', 'Chargement', 'Avec erreur', etc.) since those
  are intentional.

react-refresh/only-export-components (25 → 0) — 21 files
  Each warning marks a file that exports a React component AND a
  hook / context / constant / barrel re-export. Suppressed
  per-line with the suppression-with-justification pattern :
    // eslint-disable-next-line react-refresh/only-export-components -- <kind>; refactor would split a tightly-coupled API
  The justification matters — every comment names the specific
  thing being co-located (hook / context / CVA constant / lazy
  registry / route config / test util / backward-compat barrel).
  Splitting these would create 21 new files for a HMR-only DX
  win that's already a non-issue in practice.

@typescript-eslint/no-non-null-assertion (139 → 0) — 43 files
  Distribution of fixes :
    ~85 cases : refactored to explicit guard
                `if (!x) throw new Error('invariant: …')`
                or hoisted into local with narrowing.
    ~36 cases : helper extraction (one tooltip test had 16
                `wrapper!` patterns reduced to a single
                `getWrapper()` helper).
    ~18 cases : suppressed with specific reason :
                static literal arrays where index is provably
                in bounds, mock fixtures with structural
                guarantees, filter-then-map patterns where the
                filter excludes the null branch.
  One incidental find : services/api/auth.ts threw on missing
  tokens but didn't guard `user` ; added the missing check while
  refactoring the `user!` to a guard.

baseline post-commit : 921 warnings, 0 errors, 0 TS errors.
The remaining buckets are no-restricted-syntax (757, design-system
guardrail), no-explicit-any (115), exhaustive-deps (49).

CI --max-warnings will be lowered to 921 in the follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:30:22 +02:00

136 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from 'react';
import toast from '@/utils/toast';
import { logger } from '@/utils/logger';
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
// eslint-disable-next-line react-refresh/only-export-components -- co-located hook; tightly coupled to the provider's context
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. Works without ToastProvider.
*/
// eslint-disable-next-line react-refresh/only-export-components -- co-located deprecated hook; kept here until consumers migrate to @/hooks/useToast
export function useToast() {
const addToast = (messageOrToast: string | Omit<Toast, 'id'>, type?: 'success' | 'error' | 'warning' | 'info') => {
if (typeof messageOrToast === 'string') {
const t = type || 'info';
if (t === 'success') toast.success(messageOrToast);
else if (t === 'error') toast.error(messageOrToast);
else if (t === 'warning') toast(messageOrToast, { icon: '⚠️' });
else toast(messageOrToast, { icon: '' });
} else {
const msg = messageOrToast.message;
const t = messageOrToast.type || 'info';
if (t === 'success') toast.success(msg);
else if (t === 'error') toast.error(msg);
else if (t === 'warning') toast(msg, { icon: '⚠️' });
else toast(msg, { icon: '' });
}
};
return {
addToast,
toasts: [] as Toast[],
removeToast: (_id: string) => {},
};
}
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') {
logger.debug('[Storybook Toast]', { type: toast.type ?? 'info', message: 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>
);
}