# 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<string>()
export const dismissAll = createEvent()

export const $notifications = createStore<Notification[]>([])
  .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(
      <Provider value={scope}>
        <NotificationStack />
      </Provider>,
    ),
  }
}

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(
      <Provider value={scope}>
        <NotificationStack />
      </Provider>,
    )
    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(
      <Provider value={scope}>
        <NotificationStack />
      </Provider>,
    )
    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 (
    <Stack
      spacing={1}
      sx={{
        position: 'fixed',
        bottom: 80,
        left: '50%',
        transform: 'translateX(-50%)',
        zIndex: 2000,
        width: 'auto',
        maxWidth: 400,
      }}
    >
      {notifications.map((n) => (
        <Snackbar
          key={n.id}
          open
          autoHideDuration={n.autoHideDuration}
          onClose={() => dismissNotification(n.id)}
          anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
        >
          <Alert
            severity={n.type}
            variant="filled"
            action={
              <IconButton size="small" color="inherit" onClick={() => dismissNotification(n.id)}>
                <CloseIcon fontSize="small" />
              </IconButton>
            }
          >
            {n.message}
          </Alert>
        </Snackbar>
      ))}
    </Stack>
  )
}
```

```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 </Router>
<>
  <ErrorBoundary>
    <AppProviders>
      <BrowserRouter>
        <MainLayout>
          <AppRoutes />
        </MainLayout>
      </BrowserRouter>
    </AppProviders>
  </ErrorBoundary>
  <NotificationStack />
</>
```

- [ ] **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 `<CartSnackbar />` 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"
```
