198 lines
6.4 KiB
TypeScript
198 lines
6.4 KiB
TypeScript
import type { ReactNode } from 'react'
|
||
import { useMemo, useState } from 'react'
|
||
import Alert from '@mui/material/Alert'
|
||
import Badge from '@mui/material/Badge'
|
||
import Box from '@mui/material/Box'
|
||
import Divider from '@mui/material/Divider'
|
||
import Drawer from '@mui/material/Drawer'
|
||
import IconButton from '@mui/material/IconButton'
|
||
import List from '@mui/material/List'
|
||
import ListItemButton from '@mui/material/ListItemButton'
|
||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||
import ListItemText from '@mui/material/ListItemText'
|
||
import Stack from '@mui/material/Stack'
|
||
import { useTheme } from '@mui/material/styles'
|
||
import Typography from '@mui/material/Typography'
|
||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { useUnit } from 'effector-react'
|
||
import { MapPin, MessageCircle, Settings, SlidersHorizontal, Truck } from 'lucide-react'
|
||
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
||
import { fetchUnreadMessageCount } from '@/entities/user/api/messages-api'
|
||
import { AddressesPage } from '@/pages/me/ui/sections/AddressesPage'
|
||
import { MessagesPage } from '@/pages/me/ui/sections/MessagesPage'
|
||
import { OrderDetailPage } from '@/pages/me/ui/sections/OrderDetailPage'
|
||
import { OrdersPage } from '@/pages/me/ui/sections/OrdersPage'
|
||
import { SettingsPage } from '@/pages/me/ui/sections/SettingsPage'
|
||
import { $user } from '@/shared/model/auth'
|
||
|
||
type NavItem = {
|
||
to: string
|
||
label: string
|
||
icon: ReactNode
|
||
}
|
||
|
||
export function MeLayoutPage() {
|
||
const user = useUnit($user)
|
||
const navigate = useNavigate()
|
||
const location = useLocation()
|
||
const theme = useTheme()
|
||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||
const [mobileOpen, setMobileOpen] = useState(false)
|
||
|
||
const unreadQuery = useQuery({
|
||
queryKey: ['me', 'messages', 'unread-count'],
|
||
queryFn: fetchUnreadMessageCount,
|
||
enabled: Boolean(user),
|
||
refetchInterval: 45_000,
|
||
refetchOnWindowFocus: true,
|
||
})
|
||
|
||
const unreadMessages = unreadQuery.data?.count ?? 0
|
||
|
||
const navItems: NavItem[] = useMemo(
|
||
() => [
|
||
{ to: '/me/orders', label: 'Заказы', icon: <Truck /> },
|
||
{ to: '/me/messages', label: 'Сообщения', icon: <MessageCircle /> },
|
||
{ to: '/me/settings', label: 'Настройки', icon: <Settings /> },
|
||
{ to: '/me/addresses', label: 'Адреса доставки', icon: <MapPin /> },
|
||
],
|
||
[],
|
||
)
|
||
|
||
if (!user) {
|
||
return <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
|
||
}
|
||
if (user.isAdmin) {
|
||
return <Navigate to="/admin" replace />
|
||
}
|
||
|
||
const activeTo =
|
||
navItems.find((x) => location.pathname === x.to)?.to ??
|
||
navItems.find((x) => location.pathname.startsWith(`${x.to}/`))?.to ??
|
||
null
|
||
|
||
const nav = (
|
||
<Box sx={{ width: 300, maxWidth: '88vw', py: 1 }}>
|
||
<Box sx={{ px: 2, py: 2, mx: 1, borderRadius: 2, bgcolor: 'action.hover' }}>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||
Кабинет
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{user.name?.trim() || user.email}
|
||
</Typography>
|
||
</Box>
|
||
<Divider sx={{ my: 1 }} />
|
||
<List disablePadding>
|
||
{navItems.map((i) => (
|
||
<ListItemButton
|
||
key={i.to}
|
||
selected={activeTo === i.to}
|
||
sx={{
|
||
mx: 1,
|
||
mb: 0.5,
|
||
borderRadius: 2,
|
||
'&.Mui-selected': {
|
||
bgcolor: 'primary.50',
|
||
},
|
||
'&.Mui-selected:hover': {
|
||
bgcolor: 'primary.100',
|
||
},
|
||
}}
|
||
onClick={() => {
|
||
navigate(i.to)
|
||
setMobileOpen(false)
|
||
}}
|
||
>
|
||
<ListItemIcon>
|
||
{i.to === '/me/messages' ? (
|
||
<Badge
|
||
color="error"
|
||
badgeContent={unreadMessages > 99 ? '99+' : unreadMessages}
|
||
invisible={unreadMessages === 0}
|
||
>
|
||
{i.icon}
|
||
</Badge>
|
||
) : (
|
||
i.icon
|
||
)}
|
||
</ListItemIcon>
|
||
<ListItemText primary={i.label} />
|
||
</ListItemButton>
|
||
))}
|
||
</List>
|
||
</Box>
|
||
)
|
||
|
||
return (
|
||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||
{isMobile ? (
|
||
<>
|
||
<Stack direction="row" spacing={1} sx={{ width: '100%', alignItems: 'center' }}>
|
||
<IconButton
|
||
onClick={() => setMobileOpen(true)}
|
||
aria-label="Открыть меню кабинета"
|
||
sx={{
|
||
borderRadius: 2,
|
||
border: 1,
|
||
borderColor: 'divider',
|
||
bgcolor: 'background.paper',
|
||
}}
|
||
>
|
||
<SlidersHorizontal />
|
||
</IconButton>
|
||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||
Профиль
|
||
</Typography>
|
||
</Stack>
|
||
<Drawer
|
||
open={mobileOpen}
|
||
onClose={() => setMobileOpen(false)}
|
||
anchor="right"
|
||
ModalProps={{ keepMounted: true }}
|
||
slotProps={{
|
||
paper: {
|
||
sx: {
|
||
borderTopLeftRadius: 16,
|
||
borderBottomLeftRadius: 16,
|
||
borderLeft: 1,
|
||
borderColor: 'divider',
|
||
},
|
||
},
|
||
}}
|
||
>
|
||
{nav}
|
||
</Drawer>
|
||
</>
|
||
) : (
|
||
<Box
|
||
sx={{
|
||
position: 'sticky',
|
||
top: 88,
|
||
alignSelf: 'flex-start',
|
||
border: 1,
|
||
borderColor: 'divider',
|
||
borderRadius: 2,
|
||
bgcolor: 'background.paper',
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{nav}
|
||
</Box>
|
||
)}
|
||
|
||
<Box sx={{ flexGrow: 1, minWidth: 0, width: '100%' }}>
|
||
<Routes>
|
||
<Route index element={<Navigate to="/me/settings" replace />} />
|
||
<Route path="orders" element={<OrdersPage />} />
|
||
<Route path="orders/:id" element={<OrderDetailPage />} />
|
||
<Route path="messages" element={<MessagesPage />} />
|
||
<Route path="settings" element={<SettingsPage />} />
|
||
<Route path="addresses" element={<AddressesPage />} />
|
||
<Route path="*" element={<Navigate to="/me/settings" replace />} />
|
||
</Routes>
|
||
</Box>
|
||
</Stack>
|
||
)
|
||
}
|