This commit is contained in:
@kirill.komarov
2026-05-13 12:33:46 +05:00
parent c6228dfaab
commit 40483679de
3 changed files with 150 additions and 27 deletions
+3 -3
View File
@@ -41,6 +41,9 @@ export function MainLayout({ children }: PropsWithChildren) {
<Link component={RouterLink} to="/" color="inherit" underline="hover" variant="body2"> <Link component={RouterLink} to="/" color="inherit" underline="hover" variant="body2">
Каталог Каталог
</Link> </Link>
<Link component={RouterLink} to="/privacy" color="inherit" underline="hover" variant="body2">
Политика конфиденциальности
</Link>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Изделия ручной работы: вещи с характером и вниманием к деталям. Изделия ручной работы: вещи с характером и вниманием к деталям.
</Typography> </Typography>
@@ -60,9 +63,6 @@ export function MainLayout({ children }: PropsWithChildren) {
<Link component={RouterLink} to="/about" color="inherit" underline="hover" variant="body2"> <Link component={RouterLink} to="/about" color="inherit" underline="hover" variant="body2">
О нас О нас
</Link> </Link>
<Link component={RouterLink} to="/privacy" color="inherit" underline="hover" variant="body2">
Политика конфиденциальности
</Link>
</Stack> </Stack>
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 4 }}> <Grid size={{ xs: 12, sm: 4 }}>
+13 -14
View File
@@ -1,14 +1,13 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { useMemo, useRef } from 'react' import { useCallback, useMemo, useRef } from 'react'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Card from '@mui/material/Card' import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent' import CardContent from '@mui/material/CardContent'
import CardMedia from '@mui/material/CardMedia' import CardMedia from '@mui/material/CardMedia'
import Chip from '@mui/material/Chip' import Chip from '@mui/material/Chip'
import Link from '@mui/material/Link'
import Stack from '@mui/material/Stack' import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { Link as RouterLink } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Swiper, SwiperSlide } from 'swiper/react' import { Swiper, SwiperSlide } from 'swiper/react'
import 'swiper/css' import 'swiper/css'
import type { Product } from '@/entities/product/model/types' import type { Product } from '@/entities/product/model/types'
@@ -18,6 +17,7 @@ import type { Swiper as SwiperType } from 'swiper/types'
type Props = { product: Product; mediaHeight?: number; actions?: ReactNode } type Props = { product: Product; mediaHeight?: number; actions?: ReactNode }
export function ProductCard({ product, mediaHeight = 200, actions }: Props) { export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
const navigate = useNavigate()
const swiperRef = useRef<SwiperType | null>(null) const swiperRef = useRef<SwiperType | null>(null)
const imageUrls = useMemo(() => { const imageUrls = useMemo(() => {
const fromImages = (product.images ?? []) const fromImages = (product.images ?? [])
@@ -40,6 +40,10 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
swiperRef.current.slideTo(idx, 0) swiperRef.current.slideTo(idx, 0)
} }
const goToProduct = useCallback(() => {
navigate(`/products/${product.id}`)
}, [navigate, product.id])
const stockLabel = const stockLabel =
product.inStock && product.quantity === 0 product.inStock && product.quantity === 0
? { label: 'Нет в наличии', color: 'default' as const } ? { label: 'Нет в наличии', color: 'default' as const }
@@ -49,7 +53,9 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
return ( return (
<Card <Card
onClick={goToProduct}
sx={{ sx={{
cursor: 'pointer',
height: '100%', height: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -64,6 +70,7 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
boxShadow: '0 8px 30px rgba(0,0,0,0.10)', boxShadow: '0 8px 30px rgba(0,0,0,0.10)',
}, },
'&:hover .product-card__media': { transform: 'scale(1.06)' }, '&:hover .product-card__media': { transform: 'scale(1.06)' },
'&:hover .product-card__title': { color: 'primary.main' },
'@media (prefers-reduced-motion: reduce)': { '@media (prefers-reduced-motion: reduce)': {
transition: 'none', transition: 'none',
'&:hover': { transform: 'none' }, '&:hover': { transform: 'none' },
@@ -71,13 +78,7 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
}, },
}} }}
> >
<Link <Box sx={{ position: 'relative' }}>
component={RouterLink}
to={`/products/${product.id}`}
underline="none"
color="inherit"
sx={{ display: 'block', position: 'relative' }}
>
{imageUrls.length ? ( {imageUrls.length ? (
<Box onMouseMove={onMouseMove} sx={{ height: mediaHeight, overflow: 'hidden' }}> <Box onMouseMove={onMouseMove} sx={{ height: mediaHeight, overflow: 'hidden' }}>
<Swiper <Swiper
@@ -144,7 +145,7 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
}} }}
/> />
)} )}
</Link> </Box>
<CardContent sx={{ flexGrow: 1, p: 2, '&:last-child': { pb: 2 } }}> <CardContent sx={{ flexGrow: 1, p: 2, '&:last-child': { pb: 2 } }}>
<Stack spacing={1.25}> <Stack spacing={1.25}>
@@ -159,15 +160,13 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component={RouterLink} className="product-card__title"
to={`/products/${product.id}`}
sx={{ sx={{
textDecoration: 'none', textDecoration: 'none',
color: 'text.primary', color: 'text.primary',
fontWeight: 600, fontWeight: 600,
lineHeight: 1.3, lineHeight: 1.3,
transition: 'color 150ms ease', transition: 'color 150ms ease',
'&:hover': { color: 'primary.main' },
}} }}
> >
{product.title} {product.title}
@@ -1,22 +1,146 @@
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { STORE_EMAIL, STORE_PUBLIC_SITE_URL } from '@/shared/config' import { STORE_EMAIL } from '@/shared/config'
import template from '../content/privacy-policy.template.txt?raw'
const OP_NAME = 'Индивидуальный предприниматель Новоселова Наталия Владимировна'
const OP_INN = '402900832341'
const OP_OGRN = '305402922700051'
const OP_ADDR = '248000, Россия, г. Калуга, ул. Никитина, д. 12А'
const SITE_URL = window.location.origin
const sections = [
{
title: '1. Общие положения',
items: [
`1.1. Настоящая Политика конфиденциальности (далее — Политика) действует в отношении всех персональных данных, которые ${OP_NAME} (далее — Оператор) может получить от Пользователя во время использования сайта ${SITE_URL}.`,
`1.2. ИНН Оператора: ${OP_INN}`,
`1.3. ОГРН/ОГРНИП Оператора: ${OP_OGRN}`,
`1.4. Адрес Оператора: ${OP_ADDR}`,
`1.5. Контактный email: ${STORE_EMAIL}`,
],
},
{
title: '2. Персональные данные, которые обрабатывает Оператор',
items: [
'2.1. Оператор обрабатывает следующие персональные данные Пользователей:',
'— фамилия, имя, отчество;',
'— адрес электронной почты;',
'— номер телефона;',
'— данные файлов cookie;',
'— данные о действиях на сайте (аналитика);',
'— адрес доставки и геолокационные координаты.',
],
},
{
title: '3. Цели обработки персональных данных',
items: [
'3.1. Оператор обрабатывает персональные данные в следующих целях:',
'— идентификация Пользователя;',
'— оказание услуг / продажа товаров;',
'— направление уведомлений и информационных сообщений;',
'— улучшение качества работы сайта;',
'— построение персонализированных предложений и рекомендаций.',
],
},
{
title: '4. Правовые основания обработки',
items: [
'4.1. Оператор обрабатывает персональные данные на основании:',
'— Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных»;',
'— согласия субъекта персональных данных;',
'— договора, стороной которого является субъект;',
'— договорных отношений с контрагентами и исполнителями услуг (доставка, платёжные сервисы).',
],
},
{
title: '5. Порядок и условия обработки',
items: [
'5.1. Обработка осуществляется путём сбора, записи, систематизации, накопления, хранения, уточнения, извлечения, использования, передачи, обезличивания, блокирования, удаления и уничтожения персональных данных.',
'5.2. Обработка осуществляется автоматизированным и неавтоматизированным способами.',
'5.3. Срок хранения персональных данных: не более 7 лет с момента последнего обращения Пользователя либо до момента отзыва согласия на обработку.',
],
},
{
title: '6. Передача персональных данных третьим лицам',
items: [
'6.1. Оператор может передать персональные данные третьим лицам в следующих случаях:',
'— с согласия субъекта;',
'— по требованию законодательства РФ;',
'— для выполнения договорных обязательств (перечень третьих лиц): службы доставки, платёжные агрегаторы, сервисы аналитики (Яндекс.Метрика).',
],
},
{
title: '7. Права субъекта персональных данных',
items: [
'7.1. Пользователь имеет право на доступ к своим данным, их уточнение, блокирование или уничтожение.',
'7.2. Для реализации своих прав Пользователь может направить запрос на электронный адрес: ' + STORE_EMAIL,
],
},
]
export function PrivacyPolicyPage() { export function PrivacyPolicyPage() {
const body = template.replaceAll('{{SITE_URL}}', STORE_PUBLIC_SITE_URL).replaceAll('{{STORE_EMAIL}}', STORE_EMAIL)
return ( return (
<Box> <Box sx={{ maxWidth: 800, mx: 'auto', py: { xs: 3, md: 5 }, px: { xs: 2, md: 0 } }}>
<Typography variant="h4" gutterBottom> <Typography
variant="h4"
component="h1"
gutterBottom
sx={{ fontWeight: 700, fontSize: { xs: '1.5rem', md: '2rem' } }}
>
Политика конфиденциальности Политика конфиденциальности
</Typography> </Typography>
<Typography color="text.secondary" sx={{ mb: 2 }}>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 4, pb: 3, borderBottom: '1px solid', borderColor: 'divider' }}
>
Политика в отношении обработки персональных данных. Политика в отношении обработки персональных данных.
</Typography> </Typography>
<Typography component="div" variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
{body} <Box component="section" sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
</Typography> {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> </Box>
) )
} }