feat: UI style refresh — Lucide icons, theme, slider, filters, buttons, VK
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^9.0.0",
|
||||
"@mui/material": "^9.0.0",
|
||||
"@tanstack/react-query": "^5.100.5",
|
||||
"@tiptap/extension-placeholder": "^3.22.5",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'
|
||||
import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
|
||||
import { Menu, Package } from 'lucide-react'
|
||||
import AppBar from '@mui/material/AppBar'
|
||||
import Badge from '@mui/material/Badge'
|
||||
import Box from '@mui/material/Box'
|
||||
@@ -101,7 +100,7 @@ export function AppHeader() {
|
||||
edge="start"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<MenuRoundedIcon />
|
||||
<Menu />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -137,7 +136,7 @@ export function AppHeader() {
|
||||
<Tooltip title="Заказы">
|
||||
<IconButton color="inherit" sx={{ ml: 1 }} onClick={() => navigate('/me/orders')} aria-label="Заказы">
|
||||
<Badge color="secondary" badgeContent={activeOrdersCount} invisible={activeOrdersCount === 0}>
|
||||
<Inventory2OutlinedIcon />
|
||||
<Package />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -4,11 +4,12 @@ import Container from '@mui/material/Container'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import Link from '@mui/material/Link'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { Link as RouterLink } from 'react-router-dom'
|
||||
import { AppHeader } from '@/app/layout/AppHeader'
|
||||
import { STORE_EMAIL, STORE_NAME, STORE_PHONE, STORE_SOCIAL_NOTE } from '@/shared/config'
|
||||
import { STORE_EMAIL, STORE_NAME, STORE_PHONE, VK_URL } from '@/shared/config'
|
||||
|
||||
export function MainLayout({ children }: PropsWithChildren) {
|
||||
const year = new Date().getFullYear()
|
||||
@@ -82,9 +83,18 @@ export function MainLayout({ children }: PropsWithChildren) {
|
||||
{STORE_PHONE}
|
||||
</Link>
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{STORE_SOCIAL_NOTE}
|
||||
</Typography>
|
||||
<Link
|
||||
href={VK_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5, '&:hover': { color: '#4A76A8' } }}
|
||||
>
|
||||
<SvgIcon sx={{ fontSize: 20 }}>
|
||||
<path d="M12.776 2.553a.267.267 0 0 0-.104-.002C10.184 2.973 4.08 7.575 2.34 9.478c-.19.208-.33.505-.33.85 0 .344.122.634.3.85.556.688 2.005 1.759 2.005 1.759s1.168 2.52 1.76 3.677c.232.463.45.858.604 1.13.348.611.534.857.926 1.077.392.22.776.22 1.194.01.417-.212 2.452-1.61 3.46-2.36.256-.19.49-.19.73.01 1.517 1.256 3.2 2.743 4.003 3.586.373.391.701.548 1.104.548.402 0 .683-.283.805-.86.053-.25 1.025-5.076 1.025-5.076s.633-2.24 2.763-3.897c.292-.228.462-.57.462-.95 0-.38-.143-.68-.38-.895-1.198-1.088-5.9-3.039-6.365-3.226z" />
|
||||
</SvgIcon>
|
||||
VK
|
||||
</Link>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -94,6 +94,31 @@ function AppThemeInner({ children }: PropsWithChildren) {
|
||||
h4: { fontWeight: 700 },
|
||||
h5: { fontWeight: 600 },
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: { textTransform: 'none', borderRadius: 12, fontWeight: 600 },
|
||||
contained: {
|
||||
boxShadow: '0 4px 14px 0 rgba(0,0,0,0.15)',
|
||||
'&:hover': {
|
||||
boxShadow: '0 6px 20px 0 rgba(0,0,0,0.25)',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
'&:hover': { boxShadow: '0 2px 8px 0 rgba(0,0,0,0.1)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': { transform: 'scale(1.1)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[controller.resolvedMode, controller.scheme],
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ShoppingCartRoundedIcon from '@mui/icons-material/ShoppingCartRounded'
|
||||
import { ShoppingCart } from 'lucide-react'
|
||||
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="Корзина"
|
||||
>
|
||||
<Badge color="secondary" badgeContent={user ? cartCount : 0} invisible={!user || cartCount === 0}>
|
||||
<ShoppingCartRoundedIcon />
|
||||
<ShoppingCart />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import AddShoppingCartOutlinedIcon from '@mui/icons-material/AddShoppingCartOutlined'
|
||||
import ShoppingCartOutlinedIcon from '@mui/icons-material/ShoppingCartOutlined'
|
||||
import ShoppingCartRoundedIcon from '@mui/icons-material/ShoppingCartRounded'
|
||||
import { ShoppingCart, ShoppingCartOff } from 'lucide-react'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -65,7 +63,7 @@ export function ToggleCartIcon(props: {
|
||||
<Tooltip title={tooltip}>
|
||||
<span>
|
||||
<IconButton size={size} onClick={onClick} disabled={disabled || busy} aria-label={tooltip} type="button">
|
||||
{user ? inCart ? <ShoppingCartRoundedIcon /> : <AddShoppingCartOutlinedIcon /> : <ShoppingCartOutlinedIcon />}
|
||||
{user ? inCart ? <ShoppingCart /> : <ShoppingCartOff /> : <ShoppingCart />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import PersonOutlineRoundedIcon from '@mui/icons-material/PersonOutlineRounded'
|
||||
import { User } from 'lucide-react'
|
||||
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' }}
|
||||
>
|
||||
<PersonOutlineRoundedIcon />
|
||||
<User />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined'
|
||||
import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined'
|
||||
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined'
|
||||
import PeopleOutlinedIcon from '@mui/icons-material/PeopleOutlined'
|
||||
import PhotoLibraryOutlinedIcon from '@mui/icons-material/PhotoLibraryOutlined'
|
||||
import RateReviewOutlinedIcon from '@mui/icons-material/RateReviewOutlined'
|
||||
import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined'
|
||||
import { FileText, Image, LayoutGrid, ListOrdered, MessageSquare, People, Store } from 'lucide-react'
|
||||
import Badge from '@mui/material/Badge'
|
||||
import Box from '@mui/material/Box'
|
||||
import Divider from '@mui/material/Divider'
|
||||
@@ -60,13 +54,13 @@ export function AdminLayoutPage() {
|
||||
|
||||
const navItems: NavItem[] = useMemo(
|
||||
() => [
|
||||
{ to: '/admin', label: 'Товары', icon: <StorefrontOutlinedIcon /> },
|
||||
{ to: '/admin/categories', label: 'Категории', icon: <AdminPanelSettingsOutlinedIcon /> },
|
||||
{ to: '/admin/gallery', label: 'Галерея', icon: <PhotoLibraryOutlinedIcon /> },
|
||||
{ to: '/admin/orders', label: 'Заказы', icon: <AssignmentOutlinedIcon /> },
|
||||
{ to: '/admin/reviews', label: 'Отзывы', icon: <RateReviewOutlinedIcon /> },
|
||||
{ to: '/admin/users', label: 'Пользователи', icon: <PeopleOutlinedIcon /> },
|
||||
{ to: '/admin/info', label: 'Инфо-страница', icon: <DescriptionOutlinedIcon /> },
|
||||
{ to: '/admin', label: 'Товары', icon: <Store /> },
|
||||
{ to: '/admin/categories', label: 'Категории', icon: <LayoutGrid /> },
|
||||
{ to: '/admin/gallery', label: 'Галерея', icon: <Image /> },
|
||||
{ to: '/admin/orders', label: 'Заказы', icon: <ListOrdered /> },
|
||||
{ to: '/admin/reviews', label: 'Отзывы', icon: <MessageSquare /> },
|
||||
{ to: '/admin/users', label: 'Пользователи', icon: <People /> },
|
||||
{ to: '/admin/info', label: 'Инфо-страница', icon: <FileText /> },
|
||||
],
|
||||
[],
|
||||
)
|
||||
@@ -143,7 +137,7 @@ export function AdminLayoutPage() {
|
||||
bgcolor: 'warning.50',
|
||||
}}
|
||||
>
|
||||
<AdminPanelSettingsOutlinedIcon />
|
||||
<LayoutGrid />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||
Админка
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'
|
||||
import RemoveIcon from '@mui/icons-material/Remove'
|
||||
import { Minus, Plus, Trash2 } from 'lucide-react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
@@ -115,7 +113,7 @@ export function CartPage() {
|
||||
size="small"
|
||||
sx={{ width: 32, height: 32 }}
|
||||
>
|
||||
<RemoveIcon />
|
||||
<Minus />
|
||||
</IconButton>
|
||||
<Typography
|
||||
sx={{
|
||||
@@ -137,7 +135,7 @@ export function CartPage() {
|
||||
size="small"
|
||||
sx={{ width: 32, height: 32 }}
|
||||
>
|
||||
<AddIcon />
|
||||
<Plus />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -149,7 +147,7 @@ export function CartPage() {
|
||||
size="small"
|
||||
sx={{ width: 32, height: 32 }}
|
||||
>
|
||||
<DeleteOutlineOutlinedIcon />
|
||||
<Trash2 />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useMemo } from 'react'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Collapse from '@mui/material/Collapse'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import FormControl from '@mui/material/FormControl'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Paper from '@mui/material/Paper'
|
||||
@@ -13,6 +15,7 @@ import TextField from '@mui/material/TextField'
|
||||
import ToggleButton from '@mui/material/ToggleButton'
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { Search, SlidersHorizontal } from 'lucide-react'
|
||||
import type { Category } from '@/entities/product/model/types'
|
||||
import type { UseProductFiltersResult } from '../lib/use-product-filters'
|
||||
|
||||
@@ -60,53 +63,49 @@ export function ProductFilters({
|
||||
spacing={2}
|
||||
sx={{ alignItems: { md: 'center' }, flexWrap: { md: 'wrap' } }}
|
||||
>
|
||||
<FormControl sx={{ minWidth: 220 }} size="small">
|
||||
<InputLabel id="category-filter-label">Категория</InputLabel>
|
||||
<Select<string>
|
||||
labelId="category-filter-label"
|
||||
label="Категория"
|
||||
value={categorySlug}
|
||||
onChange={handleCategoryChange}
|
||||
disabled={categoriesLoading}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Все</em>
|
||||
</MenuItem>
|
||||
{categoriesForFilter.map((c) => (
|
||||
<MenuItem key={c.id} value={c.slug}>
|
||||
{c.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label="Поиск"
|
||||
placeholder="Поиск"
|
||||
value={qInput}
|
||||
onChange={(e) => setQInput(e.target.value)}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Search size={18} />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ flexGrow: 1, minWidth: { xs: '100%', md: 360 } }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
bgcolor: 'background.paper',
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 1.5,
|
||||
alignItems: { sm: 'center' },
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
sx={{ p: 1.5, borderRadius: 3, bgcolor: 'background.paper', display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, alignItems: { sm: 'center' }, justifyContent: 'space-between' }}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="subtitle2">Наличие</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Быстрый фильтр по наличию
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { sm: 'center' }, gap: 1.5, flexGrow: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75, alignItems: 'center' }}>
|
||||
<Chip
|
||||
label="Все"
|
||||
size="small"
|
||||
variant={categorySlug === '' ? 'filled' : 'outlined'}
|
||||
color={categorySlug === '' ? 'primary' : 'default'}
|
||||
onClick={() => handleCategoryChange('')}
|
||||
/>
|
||||
{categoriesForFilter.map((c) => (
|
||||
<Chip
|
||||
key={c.id}
|
||||
label={c.name}
|
||||
size="small"
|
||||
variant={categorySlug === c.slug ? 'filled' : 'outlined'}
|
||||
color={categorySlug === c.slug ? 'primary' : 'default'}
|
||||
onClick={() => handleCategoryChange(c.slug)}
|
||||
disabled={categoriesLoading}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ToggleButtonGroup
|
||||
@@ -116,7 +115,7 @@ export function ProductFilters({
|
||||
onChange={(_, v) => handleAvailabilityChange(v)}
|
||||
sx={{
|
||||
alignSelf: { xs: 'flex-start', sm: 'auto' },
|
||||
'& .MuiToggleButton-root': { px: 2, fontWeight: 700, letterSpacing: 0.2, textTransform: 'none' },
|
||||
'& .MuiToggleButton-root': { px: 1.5, fontWeight: 600, textTransform: 'none' },
|
||||
'& .MuiToggleButton-root.Mui-selected': {
|
||||
bgcolor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
@@ -135,114 +134,81 @@ export function ProductFilters({
|
||||
spacing={1.5}
|
||||
sx={{ alignItems: { sm: 'center' }, justifyContent: 'space-between', flexWrap: 'wrap' }}
|
||||
>
|
||||
<Button variant="text" onClick={() => setMoreOpen((v) => !v)} sx={{ alignSelf: { xs: 'flex-start' } }}>
|
||||
{moreOpen ? 'Скрыть фильтры' : 'Фильтры и сортировка'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={resetFilters}
|
||||
variant="text"
|
||||
onClick={() => setMoreOpen((v) => !v)}
|
||||
startIcon={<SlidersHorizontal size={18} />}
|
||||
sx={{ alignSelf: { xs: 'flex-start' } }}
|
||||
>
|
||||
{moreOpen ? 'Скрыть' : 'Фильтры'}
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={resetFilters} sx={{ alignSelf: { xs: 'flex-start' } }}>
|
||||
Сбросить
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Collapse in={moreOpen} unmountOnExit>
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
spacing={2}
|
||||
sx={{ mt: 2, alignItems: { md: 'center' }, flexWrap: { md: 'wrap' } }}
|
||||
>
|
||||
<FormControl sx={{ minWidth: 220 }} size="small">
|
||||
<InputLabel id="sort-label">Сортировка</InputLabel>
|
||||
<Select<string> labelId="sort-label" label="Сортировка" value={sort} onChange={handleSortChange}>
|
||||
<MenuItem value="">
|
||||
<em>Сначала новые</em>
|
||||
</MenuItem>
|
||||
<MenuItem value="price_asc">Цена: по возрастанию</MenuItem>
|
||||
<MenuItem value="price_desc">Цена: по убыванию</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Paper variant="outlined" sx={{ p: 2, borderRadius: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ alignItems: { md: 'center' }, flexWrap: { md: 'wrap' } }}>
|
||||
<FormControl sx={{ minWidth: 200 }} size="small">
|
||||
<InputLabel id="sort-label">Сортировка</InputLabel>
|
||||
<Select<string> labelId="sort-label" label="Сортировка" value={sort} onChange={handleSortChange}>
|
||||
<MenuItem value=""><em>Сначала новые</em></MenuItem>
|
||||
<MenuItem value="price_asc">Цена: по возрастанию</MenuItem>
|
||||
<MenuItem value="price_desc">Цена: по убыванию</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label="Цена от, ₽"
|
||||
value={priceMinRub}
|
||||
onChange={(e) => handlePriceMinChange(e.target.value)}
|
||||
sx={{ width: { xs: '100%', md: 180 } }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Цена до, ₽"
|
||||
value={priceMaxRub}
|
||||
onChange={(e) => handlePriceMaxChange(e.target.value)}
|
||||
sx={{ width: { xs: '100%', md: 180 } }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Цена от, ₽"
|
||||
value={priceMinRub}
|
||||
onChange={(e) => handlePriceMinChange(e.target.value)}
|
||||
sx={{ width: { xs: '100%', md: 160 } }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Цена до, ₽"
|
||||
value={priceMaxRub}
|
||||
onChange={(e) => handlePriceMaxChange(e.target.value)}
|
||||
sx={{ width: { xs: '100%', md: 160 } }}
|
||||
/>
|
||||
|
||||
<FormControl sx={{ minWidth: 220 }} size="small">
|
||||
<InputLabel id="page-size-label">На странице</InputLabel>
|
||||
<Select<string>
|
||||
labelId="page-size-label"
|
||||
label="На странице"
|
||||
value={String(pageSize)}
|
||||
onChange={handlePageSizeChange}
|
||||
>
|
||||
{[6, 12, 18, 24].map((n) => (
|
||||
<MenuItem key={n} value={String(n)}>
|
||||
{n}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<FormControl sx={{ minWidth: 160 }} size="small">
|
||||
<InputLabel id="page-size-label">На странице</InputLabel>
|
||||
<Select<string> labelId="page-size-label" label="На странице" value={String(pageSize)} onChange={handlePageSizeChange}>
|
||||
{[6, 12, 18, 24].map((n) => (
|
||||
<MenuItem key={n} value={String(n)}>{n}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Divider />
|
||||
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
bgcolor: 'background.paper',
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 1.5,
|
||||
alignItems: { sm: 'center' },
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, alignItems: { sm: 'center' }, justifyContent: 'space-between' }}>
|
||||
<Typography variant="subtitle2">Масштаб карточек</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Выберите размер карточек в каталоге
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
size="small"
|
||||
value={cardScale}
|
||||
onChange={(_, v) => handleCardScaleChange(v)}
|
||||
sx={{
|
||||
alignSelf: { xs: 'flex-start', sm: 'auto' },
|
||||
'& .MuiToggleButton-root': { px: 1.5, fontWeight: 600, textTransform: 'none' },
|
||||
'& .MuiToggleButton-root.Mui-selected': {
|
||||
bgcolor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': { bgcolor: 'primary.dark' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={70}>S</ToggleButton>
|
||||
<ToggleButton value={90}>M</ToggleButton>
|
||||
<ToggleButton value={110}>L</ToggleButton>
|
||||
<ToggleButton value={130}>XL</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
size="small"
|
||||
value={cardScale}
|
||||
onChange={(_, v) => handleCardScaleChange(v)}
|
||||
sx={{
|
||||
alignSelf: { xs: 'flex-start', sm: 'auto' },
|
||||
'& .MuiToggleButton-root': {
|
||||
px: 2,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.2,
|
||||
textTransform: 'none',
|
||||
},
|
||||
'& .MuiToggleButton-root.Mui-selected': {
|
||||
bgcolor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': { bgcolor: 'primary.dark' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={70}>S</ToggleButton>
|
||||
<ToggleButton value={90}>M</ToggleButton>
|
||||
<ToggleButton value={110}>L</ToggleButton>
|
||||
<ToggleButton value={130}>XL</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Paper>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import ChatOutlinedIcon from '@mui/icons-material/ChatOutlined'
|
||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'
|
||||
import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'
|
||||
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
|
||||
import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined'
|
||||
import { MapPin, MessageCircle, Settings, SlidersHorizontal, Truck } from 'lucide-react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Badge from '@mui/material/Badge'
|
||||
import Box from '@mui/material/Box'
|
||||
@@ -56,10 +52,10 @@ export function MeLayoutPage() {
|
||||
|
||||
const navItems: NavItem[] = useMemo(
|
||||
() => [
|
||||
{ to: '/me/orders', label: 'Заказы', icon: <LocalShippingOutlinedIcon /> },
|
||||
{ to: '/me/messages', label: 'Сообщения', icon: <ChatOutlinedIcon /> },
|
||||
{ to: '/me/settings', label: 'Настройки', icon: <SettingsOutlinedIcon /> },
|
||||
{ to: '/me/addresses', label: 'Адреса доставки', icon: <PlaceOutlinedIcon /> },
|
||||
{ to: '/me/orders', label: 'Заказы', icon: <Truck /> },
|
||||
{ to: '/me/messages', label: 'Сообщения', icon: <MessageCircle /> },
|
||||
{ to: '/me/settings', label: 'Настройки', icon: <Settings /> },
|
||||
{ to: '/me/addresses', label: 'Адреса доставки', icon: <MapPin /> },
|
||||
],
|
||||
[],
|
||||
)
|
||||
@@ -143,7 +139,7 @@ export function MeLayoutPage() {
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<TuneOutlinedIcon />
|
||||
<SlidersHorizontal />
|
||||
</IconButton>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||
Профиль
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded'
|
||||
import { Star, X } from 'lucide-react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Chip from '@mui/material/Chip'
|
||||
@@ -180,8 +179,8 @@ export function ProductPage() {
|
||||
value={p.reviewsSummary.avgRating ?? 0}
|
||||
readOnly
|
||||
precision={0.25}
|
||||
icon={<StarRoundedIcon fontSize="inherit" />}
|
||||
emptyIcon={<StarRoundedIcon fontSize="inherit" />}
|
||||
icon={<Star fontSize="inherit" />}
|
||||
emptyIcon={<Star fontSize="inherit" />}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{reviewsCountRu(p.reviewsSummary.approvedReviewCount)}
|
||||
@@ -211,8 +210,8 @@ export function ProductPage() {
|
||||
value={rv.rating}
|
||||
readOnly
|
||||
size="small"
|
||||
icon={<StarRoundedIcon fontSize="inherit" />}
|
||||
emptyIcon={<StarRoundedIcon fontSize="inherit" />}
|
||||
icon={<Star fontSize="inherit" />}
|
||||
emptyIcon={<Star fontSize="inherit" />}
|
||||
/>
|
||||
{body ? (
|
||||
<Box sx={{ color: 'text.secondary' }}>
|
||||
@@ -259,7 +258,7 @@ export function ProductPage() {
|
||||
sx={{ position: 'absolute', top: 12, right: 12, zIndex: 2, color: 'white' }}
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<CloseIcon />
|
||||
<X />
|
||||
</IconButton>
|
||||
|
||||
<Swiper
|
||||
|
||||
@@ -15,4 +15,4 @@ export const STORE_PUBLIC_SITE_URL = (() => {
|
||||
/** Демо-контакты для футера; при необходимости задайте через VITE_* в `.env`. */
|
||||
export const STORE_EMAIL = import.meta.env.VITE_STORE_EMAIL ?? 'hello@example.com'
|
||||
export const STORE_PHONE = import.meta.env.VITE_STORE_PHONE ?? '+7 (900) 000-00-00'
|
||||
export const STORE_SOCIAL_NOTE = import.meta.env.VITE_STORE_SOCIAL_NOTE ?? 'Соцсети: укажите ссылки при публикации'
|
||||
export const VK_URL = import.meta.env.VITE_VK_URL ?? '#'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'
|
||||
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'
|
||||
import { Monitor, Moon, Sun } from 'lucide-react'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import type { ThemeModePreference } from '@/shared/model/theme'
|
||||
@@ -22,10 +21,12 @@ function getModeLabel(mode: ThemeModePreference, resolvedMode: 'light' | 'dark')
|
||||
}
|
||||
|
||||
export function ModeSwitcher({ mode, resolvedMode, onCycleMode }: Props) {
|
||||
const icon = mode === 'system' ? <Monitor /> : resolvedMode === 'dark' ? <Sun /> : <Moon />
|
||||
|
||||
return (
|
||||
<Tooltip title={`Тема: ${getModeLabel(mode, resolvedMode)}`}>
|
||||
<IconButton color="inherit" onClick={onCycleMode} aria-label="Переключить тему">
|
||||
{resolvedMode === 'dark' ? <LightModeOutlinedIcon /> : <DarkModeOutlinedIcon />}
|
||||
{icon}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useEffect } from 'react'
|
||||
import FormatBoldOutlinedIcon from '@mui/icons-material/FormatBoldOutlined'
|
||||
import FormatItalicOutlinedIcon from '@mui/icons-material/FormatItalicOutlined'
|
||||
import FormatListBulletedOutlinedIcon from '@mui/icons-material/FormatListBulletedOutlined'
|
||||
import { Bold, Italic, List } from 'lucide-react'
|
||||
import Box from '@mui/material/Box'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Stack from '@mui/material/Stack'
|
||||
@@ -59,7 +57,7 @@ export function RichTextMessageEditor({
|
||||
disabled={disabled}
|
||||
aria-label="Жирный"
|
||||
>
|
||||
<FormatBoldOutlinedIcon fontSize="small" />
|
||||
<Bold size={18} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -68,7 +66,7 @@ export function RichTextMessageEditor({
|
||||
disabled={disabled}
|
||||
aria-label="Курсив"
|
||||
>
|
||||
<FormatItalicOutlinedIcon fontSize="small" />
|
||||
<Italic size={18} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -77,7 +75,7 @@ export function RichTextMessageEditor({
|
||||
disabled={disabled}
|
||||
aria-label="Список"
|
||||
>
|
||||
<FormatListBulletedOutlinedIcon fontSize="small" />
|
||||
<List size={18} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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'
|
||||
@@ -11,10 +10,10 @@ type Props = {
|
||||
}
|
||||
|
||||
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} /> },
|
||||
{ key: 'craft', color: '#6D4C41', label: 'Крафт', icon: <Hammer size={16} /> },
|
||||
{ key: 'forest', color: '#2E7D32', label: 'Лес', icon: <Trees size={16} /> },
|
||||
{ key: 'ocean', color: '#1565C0', label: 'Океан', icon: <WavesHorizontal size={16} /> },
|
||||
{ key: 'berry', color: '#7B1FA2', label: 'Ягоды', icon: <Cherry size={16} /> },
|
||||
]
|
||||
|
||||
export function SchemeSwitcher({ value, onChange, orientation = 'horizontal' }: Props) {
|
||||
@@ -36,23 +35,24 @@ export function SchemeSwitcher({ value, onChange, orientation = 'horizontal' }:
|
||||
size="small"
|
||||
title={s.label}
|
||||
sx={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
minWidth: 30,
|
||||
bgcolor: s.color,
|
||||
width: 36,
|
||||
height: 36,
|
||||
minWidth: 36,
|
||||
bgcolor: 'transparent',
|
||||
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',
|
||||
borderColor: active ? s.color : 'transparent',
|
||||
boxShadow: active ? `0 0 8px ${s.color}66` : 'none',
|
||||
transform: active ? 'scale(1.15)' : 'scale(1)',
|
||||
color: active ? s.color : 'text.secondary',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.2)',
|
||||
bgcolor: s.color,
|
||||
borderColor: s.color,
|
||||
color: s.color,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{active ? <CheckCircleRoundedIcon sx={{ fontSize: 14 }} /> : s.icon}
|
||||
{s.icon}
|
||||
</IconButton>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -80,24 +80,22 @@ function CatalogSliderInner({ slides }: { slides: CatalogSliderSlide[] }) {
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: slides.length > 1 ? 20 : 0,
|
||||
zIndex: 4,
|
||||
pt: 4,
|
||||
pb: slides.length > 1 ? 2 : 2,
|
||||
WebkitTextStroke: '0.5px white',
|
||||
// background: 'linear-gradient(to top, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.4) 50%, transparent 100%)',
|
||||
background: 'linear-gradient(transparent, rgba(0,0,0,0.75))',
|
||||
px: 3,
|
||||
py: 2.5,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color="common.white"
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
textShadow: '0 1px 4px rgba(0,0,0,0.75)',
|
||||
fontWeight: 800,
|
||||
textAlign: 'center',
|
||||
fontSize: { xs: '1.25rem', sm: '1.5rem' },
|
||||
fontSize: { xs: '1.5rem', sm: '2rem', md: '2.5rem' },
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{captionText}
|
||||
|
||||
Reference in New Issue
Block a user