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", "axios": "^1.15.2",
"effector": "^23.4.4", "effector": "^23.4.4",
"effector-react": "^23.3.0", "effector-react": "^23.3.0",
"lucide-react": "^1.14.0",
"maplibre-gl": "^5.24.0", "maplibre-gl": "^5.24.0",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
@@ -7415,6 +7416,15 @@
"yallist": "^3.0.2" "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": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+1
View File
@@ -26,6 +26,7 @@
"axios": "^1.15.2", "axios": "^1.15.2",
"effector": "^23.4.4", "effector": "^23.4.4",
"effector-react": "^23.3.0", "effector-react": "^23.3.0",
"lucide-react": "^1.14.0",
"maplibre-gl": "^5.24.0", "maplibre-gl": "^5.24.0",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
+42 -152
View File
@@ -1,19 +1,12 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined' import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined' import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'
import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'
import AppBar from '@mui/material/AppBar' import AppBar from '@mui/material/AppBar'
import Badge from '@mui/material/Badge' import Badge from '@mui/material/Badge'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import FormControl from '@mui/material/FormControl'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import InputLabel from '@mui/material/InputLabel' import { alpha, useTheme } from '@mui/material/styles'
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 Toolbar from '@mui/material/Toolbar' import Toolbar from '@mui/material/Toolbar'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
@@ -21,7 +14,6 @@ import useMediaQuery from '@mui/material/useMediaQuery'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useUnit } from 'effector-react' import { useUnit } from 'effector-react'
import { Link as RouterLink, useNavigate } from 'react-router-dom' 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 { useThemeController } from '@/app/providers/theme-controller'
import { fetchMyCart } from '@/entities/cart/api/cart-api' import { fetchMyCart } from '@/entities/cart/api/cart-api'
import { fetchMyOrders } from '@/entities/order/api/order-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 { UserMenu } from '@/features/user/user-menu'
import { STORE_NAME } from '@/shared/config' import { STORE_NAME } from '@/shared/config'
import { $user, logout, tokenSet } from '@/shared/model/auth' import { $user, logout, tokenSet } from '@/shared/model/auth'
import type { ColorScheme } from '@/shared/model/theme'
import { BearLogo } from '@/shared/ui/BearLogo' import { BearLogo } from '@/shared/ui/BearLogo'
import { ModeSwitcher } from '@/shared/ui/ModeSwitcher'
import { SchemeSwitcher } from '@/shared/ui/SchemeSwitcher'
import { NavigationDrawer } from '@/widgets/navigation-drawer' import { NavigationDrawer } from '@/widgets/navigation-drawer'
type NavItem = { label: string; to: string } type NavItem = { label: string; to: string }
const navItems: NavItem[] = [{ label: 'Каталог', to: '/' }] 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() { export function AppHeader() {
const { mode, resolvedMode, scheme, setMode, setScheme, cycleMode } = useThemeController() const { mode, resolvedMode, scheme, setScheme, cycleMode } = useThemeController()
const user = useUnit($user) const user = useUnit($user)
const navigate = useNavigate() const navigate = useNavigate()
const isAdmin = Boolean(user?.isAdmin) const isAdmin = Boolean(user?.isAdmin)
const headerNavItems = isAdmin ? [...navItems, { label: 'Админка', to: '/admin' }] : navItems const headerNavItems = isAdmin ? [...navItems, { label: 'Админка', to: '/admin' }] : navItems
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const cartQuery = useQuery({ const cartQuery = useQuery({
queryKey: ['me', 'cart'], queryKey: ['me', 'cart'],
@@ -184,14 +57,14 @@ export function AppHeader() {
).length ).length
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
const theme = useTheme() const [scrolled, setScrolled] = useState(false)
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const onSchemeChange = (e: SelectChangeEvent<string>) => setScheme(e.target.value as ColorScheme) useEffect(() => {
const onModeChange = (e: SelectChangeEvent<string>) => { const handler = () => setScrolled(window.scrollY > 0)
const v = e.target.value handler()
if (v === 'system' || v === 'light' || v === 'dark') setMode(v) window.addEventListener('scroll', handler, { passive: true })
} return () => window.removeEventListener('scroll', handler)
}, [])
const go = (to: string) => { const go = (to: string) => {
setMobileOpen(false) setMobileOpen(false)
@@ -205,11 +78,20 @@ export function AppHeader() {
navigate('/') navigate('/')
} }
const themeControls = { scheme, mode, resolvedMode, onSchemeChange, onModeChange, onCycleMode: cycleMode }
return ( 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> <Toolbar>
{isMobile && ( {isMobile && (
<IconButton <IconButton
@@ -219,7 +101,7 @@ export function AppHeader() {
edge="start" edge="start"
sx={{ mr: 1 }} sx={{ mr: 1 }}
> >
<MenuOutlinedIcon /> <MenuRoundedIcon />
</IconButton> </IconButton>
)} )}
@@ -255,7 +137,7 @@ export function AppHeader() {
<Tooltip title="Заказы"> <Tooltip title="Заказы">
<IconButton color="inherit" sx={{ ml: 1 }} onClick={() => navigate('/me/orders')} aria-label="Заказы"> <IconButton color="inherit" sx={{ ml: 1 }} onClick={() => navigate('/me/orders')} aria-label="Заказы">
<Badge color="secondary" badgeContent={activeOrdersCount} invisible={activeOrdersCount === 0}> <Badge color="secondary" badgeContent={activeOrdersCount} invisible={activeOrdersCount === 0}>
<LocalShippingOutlinedIcon /> <Inventory2OutlinedIcon />
</Badge> </Badge>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -273,7 +155,12 @@ export function AppHeader() {
</Button> </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> </Toolbar>
</AppBar> </AppBar>
@@ -283,10 +170,13 @@ export function AppHeader() {
user={user} user={user}
isAdmin={isAdmin} isAdmin={isAdmin}
navItems={headerNavItems} navItems={headerNavItems}
themeControls={themeControls} scheme={scheme}
mode={mode}
resolvedMode={resolvedMode}
onSchemeChange={(s: ColorScheme) => setScheme(s)}
onCycleMode={cycleMode}
onNavigate={go} onNavigate={go}
onLogout={onLogout} onLogout={onLogout}
ThemeControlsMobile={ThemeControlsMobile}
/> />
</> </>
) )
@@ -1,9 +1,6 @@
import { createContext, type PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react' import { createContext, type PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react'
import type { PaletteMode } from '@mui/material' import type { PaletteMode } from '@mui/material'
import type { ColorScheme, ThemeModePreference } from '@/shared/model/theme'
export type ColorScheme = 'craft' | 'forest' | 'ocean' | 'berry'
export type ThemeModePreference = 'system' | PaletteMode
export type ThemeSettings = { export type ThemeSettings = {
mode: ThemeModePreference 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 Badge from '@mui/material/Badge'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
@@ -23,7 +23,7 @@ export function CartBadge({ user, cartCount, onNavigate }: Props) {
aria-label="Корзина" aria-label="Корзина"
> >
<Badge color="secondary" badgeContent={user ? cartCount : 0} invisible={!user || cartCount === 0}> <Badge color="secondary" badgeContent={user ? cartCount : 0} invisible={!user || cartCount === 0}>
<ShoppingCartOutlinedIcon /> <ShoppingCartRoundedIcon />
</Badge> </Badge>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -1,5 +1,5 @@
import { useState } from 'react' 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 Badge from '@mui/material/Badge'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import ListItemText from '@mui/material/ListItemText' import ListItemText from '@mui/material/ListItemText'
@@ -40,7 +40,7 @@ export function UserMenu({ user, onNavigate, onLogout }: Props) {
invisible={!user} invisible={!user}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
> >
<AccountCircleOutlinedIcon /> <PersonOutlineRoundedIcon />
</Badge> </Badge>
</IconButton> </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 Button from '@mui/material/Button'
import Divider from '@mui/material/Divider' import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer' import Drawer from '@mui/material/Drawer'
import type { SelectChangeEvent } from '@mui/material/Select'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { STORE_NAME } from '@/shared/config' import { STORE_NAME } from '@/shared/config'
import type { AuthUser } from '@/shared/model/auth' import type { AuthUser } from '@/shared/model/auth'
import type { ColorScheme, ThemeModePreference } from '@/shared/model/theme'
import { BearLogo } from '@/shared/ui/BearLogo' import { BearLogo } from '@/shared/ui/BearLogo'
import { ModeSwitcher } from '@/shared/ui/ModeSwitcher'
type ThemeControls = { import { SchemeSwitcher } from '@/shared/ui/SchemeSwitcher'
scheme: string
mode: string
resolvedMode: 'light' | 'dark'
onSchemeChange: (e: SelectChangeEvent<string>) => void
onModeChange: (e: SelectChangeEvent<string>) => void
onCycleMode: () => void
}
type Props = { type Props = {
open: boolean open: boolean
@@ -23,10 +16,13 @@ type Props = {
user: AuthUser | null user: AuthUser | null
isAdmin: boolean isAdmin: boolean
navItems: { label: string; to: string }[] navItems: { label: string; to: string }[]
themeControls: ThemeControls scheme: ColorScheme
mode: ThemeModePreference
resolvedMode: 'light' | 'dark'
onSchemeChange: (scheme: ColorScheme) => void
onCycleMode: () => void
onNavigate: (to: string) => void onNavigate: (to: string) => void
onLogout: () => void onLogout: () => void
ThemeControlsMobile: React.ComponentType<ThemeControls>
} }
export function NavigationDrawer({ export function NavigationDrawer({
@@ -35,10 +31,13 @@ export function NavigationDrawer({
user, user,
isAdmin, isAdmin,
navItems, navItems,
themeControls, scheme,
mode,
resolvedMode,
onSchemeChange,
onCycleMode,
onNavigate, onNavigate,
onLogout, onLogout,
ThemeControlsMobile,
}: Props) { }: Props) {
const go = (to: string) => { const go = (to: string) => {
onClose() onClose()
@@ -93,7 +92,10 @@ export function NavigationDrawer({
<Divider sx={{ my: 2 }} /> <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> </Box>
</Drawer> </Drawer>
) )