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 { type PropsWithChildren } 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 Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Container from '@mui/material/Container'
|
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 Typography from '@mui/material/Typography'
|
||||||
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
import { AppHeader } from '@/app/layout/AppHeader'
|
||||||
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'
|
|
||||||
|
|
||||||
export function MainLayout({ children }: PropsWithChildren) {
|
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 (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||||
<AppBar position="sticky" color="primary" elevation={0} sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
<AppHeader />
|
||||||
<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>
|
|
||||||
|
|
||||||
<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 }}>
|
<Box component="main" sx={{ flex: 1, py: 3 }}>
|
||||||
<Container maxWidth="lg">{children}</Container>
|
<Container maxWidth="lg">{children}</Container>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -11,14 +11,39 @@ type Props = { product: Product }
|
|||||||
|
|
||||||
export function ProductCard({ product }: Props) {
|
export function ProductCard({ product }: Props) {
|
||||||
return (
|
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 ? (
|
{product.imageUrl ? (
|
||||||
<CardMedia
|
<CardMedia
|
||||||
component="img"
|
component="img"
|
||||||
height="200"
|
height="200"
|
||||||
image={product.imageUrl}
|
image={product.imageUrl}
|
||||||
alt={product.title}
|
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
|
<CardMedia
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { MePage } from './ui/MePage'
|
export { MePage } from './ui/MePage'
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ import {
|
|||||||
updateProfileFx,
|
updateProfileFx,
|
||||||
verifyEmailChangeFx,
|
verifyEmailChangeFx,
|
||||||
} from '@/shared/model/auth'
|
} 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() {
|
export function MePage() {
|
||||||
const user = useUnit($user)
|
const user = useUnit($user)
|
||||||
@@ -45,16 +52,9 @@ export function MePage() {
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
})
|
})
|
||||||
|
|
||||||
const passwordErrorMsg =
|
const passwordErrorMsg = getApiErrorMessage(errorPassword)
|
||||||
(errorPassword as any)?.response?.data?.error ? String((errorPassword as any).response.data.error) : null
|
const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify)
|
||||||
const emailErrorMsg =
|
const profileErrorMsg = getApiErrorMessage(errorProfile)
|
||||||
(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
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
|
return <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
|
||||||
@@ -175,4 +175,3 @@ export function MePage() {
|
|||||||
</Box>
|
</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,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* TS 7: baseUrl удалят; временно допускаем TS 6 */
|
|
||||||
"ignoreDeprecations": "6.0",
|
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user