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
+30 -3
View File
@@ -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)
+20 -6
View File
@@ -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"
+47
View File
@@ -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}` },
})
}
+8
View File
@@ -0,0 +1,8 @@
export type AdminUser = {
id: string
email: string
name: string | null
hasPassword: boolean
createdAt: string
updatedAt: string
}