193 lines
6.3 KiB
TypeScript
193 lines
6.3 KiB
TypeScript
import * as React from 'react'
|
|
import { useEffect, useState } from 'react'
|
|
import AppBar from '@mui/material/AppBar'
|
|
import Badge from '@mui/material/Badge'
|
|
import Box from '@mui/material/Box'
|
|
import Button from '@mui/material/Button'
|
|
import IconButton from '@mui/material/IconButton'
|
|
import { alpha, useTheme } from '@mui/material/styles'
|
|
import Toolbar from '@mui/material/Toolbar'
|
|
import Tooltip from '@mui/material/Tooltip'
|
|
import Typography from '@mui/material/Typography'
|
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useUnit } from 'effector-react'
|
|
import { Menu, Package } from 'lucide-react'
|
|
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
|
import { useThemeController } from '@/app/providers/theme-controller'
|
|
import { fetchMyCart } from '@/entities/cart/api/cart-api'
|
|
import { fetchMyOrders } from '@/entities/order/api/order-api'
|
|
import { CartBadge } from '@/features/cart/cart-badge'
|
|
import { UserMenu } from '@/features/user/user-menu'
|
|
import { STORE_NAME } from '@/shared/config'
|
|
import { $user, logout, tokenSet } from '@/shared/model/auth'
|
|
import type { ColorScheme } from '@/shared/model/theme'
|
|
import { BearLogo } from '@/shared/ui/BearLogo'
|
|
import { ModeSwitcher } from '@/shared/ui/ModeSwitcher'
|
|
import { SchemeSwitcher } from '@/shared/ui/SchemeSwitcher'
|
|
import { NavigationDrawer } from '@/widgets/navigation-drawer'
|
|
|
|
type NavItem = { label: string; to: string }
|
|
|
|
const navItems: NavItem[] = [{ label: 'Каталог', to: '/' }]
|
|
|
|
export const AppHeader = React.memo(function AppHeader() {
|
|
const { mode, resolvedMode, scheme, setScheme, cycleMode } = useThemeController()
|
|
const user = useUnit($user)
|
|
const navigate = useNavigate()
|
|
const isAdmin = Boolean(user?.isAdmin)
|
|
const headerNavItems = isAdmin ? [...navItems, { label: 'Админка', to: '/admin' }] : navItems
|
|
const theme = useTheme()
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
|
|
|
const cartQuery = useQuery({
|
|
queryKey: ['me', 'cart'],
|
|
queryFn: fetchMyCart,
|
|
enabled: Boolean(user) && !isAdmin,
|
|
})
|
|
const cartCount = cartQuery.data?.items?.length ?? 0
|
|
|
|
const ordersQuery = useQuery({
|
|
queryKey: ['me', 'orders'],
|
|
queryFn: fetchMyOrders,
|
|
enabled: Boolean(user) && !isAdmin,
|
|
})
|
|
const activeOrdersCount = (ordersQuery.data?.items ?? []).filter(
|
|
(o) => o.status !== 'DONE' && o.status !== 'CANCELLED',
|
|
).length
|
|
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
const [scrolled, setScrolled] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const handler = () => setScrolled(window.scrollY > 0)
|
|
handler()
|
|
window.addEventListener('scroll', handler, { passive: true })
|
|
return () => window.removeEventListener('scroll', handler)
|
|
}, [])
|
|
|
|
const go = (to: string) => {
|
|
setMobileOpen(false)
|
|
navigate(to)
|
|
}
|
|
|
|
const onLogout = () => {
|
|
tokenSet(null)
|
|
logout()
|
|
setMobileOpen(false)
|
|
navigate('/')
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<AppBar
|
|
position="sticky"
|
|
color="primary"
|
|
elevation={scrolled ? 2 : 0}
|
|
sx={{
|
|
borderBottom: 1,
|
|
borderColor: 'divider',
|
|
bgcolor: alpha(theme.palette.primary.main, 0.88),
|
|
backdropFilter: 'blur(8px)',
|
|
transition: 'box-shadow 0.2s ease, background-color 0.2s ease',
|
|
}}
|
|
>
|
|
<Toolbar>
|
|
{isMobile && (
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={() => setMobileOpen(true)}
|
|
aria-label="Открыть меню"
|
|
edge="start"
|
|
sx={{ mr: 1 }}
|
|
>
|
|
<Menu />
|
|
</IconButton>
|
|
)}
|
|
|
|
<Box
|
|
component={RouterLink}
|
|
to="/"
|
|
sx={{
|
|
flexGrow: 1,
|
|
textDecoration: 'none',
|
|
color: 'inherit',
|
|
minWidth: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
}}
|
|
>
|
|
<BearLogo sx={{ width: 28, height: 28 }} />
|
|
<Typography variant="h6" sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
{STORE_NAME}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{!isMobile &&
|
|
headerNavItems.map((i) => (
|
|
<Button key={i.to} component={RouterLink} to={i.to} color="inherit">
|
|
{i.label}
|
|
</Button>
|
|
))}
|
|
|
|
{!isAdmin && (
|
|
<>
|
|
{user && (
|
|
<Tooltip title="Заказы">
|
|
<IconButton color="inherit" sx={{ ml: 1 }} onClick={() => navigate('/me/orders')} aria-label="Заказы">
|
|
<Badge color="secondary" badgeContent={activeOrdersCount} invisible={activeOrdersCount === 0}>
|
|
<Package />
|
|
</Badge>
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
|
|
<CartBadge user={user} cartCount={cartCount} onNavigate={navigate} />
|
|
</>
|
|
)}
|
|
|
|
{!isAdmin && <UserMenu user={user} isAdmin={false} onNavigate={navigate} onLogout={onLogout} />}
|
|
|
|
{isAdmin && user && !isMobile && (
|
|
<UserMenu user={user} isAdmin={true} onNavigate={navigate} onLogout={onLogout} />
|
|
)}
|
|
|
|
{!isMobile && (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 1.5 }}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
bgcolor: 'rgba(255, 255, 255, 0.25)',
|
|
borderRadius: 3,
|
|
px: 0.5,
|
|
py: 0.5,
|
|
}}
|
|
>
|
|
<SchemeSwitcher value={scheme} onChange={(s: ColorScheme) => setScheme(s)} />
|
|
</Box>
|
|
<ModeSwitcher mode={mode} resolvedMode={resolvedMode} onCycleMode={cycleMode} />
|
|
</Box>
|
|
)}
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
<NavigationDrawer
|
|
open={mobileOpen}
|
|
onClose={() => setMobileOpen(false)}
|
|
user={user}
|
|
isAdmin={isAdmin}
|
|
navItems={headerNavItems}
|
|
scheme={scheme}
|
|
mode={mode}
|
|
resolvedMode={resolvedMode}
|
|
onSchemeChange={(s: ColorScheme) => setScheme(s)}
|
|
onCycleMode={cycleMode}
|
|
onNavigate={go}
|
|
onLogout={onLogout}
|
|
/>
|
|
</>
|
|
)
|
|
})
|