Files
shop-server/client/src/pages/admin-layout/ui/AdminLayoutPage.tsx
T

197 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from 'react'
import { useMemo, useState } from 'react'
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 { Bell, Image, LayoutGrid, ListOrdered, MessageSquare, Store, Users } from 'lucide-react'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import { fetchAdminOrdersSummary } from '@/entities/order/api/admin-order-api'
import { AdminCategoriesPage } from '@/pages/admin-categories'
import { AdminGalleryPage } from '@/pages/admin-gallery'
import { AdminOrdersPage } from '@/pages/admin-orders'
import { AdminProductsPage } from '@/pages/admin-products'
import { AdminReviewsPage } from '@/pages/admin-reviews'
import { AdminUsersPage } from '@/pages/admin-users'
import { $user } from '@/shared/model/auth'
import { AdminNotificationsPage } from './AdminNotificationsPage'
type NavItem = {
to: string
label: string
icon: ReactNode
}
export function AdminLayoutPage() {
const navigate = useNavigate()
const location = useLocation()
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const [mobileOpen, setMobileOpen] = useState(false)
const user = useUnit($user)
const isAdmin = Boolean(user?.isAdmin)
const ordersSummaryQuery = useQuery({
queryKey: ['admin', 'orders', 'summary'],
queryFn: fetchAdminOrdersSummary,
enabled: isAdmin,
refetchInterval: 45_000,
refetchOnWindowFocus: true,
})
const newOrdersAttention = ordersSummaryQuery.data?.attentionCount ?? 0
const navItems: NavItem[] = useMemo(
() => [
{ to: '/admin', label: 'Товары', icon: <Store /> },
{ to: '/admin/categories', label: 'Категории', icon: <LayoutGrid /> },
{ to: '/admin/gallery', label: 'Галерея', icon: <Image /> },
{ to: '/admin/orders', label: 'Заказы', icon: <ListOrdered /> },
{ to: '/admin/reviews', label: 'Отзывы', icon: <MessageSquare /> },
{ to: '/admin/users', label: 'Пользователи', icon: <Users /> },
{ to: '/admin/notifications', label: 'Оповещения', icon: <Bell /> },
],
[],
)
if (!isAdmin) {
return <Navigate to="/auth" 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: 'warning.50' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
Админка
</Typography>
<Typography variant="body2" color="text.secondary">
Управление магазином
</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: 'warning.100',
},
'&.Mui-selected:hover': {
bgcolor: 'warning.200',
},
}}
onClick={() => {
navigate(i.to)
setMobileOpen(false)
}}
>
<ListItemIcon>
{i.to === '/admin/orders' ? (
<Badge color="error" badgeContent={newOrdersAttention} max={99} invisible={newOrdersAttention === 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: 'warning.300',
bgcolor: 'warning.50',
}}
>
<LayoutGrid />
</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={<AdminProductsPage />} />
<Route path="categories" element={<AdminCategoriesPage />} />
<Route path="gallery" element={<AdminGalleryPage />} />
<Route path="orders" element={<AdminOrdersPage />} />
<Route path="reviews" element={<AdminReviewsPage />} />
<Route path="users" element={<AdminUsersPage />} />
<Route path="notifications" element={<AdminNotificationsPage />} />
<Route path="*" element={<Navigate to="/admin" replace />} />
</Routes>
</Box>
</Stack>
)
}