Merge branch 'redisign'

This commit is contained in:
Kirill
2026-05-19 10:33:42 +05:00
10 changed files with 426 additions and 102 deletions
+31 -23
View File
@@ -18,7 +18,7 @@ export function MainLayout({ children }: PropsWithChildren) {
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppHeader />
<Box component="main" sx={{ flex: 1, py: 3 }}>
<Box component="main" sx={{ flex: 1, py: { xs: 4, md: 6 } }}>
<Container maxWidth="lg">{children}</Container>
</Box>
@@ -29,32 +29,29 @@ export function MainLayout({ children }: PropsWithChildren) {
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.default',
py: { xs: 3, md: 4 },
py: { xs: 4, md: 6 },
}}
>
<Container maxWidth="lg">
<Grid container spacing={3}>
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 700 }}>
<Grid container spacing={4}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, letterSpacing: '-0.25px' }}>
Магазин
</Typography>
<Stack spacing={1}>
<Stack spacing={1.5}>
<Link component={RouterLink} to="/" color="inherit" underline="hover" variant="body2">
Каталог
</Link>
<Link component={RouterLink} to="/privacy" color="inherit" underline="hover" variant="body2">
Политика конфиденциальности
</Link>
<Typography variant="body2" color="text.secondary">
Изделия ручной работы: вещи с характером и вниманием к деталям.
</Typography>
</Stack>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 700 }}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, letterSpacing: '-0.25px' }}>
Покупателям
</Typography>
<Stack spacing={1}>
<Stack spacing={1.5}>
<Link component={RouterLink} to="/me" color="inherit" underline="hover" variant="body2">
Личный кабинет
</Link>
@@ -66,11 +63,11 @@ export function MainLayout({ children }: PropsWithChildren) {
</Link>
</Stack>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 700 }}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, letterSpacing: '-0.25px' }}>
Контакты
</Typography>
<Stack spacing={0.75}>
<Stack spacing={1}>
<Typography variant="body2">
Email:{' '}
<Link href={`mailto:${STORE_EMAIL}`} underline="hover">
@@ -97,15 +94,26 @@ export function MainLayout({ children }: PropsWithChildren) {
</Link>
</Stack>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, letterSpacing: '-0.25px' }}>
Юридическая информация
</Typography>
<Stack spacing={1.5}>
<Link component={RouterLink} to="/privacy" color="inherit" underline="hover" variant="body2">
Политика конфиденциальности
</Link>
<Link component={RouterLink} to="/terms" color="inherit" underline="hover" variant="body2">
Пользовательское соглашение
</Link>
</Stack>
</Grid>
</Grid>
<Divider sx={{ my: 2 }} />
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', textAlign: { xs: 'left', sm: 'center' } }}
>
© {year} {STORE_NAME}
</Typography>
<Divider sx={{ my: 3 }} />
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', textAlign: 'center' }}>
© {year} {STORE_NAME}
</Typography>
</Box>
</Container>
</Box>
</Box>
+82 -44
View File
@@ -24,97 +24,135 @@ function AppThemeInner({ children }: PropsWithChildren) {
case 'forest':
return {
...common,
primary: { main: isDark ? '#4CAF50' : '#2E7D32' },
secondary: { main: isDark ? '#A1887F' : '#6D4C41' },
info: { main: isDark ? '#29B6F6' : '#0288D1' },
success: { main: isDark ? '#66BB6A' : '#2E7D32' },
warning: { main: isDark ? '#FFB74D' : '#ED6C02' },
error: { main: isDark ? '#EF5350' : '#D32F2F' },
divider: isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.10)',
primary: { main: isDark ? '#8FBC8F' : '#2E8B57' },
secondary: { main: isDark ? '#CD853F' : '#8B4513' },
info: { main: isDark ? '#4682B4' : '#1E90FF' },
success: { main: isDark ? '#90EE90' : '#32CD32' },
warning: { main: isDark ? '#FFD700' : '#FFA500' },
error: { main: isDark ? '#F08080' : '#CD5C5C' },
divider: isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)',
text,
chip,
background: isDark
? { default: '#0E1510', paper: '#121B14' }
: { default: '#F6FAF6', paper: '#FFFFFF' },
? { default: '#0F1720', paper: '#1A242E' }
: { default: '#F8F6F3', paper: '#FFFFFF' },
}
case 'ocean':
return {
...common,
primary: { main: isDark ? '#42A5F5' : '#1565C0' },
secondary: { main: isDark ? '#4DD0E1' : '#00838F' },
info: { main: isDark ? '#4FC3F7' : '#0288D1' },
success: { main: isDark ? '#26C6DA' : '#00838F' },
warning: { main: isDark ? '#FFCC80' : '#ED6C02' },
error: { main: isDark ? '#EF5350' : '#D32F2F' },
divider: isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.10)',
primary: { main: isDark ? '#5F9EA0' : '#20B2AA' },
secondary: { main: isDark ? '#7B68EE' : '#6A5ACD' },
info: { main: isDark ? '#87CEEB' : '#00BFFF' },
success: { main: isDark ? '#98FB98' : '#00FA9A' },
warning: { main: isDark ? '#FFE4B5' : '#FFDAB9' },
error: { main: isDark ? '#FF6347' : '#FF4500' },
divider: isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)',
text,
chip,
background: isDark
? { default: '#0B1220', paper: '#0F172A' }
: { default: '#F6FAFF', paper: '#FFFFFF' },
? { default: '#0A1A2A', paper: '#0F1D35' }
: { default: '#F0F8FF', paper: '#FFFFFF' },
}
case 'berry':
return {
...common,
primary: { main: isDark ? '#BA68C8' : '#7B1FA2' },
secondary: { main: isDark ? '#F06292' : '#C2185B' },
info: { main: isDark ? '#64B5F6' : '#1976D2' },
success: { main: isDark ? '#81C784' : '#2E7D32' },
warning: { main: isDark ? '#FFB74D' : '#ED6C02' },
error: { main: isDark ? '#EF5350' : '#D32F2F' },
divider: isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.10)',
primary: { main: isDark ? '#9370DB' : '#8A2BE2' },
secondary: { main: isDark ? '#FF69B4' : '#FF1493' },
info: { main: isDark ? '#00CED1' : '#00BFFF' },
success: { main: isDark ? '#00FF7F' : '#7CFC00' },
warning: { main: isDark ? '#FFD700' : '#FFA500' },
error: { main: isDark ? '#FF4500' : '#FF6347' },
divider: isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)',
text,
chip,
background: isDark
? { default: '#140A17', paper: '#1B0F20' }
: { default: '#FFF7FD', paper: '#FFFFFF' },
? { default: '#1A0A1A', paper: '#250E25' }
: { default: '#FFF0F5', paper: '#FFFFFF' },
}
case 'craft':
default:
return {
...common,
primary: { main: isDark ? '#BCAAA4' : '#6D4C41' },
secondary: { main: isDark ? '#FFCCBC' : '#8D6E63' },
info: { main: isDark ? '#90CAF9' : '#1976D2' },
success: { main: isDark ? '#A5D6A7' : '#2E7D32' },
warning: { main: isDark ? '#FFB74D' : '#ED6C02' },
error: { main: isDark ? '#EF9A9A' : '#D32F2F' },
divider: isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.10)',
primary: { main: isDark ? '#90A4AE' : '#546E7A' },
secondary: { main: isDark ? '#78909C' : '#78909C' },
info: { main: isDark ? '#7986CB' : '#3F51B5' },
success: { main: isDark ? '#66BB6A' : '#43A047' },
warning: { main: isDark ? '#FFB74D' : '#F57C00' },
error: { main: isDark ? '#EF5350' : '#D32F2F' },
divider: isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)',
text,
chip,
background: isDark
? { default: '#12100F', paper: '#191615' }
: { default: '#FAF8F5', paper: '#FFFFFF' },
? { default: '#121212', paper: '#1E1E1E' }
: { default: '#F5F5F5', paper: '#FFFFFF' },
}
}
})(),
shape: { borderRadius: 12 },
typography: {
fontFamily: '"Segoe UI", system-ui, sans-serif',
h4: { fontWeight: 700 },
h5: { fontWeight: 600 },
fontFamily: '"Outfit", "Segoe UI", system-ui, sans-serif',
h4: { fontWeight: 700, letterSpacing: '-0.5px' },
h5: { fontWeight: 600, letterSpacing: '-0.25px' },
h6: { fontWeight: 600 },
subtitle1: { fontWeight: 600 },
subtitle2: { fontWeight: 500 },
body1: { fontSize: '0.875rem' },
body2: { fontSize: '0.75rem' },
button: { textTransform: 'none', fontWeight: 600 },
},
components: {
MuiButton: {
styleOverrides: {
root: { textTransform: 'none', borderRadius: 12, fontWeight: 600 },
root: {
textTransform: 'none',
borderRadius: 12,
fontWeight: 600,
transition: 'all 0.2s ease-in-out',
},
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)',
transform: 'translateY(-2px)',
},
'&:active': {
boxShadow: '0 2px 8px 0 rgba(0,0,0,0.15)',
transform: 'translateY(0)',
},
},
outlined: {
'&:hover': { boxShadow: '0 2px 8px 0 rgba(0,0,0,0.1)' },
border: '1px solid',
'&:hover': {
boxShadow: '0 2px 8px 0 rgba(0,0,0,0.1)',
borderWidth: '2px',
},
'&:active': {
boxShadow: 'none',
},
},
text: {
'&:hover': {
boxShadow: '0 1px 3px 0 rgba(0,0,0,0.1)',
backgroundColor: 'action.hover',
},
'&:active': {
backgroundColor: 'action.selected',
},
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
transition: 'all 0.2s ease',
'&:hover': { transform: 'scale(1.1)' },
transition: 'all 0.2s ease-in-out',
'&:hover': {
backgroundColor: 'action.hover',
transform: 'scale(1.1)',
},
'&:active': {
backgroundColor: 'action.selected',
transform: 'scale(0.95)',
},
},
},
},
+5 -2
View File
@@ -1,5 +1,5 @@
import { lazy, Suspense } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { Route, Routes } from 'react-router-dom'
import { MainLayout } from '@/app/layout/MainLayout'
import { AboutPage } from '@/pages/about'
// import { AdminLayoutPage } from '@/pages/admin-layout'
@@ -9,8 +9,10 @@ import { CheckoutPage } from '@/pages/checkout'
import { HomePage } from '@/pages/home'
import { InfoPage } from '@/pages/info'
// import { MeLayoutPage } from '@/pages/me'
import { NotFoundPage } from '@/pages/not-found'
import { PrivacyPolicyPage } from '@/pages/privacy-policy'
import { ProductPage } from '@/pages/product'
import { TermsPage } from '@/pages/terms'
import { SkeletonPage } from '@/shared/ui/SkeletonPage'
const AdminLayoutPage = lazy(() => import('@/pages/admin-layout').then((m) => ({ default: m.AdminLayoutPage })))
@@ -36,6 +38,7 @@ export function AppRoutes() {
<Route path="/about" element={<AboutPage />} />
<Route path="/info" element={<InfoPage />} />
<Route path="/privacy" element={<PrivacyPolicyPage />} />
<Route path="/terms" element={<TermsPage />} />
<Route
path="/me/*"
element={
@@ -45,7 +48,7 @@ export function AppRoutes() {
}
/>
<Route path="/products/:id" element={<ProductPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</MainLayout>
)
+1
View File
@@ -0,0 +1 @@
export { NotFoundPage } from './ui/NotFoundPage'
@@ -0,0 +1,46 @@
import { Box, Typography, Button, Stack, Paper } from '@mui/material'
import { Link as RouterLink } from 'react-router-dom'
export function NotFoundPage() {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
bgcolor: 'background.default',
alignItems: 'center',
justifyContent: 'center',
py: 4,
px: 2,
}}
>
<Paper sx={{ p: 4, borderRadius: 3, bgcolor: 'background.paper' }}>
<Stack spacing={3} sx={{ alignItems: 'center', textAlign: 'center' }}>
<Box sx={{ fontSize: 96, lineHeight: 1 }}>404</Box>
<Typography variant="h4" component="h1" gutterBottom>
Страница не найдена
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 400 }}>
Извините, но запрашиваемая страница не существует или была удалена.
</Typography>
<Stack direction="row" spacing={2} sx={{ mt: 2 }}>
<Button
variant="contained"
size="large"
onClick={() => {
window.location.href = '/'
}}
sx={{ px: 4 }}
>
Вернуться на главную
</Button>
<Button variant="outlined" size="large" component={RouterLink} to="/" sx={{ px: 4 }}>
Посмотреть каталог
</Button>
</Stack>
</Stack>
</Paper>
</Box>
)
}
+8 -1
View File
@@ -194,7 +194,14 @@ export function ProductPage() {
{reviewsQuery.isLoading && <Typography color="text.secondary">Загрузка отзывов</Typography>}
{reviewsQuery.isError && <Alert severity="warning">Не удалось загрузить отзывы.</Alert>}
{reviewsQuery.data && reviewsQuery.data.total === 0 && (
<Typography color="text.secondary">Пока нет опубликованных отзывов на этот товар.</Typography>
<Box sx={{ py: 3 }}>
<Typography variant="h6" color="text.secondary" sx={{ mb: 1 }}>
Отзывов пока нет
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 400 }}>
Будьте первым, кто оставит отзыв на этот товар. Ваше мнение поможет улучшить качество наших изделий.
</Typography>
</Box>
)}
{reviewsQuery.data && reviewsQuery.data.items.length > 0 && (
<Stack spacing={1.25}>
+1
View File
@@ -0,0 +1 @@
export { TermsPage } from './ui/TermsPage'
+211
View File
@@ -0,0 +1,211 @@
import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper'
import Typography from '@mui/material/Typography'
import { STORE_EMAIL, STORE_PUBLIC_SITE_URL } from '@/shared/config'
const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
const OP_NAME = 'Индивидуальный предприниматель Новоселова Наталия Владимировна'
const OP_INN = '402900832341'
const OP_OGRN = '305402922700051'
const OP_ADDR = '248000, Россия, г. Калуга, ул. Никитина, д. 12А'
const sections = [
{
title: '1. Общие положения',
items: [
`1.1. Настоящее Пользовательское соглашение (далее — «Соглашение») определяет порядок и условия использования материалов и сервисов, размещённых в сети Интернет по адресу ${SITE_URL} (далее — «Сайт»), Пользователями данного Сайта.`,
`1.2. Использование Пользователями Сайта означает, что они безоговорочно принимают и обязуются соблюдать все условия настоящего Соглашения.`,
'1.3. В настоящем Соглашении используются следующие термины:',
`— Администратор — ${OP_NAME}, ИНН ${OP_INN}, ОГРН ${OP_OGRN}, адрес: ${OP_ADDR}, которому принадлежат все соответствующие права на Сайт.`,
`— Акцепт — полное и безоговорочное принятие условий настоящего Соглашения, размещённого на Сайте по адресу ${SITE_URL}/terms, осуществляемое путём совершения Пользователем любых действий по использованию Сайта.`,
'— Аутентификационные данные Пользователя — адрес электронной почты Пользователя и пароль (код доступа), которые в совокупности признаются простой электронной подписью Пользователя.',
'— Пользователь — лицо, осуществляющее доступ к Сайту и использующее материалы и сервисы, размещённые на Сайте.',
'— Контент — любое информационно значимое наполнение Сайта, включая фото, текст и иные медиаматериалы.',
'— Личный кабинет — персонализированная часть Сайта, посредством которой обеспечивается обмен информацией между Пользователем и Сайтом.',
'— Персональные данные — любая информация, относящаяся к определённому или определяемому на основании такой информации физическому лицу.',
'— Обработка персональных данных — любое действие (операция) или совокупность действий с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение, использование, передачу, обезличивание, блокирование, удаление, уничтожение.',
`— Сайт — ресурс в сети Интернет по адресу ${SITE_URL}, представляющий собой совокупность информации и объектов интеллектуальной собственности.`,
],
},
{
title: '2. Предмет соглашения',
items: [
'2.1. В соответствии с настоящим Соглашением Администратор предоставляет любому Пользователю право безвозмездного использования Сайта в пределах его объявленных функциональных возможностей.',
'2.2. Использование Сайта осуществляется в соответствии с принципом «как есть» (as is). Администратор не гарантирует, что Сайт будет работать непрерывно, быстро и без ошибок.',
'2.3. Пользователь считается присоединившимся к Соглашению в соответствии со ст. 438 ГК РФ при совершении любых из следующих действий:',
'— просмотр материалов, размещённых на Сайте;',
'— использование сервисов Сайта;',
'— регистрация в Личном кабинете;',
'— направление сообщений с использованием онлайн-форм на Сайте;',
'— иное использование Сайта.',
'2.4. Используя Сайт, Пользователь подтверждает, что ознакомился с условиями Соглашения в полном объёме и безоговорочно принимает их.',
'2.5. Положения Соглашения не устанавливают между Администратором и Пользователем агентских отношений, отношений товарищества или иных отношений, прямо не предусмотренных Соглашением.',
'2.6. Все возможные споры, вытекающие из Соглашения, подлежат разрешению в соответствии с действующим законодательством РФ.',
],
},
{
title: '3. Регистрация',
items: [
'3.1. Для использования отдельных функций Сайта Пользователю необходимо пройти процедуру регистрации, в результате которой ему будет предоставлен доступ в Личный кабинет.',
'3.2. При регистрации Пользователь обязуется предоставить достоверную и полную информацию и поддерживать её в актуальном состоянии.',
'3.3. В случае предоставления неверной информации Администратор вправе заблокировать Личный кабинет либо удалить учётную запись Пользователя.',
'3.4. Пользователь самостоятельно несёт ответственность за сохранность своих Аутентификационных данных и за все действия, совершённые с их использованием.',
'3.5. Пользователь обязан незамедлительно уведомить Администратора о любом случае несанкционированного доступа к Личному кабинету.',
'3.6. Пользователь не вправе воспроизводить, копировать, продавать и использовать в коммерческих целях Сайт или его Контент без разрешения Администратора.',
'3.7. При регистрации Пользователь даёт согласие на получение информационных и рекламных сообщений от Администратора на указанный адрес электронной почты.',
],
},
{
title: '4. Права и обязанности Администратора',
items: [
'4.1. Администратор вправе осуществлять сбор мнений и отзывов Пользователей для формирования статистических данных и улучшения качества Сайта.',
'4.2. Администратор вправе направлять Пользователю информационные сообщения, связанные с функционированием Сайта, исполнением договоров, восстановлением пароля.',
'4.3. Администратор оставляет за собой право заблокировать Личный кабинет Пользователя в случае нарушения условий Соглашения.',
'4.4. Сайт может быть частично или полностью недоступен по причине проведения технических работ. Администратор вправе приостанавливать работу Сайта по своему усмотрению.',
'4.5. Администратор не несёт ответственности за ошибки, сбои линий связи, неправомерный доступ к информации Пользователя, возникшие не по вине Администратора.',
'4.6. Администратор предпримет разумные усилия для устранения технических сбоев и ошибок, но не гарантирует их полного отсутствия.',
'4.7. Пользователю не предоставляется никаких интеллектуальных прав на Сайт и его составные части, кроме прямо предусмотренных Соглашением.',
],
},
{
title: '5. Права и обязанности Пользователя',
items: [
'5.1. Пользователь обязуется знакомиться с актуальной версией Соглашения при каждом посещении Сайта.',
'5.2. Пользователь обязуется предоставлять достоверную информацию при использовании Сайта.',
'5.3. Пользователь обязуется не совершать действий, нарушающих российское законодательство, нормы морали и нравственности, а также действий, приводящих к нарушению работы Сайта.',
'5.4. Использование материалов Сайта без согласия правообладателей не допускается. При цитировании материалов Сайта ссылка на Сайт обязательна.',
'5.5. Пользователь обязуется не нарушать права и законные интересы третьих лиц, не причинять вред деловой репутации.',
'5.6. Пользователь не вправе нарушать нормальную работу отдельных сервисов и Сайта в целом.',
'5.7. Пользователь обязан самостоятельно отслеживать внесение изменений в настоящее Соглашение.',
'5.8. Пользователь вправе прекратить доступ к Личному кабинету, направив уведомление Администратору.',
],
},
{
title: '6. Ограничение ответственности',
items: [
'6.1. Администратор гарантирует достоверность и полноту только той информации, которую он разместил на Сайте самостоятельно.',
'6.2. Администратор не несёт ответственности за недостоверность информации, размещённой третьими лицами, в том числе Пользователями.',
'6.3. Администратор не гарантирует, что Сайт будет соответствовать требованиям Пользователя, работать непрерывно и без ошибок.',
'6.4. Администратор не несёт ответственности перед Пользователем за любые убытки, включая упущенную выгоду, потерю данных, вред деловой репутации, причинённые в связи с использованием Сайта.',
'6.5. Администратор исходит из того, что все формы на Сайте заполняет непосредственно Пользователь. Ответственность за достоверность предоставленных данных несёт Пользователь.',
'6.6. Администратор не несёт ответственности за утрату или порчу данных в результате невыполнения Пользователем условий Соглашения.',
],
},
{
title: '7. Доступ к ресурсам третьих лиц',
items: [
'7.1. Доступ Пользователя к Сайту может вызывать обращение к интернет-ресурсам третьих лиц (реклама, сбор статистики).',
'7.2. Владельцы таких ресурсов имеют техническую возможность собирать информацию о Пользователях и самостоятельно определяют условия её использования.',
'7.3. При переходе на сторонние ресурсы Пользователи самостоятельно определяют пределы использования своей информации согласно правилам соответствующих ресурсов.',
],
},
{
title: '8. Информация, хранящаяся на стороне браузера',
items: [
'8.1. Администратор использует cookie-файлы для определения уникального идентификатора доступа Пользователя к Сайту.',
'8.2. Цели использования cookie:',
'— поддержка функциональности Сайта, требующей использования cookie;',
'— измерение аудитории Сайта;',
'— определение статистических предпочтений Пользователей;',
'— исследование корреляции статистических данных.',
'8.3. Пользователь может запретить использование cookie в настройках браузера, однако это может привести к частичной или полной потере функциональности Сайта.',
],
},
{
title: '9. Согласие на обработку персональных данных',
items: [
`9.1. Обработка персональных данных Пользователей осуществляется Администратором в соответствии с Политикой конфиденциальности, размещённой по адресу ${SITE_URL}/privacy.`,
'9.2. Передавая свои персональные данные при регистрации или заполнении форм на Сайте, Пользователь даёт согласие на их обработку Администратором.',
'9.3. Администратор обрабатывает следующие персональные данные: Ф. И. О., адрес электронной почты, номер телефона, IP-адрес, тип браузера, данные о действиях на Сайте.',
'9.4. Цели обработки персональных данных: обеспечение функционирования Сайта, оказание информационной поддержки, предоставление персонализированных сервисов, направление информационных сообщений.',
`9.5. Согласие на обработку персональных данных может быть отозвано Пользователем путём направления заявления на адрес электронной почты: ${STORE_EMAIL}.`,
],
},
{
title: '10. Изменение условий и расторжение соглашения',
items: [
'10.1. Соглашение может быть расторгнуто в любое время по инициативе любой из сторон. Администратор уведомляет о расторжении путём размещения информации на Сайте.',
`10.2. Пользователь может расторгнуть Соглашение, направив уведомление на адрес электронной почты: ${STORE_EMAIL}.`,
'10.3. Администратор вправе в одностороннем порядке изменять условия Соглашения. Новая редакция вступает в силу с момента размещения на Сайте.',
'10.4. Продолжение использования Сайта после изменения условий означает согласие Пользователя с новой редакцией. При несогласии Пользователь обязуется прекратить использование Сайта.',
],
},
{
title: '11. Информация об Администраторе',
items: [
`${OP_NAME}`,
`ИНН: ${OP_INN}`,
`ОГРН: ${OP_OGRN}`,
`Адрес: ${OP_ADDR}`,
`Телефон: +7 (900) 000-00-00`, // TODO: заменить на реальный номер телефона
`Email: ${STORE_EMAIL}`, // TODO: заменить на реальный email при настройке STORE_EMAIL
],
},
]
export function TermsPage() {
return (
<Box sx={{ maxWidth: 800, mx: 'auto', py: { xs: 3, md: 5 }, px: { xs: 2, md: 0 } }}>
<Typography
variant="h4"
component="h1"
gutterBottom
sx={{ fontWeight: 700, fontSize: { xs: '1.5rem', md: '2rem' } }}
>
Пользовательское соглашение
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 4, pb: 3, borderBottom: '1px solid', borderColor: 'divider' }}
>
Последнее обновление: 19 мая 2026 г.
</Typography>
<Box component="section" sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{sections.map((section) => (
<Paper
key={section.title}
variant="outlined"
sx={{
p: { xs: 2, md: 3 },
borderRadius: 3,
bgcolor: 'background.paper',
borderLeft: '4px solid',
borderLeftColor: 'primary.main',
}}
>
<Typography
variant="h6"
component="h2"
gutterBottom
sx={{ fontWeight: 600, fontSize: '1rem', color: 'primary.main' }}
>
{section.title}
</Typography>
<Box component="ul" sx={{ m: 0, p: 0, listStyle: 'none' }}>
{section.items.map((item, idx) => (
<Box
component="li"
key={idx}
sx={{
color: 'text.primary',
fontSize: '0.9rem',
lineHeight: 1.65,
mb: 0.5,
'&:last-child': { mb: 0 },
}}
>
{item}
</Box>
))}
</Box>
</Paper>
))}
</Box>
</Box>
)
}
@@ -10,10 +10,10 @@ type Props = {
}
const SCHEMES: { key: ColorScheme; color: string; label: string; icon: React.ReactNode }[] = [
{ 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} /> },
{ key: 'craft', color: '#546E7A', label: 'Крафт', icon: <Hammer size={16} /> },
{ key: 'forest', color: '#2E8B57', label: 'Лес', icon: <Trees size={16} /> },
{ key: 'ocean', color: '#20B2AA', label: 'Океан', icon: <WavesHorizontal size={16} /> },
{ key: 'berry', color: '#8A2BE2', label: 'Ягоды', icon: <Cherry size={16} /> },
]
export function SchemeSwitcher({ value, onChange, orientation = 'horizontal' }: Props) {
@@ -53,7 +53,15 @@ export function ReviewsBlock() {
)}
{q.isError && <Alert severity="error">Не удалось загрузить отзывы.</Alert>}
{!q.isLoading && !q.isError && q.data && items.length === 0 && (
<Typography color="text.secondary">Пока нет опубликованных отзывов о товарах.</Typography>
<Box sx={{ py: 4 }}>
<Typography variant="h6" color="text.secondary" sx={{ mb: 2 }}>
Отзывов пока нет
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 400 }}>
Будьте первым, кто оставит отзыв о наших товарах. Ваше мнение поможет другим покупателям сделать правильный
выбор.
</Typography>
</Box>
)}
{items.length > 0 && (
<Stack spacing={2}>
@@ -71,8 +79,34 @@ export function ReviewsBlock() {
...(zebra ? { bgcolor: 'action.hover' } : {}),
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<Stack direction="row" spacing={1.5} sx={{ minWidth: { sm: 220 }, alignItems: 'center' }}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ alignItems: { sm: 'flex-start' } }}>
{r.imageUrl && (
<Box
sx={{
width: { xs: 80, sm: 80 },
height: { xs: 80, sm: 80 },
borderRadius: 1.5,
border: 1,
borderColor: 'divider',
overflow: 'hidden',
flexShrink: 0,
alignSelf: { xs: 'flex-start', sm: 'center' },
}}
>
<OptimizedImage
src={r.imageUrl}
alt="Фото к отзыву"
widths={[160, 320]}
sizes="80px"
sx={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
</Box>
)}
<Stack direction="row" spacing={1.5} sx={{ minWidth: { sm: 200 }, alignItems: 'center' }}>
<Avatar sx={{ bgcolor: 'primary.main', color: 'primary.contrastText', fontWeight: 800 }}>
{initials(r.authorDisplay)}
</Avatar>
@@ -111,31 +145,6 @@ export function ReviewsBlock() {
<RichTextMessageContent value={text} tone="review" />
</Box>
</Stack>
{r.imageUrl && (
<Box
sx={{
mt: 1.5,
width: 120,
height: 120,
borderRadius: 1.5,
border: 1,
borderColor: 'divider',
overflow: 'hidden',
}}
>
<OptimizedImage
src={r.imageUrl}
alt="Фото к отзыву"
widths={[320, 640]}
sizes="120px"
sx={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
</Box>
)}
</Paper>
)
})}