base commit

This commit is contained in:
@kirill.komarov
2026-04-29 17:32:21 +05:00
parent 3f7fdb1e15
commit f6b6959268
16 changed files with 1251 additions and 48 deletions
+237 -30
View File
@@ -1,13 +1,20 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Collapse from '@mui/material/Collapse'
import FormControl from '@mui/material/FormControl'
import Grid from '@mui/material/Grid'
import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Pagination from '@mui/material/Pagination'
import Select from '@mui/material/Select'
import type { SelectChangeEvent } from '@mui/material/Select'
import Skeleton from '@mui/material/Skeleton'
import Stack from '@mui/material/Stack'
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 { useQuery } from '@tanstack/react-query'
import { fetchCategories, fetchPublicProducts } from '@/entities/product/api/product-api'
@@ -15,19 +22,79 @@ import { ProductCard } from '@/entities/product/ui/ProductCard'
export function HomePage() {
const [categorySlug, setCategorySlug] = useState<string>('')
const [qInput, setQInput] = useState('')
const [q, setQ] = useState('')
const [moreOpen, setMoreOpen] = useState(false)
const [sort, setSort] = useState<'price_asc' | 'price_desc' | ''>('')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(12)
const [priceMinRub, setPriceMinRub] = useState('')
const [priceMaxRub, setPriceMaxRub] = useState('')
const [cardScale, setCardScale] = useState<70 | 90 | 110 | 130>(90)
const categoriesQuery = useQuery({
queryKey: ['categories'],
queryFn: () => fetchCategories(),
})
useEffect(() => {
const t = window.setTimeout(() => {
setQ(qInput.trim())
setPage(1)
}, 250)
return () => window.clearTimeout(t)
}, [qInput])
const productsQuery = useQuery({
queryKey: ['products', 'public', categorySlug || 'all'],
queryFn: () => fetchPublicProducts(categorySlug || undefined),
queryKey: [
'products',
'public',
{
categorySlug: categorySlug || 'all',
q,
sort,
page,
pageSize,
priceMinRub,
priceMaxRub,
},
],
queryFn: () => {
const toCents = (v: string) => {
const n = Number(String(v).trim().replace(',', '.'))
return Number.isFinite(n) && n >= 0 ? Math.round(n * 100) : undefined
}
return fetchPublicProducts({
categorySlug: categorySlug || undefined,
q: q || undefined,
sort: sort || '',
page,
pageSize,
priceMinCents: toCents(priceMinRub),
priceMaxCents: toCents(priceMaxRub),
})
},
})
const handleCategoryChange = (e: SelectChangeEvent<string>) => {
setCategorySlug(e.target.value)
setPage(1)
}
const handleSortChange = (e: SelectChangeEvent<string>) => {
const v = e.target.value
if (v === '' || v === 'price_asc' || v === 'price_desc') {
setSort(v)
setPage(1)
}
}
const handlePageSizeChange = (e: SelectChangeEvent<string>) => {
const n = Number(e.target.value)
if (Number.isFinite(n) && n > 0) {
setPageSize(n)
setPage(1)
}
}
const title = useMemo(
@@ -36,6 +103,11 @@ export function HomePage() {
[categorySlug, categoriesQuery.data],
)
const products = productsQuery.data?.items ?? []
const total = productsQuery.data?.total ?? 0
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const mediaHeight = Math.round(200 * (cardScale / 100))
return (
<Box>
<Typography variant="h4" component="h1" gutterBottom>
@@ -45,25 +117,146 @@ export function HomePage() {
Игрушки, сувениры и другие изделия ручной работы.
</Typography>
<FormControl sx={{ minWidth: 220, mb: 3 }} size="small">
<InputLabel id="category-filter-label">Категория</InputLabel>
<Select<string>
labelId="category-filter-label"
label="Категория"
value={categorySlug}
onChange={handleCategoryChange}
disabled={categoriesQuery.isLoading}
<Stack spacing={2} sx={{ mb: 3 }}>
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={2}
alignItems={{ md: 'center' }}
sx={{ flexWrap: { md: 'wrap' } }}
>
<MenuItem value="">
<em>Все</em>
</MenuItem>
{(categoriesQuery.data ?? []).map((c) => (
<MenuItem key={c.id} value={c.slug}>
{c.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ minWidth: 220 }} size="small">
<InputLabel id="category-filter-label">Категория</InputLabel>
<Select<string>
labelId="category-filter-label"
label="Категория"
value={categorySlug}
onChange={handleCategoryChange}
disabled={categoriesQuery.isLoading}
>
<MenuItem value="">
<em>Все</em>
</MenuItem>
{(categoriesQuery.data ?? []).map((c) => (
<MenuItem key={c.id} value={c.slug}>
{c.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
size="small"
label="Поиск"
value={qInput}
onChange={(e) => setQInput(e.target.value)}
sx={{ flexGrow: 1, minWidth: { xs: '100%', md: 360 } }}
/>
</Stack>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={1.5}
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={() => {
setCategorySlug('')
setQInput('')
setSort('')
setPriceMinRub('')
setPriceMaxRub('')
setPageSize(12)
setCardScale(90)
setMoreOpen(false)
}}
sx={{ alignSelf: { xs: 'flex-start' } }}
>
Сбросить
</Button>
</Stack>
<Collapse in={moreOpen} unmountOnExit>
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={2}
alignItems={{ md: 'center' }}
sx={{ mt: 2, 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>
<TextField
size="small"
label="Цена от, ₽"
value={priceMinRub}
onChange={(e) => {
setPriceMinRub(e.target.value)
setPage(1)
}}
sx={{ width: { xs: '100%', md: 180 } }}
/>
<TextField
size="small"
label="Цена до, ₽"
value={priceMaxRub}
onChange={(e) => {
setPriceMaxRub(e.target.value)
setPage(1)
}}
sx={{ width: { xs: '100%', md: 180 } }}
/>
<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>
<Box sx={{ flexGrow: 1, minWidth: { xs: '100%', md: 260 } }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
Масштаб карточек
</Typography>
<ToggleButtonGroup
exclusive
size="small"
value={cardScale}
onChange={(_, v) => {
if (v === 70 || v === 90 || v === 110 || v === 130) setCardScale(v)
}}
>
<ToggleButton value={70}>S</ToggleButton>
<ToggleButton value={90}>M</ToggleButton>
<ToggleButton value={110}>L</ToggleButton>
<ToggleButton value={130}>XL</ToggleButton>
</ToggleButtonGroup>
</Box>
</Stack>
</Collapse>
</Stack>
{productsQuery.isLoading && (
<Grid container spacing={2}>
@@ -79,18 +272,32 @@ export function HomePage() {
<Alert severity="error">Не удалось загрузить товары. Проверьте, что API запущен.</Alert>
)}
{productsQuery.isSuccess && productsQuery.data.length === 0 && (
{productsQuery.isSuccess && products.length === 0 && (
<Typography color="text.secondary">Пока нет опубликованных товаров.</Typography>
)}
{productsQuery.isSuccess && productsQuery.data.length > 0 && (
<Grid container spacing={2}>
{productsQuery.data.map((p) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={p.id}>
<ProductCard product={p} />
</Grid>
))}
</Grid>
{productsQuery.isSuccess && products.length > 0 && (
<>
<Grid container spacing={2}>
{products.map((p) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={p.id}>
<ProductCard product={p} mediaHeight={mediaHeight} />
</Grid>
))}
</Grid>
<Stack direction="row" justifyContent="center" sx={{ mt: 3 }}>
<Pagination
page={page}
count={totalPages}
onChange={(_, v) => setPage(v)}
color="primary"
shape="rounded"
showFirstButton
showLastButton
/>
</Stack>
</>
)}
</Box>
)