fix: restore CartSnackbar styling and 'Перейти в корзину' action button in NotificationStack
This commit is contained in:
@@ -21,7 +21,12 @@ export function AddToCartButton(props: Props) {
|
|||||||
mutationFn: () => addToCart({ productId, qty }),
|
mutationFn: () => addToCart({ productId, qty }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void qc.invalidateQueries({ queryKey: ['me', 'cart'] })
|
void qc.invalidateQueries({ queryKey: ['me', 'cart'] })
|
||||||
addNotification({ type: 'info', message: 'Товар добавлен в корзину' })
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Товар добавлен в корзину',
|
||||||
|
actionLabel: 'Перейти в корзину',
|
||||||
|
actionPath: '/cart',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('AddToCartButton', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: /в корзину/i }))
|
fireEvent.click(screen.getByRole('button', { name: /в корзину/i }))
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalledWith({ type: 'info', message: 'Товар добавлен в корзину' })
|
expect(spy).toHaveBeenCalledWith({ type: 'success', message: 'Товар добавлен в корзину', actionLabel: 'Перейти в корзину', actionPath: '/cart' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ export function ToggleCartIcon(props: {
|
|||||||
mutationFn: () => addToCart({ productId, qty: 1 }),
|
mutationFn: () => addToCart({ productId, qty: 1 }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void qc.invalidateQueries({ queryKey: ['me', 'cart'] })
|
void qc.invalidateQueries({ queryKey: ['me', 'cart'] })
|
||||||
addNotification({ type: 'info', message: 'Товар добавлен в корзину' })
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Товар добавлен в корзину',
|
||||||
|
actionLabel: 'Перейти в корзину',
|
||||||
|
actionPath: '/cart',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe('ToggleCartIcon', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: /в корзину/i }))
|
fireEvent.click(screen.getByRole('button', { name: /в корзину/i }))
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalledWith({ type: 'info', message: 'Товар добавлен в корзину' })
|
expect(spy).toHaveBeenCalledWith({ type: 'success', message: 'Товар добавлен в корзину', actionLabel: 'Перейти в корзину', actionPath: '/cart' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export interface Notification {
|
|||||||
type: NotificationType
|
type: NotificationType
|
||||||
message: string
|
message: string
|
||||||
autoHideDuration?: number
|
autoHideDuration?: number
|
||||||
|
actionLabel?: string
|
||||||
|
actionPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_VISIBLE = 3
|
const MAX_VISIBLE = 3
|
||||||
@@ -16,18 +18,22 @@ export const addNotification = createEvent<{
|
|||||||
type: NotificationType
|
type: NotificationType
|
||||||
message: string
|
message: string
|
||||||
autoHideDuration?: number
|
autoHideDuration?: number
|
||||||
|
actionLabel?: string
|
||||||
|
actionPath?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
export const dismissNotification = createEvent<string>()
|
export const dismissNotification = createEvent<string>()
|
||||||
export const dismissAll = createEvent()
|
export const dismissAll = createEvent()
|
||||||
|
|
||||||
export const $notifications = createStore<Notification[]>([])
|
export const $notifications = createStore<Notification[]>([])
|
||||||
.on(addNotification, (state, { type, message, autoHideDuration }) => {
|
.on(addNotification, (state, { type, message, autoHideDuration, actionLabel, actionPath }) => {
|
||||||
const notification: Notification = {
|
const notification: Notification = {
|
||||||
id: String(nextId++),
|
id: String(nextId++),
|
||||||
type,
|
type,
|
||||||
message,
|
message,
|
||||||
autoHideDuration: autoHideDuration ?? (type === 'error' ? 6000 : 4000),
|
autoHideDuration: autoHideDuration ?? (type === 'error' ? 6000 : 4000),
|
||||||
|
actionLabel,
|
||||||
|
actionPath,
|
||||||
}
|
}
|
||||||
return [...state, notification].slice(-MAX_VISIBLE)
|
return [...state, notification].slice(-MAX_VISIBLE)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
import { allSettled, fork } from 'effector'
|
import { allSettled, fork } from 'effector'
|
||||||
import { Provider } from 'effector-react'
|
import { Provider } from 'effector-react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
import { describe, it, expect, afterEach } from 'vitest'
|
import { describe, it, expect, afterEach } from 'vitest'
|
||||||
import { addNotification, dismissAll, $notifications } from '../../model/notification'
|
import { addNotification, dismissAll, $notifications } from '../../model/notification'
|
||||||
import { NotificationStack } from './NotificationStack'
|
import { NotificationStack } from './NotificationStack'
|
||||||
@@ -16,9 +17,11 @@ describe('NotificationStack', () => {
|
|||||||
|
|
||||||
function renderWithScope(scope: ReturnType<typeof fork>) {
|
function renderWithScope(scope: ReturnType<typeof fork>) {
|
||||||
return render(
|
return render(
|
||||||
<Provider value={scope}>
|
<MemoryRouter>
|
||||||
<NotificationStack />
|
<Provider value={scope}>
|
||||||
</Provider>,
|
<NotificationStack />
|
||||||
|
</Provider>
|
||||||
|
</MemoryRouter>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import CloseIcon from '@mui/icons-material/Close'
|
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 { useUnit } from 'effector-react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { $notifications, dismissNotification as dismissNotificationEvent } from '../../model/notification'
|
import { $notifications, dismissNotification as dismissNotificationEvent } from '../../model/notification'
|
||||||
|
|
||||||
export function NotificationStack() {
|
export function NotificationStack() {
|
||||||
const notifications = useUnit($notifications)
|
const notifications = useUnit($notifications)
|
||||||
const dismissNotification = useUnit(dismissNotificationEvent)
|
const dismissNotification = useUnit(dismissNotificationEvent)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
if (notifications.length === 0) return null
|
if (notifications.length === 0) return null
|
||||||
|
|
||||||
@@ -29,14 +31,72 @@ export function NotificationStack() {
|
|||||||
autoHideDuration={n.autoHideDuration}
|
autoHideDuration={n.autoHideDuration}
|
||||||
onClose={() => dismissNotification(n.id)}
|
onClose={() => dismissNotification(n.id)}
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
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)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Alert
|
<Alert
|
||||||
severity={n.type}
|
severity={n.type}
|
||||||
variant="filled"
|
variant="standard"
|
||||||
|
onClose={() => 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={
|
action={
|
||||||
<IconButton size="small" color="inherit" onClick={() => dismissNotification(n.id)}>
|
<>
|
||||||
<CloseIcon fontSize="small" />
|
{n.actionLabel && n.actionPath && (
|
||||||
</IconButton>
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => {
|
||||||
|
dismissNotification(n.id)
|
||||||
|
navigate(n.actionPath!)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 8,
|
||||||
|
color: 'success.main',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{n.actionLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<IconButton size="small" color="inherit" onClick={() => dismissNotification(n.id)}>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{n.message}
|
{n.message}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user