From ce9883f8c9baf1c546d54029f685182be3c303e5 Mon Sep 17 00:00:00 2001 From: "@kirill.komarov" Date: Thu, 14 May 2026 19:54:45 +0500 Subject: [PATCH] test refactor --- client/package-lock.json | 10 + client/package.json | 1 + client/src/app/layout/AppHeader.tsx | 194 ++++-------------- client/src/app/providers/theme-controller.tsx | 5 +- .../features/cart/cart-badge/ui/CartBadge.tsx | 4 +- .../features/user/user-menu/ui/UserMenu.tsx | 4 +- client/src/shared/model/theme.ts | 5 + .../shared/ui/ModeSwitcher/ModeSwitcher.tsx | 32 +++ client/src/shared/ui/ModeSwitcher/index.ts | 1 + .../ui/SchemeSwitcher/SchemeSwitcher.tsx | 61 ++++++ client/src/shared/ui/SchemeSwitcher/index.ts | 1 + .../navigation-drawer/ui/NavigationDrawer.tsx | 32 +-- 12 files changed, 175 insertions(+), 175 deletions(-) create mode 100644 client/src/shared/model/theme.ts create mode 100644 client/src/shared/ui/ModeSwitcher/ModeSwitcher.tsx create mode 100644 client/src/shared/ui/ModeSwitcher/index.ts create mode 100644 client/src/shared/ui/SchemeSwitcher/SchemeSwitcher.tsx create mode 100644 client/src/shared/ui/SchemeSwitcher/index.ts diff --git a/client/package-lock.json b/client/package-lock.json index 02e1efb..b607d30 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index c32edb5..54fb1cc 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/app/layout/AppHeader.tsx b/client/src/app/layout/AppHeader.tsx index 2eda4d6..c1218c3 100644 --- a/client/src/app/layout/AppHeader.tsx +++ b/client/src/app/layout/AppHeader.tsx @@ -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) => void - onModeChange: (e: SelectChangeEvent) => void - onCycleMode: () => void -}) { - const { scheme, mode, resolvedMode, onSchemeChange, onModeChange, onCycleMode } = props - - return ( - <> - - Схема - - - - - Тема - - - - - {resolvedMode === 'dark' ? : } - - - ) -} - -function ThemeControlsMobile(props: { - scheme: string - mode: string - resolvedMode: 'light' | 'dark' - onSchemeChange: (e: SelectChangeEvent) => void - onModeChange: (e: SelectChangeEvent) => void - onCycleMode: () => void -}) { - const { scheme, mode, resolvedMode, onSchemeChange, onModeChange, onCycleMode } = props - - return ( - - - Схема - - - - - Тема - - - - - - ) -} - 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) => setScheme(e.target.value as ColorScheme) - const onModeChange = (e: SelectChangeEvent) => { - 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 ( <> - + {isMobile && ( - + )} @@ -255,7 +137,7 @@ export function AppHeader() { navigate('/me/orders')} aria-label="Заказы"> - + @@ -273,7 +155,12 @@ export function AppHeader() { )} - {!isMobile && } + {!isMobile && ( + + setScheme(s)} /> + + + )} @@ -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} /> ) diff --git a/client/src/app/providers/theme-controller.tsx b/client/src/app/providers/theme-controller.tsx index b0937b0..9537648 100644 --- a/client/src/app/providers/theme-controller.tsx +++ b/client/src/app/providers/theme-controller.tsx @@ -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 diff --git a/client/src/features/cart/cart-badge/ui/CartBadge.tsx b/client/src/features/cart/cart-badge/ui/CartBadge.tsx index d37ec61..40c2a83 100644 --- a/client/src/features/cart/cart-badge/ui/CartBadge.tsx +++ b/client/src/features/cart/cart-badge/ui/CartBadge.tsx @@ -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="Корзина" > - + diff --git a/client/src/features/user/user-menu/ui/UserMenu.tsx b/client/src/features/user/user-menu/ui/UserMenu.tsx index 2c866fe..fca6d69 100644 --- a/client/src/features/user/user-menu/ui/UserMenu.tsx +++ b/client/src/features/user/user-menu/ui/UserMenu.tsx @@ -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' }} > - + diff --git a/client/src/shared/model/theme.ts b/client/src/shared/model/theme.ts new file mode 100644 index 0000000..94087a3 --- /dev/null +++ b/client/src/shared/model/theme.ts @@ -0,0 +1,5 @@ +import type { PaletteMode } from '@mui/material' + +export type ColorScheme = 'craft' | 'forest' | 'ocean' | 'berry' + +export type ThemeModePreference = 'system' | PaletteMode diff --git a/client/src/shared/ui/ModeSwitcher/ModeSwitcher.tsx b/client/src/shared/ui/ModeSwitcher/ModeSwitcher.tsx new file mode 100644 index 0000000..2f70c70 --- /dev/null +++ b/client/src/shared/ui/ModeSwitcher/ModeSwitcher.tsx @@ -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 ( + + + {resolvedMode === 'dark' ? : } + + + ) +} diff --git a/client/src/shared/ui/ModeSwitcher/index.ts b/client/src/shared/ui/ModeSwitcher/index.ts new file mode 100644 index 0000000..be9230a --- /dev/null +++ b/client/src/shared/ui/ModeSwitcher/index.ts @@ -0,0 +1 @@ +export { ModeSwitcher } from './ModeSwitcher' diff --git a/client/src/shared/ui/SchemeSwitcher/SchemeSwitcher.tsx b/client/src/shared/ui/SchemeSwitcher/SchemeSwitcher.tsx new file mode 100644 index 0000000..a2abc5f --- /dev/null +++ b/client/src/shared/ui/SchemeSwitcher/SchemeSwitcher.tsx @@ -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: }, + { key: 'forest', color: '#2E7D32', label: 'Лес', icon: }, + { key: 'ocean', color: '#1565C0', label: 'Океан', icon: }, + { key: 'berry', color: '#7B1FA2', label: 'Ягоды', icon: }, +] + +export function SchemeSwitcher({ value, onChange, orientation = 'horizontal' }: Props) { + return ( + + {SCHEMES.map((s) => { + const active = value === s.key + return ( + 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 ? : s.icon} + + ) + })} + + ) +} diff --git a/client/src/shared/ui/SchemeSwitcher/index.ts b/client/src/shared/ui/SchemeSwitcher/index.ts new file mode 100644 index 0000000..224cc8f --- /dev/null +++ b/client/src/shared/ui/SchemeSwitcher/index.ts @@ -0,0 +1 @@ +export { SchemeSwitcher } from './SchemeSwitcher' diff --git a/client/src/widgets/navigation-drawer/ui/NavigationDrawer.tsx b/client/src/widgets/navigation-drawer/ui/NavigationDrawer.tsx index 0e30d20..42968b7 100644 --- a/client/src/widgets/navigation-drawer/ui/NavigationDrawer.tsx +++ b/client/src/widgets/navigation-drawer/ui/NavigationDrawer.tsx @@ -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) => void - onModeChange: (e: SelectChangeEvent) => 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 } 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({ - + + + + )