base commit
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user