base commit

This commit is contained in:
@kirill.komarov
2026-04-29 19:14:34 +05:00
parent c1773e5c57
commit bfc9661d22
25 changed files with 1885 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
export { CartPage } from './ui/CartPage'
+127
View File
@@ -0,0 +1,127 @@
import AddIcon from '@mui/icons-material/Add'
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'
import RemoveIcon from '@mui/icons-material/Remove'
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import IconButton from '@mui/material/IconButton'
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useUnit } from 'effector-react'
import { Link as RouterLink } from 'react-router-dom'
import { fetchMyCart, removeCartItem, setCartQty } from '@/entities/cart/api/cart-api'
import { $user } from '@/shared/model/auth'
import { formatPriceRub } from '@/shared/lib/format-price'
export function CartPage() {
const user = useUnit($user)
const qc = useQueryClient()
const cartQuery = useQuery({
queryKey: ['me', 'cart'],
queryFn: fetchMyCart,
enabled: Boolean(user),
})
const qtyMut = useMutation({
mutationFn: (params: { id: string; qty: number }) => setCartQty(params.id, params.qty),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['me', 'cart'] }),
})
const removeMut = useMutation({
mutationFn: (id: string) => removeCartItem(id),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['me', 'cart'] }),
})
if (!user) {
return (
<Alert severity="info">
Чтобы пользоваться корзиной, нужно войти. Перейдите на страницу{' '}
<Typography component={RouterLink} to="/auth" sx={{ textDecoration: 'underline' }}>
Вход
</Typography>
.
</Alert>
)
}
const items = cartQuery.data?.items ?? []
const total = items.reduce((s, x) => s + x.product.priceCents * x.qty, 0)
return (
<Box>
<Typography variant="h4" gutterBottom>
Корзина
</Typography>
{cartQuery.isError && <Alert severity="error">Не удалось загрузить корзину.</Alert>}
{cartQuery.isSuccess && items.length === 0 && <Alert severity="info">Корзина пуста.</Alert>}
{items.length > 0 && (
<Stack spacing={2}>
{items.map((x) => (
<Box
key={x.id}
sx={{
border: 1,
borderColor: 'divider',
borderRadius: 2,
p: 2,
bgcolor: 'background.paper',
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ sm: 'center' }}>
<Box sx={{ flexGrow: 1 }}>
<Typography sx={{ fontWeight: 700 }}>{x.product.title}</Typography>
<Typography color="text.secondary" variant="body2">
{formatPriceRub(x.product.priceCents)} · {x.qty} шт.
</Typography>
</Box>
<Stack direction="row" spacing={1} alignItems="center">
<IconButton
onClick={() => qtyMut.mutate({ id: x.id, qty: Math.max(0, x.qty - 1) })}
disabled={qtyMut.isPending}
aria-label="Уменьшить количество"
>
<RemoveIcon />
</IconButton>
<Typography sx={{ minWidth: 24, textAlign: 'center' }}>{x.qty}</Typography>
<IconButton
onClick={() => qtyMut.mutate({ id: x.id, qty: x.qty + 1 })}
disabled={qtyMut.isPending}
aria-label="Увеличить количество"
>
<AddIcon />
</IconButton>
<IconButton
onClick={() => removeMut.mutate(x.id)}
disabled={removeMut.isPending}
aria-label="Удалить"
>
<DeleteOutlineOutlinedIcon />
</IconButton>
</Stack>
</Stack>
</Box>
))}
<Divider />
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ sm: 'center' }}>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Итого: {formatPriceRub(total)}
</Typography>
<Button component={RouterLink} to="/checkout" variant="contained">
Оформить заказ
</Button>
</Stack>
</Stack>
)}
</Box>
)
}