base commit
This commit is contained in:
@@ -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<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 user = useUnit($user)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [userAnchorEl, setUserAnchorEl] = useState<null | HTMLElement>(null)
|
||||
const userMenuOpen = Boolean(userAnchorEl)
|
||||
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const openUserMenu = (e: React.MouseEvent<HTMLElement>) => 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 (
|
||||
<>
|
||||
<AppBar position="sticky" color="primary" elevation={0} sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Toolbar>
|
||||
{isMobile && (
|
||||
<IconButton color="inherit" onClick={openMobile} aria-label="Открыть меню" edge="start" sx={{ mr: 1 }}>
|
||||
<MenuOutlinedIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Box
|
||||
component={RouterLink}
|
||||
to="/"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<BearLogo sx={{ fontSize: 28 }} />
|
||||
<Typography variant="h6" sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{STORE_NAME}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{!isMobile &&
|
||||
navItems.map((i) => (
|
||||
<Button key={i.to} component={RouterLink} to={i.to} color="inherit">
|
||||
{i.label}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<IconButton color="inherit" onClick={openUserMenu} sx={{ ml: 1 }} aria-label="Пользователь">
|
||||
<Badge
|
||||
variant="dot"
|
||||
color="success"
|
||||
overlap="circular"
|
||||
invisible={!user}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
>
|
||||
<AccountCircleOutlinedIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
anchorEl={userAnchorEl}
|
||||
open={userMenuOpen}
|
||||
onClose={closeUserMenu}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
{user ? (
|
||||
<>
|
||||
<MenuItem onClick={() => go('/me')}>
|
||||
<ListItemText primary={(user.name && user.name.trim()) || user.email} secondary="Профиль" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onLogout}>Выход</MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<MenuItem onClick={() => go('/auth')}>Войти / регистрация</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
{!isMobile && (
|
||||
<ThemeControlsDesktop
|
||||
scheme={scheme}
|
||||
mode={mode}
|
||||
resolvedMode={resolvedMode}
|
||||
onSchemeChange={onSchemeChange}
|
||||
onModeChange={onModeChange}
|
||||
onCycleMode={cycleMode}
|
||||
/>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer
|
||||
open={mobileOpen}
|
||||
onClose={closeMobile}
|
||||
slotProps={{ paper: { sx: { width: 320, maxWidth: '85vw' } } }}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<BearLogo sx={{ fontSize: 28 }} />
|
||||
<Typography variant="h6">{STORE_NAME}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{navItems.map((i) => (
|
||||
<Button key={i.to} variant="text" onClick={() => go(i.to)} sx={{ justifyContent: 'flex-start' }}>
|
||||
{i.label}
|
||||
</Button>
|
||||
))}
|
||||
<Button variant="text" onClick={() => go(user ? '/me' : '/auth')} sx={{ justifyContent: 'flex-start' }}>
|
||||
{user ? 'Профиль' : 'Вход / регистрация'}
|
||||
</Button>
|
||||
{user && (
|
||||
<Button variant="text" color="error" onClick={onLogout} sx={{ justifyContent: 'flex-start' }}>
|
||||
Выход
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<ThemeControlsMobile
|
||||
scheme={scheme}
|
||||
mode={mode}
|
||||
resolvedMode={resolvedMode}
|
||||
onSchemeChange={onSchemeChange}
|
||||
onModeChange={onModeChange}
|
||||
onCycleMode={cycleMode}
|
||||
/>
|
||||
</Box>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 | HTMLElement>(null)
|
||||
const menuOpen = Boolean(anchorEl)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const openUserMenu = (e: React.MouseEvent<HTMLElement>) => 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 (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<AppBar position="sticky" color="primary" elevation={0} sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Toolbar>
|
||||
<Typography
|
||||
component={RouterLink}
|
||||
to="/"
|
||||
variant="h6"
|
||||
sx={{ flexGrow: 1, textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
{STORE_NAME}
|
||||
</Typography>
|
||||
<Button component={RouterLink} to="/" color="inherit">
|
||||
Каталог
|
||||
</Button>
|
||||
<Button component={RouterLink} to="/admin" color="inherit">
|
||||
Админка
|
||||
</Button>
|
||||
<AppHeader />
|
||||
|
||||
<IconButton color="inherit" onClick={openUserMenu} sx={{ ml: 1 }} aria-label="Пользователь">
|
||||
<Badge
|
||||
variant="dot"
|
||||
color="success"
|
||||
overlap="circular"
|
||||
invisible={!user}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
>
|
||||
<AccountCircleOutlinedIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={menuOpen}
|
||||
onClose={closeUserMenu}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
{user ? (
|
||||
<>
|
||||
<MenuItem onClick={goToMe}>
|
||||
<ListItemText primary={(user.name && user.name.trim()) || user.email} secondary="Профиль" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onLogout}>Выход</MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<MenuItem onClick={goToAuth}>Войти / регистрация</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
<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={cycleMode}
|
||||
sx={{ ml: 1 }}
|
||||
aria-label="Переключить режим темы (авто/светлая/тёмная)"
|
||||
title={`Сейчас: ${mode === 'system' ? `авто (${resolvedMode})` : mode}`}
|
||||
>
|
||||
{resolvedMode === 'dark' ? <LightModeOutlinedIcon /> : <DarkModeOutlinedIcon />}
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box component="main" sx={{ flex: 1, py: 3 }}>
|
||||
<Container maxWidth="lg">{children}</Container>
|
||||
</Box>
|
||||
|
||||
@@ -11,14 +11,39 @@ type Props = { product: Product }
|
||||
|
||||
export function ProductCard({ product }: Props) {
|
||||
return (
|
||||
<Card variant="outlined" sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
transition: 'transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: 6,
|
||||
borderColor: 'divider',
|
||||
},
|
||||
'&:hover .product-card__media': { transform: 'scale(1.03)' },
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
transition: 'none',
|
||||
'&:hover': { transform: 'none' },
|
||||
'&:hover .product-card__media': { transform: 'none' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{product.imageUrl ? (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="200"
|
||||
image={product.imageUrl}
|
||||
alt={product.title}
|
||||
sx={{ objectFit: 'cover' }}
|
||||
sx={{
|
||||
objectFit: 'cover',
|
||||
transition: 'transform 240ms ease',
|
||||
'@media (prefers-reduced-motion: reduce)': { transition: 'none' },
|
||||
}}
|
||||
className="product-card__media"
|
||||
/>
|
||||
) : (
|
||||
<CardMedia
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { MePage } from './ui/MePage'
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ import {
|
||||
updateProfileFx,
|
||||
verifyEmailChangeFx,
|
||||
} from '@/shared/model/auth'
|
||||
import type { AxiosError } from 'axios'
|
||||
|
||||
function getApiErrorMessage(error: unknown): string | null {
|
||||
const e = error as AxiosError<{ error?: string }>
|
||||
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 <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
|
||||
@@ -175,4 +175,3 @@ export function MePage() {
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
import type { SvgIconProps } from '@mui/material/SvgIcon'
|
||||
|
||||
export function BearLogo(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="0 0 64 64" {...props}>
|
||||
<path
|
||||
d="M18 24c-3.9 0-7-3.1-7-7s3.1-7 7-7c2.8 0 5.3 1.7 6.4 4.1C26.5 12.8 29.1 11 32 11s5.5 1.8 7.6 4.1C40.7 11.7 43.2 10 46 10c3.9 0 7 3.1 7 7s-3.1 7-7 7"
|
||||
fill="currentColor"
|
||||
opacity="0.35"
|
||||
/>
|
||||
<path
|
||||
d="M32 18c-12.1 0-22 9.4-22 21 0 12.2 10.6 20 22 20s22-7.8 22-20c0-11.6-9.9-21-22-21Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M23 39c0 2.2-1.6 4-3.5 4S16 41.2 16 39s1.6-4 3.5-4S23 36.8 23 39Zm25 0c0 2.2-1.6 4-3.5 4S41 41.2 41 39s1.6-4 3.5-4S48 36.8 48 39Z"
|
||||
fill="#111"
|
||||
opacity="0.85"
|
||||
/>
|
||||
<path
|
||||
d="M32 33c-6.2 0-11.2 4.6-11.2 10.2 0 5.5 5 9.8 11.2 9.8s11.2-4.3 11.2-9.8C43.2 37.6 38.2 33 32 33Z"
|
||||
fill="#fff"
|
||||
opacity="0.9"
|
||||
/>
|
||||
<path
|
||||
d="M32 40.2c-1.9 0-3.4-1.2-3.4-2.7s1.5-2.7 3.4-2.7 3.4 1.2 3.4 2.7-1.5 2.7-3.4 2.7Z"
|
||||
fill="#111"
|
||||
opacity="0.9"
|
||||
/>
|
||||
<path
|
||||
d="M27.8 44.8c1.2 1.5 2.8 2.2 4.2 2.2s3-.7 4.2-2.2"
|
||||
fill="none"
|
||||
stroke="#111"
|
||||
strokeWidth="2.6"
|
||||
strokeLinecap="round"
|
||||
opacity="0.85"
|
||||
/>
|
||||
</SvgIcon>
|
||||
)
|
||||
}
|
||||
@@ -19,9 +19,6 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* TS 7: baseUrl удалят; временно допускаем TS 6 */
|
||||
"ignoreDeprecations": "6.0",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
Reference in New Issue
Block a user