From d40edf97e75f5c1f9c010923e1956aedd42b382f Mon Sep 17 00:00:00 2001 From: "@kirill.komarov" Date: Tue, 28 Apr 2026 21:47:43 +0500 Subject: [PATCH] base commit --- client/src/app/layout/AppHeader.tsx | 326 ++++++++++++++++++ client/src/app/layout/MainLayout.tsx | 178 +--------- .../src/entities/product/ui/ProductCard.tsx | 29 +- client/src/pages/me/index.ts | 1 - client/src/pages/me/ui/MePage.tsx | 21 +- client/src/shared/ui/BearLogo.tsx | 41 +++ client/tsconfig.app.json | 3 - 7 files changed, 407 insertions(+), 192 deletions(-) create mode 100644 client/src/app/layout/AppHeader.tsx create mode 100644 client/src/shared/ui/BearLogo.tsx diff --git a/client/src/app/layout/AppHeader.tsx b/client/src/app/layout/AppHeader.tsx new file mode 100644 index 0000000..6366586 --- /dev/null +++ b/client/src/app/layout/AppHeader.tsx @@ -0,0 +1,326 @@ +import { useState } from 'react' +import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined' +import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined' +import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined' +import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined' +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 Divider from '@mui/material/Divider' +import Drawer from '@mui/material/Drawer' +import FormControl from '@mui/material/FormControl' +import IconButton from '@mui/material/IconButton' +import InputLabel from '@mui/material/InputLabel' +import ListItemText from '@mui/material/ListItemText' +import Menu from '@mui/material/Menu' +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 Typography from '@mui/material/Typography' +import useMediaQuery from '@mui/material/useMediaQuery' +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 { STORE_NAME } from '@/shared/config' +import { $user, logout, tokenSet } from '@/shared/model/auth' +import { BearLogo } from '@/shared/ui/BearLogo' + +type NavItem = { label: string; to: string } + +const navItems: NavItem[] = [ + { label: 'Каталог', to: '/' }, + { label: 'Админка', to: '/admin' }, +] + +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 user = useUnit($user) + const navigate = useNavigate() + + const [userAnchorEl, setUserAnchorEl] = useState(null) + const userMenuOpen = Boolean(userAnchorEl) + + const [mobileOpen, setMobileOpen] = useState(false) + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('md')) + + 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) + } + + const openUserMenu = (e: React.MouseEvent) => setUserAnchorEl(e.currentTarget) + const closeUserMenu = () => setUserAnchorEl(null) + + const openMobile = () => setMobileOpen(true) + const closeMobile = () => setMobileOpen(false) + + const go = (to: string) => { + closeMobile() + closeUserMenu() + navigate(to) + } + + const onLogout = () => { + tokenSet(null) + logout() + closeMobile() + closeUserMenu() + navigate('/') + } + + return ( + <> + + + {isMobile && ( + + + + )} + + + + + {STORE_NAME} + + + + {!isMobile && + navItems.map((i) => ( + + ))} + + + + + + + + + {user ? ( + <> + go('/me')}> + + + Выход + + ) : ( + go('/auth')}>Войти / регистрация + )} + + + {!isMobile && ( + + )} + + + + + + + + {STORE_NAME} + + + + {navItems.map((i) => ( + + ))} + + {user && ( + + )} + + + + + + + + + ) +} diff --git a/client/src/app/layout/MainLayout.tsx b/client/src/app/layout/MainLayout.tsx index 6fa7063..6f0e592 100644 --- a/client/src/app/layout/MainLayout.tsx +++ b/client/src/app/layout/MainLayout.tsx @@ -1,186 +1,14 @@ -import { type PropsWithChildren, useState } from 'react' -import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined' -import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined' -import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined' -import AppBar from '@mui/material/AppBar' -import Badge from '@mui/material/Badge' +import { type PropsWithChildren } from 'react' import Box from '@mui/material/Box' -import Button from '@mui/material/Button' import Container from '@mui/material/Container' -import FormControl from '@mui/material/FormControl' -import IconButton from '@mui/material/IconButton' -import InputLabel from '@mui/material/InputLabel' -import ListItemText from '@mui/material/ListItemText' -import Menu from '@mui/material/Menu' -import MenuItem from '@mui/material/MenuItem' -import Select from '@mui/material/Select' -import type { SelectChangeEvent } from '@mui/material/Select' -import Toolbar from '@mui/material/Toolbar' import Typography from '@mui/material/Typography' -import { Link as RouterLink, useNavigate } from 'react-router-dom' -import { useUnit } from 'effector-react' -import type { ColorScheme } from '@/app/providers/theme-controller' -import { useThemeController } from '@/app/providers/theme-controller' -import { STORE_NAME } from '@/shared/config' -import { $user, logout, tokenSet } from '@/shared/model/auth' +import { AppHeader } from '@/app/layout/AppHeader' export function MainLayout({ children }: PropsWithChildren) { - const { mode, resolvedMode, scheme, setMode, setScheme, cycleMode } = useThemeController() - const user = useUnit($user) - const navigate = useNavigate() - const [anchorEl, setAnchorEl] = useState(null) - const menuOpen = Boolean(anchorEl) - - 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) - } - - const openUserMenu = (e: React.MouseEvent) => setAnchorEl(e.currentTarget) - const closeUserMenu = () => setAnchorEl(null) - - const onLogout = () => { - tokenSet(null) - logout() - closeUserMenu() - navigate('/') - } - - const goToAuth = () => { - closeUserMenu() - navigate('/auth') - } - - const goToMe = () => { - closeUserMenu() - navigate('/me') - } - return ( - - - - {STORE_NAME} - - - + - - - - - - - {user ? ( - <> - - - - Выход - - ) : ( - Войти / регистрация - )} - - - - Тема - - - - - Режим - - - - - {resolvedMode === 'dark' ? : } - - - {children} diff --git a/client/src/entities/product/ui/ProductCard.tsx b/client/src/entities/product/ui/ProductCard.tsx index 268928a..7db4069 100644 --- a/client/src/entities/product/ui/ProductCard.tsx +++ b/client/src/entities/product/ui/ProductCard.tsx @@ -11,14 +11,39 @@ type Props = { product: Product } export function ProductCard({ product }: Props) { return ( - + {product.imageUrl ? ( ) : ( + const msg = e?.response?.data?.error + return msg ? String(msg) : null +} export function MePage() { const user = useUnit($user) @@ -45,16 +52,9 @@ export function MePage() { mode: 'onChange', }) - const passwordErrorMsg = - (errorPassword as any)?.response?.data?.error ? String((errorPassword as any).response.data.error) : null - const emailErrorMsg = - (errorEmailReq as any)?.response?.data?.error - ? String((errorEmailReq as any).response.data.error) - : (errorEmailVerify as any)?.response?.data?.error - ? String((errorEmailVerify as any).response.data.error) - : null - const profileErrorMsg = - (errorProfile as any)?.response?.data?.error ? String((errorProfile as any).response.data.error) : null + const passwordErrorMsg = getApiErrorMessage(errorPassword) + const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify) + const profileErrorMsg = getApiErrorMessage(errorProfile) if (!user) { return Нужно войти. Перейдите на страницу «Вход». @@ -175,4 +175,3 @@ export function MePage() { ) } - diff --git a/client/src/shared/ui/BearLogo.tsx b/client/src/shared/ui/BearLogo.tsx new file mode 100644 index 0000000..31893bc --- /dev/null +++ b/client/src/shared/ui/BearLogo.tsx @@ -0,0 +1,41 @@ +import SvgIcon from '@mui/material/SvgIcon' +import type { SvgIconProps } from '@mui/material/SvgIcon' + +export function BearLogo(props: SvgIconProps) { + return ( + + + + + + + + + ) +} diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 2fec65c..d2d4d99 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -19,9 +19,6 @@ "noEmit": true, "jsx": "react-jsx", - /* TS 7: baseUrl удалят; временно допускаем TS 6 */ - "ignoreDeprecations": "6.0", - /* Linting */ "noUnusedLocals": true, "noUnusedParameters": true,