base commit
This commit is contained in:
@@ -1,9 +1,32 @@
|
||||
import type { Category, Product } from '@/entities/product/model/types'
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
|
||||
export async function fetchPublicProducts(categorySlug?: string): Promise<Product[]> {
|
||||
const { data } = await apiClient.get<Product[]>('products', {
|
||||
params: categorySlug ? { categorySlug } : undefined,
|
||||
export type PublicProductsResponse = {
|
||||
items: Product[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export async function fetchPublicProducts(params?: {
|
||||
categorySlug?: string
|
||||
q?: string
|
||||
sort?: 'price_asc' | 'price_desc' | ''
|
||||
page?: number
|
||||
pageSize?: number
|
||||
priceMinCents?: number
|
||||
priceMaxCents?: number
|
||||
}): Promise<PublicProductsResponse> {
|
||||
const { data } = await apiClient.get<PublicProductsResponse>('products', {
|
||||
params: {
|
||||
categorySlug: params?.categorySlug || undefined,
|
||||
q: params?.q || undefined,
|
||||
sort: params?.sort || undefined,
|
||||
page: params?.page || undefined,
|
||||
pageSize: params?.pageSize || undefined,
|
||||
priceMin: params?.priceMinCents ?? undefined,
|
||||
priceMax: params?.priceMaxCents ?? undefined,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -32,6 +55,8 @@ export async function createProduct(
|
||||
slug?: string
|
||||
shortDescription?: string | null
|
||||
description?: string | null
|
||||
quantity?: number | null
|
||||
materials?: string[]
|
||||
priceCents: number
|
||||
imageUrl?: string | null
|
||||
imageUrls?: string[]
|
||||
@@ -55,6 +80,8 @@ export async function updateProduct(
|
||||
slug: string
|
||||
shortDescription: string | null
|
||||
description: string | null
|
||||
quantity: number | null
|
||||
materials: string[]
|
||||
priceCents: number
|
||||
imageUrl: string | null
|
||||
imageUrls: string[]
|
||||
|
||||
@@ -11,6 +11,8 @@ export type Product = {
|
||||
slug: string
|
||||
shortDescription: string | null
|
||||
description: string | null
|
||||
quantity?: number | null
|
||||
materials?: string[]
|
||||
priceCents: number
|
||||
imageUrl: string | null
|
||||
imageUrls?: string[] // legacy-friendly (used only in admin payloads)
|
||||
|
||||
@@ -14,16 +14,22 @@ 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 }
|
||||
type Props = { product: Product; mediaHeight?: number }
|
||||
|
||||
export function ProductCard({ product }: Props) {
|
||||
export function ProductCard({ product, mediaHeight = 200 }: 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 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 materials = (product.materials ?? []).slice(0, 3)
|
||||
const moreMaterials = Math.max(0, (product.materials?.length ?? 0) - materials.length)
|
||||
|
||||
const onMouseMove = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!swiperRef.current) return
|
||||
if (imageUrls.length <= 1) return
|
||||
@@ -63,13 +69,13 @@ export function ProductCard({ product }: Props) {
|
||||
sx={{ display: 'block' }}
|
||||
>
|
||||
{imageUrls.length ? (
|
||||
<Box onMouseMove={onMouseMove} sx={{ height: 200 }}>
|
||||
<Box onMouseMove={onMouseMove} sx={{ height: mediaHeight }}>
|
||||
<Swiper
|
||||
onSwiper={(s) => {
|
||||
swiperRef.current = s
|
||||
}}
|
||||
allowTouchMove={false}
|
||||
style={{ width: '100%', height: 200 }}
|
||||
style={{ width: '100%', height: mediaHeight }}
|
||||
>
|
||||
{imageUrls.map((url) => (
|
||||
<SwiperSlide key={url}>
|
||||
@@ -80,7 +86,7 @@ export function ProductCard({ product }: Props) {
|
||||
className="product-card__media"
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 200,
|
||||
height: mediaHeight,
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
transition: 'transform 240ms ease',
|
||||
@@ -118,6 +124,14 @@ export function ProductCard({ product }: Props) {
|
||||
>
|
||||
{product.title}
|
||||
</Typography>
|
||||
{(product.materials?.length ?? 0) > 0 && (
|
||||
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
|
||||
{materials.map((m) => (
|
||||
<Chip key={m} label={m} size="small" variant="outlined" />
|
||||
))}
|
||||
{moreMaterials > 0 && <Chip label={`+${moreMaterials}`} size="small" variant="outlined" />}
|
||||
</Stack>
|
||||
)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { AdminUser } from '@/entities/user/model/types'
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
|
||||
export type AdminUsersListResponse = {
|
||||
items: AdminUser[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export async function fetchAdminUsers(
|
||||
token: string,
|
||||
params?: { q?: string; page?: number; pageSize?: number },
|
||||
): Promise<AdminUsersListResponse> {
|
||||
const { data } = await apiClient.get<AdminUsersListResponse>('admin/users', {
|
||||
params,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createAdminUser(
|
||||
token: string,
|
||||
body: { email: string; name?: string | null; password?: string },
|
||||
): Promise<AdminUser> {
|
||||
const { data } = await apiClient.post<AdminUser>('admin/users', body, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function updateAdminUser(
|
||||
token: string,
|
||||
id: string,
|
||||
body: Partial<{ email: string; name: string | null; password: string }>,
|
||||
): Promise<AdminUser> {
|
||||
const { data } = await apiClient.patch<AdminUser>(`admin/users/${id}`, body, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function deleteAdminUser(token: string, id: string): Promise<void> {
|
||||
await apiClient.delete(`admin/users/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type AdminUser = {
|
||||
id: string
|
||||
email: string
|
||||
name: string | null
|
||||
hasPassword: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
Reference in New Issue
Block a user