diff --git a/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx b/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx index d68cf17..a860d2d 100644 --- a/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx +++ b/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx @@ -21,7 +21,12 @@ export function AddToCartButton(props: Props) { mutationFn: () => addToCart({ productId, qty }), onSuccess: () => { void qc.invalidateQueries({ queryKey: ['me', 'cart'] }) - addNotification({ type: 'info', message: 'Товар добавлен в корзину' }) + addNotification({ + type: 'success', + message: 'Товар добавлен в корзину', + actionLabel: 'Перейти в корзину', + actionPath: '/cart', + }) }, }) diff --git a/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx b/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx index c1f16e2..585037c 100644 --- a/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx +++ b/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx @@ -32,7 +32,7 @@ describe('AddToCartButton', () => { fireEvent.click(screen.getByRole('button', { name: /в корзину/i })) await vi.waitFor(() => { - expect(spy).toHaveBeenCalledWith({ type: 'info', message: 'Товар добавлен в корзину' }) + expect(spy).toHaveBeenCalledWith({ type: 'success', message: 'Товар добавлен в корзину', actionLabel: 'Перейти в корзину', actionPath: '/cart' }) }) }) }) diff --git a/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx b/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx index 1d13fc4..f324de9 100644 --- a/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx +++ b/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx @@ -28,7 +28,12 @@ export function ToggleCartIcon(props: { mutationFn: () => addToCart({ productId, qty: 1 }), onSuccess: () => { void qc.invalidateQueries({ queryKey: ['me', 'cart'] }) - addNotification({ type: 'info', message: 'Товар добавлен в корзину' }) + addNotification({ + type: 'success', + message: 'Товар добавлен в корзину', + actionLabel: 'Перейти в корзину', + actionPath: '/cart', + }) }, }) diff --git a/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx b/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx index 225e153..aa4205c 100644 --- a/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx +++ b/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx @@ -38,7 +38,7 @@ describe('ToggleCartIcon', () => { fireEvent.click(screen.getByRole('button', { name: /в корзину/i })) await vi.waitFor(() => { - expect(spy).toHaveBeenCalledWith({ type: 'info', message: 'Товар добавлен в корзину' }) + expect(spy).toHaveBeenCalledWith({ type: 'success', message: 'Товар добавлен в корзину', actionLabel: 'Перейти в корзину', actionPath: '/cart' }) }) }) diff --git a/client/src/shared/model/notification.ts b/client/src/shared/model/notification.ts index 15e08cf..f9096d4 100644 --- a/client/src/shared/model/notification.ts +++ b/client/src/shared/model/notification.ts @@ -7,6 +7,8 @@ export interface Notification { type: NotificationType message: string autoHideDuration?: number + actionLabel?: string + actionPath?: string } const MAX_VISIBLE = 3 @@ -16,18 +18,22 @@ export const addNotification = createEvent<{ type: NotificationType message: string autoHideDuration?: number + actionLabel?: string + actionPath?: string }>() export const dismissNotification = createEvent() export const dismissAll = createEvent() export const $notifications = createStore([]) - .on(addNotification, (state, { type, message, autoHideDuration }) => { + .on(addNotification, (state, { type, message, autoHideDuration, actionLabel, actionPath }) => { const notification: Notification = { id: String(nextId++), type, message, autoHideDuration: autoHideDuration ?? (type === 'error' ? 6000 : 4000), + actionLabel, + actionPath, } return [...state, notification].slice(-MAX_VISIBLE) }) diff --git a/client/src/shared/ui/NotificationStack/NotificationStack.test.tsx b/client/src/shared/ui/NotificationStack/NotificationStack.test.tsx index b84798d..8e8ce87 100644 --- a/client/src/shared/ui/NotificationStack/NotificationStack.test.tsx +++ b/client/src/shared/ui/NotificationStack/NotificationStack.test.tsx @@ -1,6 +1,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { allSettled, fork } from 'effector' import { Provider } from 'effector-react' +import { MemoryRouter } from 'react-router-dom' import { describe, it, expect, afterEach } from 'vitest' import { addNotification, dismissAll, $notifications } from '../../model/notification' import { NotificationStack } from './NotificationStack' @@ -16,9 +17,11 @@ describe('NotificationStack', () => { function renderWithScope(scope: ReturnType) { return render( - - - , + + + + + , ) } diff --git a/client/src/shared/ui/NotificationStack/NotificationStack.tsx b/client/src/shared/ui/NotificationStack/NotificationStack.tsx index 9d48710..7ab3c73 100644 --- a/client/src/shared/ui/NotificationStack/NotificationStack.tsx +++ b/client/src/shared/ui/NotificationStack/NotificationStack.tsx @@ -1,11 +1,13 @@ import CloseIcon from '@mui/icons-material/Close' -import { Snackbar, Alert, Stack, IconButton } from '@mui/material' +import { Snackbar, Alert, Stack, IconButton, Button } from '@mui/material' import { useUnit } from 'effector-react' +import { useNavigate } from 'react-router-dom' import { $notifications, dismissNotification as dismissNotificationEvent } from '../../model/notification' export function NotificationStack() { const notifications = useUnit($notifications) const dismissNotification = useUnit(dismissNotificationEvent) + const navigate = useNavigate() if (notifications.length === 0) return null @@ -29,14 +31,72 @@ export function NotificationStack() { autoHideDuration={n.autoHideDuration} onClose={() => dismissNotification(n.id)} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + sx={{ + '& .MuiSnackbarContent-root': { + borderRadius: 12, + border: '1px solid', + borderColor: 'rgba(0,0,0,0.06)', + bgcolor: 'background.paper', + boxShadow: '0 4px 20px rgba(0,0,0,0.08)', + }, + }} > dismissNotification(n.id)} + sx={{ + bgcolor: 'transparent', + border: 'none', + boxShadow: 'none', + p: 1.5, + alignItems: 'center', + '& .MuiAlert-icon': { + padding: 0, + mr: 1.5, + display: 'flex', + alignItems: 'center', + }, + '& .MuiAlert-message': { + padding: 0, + fontSize: '0.875rem', + fontWeight: 600, + }, + '& .MuiAlert-action': { + padding: 0, + mr: 0, + ml: 1, + }, + }} action={ - dismissNotification(n.id)}> - - + <> + {n.actionLabel && n.actionPath && ( + + )} + dismissNotification(n.id)}> + + + } > {n.message} diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db index 08700b1..1ef69fb 100644 Binary files a/server/prisma/prisma/dev.db and b/server/prisma/prisma/dev.db differ