test refactor

This commit is contained in:
@kirill.komarov
2026-05-14 19:54:45 +05:00
parent 8165f75a78
commit ce9883f8c9
12 changed files with 175 additions and 175 deletions
+10
View File
@@ -19,6 +19,7 @@
"axios": "^1.15.2",
"effector": "^23.4.4",
"effector-react": "^23.3.0",
"lucide-react": "^1.14.0",
"maplibre-gl": "^5.24.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
@@ -7415,6 +7416,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz",
"integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+1
View File
@@ -26,6 +26,7 @@
"axios": "^1.15.2",
"effector": "^23.4.4",
"effector-react": "^23.3.0",
"lucide-react": "^1.14.0",
"maplibre-gl": "^5.24.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
+42 -152
View File
@@ -1,19 +1,12 @@
import { useState } from 'react'
import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'
import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'
import { useEffect, useState } from 'react'
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'
import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
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 FormControl from '@mui/material/FormControl'
import IconButton from '@mui/material/IconButton'
import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Select from '@mui/material/Select'
import type { SelectChangeEvent } from '@mui/material/Select'
import { useTheme } from '@mui/material/styles'
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'
@@ -21,7 +14,6 @@ import useMediaQuery from '@mui/material/useMediaQuery'
import { useQuery } from '@tanstack/react-query'
import { useUnit } from 'effector-react'
import { Link as RouterLink, useNavigate } from 'react-router-dom'
import type { ColorScheme } from '@/app/providers/theme-controller'
import { useThemeController } from '@/app/providers/theme-controller'
import { fetchMyCart } from '@/entities/cart/api/cart-api'
import { fetchMyOrders } from '@/entities/order/api/order-api'
@@ -29,143 +21,24 @@ 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: '/' }]
function ThemeControlsDesktop(props: {
scheme: string
mode: string
resolvedMode: 'light' | 'dark'
onSchemeChange: (e: SelectChangeEvent<string>) => void
onModeChange: (e: SelectChangeEvent<string>) => void
onCycleMode: () => void
}) {
const { scheme, mode, resolvedMode, onSchemeChange, onModeChange, onCycleMode } = props
return (
<>
<FormControl
size="small"
sx={{
ml: 2,
minWidth: 160,
'& .MuiInputLabel-root': { color: 'rgba(255,255,255,0.85)' },
'& .MuiInputLabel-root.Mui-focused': { color: '#fff' },
'& .MuiInputLabel-root.MuiInputLabel-shrink': { color: 'rgba(255,255,255,0.92)' },
}}
>
<InputLabel id="scheme-label">Схема</InputLabel>
<Select
labelId="scheme-label"
value={scheme}
label="Схема"
onChange={onSchemeChange}
sx={{
color: 'inherit',
'.MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.5)' },
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.9)' },
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.95)' },
'.MuiSvgIcon-root': { color: 'inherit' },
}}
>
<MenuItem value="craft">Крафт</MenuItem>
<MenuItem value="forest">Лес</MenuItem>
<MenuItem value="ocean">Океан</MenuItem>
<MenuItem value="berry">Ягоды</MenuItem>
</Select>
</FormControl>
<FormControl
size="small"
sx={{
ml: 2,
minWidth: 150,
'& .MuiInputLabel-root': { color: 'rgba(255,255,255,0.85)' },
'& .MuiInputLabel-root.Mui-focused': { color: '#fff' },
'& .MuiInputLabel-root.MuiInputLabel-shrink': { color: 'rgba(255,255,255,0.92)' },
}}
>
<InputLabel id="mode-label">Тема</InputLabel>
<Select
labelId="mode-label"
value={mode}
label="Тема"
onChange={onModeChange}
sx={{
color: 'inherit',
'.MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.5)' },
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.9)' },
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.95)' },
'.MuiSvgIcon-root': { color: 'inherit' },
}}
>
<MenuItem value="system">Авто (система)</MenuItem>
<MenuItem value="light">Светлая</MenuItem>
<MenuItem value="dark">Тёмная</MenuItem>
</Select>
</FormControl>
<IconButton
color="inherit"
onClick={onCycleMode}
sx={{ ml: 1 }}
aria-label="Переключить режим темы (авто/светлая/тёмная)"
title={`Сейчас: ${mode === 'system' ? `авто (${resolvedMode})` : mode}`}
>
{resolvedMode === 'dark' ? <LightModeOutlinedIcon /> : <DarkModeOutlinedIcon />}
</IconButton>
</>
)
}
function ThemeControlsMobile(props: {
scheme: string
mode: string
resolvedMode: 'light' | 'dark'
onSchemeChange: (e: SelectChangeEvent<string>) => void
onModeChange: (e: SelectChangeEvent<string>) => void
onCycleMode: () => void
}) {
const { scheme, mode, resolvedMode, onSchemeChange, onModeChange, onCycleMode } = props
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControl size="small" fullWidth>
<InputLabel id="scheme-label-mobile">Схема</InputLabel>
<Select labelId="scheme-label-mobile" value={scheme} label="Схема" onChange={onSchemeChange}>
<MenuItem value="craft">Крафт</MenuItem>
<MenuItem value="forest">Лес</MenuItem>
<MenuItem value="ocean">Океан</MenuItem>
<MenuItem value="berry">Ягоды</MenuItem>
</Select>
</FormControl>
<FormControl size="small" fullWidth>
<InputLabel id="mode-label-mobile">Тема</InputLabel>
<Select labelId="mode-label-mobile" value={mode} label="Тема" onChange={onModeChange}>
<MenuItem value="system">Авто (система)</MenuItem>
<MenuItem value="light">Светлая</MenuItem>
<MenuItem value="dark">Тёмная</MenuItem>
</Select>
</FormControl>
<Button variant="outlined" onClick={onCycleMode}>
Быстро переключить: {mode === 'system' ? `авто (${resolvedMode})` : mode}
</Button>
</Box>
)
}
export function AppHeader() {
const { mode, resolvedMode, scheme, setMode, setScheme, cycleMode } = useThemeController()
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'],
@@ -184,14 +57,14 @@ export function AppHeader() {
).length
const [mobileOpen, setMobileOpen] = useState(false)
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const [scrolled, setScrolled] = useState(false)
const onSchemeChange = (e: SelectChangeEvent<string>) => setScheme(e.target.value as ColorScheme)
const onModeChange = (e: SelectChangeEvent<string>) => {
const v = e.target.value
if (v === 'system' || v === 'light' || v === 'dark') setMode(v)
}
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)
@@ -205,11 +78,20 @@ export function AppHeader() {
navigate('/')
}
const themeControls = { scheme, mode, resolvedMode, onSchemeChange, onModeChange, onCycleMode: cycleMode }
return (
<>
<AppBar position="sticky" color="primary" elevation={0} sx={{ borderBottom: 1, borderColor: 'divider' }}>
<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
@@ -219,7 +101,7 @@ export function AppHeader() {
edge="start"
sx={{ mr: 1 }}
>
<MenuOutlinedIcon />
<MenuRoundedIcon />
</IconButton>
)}
@@ -255,7 +137,7 @@ export function AppHeader() {
<Tooltip title="Заказы">
<IconButton color="inherit" sx={{ ml: 1 }} onClick={() => navigate('/me/orders')} aria-label="Заказы">
<Badge color="secondary" badgeContent={activeOrdersCount} invisible={activeOrdersCount === 0}>
<LocalShippingOutlinedIcon />
<Inventory2OutlinedIcon />
</Badge>
</IconButton>
</Tooltip>
@@ -273,7 +155,12 @@ export function AppHeader() {
</Button>
)}
{!isMobile && <ThemeControlsDesktop {...themeControls} />}
{!isMobile && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ml: 1.5 }}>
<SchemeSwitcher value={scheme} onChange={(s: ColorScheme) => setScheme(s)} />
<ModeSwitcher mode={mode} resolvedMode={resolvedMode} onCycleMode={cycleMode} />
</Box>
)}
</Toolbar>
</AppBar>
@@ -283,10 +170,13 @@ export function AppHeader() {
user={user}
isAdmin={isAdmin}
navItems={headerNavItems}
themeControls={themeControls}
scheme={scheme}
mode={mode}
resolvedMode={resolvedMode}
onSchemeChange={(s: ColorScheme) => setScheme(s)}
onCycleMode={cycleMode}
onNavigate={go}
onLogout={onLogout}
ThemeControlsMobile={ThemeControlsMobile}
/>
</>
)
@@ -1,9 +1,6 @@
import { createContext, type PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react'
import type { PaletteMode } from '@mui/material'
export type ColorScheme = 'craft' | 'forest' | 'ocean' | 'berry'
export type ThemeModePreference = 'system' | PaletteMode
import type { ColorScheme, ThemeModePreference } from '@/shared/model/theme'
export type ThemeSettings = {
mode: ThemeModePreference
@@ -1,4 +1,4 @@
import ShoppingCartOutlinedIcon from '@mui/icons-material/ShoppingCartOutlined'
import ShoppingCartRoundedIcon from '@mui/icons-material/ShoppingCartRounded'
import Badge from '@mui/material/Badge'
import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
@@ -23,7 +23,7 @@ export function CartBadge({ user, cartCount, onNavigate }: Props) {
aria-label="Корзина"
>
<Badge color="secondary" badgeContent={user ? cartCount : 0} invisible={!user || cartCount === 0}>
<ShoppingCartOutlinedIcon />
<ShoppingCartRoundedIcon />
</Badge>
</IconButton>
</Tooltip>
@@ -1,5 +1,5 @@
import { useState } from 'react'
import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined'
import PersonOutlineRoundedIcon from '@mui/icons-material/PersonOutlineRounded'
import Badge from '@mui/material/Badge'
import IconButton from '@mui/material/IconButton'
import ListItemText from '@mui/material/ListItemText'
@@ -40,7 +40,7 @@ export function UserMenu({ user, onNavigate, onLogout }: Props) {
invisible={!user}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<AccountCircleOutlinedIcon />
<PersonOutlineRoundedIcon />
</Badge>
</IconButton>
+5
View File
@@ -0,0 +1,5 @@
import type { PaletteMode } from '@mui/material'
export type ColorScheme = 'craft' | 'forest' | 'ocean' | 'berry'
export type ThemeModePreference = 'system' | PaletteMode
@@ -0,0 +1,32 @@
import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'
import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
import type { ThemeModePreference } from '@/shared/model/theme'
type Props = {
mode: ThemeModePreference
resolvedMode: 'light' | 'dark'
onCycleMode: () => void
}
function getModeLabel(mode: ThemeModePreference, resolvedMode: 'light' | 'dark'): string {
switch (mode) {
case 'system':
return `Авто (${resolvedMode === 'dark' ? 'тёмная' : 'светлая'})`
case 'light':
return 'Светлая'
case 'dark':
return 'Тёмная'
}
}
export function ModeSwitcher({ mode, resolvedMode, onCycleMode }: Props) {
return (
<Tooltip title={`Тема: ${getModeLabel(mode, resolvedMode)}`}>
<IconButton color="inherit" onClick={onCycleMode} aria-label="Переключить тему">
{resolvedMode === 'dark' ? <LightModeOutlinedIcon /> : <DarkModeOutlinedIcon />}
</IconButton>
</Tooltip>
)
}
@@ -0,0 +1 @@
export { ModeSwitcher } from './ModeSwitcher'
@@ -0,0 +1,61 @@
import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'
import Box from '@mui/material/Box'
import IconButton from '@mui/material/IconButton'
import { Cherry, Hammer, Trees, WavesHorizontal } from 'lucide-react'
import type { ColorScheme } from '@/shared/model/theme'
type Props = {
value: ColorScheme
onChange: (scheme: ColorScheme) => void
orientation?: 'horizontal' | 'vertical'
}
const SCHEMES: { key: ColorScheme; color: string; label: string; icon: React.ReactNode }[] = [
{ key: 'craft', color: '#6D4C41', label: 'Крафт', icon: <Hammer size={14} /> },
{ key: 'forest', color: '#2E7D32', label: 'Лес', icon: <Trees size={14} /> },
{ key: 'ocean', color: '#1565C0', label: 'Океан', icon: <WavesHorizontal size={14} /> },
{ key: 'berry', color: '#7B1FA2', label: 'Ягоды', icon: <Cherry size={14} /> },
]
export function SchemeSwitcher({ value, onChange, orientation = 'horizontal' }: Props) {
return (
<Box
sx={{
display: 'flex',
flexDirection: orientation === 'vertical' ? 'column' : 'row',
gap: 0.5,
alignItems: 'center',
}}
>
{SCHEMES.map((s) => {
const active = value === s.key
return (
<IconButton
key={s.key}
onClick={() => onChange(s.key)}
size="small"
title={s.label}
sx={{
width: 30,
height: 30,
minWidth: 30,
bgcolor: s.color,
border: 2,
borderColor: active ? 'common.white' : 'rgba(255,255,255,0.4)',
boxShadow: active ? `0 0 0 1.5px ${s.color}, 0 0 8px ${s.color}99` : 'none',
transform: active ? 'scale(1.1)' : 'scale(1)',
color: 'common.white',
transition: 'all 0.2s ease',
'&:hover': {
transform: 'scale(1.2)',
bgcolor: s.color,
},
}}
>
{active ? <CheckCircleRoundedIcon sx={{ fontSize: 14 }} /> : s.icon}
</IconButton>
)
})}
</Box>
)
}
@@ -0,0 +1 @@
export { SchemeSwitcher } from './SchemeSwitcher'
@@ -2,20 +2,13 @@ import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer'
import type { SelectChangeEvent } from '@mui/material/Select'
import Typography from '@mui/material/Typography'
import { STORE_NAME } from '@/shared/config'
import type { AuthUser } from '@/shared/model/auth'
import type { ColorScheme, ThemeModePreference } from '@/shared/model/theme'
import { BearLogo } from '@/shared/ui/BearLogo'
type ThemeControls = {
scheme: string
mode: string
resolvedMode: 'light' | 'dark'
onSchemeChange: (e: SelectChangeEvent<string>) => void
onModeChange: (e: SelectChangeEvent<string>) => void
onCycleMode: () => void
}
import { ModeSwitcher } from '@/shared/ui/ModeSwitcher'
import { SchemeSwitcher } from '@/shared/ui/SchemeSwitcher'
type Props = {
open: boolean
@@ -23,10 +16,13 @@ type Props = {
user: AuthUser | null
isAdmin: boolean
navItems: { label: string; to: string }[]
themeControls: ThemeControls
scheme: ColorScheme
mode: ThemeModePreference
resolvedMode: 'light' | 'dark'
onSchemeChange: (scheme: ColorScheme) => void
onCycleMode: () => void
onNavigate: (to: string) => void
onLogout: () => void
ThemeControlsMobile: React.ComponentType<ThemeControls>
}
export function NavigationDrawer({
@@ -35,10 +31,13 @@ export function NavigationDrawer({
user,
isAdmin,
navItems,
themeControls,
scheme,
mode,
resolvedMode,
onSchemeChange,
onCycleMode,
onNavigate,
onLogout,
ThemeControlsMobile,
}: Props) {
const go = (to: string) => {
onClose()
@@ -93,7 +92,10 @@ export function NavigationDrawer({
<Divider sx={{ my: 2 }} />
<ThemeControlsMobile {...themeControls} />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'flex-start' }}>
<SchemeSwitcher value={scheme} onChange={onSchemeChange} orientation="vertical" />
<ModeSwitcher mode={mode} resolvedMode={resolvedMode} onCycleMode={onCycleMode} />
</Box>
</Box>
</Drawer>
)