base commit
This commit is contained in:
@@ -8,6 +8,11 @@ export async function fetchPublicProducts(categorySlug?: string): Promise<Produc
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchPublicProduct(id: string): Promise<Product> {
|
||||
const { data } = await apiClient.get<Product>(`products/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchCategories(): Promise<Category[]> {
|
||||
const { data } = await apiClient.get<Category[]>('categories')
|
||||
return data
|
||||
@@ -25,10 +30,14 @@ export async function createProduct(
|
||||
body: {
|
||||
title: string
|
||||
slug?: string
|
||||
shortDescription?: string | null
|
||||
description?: string | null
|
||||
priceCents: number
|
||||
imageUrl?: string | null
|
||||
imageUrls?: string[]
|
||||
published: boolean
|
||||
inStock?: boolean
|
||||
leadTimeDays?: number | null
|
||||
categoryId: string
|
||||
},
|
||||
): Promise<Product> {
|
||||
@@ -44,10 +53,14 @@ export async function updateProduct(
|
||||
body: Partial<{
|
||||
title: string
|
||||
slug: string
|
||||
shortDescription: string | null
|
||||
description: string | null
|
||||
priceCents: number
|
||||
imageUrl: string | null
|
||||
imageUrls: string[]
|
||||
published: boolean
|
||||
inStock: boolean
|
||||
leadTimeDays: number | null
|
||||
categoryId: string
|
||||
}>,
|
||||
): Promise<Product> {
|
||||
|
||||
@@ -9,12 +9,17 @@ export type Product = {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
shortDescription: string | null
|
||||
description: string | null
|
||||
priceCents: number
|
||||
imageUrl: string | null
|
||||
imageUrls?: string[] // legacy-friendly (used only in admin payloads)
|
||||
published: boolean
|
||||
inStock: boolean
|
||||
leadTimeDays: number | null
|
||||
categoryId: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
category?: Category
|
||||
images?: { id: string; url: string; sort: number }[]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
import { useMemo, useRef } from 'react'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import CardMedia from '@mui/material/CardMedia'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Box from '@mui/material/Box'
|
||||
import Link from '@mui/material/Link'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import type { Swiper as SwiperType } from 'swiper/types'
|
||||
import 'swiper/css'
|
||||
import { Link as RouterLink } from 'react-router-dom'
|
||||
import type { Product } from '@/entities/product/model/types'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
|
||||
type Props = { product: Product }
|
||||
|
||||
export function ProductCard({ product }: Props) {
|
||||
const swiperRef = useRef<SwiperType | null>(null)
|
||||
const imageUrls = useMemo(() => {
|
||||
const fromImages = (product.images ?? []).slice().sort((a, b) => a.sort - b.sort).map((x) => x.url)
|
||||
const urls = fromImages.length ? fromImages : product.imageUrl ? [product.imageUrl] : []
|
||||
return urls
|
||||
}, [product.images, product.imageUrl])
|
||||
|
||||
const onMouseMove = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!swiperRef.current) return
|
||||
if (imageUrls.length <= 1) return
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const rel = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width))
|
||||
const idx = Math.min(imageUrls.length - 1, Math.floor(rel * imageUrls.length))
|
||||
swiperRef.current.slideTo(idx, 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
@@ -32,37 +55,67 @@ export function ProductCard({ product }: Props) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{product.imageUrl ? (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="200"
|
||||
image={product.imageUrl}
|
||||
alt={product.title}
|
||||
sx={{
|
||||
objectFit: 'cover',
|
||||
transition: 'transform 240ms ease',
|
||||
'@media (prefers-reduced-motion: reduce)': { transition: 'none' },
|
||||
}}
|
||||
className="product-card__media"
|
||||
/>
|
||||
) : (
|
||||
<CardMedia
|
||||
component="div"
|
||||
sx={{
|
||||
height: 200,
|
||||
bgcolor: 'grey.100',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Нет фото</Typography>
|
||||
</CardMedia>
|
||||
)}
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/products/${product.id}`}
|
||||
underline="none"
|
||||
color="inherit"
|
||||
sx={{ display: 'block' }}
|
||||
>
|
||||
{imageUrls.length ? (
|
||||
<Box onMouseMove={onMouseMove} sx={{ height: 200 }}>
|
||||
<Swiper
|
||||
onSwiper={(s) => {
|
||||
swiperRef.current = s
|
||||
}}
|
||||
allowTouchMove={false}
|
||||
style={{ width: '100%', height: 200 }}
|
||||
>
|
||||
{imageUrls.map((url) => (
|
||||
<SwiperSlide key={url}>
|
||||
<Box
|
||||
component="img"
|
||||
src={url}
|
||||
alt={product.title}
|
||||
className="product-card__media"
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 200,
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
transition: 'transform 240ms ease',
|
||||
'@media (prefers-reduced-motion: reduce)': { transition: 'none' },
|
||||
userSelect: 'none',
|
||||
}}
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</Box>
|
||||
) : (
|
||||
<CardMedia
|
||||
component="div"
|
||||
sx={{
|
||||
height: 200,
|
||||
bgcolor: 'grey.100',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Нет фото</Typography>
|
||||
</CardMedia>
|
||||
)}
|
||||
</Link>
|
||||
<CardContent sx={{ flexGrow: 1 }}>
|
||||
<Stack spacing={1}>
|
||||
{product.category && <Chip label={product.category.name} size="small" />}
|
||||
<Typography variant="h6" component="h2">
|
||||
<Typography
|
||||
variant="h6"
|
||||
component={RouterLink}
|
||||
to={`/products/${product.id}`}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
{product.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
@@ -76,7 +129,7 @@ export function ProductCard({ product }: Props) {
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{product.description ?? 'Описание появится позже.'}
|
||||
{product.shortDescription ?? 'Описание появится позже.'}
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPriceRub(product.priceCents)}
|
||||
|
||||
Reference in New Issue
Block a user