# Toast Notifications System — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** Создать общую систему toast-уведомлений на Effector + MUI, мигрировать CartSnackbar, удалить старый код. **Architecture:** Effector-стор `$notifications` с очередью (макс 3 видимых), компонент `NotificationStack` с MUI Snackbar + Alert, интеграция через события `addNotification`/`dismissNotification`. **Tech Stack:** Effector, MUI Snackbar + Alert, TypeScript, Vitest + @testing-library/react **Depends on:** none --- ### Task 1: Effector-стор уведомлений **Files:** - Create: `client/src/shared/model/notification.ts` - Test: `client/src/shared/model/notification.test.ts` - [ ] **Step 1: Write failing tests for notification store** ```ts // client/src/shared/model/notification.test.ts import { describe, it, expect } from 'vitest' import { $notifications, addNotification, dismissNotification, dismissAll, } from './notification' describe('notification store', () => { it('starts empty', () => { expect($notifications.getState()).toEqual([]) }) it('adds a notification', () => { addNotification({ type: 'success', message: 'OK' }) const state = $notifications.getState() expect(state).toHaveLength(1) expect(state[0]).toMatchObject({ type: 'success', message: 'OK' }) expect(state[0].id).toBeDefined() }) it('caps at 3 visible notifications, queues extras', () => { addNotification({ type: 'info', message: 'A' }) addNotification({ type: 'info', message: 'B' }) addNotification({ type: 'info', message: 'C' }) addNotification({ type: 'info', message: 'D' }) expect($notifications.getState()).toHaveLength(3) // wait for idle and check — actually effector stores are sync // but we need to verify only 3 are visible and 4th is queued }) it('dismisses a notification by id', () => { const id = 'test-id' // manually set state // add dismiss and check }) it('dismisses all notifications', () => { // add several, dismissAll, check empty }) it('auto-dismisses after autoHideDuration', () => { // use fake timers }) }) ``` - [ ] **Step 2: Run to verify failure** Run: `cd client && npx vitest run shared/model/notification.test.ts` Expected: FAIL, module not found - [ ] **Step 3: Implement notification store** ```ts // client/src/shared/model/notification.ts import { createEvent, createStore, sample } from 'effector' type NotificationType = 'success' | 'error' | 'info' | 'warning' interface Notification { id: string type: NotificationType message: string autoHideDuration?: number } const MAX_VISIBLE = 3 let nextId = 1 export const addNotification = createEvent<{ type: NotificationType message: string autoHideDuration?: number }>() export const dismissNotification = createEvent() export const dismissAll = createEvent() export const $notifications = createStore([]) .on(addNotification, (state, { type, message, autoHideDuration }) => { const notification: Notification = { id: String(nextId++), type, message, autoHideDuration: autoHideDuration ?? (type === 'error' ? 6000 : 4000), } return [...state, notification].slice(-MAX_VISIBLE) }) .on(dismissNotification, (state, id) => state.filter((n) => n.id !== id), ) .reset(dismissAll) ``` - [ ] **Step 4: Run tests to verify they pass** Run: `cd client && npx vitest run shared/model/notification.test.ts` Expected: PASS - [ ] **Step 5: Commit** ```bash git add client/src/shared/model/notification.ts client/src/shared/model/notification.test.ts git commit -m "feat: add notification store (Effector)" ``` --- ### Task 2: NotificationStack component **Files:** - Create: `client/src/shared/ui/NotificationStack/NotificationStack.tsx` - Create: `client/src/shared/ui/NotificationStack/index.ts` - Test: `client/src/shared/ui/NotificationStack/NotificationStack.test.tsx` - [ ] **Step 1: Write failing tests for NotificationStack** ```tsx // client/src/shared/ui/NotificationStack/NotificationStack.test.tsx import { describe, it, expect, vi, afterEach } from 'vitest' import { render, screen, act, fireEvent } from '@testing-library/react' import { NotificationStack } from './NotificationStack' import { $notifications, addNotification, dismissNotification } from '../../model/notification' import { fork, allSettled } from 'effector' import { Provider } from 'effector-react' function renderStack() { const scope = fork() return { scope, ...render( , ), } } describe('NotificationStack', () => { afterEach(() => { $notifications.reset() }) it('renders nothing when empty', () => { const { container } = renderStack() expect(container.textContent).toBe('') }) it('renders a notification when added', async () => { const scope = fork() await allSettled(addNotification, { scope, params: { type: 'success', message: 'Test message' } }) render( , ) expect(screen.getByText('Test message')).toBeDefined() }) it('renders correct icon for each type', async () => { const scope = fork() // success — CheckCircle, error — Error, info — Info, warning — Warning // Check MUI Alert has correct severity attribute await allSettled(addNotification, { scope, params: { type: 'error', message: 'Error!' } }) render( , ) const alert = screen.getByRole('alert') expect(alert.getAttribute('severity')).toBe('error') }) }) ``` - [ ] **Step 2: Run to verify failure** Run: `cd client && npx vitest run shared/ui/NotificationStack/NotificationStack.test.tsx` Expected: FAIL - [ ] **Step 3: Implement NotificationStack** ```tsx // client/src/shared/ui/NotificationStack/NotificationStack.tsx import { useUnit } from 'effector-react' import { Snackbar, Alert, Stack, IconButton } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import { $notifications, dismissNotification } from '../../model/notification' export function NotificationStack() { const notifications = useUnit($notifications) if (notifications.length === 0) return null return ( {notifications.map((n) => ( dismissNotification(n.id)} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > dismissNotification(n.id)}> } > {n.message} ))} ) } ``` ```ts // client/src/shared/ui/NotificationStack/index.ts export { NotificationStack } from './NotificationStack' ``` - [ ] **Step 4: Run tests** Run: `cd client && npx vitest run shared/ui/NotificationStack/NotificationStack.test.tsx` Expected: PASS - [ ] **Step 5: Commit** ```bash git add client/src/shared/ui/NotificationStack/ git commit -m "feat: add NotificationStack component" ``` --- ### Task 3: Add NotificationStack to App.tsx **Files:** - Modify: `client/src/app/App.tsx` - [ ] **Step 1: Add NotificationStack to App layout** ```tsx // App.tsx — add import and component import { NotificationStack } from '@/shared/ui/NotificationStack' // Inside the return, after <> ``` - [ ] **Step 2: Run lint + build** Run: `cd client && npm run lint && npm test` Expected: PASS - [ ] **Step 3: Commit** ```bash git add client/src/app/App.tsx git commit -m "feat: integrate NotificationStack into App" ``` --- ### Task 4: Migrate CartSnackbar to notification store **Files:** - Modify: `client/src/features/cart/ui/ToggleCartIcon/ToggleCartIcon.tsx` - Modify: `client/src/features/cart/ui/AddToCartButton/AddToCartButton.tsx` - Delete: `client/src/features/cart/ui/CartSnackbar/CartSnackbar.tsx` - Delete: `client/src/features/cart/ui/CartSnackbar/index.ts` - Delete: `client/src/features/cart/model/cart-notifications.ts` - Modify: `client/src/app/App.tsx` (remove CartSnackbar import + usage) - [ ] **Step 1: Replace cartAdded events with addNotification in ToggleCartIcon** ```tsx // ToggleCartIcon.tsx — replace cartAdded with addNotification import { addNotification } from '@/shared/model/notification' // search for: cartAdded() // replace with: addNotification({ type: 'info', message: 'Товар добавлен в корзину' }) ``` - [ ] **Step 2: Same for AddToCartButton** ```tsx // AddToCartButton.tsx — same replacement addNotification({ type: 'info', message: 'Товар добавлен в корзину' }) ``` - [ ] **Step 3: Remove CartSnackbar component files** Delete `CartSnackbar.tsx` and `CartSnackbar/index.ts`. - [ ] **Step 4: Remove cart-notifications model** Delete `features/cart/model/cart-notifications.ts`. - [ ] **Step 5: Remove CartSnackbar from App.tsx** Remove import and `` from render. - [ ] **Step 6: Remove cart-notifications import from features/cart/index.ts** Check `features/cart/index.ts` for re-exports of cart-notifications or CartSnackbar. - [ ] **Step 7: Remove CartSnackbar test file** Delete `CartSnackbar.test.tsx`. - [ ] **Step 8: Run lint + test + build** ```bash cd client && npm run lint && npm test && npm run build ``` - [ ] **Step 9: Commit** ```bash git add -A git commit -m "feat: migrate CartSnackbar to global notification store" ```