From 8133e0cf6374a7337f5b382c4d051cd9d10303df Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 27 May 2026 23:16:03 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20restore=20CartSnackbar=20styling=20and?= =?UTF-8?q?=20'=D0=9F=D0=B5=D1=80=D0=B5=D0=B9=D1=82=D0=B8=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=B7=D0=B8=D0=BD=D1=83'=20action=20button?= =?UTF-8?q?=20in=20NotificationStack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cart/add-to-cart/ui/AddToCartButton.tsx | 7 +- .../ui/__tests__/AddToCartButton.test.tsx | 2 +- .../toggle-cart-icon/ui/ToggleCartIcon.tsx | 7 +- .../ui/__tests__/ToggleCartIcon.test.tsx | 2 +- client/src/shared/model/notification.ts | 8 +- .../NotificationStack.test.tsx | 9 ++- .../NotificationStack/NotificationStack.tsx | 70 ++++++++++++++++-- server/prisma/prisma/dev.db | Bin 352256 -> 352256 bytes 8 files changed, 92 insertions(+), 13 deletions(-) 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 08700b180610abc2ec5fbbff7cb49a1c5aa5e07f..1ef69fbda3b67c968ddc7bfdadca507c2a212add 100644 GIT binary patch delta 1794 zcmaJ>O>Y}j6rDFSj%+9JY=MIUbrKa7gwmil{+O`=Y21)dMJ28ag_1={#`f4Ae~icW zIAa^3MC`JmfiApAOR|7=gG4L0HoIm)q--ciRVyJjk-I>kh(7>QcyAosLMD-=yYRdB zocnR+tZaE!wmfSu(yZf)6H_?eUi|vX5Dgzt@DKO^ehY84+-MeFo!_{4{xowL=yFA$ zOq^t9fm)Ocl9Hd~1R)$oC>%nZpNr`7FvEd@Cgnmw#6{|onv`p=m#egg;T-+H)1+bIH_L6Njyo)8=F3Y$9A9?0$C=>R{+|@O8BtlWtd>>?~ zn2TeO0?7)f1%=P^908el>1dGOie5W_T?%$#x8>H>;k=g-fthaw$6!LQ`Kkv6A10Ft zSvi|gOG3TcFSQznZQlZ{%L(9P3O>ekt1Wlr6WCn+3B-q!Rs%lM0UFpB+iL)f4`SZ` zv?=$wtV*A>Y4v(+DeE+;b||<5ckrd=;D-V$9<0O)4R50m;uU1z1F4E%PgWb68Ytxv zX+r3&(yET!Z}fO*k^=sm%R=J#UR*bNPgf4R%zIr;^!K3#A~-#zp_ z3hrU_E^O|dXPnMjf3kLrVO>Kscu3j*1P`CB!Nt~v@sB_=BQc*2o4YxL*!fFhejh-1 zv`6M>k3NUMDRR{M3uBbcM&?W?B}Kq1#b#nisfsQc;d-U>yu!R@Yo1OsAKQ}2kU!YD zl4j0q#B*EHNF65ocaa2p@2Zi@V^H;60nee5~+;F%y86di~J zJLPaV>`$&ij>wT~6m;CT2?uN~= zW%lM9M7JvDEm_81@%CqYfKiLX}pTGFrwo0aG$u48P?7#t)3mkVSClCitTsV~XvqJ?z$G)DvzrEL_y_7Dhssucuo-sH{h)UJkTB50}WYmTbiE|<^#$!!x zX{oNw%+AH;ETjG|V&%XnMR~olt5$6&K#=Ce1ixBHq{PBntXAM70+Dlj-?e>Yy$r}W zL8f%xXcRn6;eq#eJO()%!m7Y>1&37juTJ2n18o0snfCcu zCNRo6F?5qO<9ADMWBY{$Uhcd%`604%3jOJrBVK@rB93@n*GrkE)%#nae>-t8J^yDI zZ4paR&%G$~)HmZ3pc#ael$atk;Tp%6bE;K{W$Qe*iM}80o!9Ac@7q_;AfNm}keaya zb?^{)HX;)IYC?&|E47N5FM=58I#ac#jQSKW^2JLJFf~ zs6@p`#~YezKRTq(UxINW%r_qQhi`B~E66WjV9!vYkrDd8otu#~po|kr#WW)IW}@s+ zo`q2*lsChD%E5Vl!2F1Xp+R}Vw0yJ&2Kc*vhVi>|_(LaPJi$^FJukEDI1A_BET^?9 z#FZ6tx+L-|YMIwlFi&EX#I*I1YsuVMAwGLS;#Npiy*LDqrCVOa`p@%G{Crrwz;Q;l{9<%)qT>i^e