test commit

This commit is contained in:
Kirill
2026-05-19 11:25:23 +05:00
parent f8867f6457
commit 5adbe9baa7
81 changed files with 6549 additions and 3108 deletions
@@ -0,0 +1,108 @@
import type { Category, Product } from '@/entities/product/model/types'
import { apiClient } from '@/shared/api/client'
import { apiBaseURL } from '@/shared/config'
import { ADMIN_UPLOAD_IMAGE_MAX_BYTES, formatAdminImageMaxSizeHint } from '@/shared/constants/upload-limits'
export async function fetchAdminProducts(): Promise<Product[]> {
const { data } = await apiClient.get<Product[]>('admin/products')
return data
}
export async function createProduct(body: {
title: string
slug?: string
shortDescription?: string | null
description?: string | null
quantity: number
materials?: string[]
priceCents: number
imageUrl?: string | null
imageUrls?: string[]
published: boolean
categoryId: string
}): Promise<Product> {
const { data } = await apiClient.post<Product>('admin/products', body)
return data
}
export async function updateProduct(
id: string,
body: Partial<{
title: string
slug: string
shortDescription: string | null
description: string | null
quantity: number
materials: string[]
priceCents: number
imageUrl: string | null
imageUrls: string[]
published: boolean
categoryId: string
}>,
): Promise<Product> {
const { data } = await apiClient.patch<Product>(`admin/products/${id}`, body)
return data
}
export async function deleteProduct(id: string): Promise<void> {
await apiClient.delete(`admin/products/${id}`)
}
export async function createCategory(body: { name: string; slug?: string; sort?: number }): Promise<Category> {
const { data } = await apiClient.post<Category>('admin/categories', body)
return data
}
export async function fetchAdminCategories(): Promise<Category[]> {
const { data } = await apiClient.get<{ items: Category[] }>('admin/categories')
return data.items
}
export async function updateAdminCategory(
id: string,
body: Partial<{ name: string; slug: string; sort: number }>,
): Promise<Category> {
const { data } = await apiClient.patch<Category>(`admin/categories/${id}`, body)
return data
}
export async function deleteAdminCategory(id: string): Promise<void> {
await apiClient.delete(`admin/categories/${id}`)
}
/** FormData: не задавать Content-Type вручную (boundary задаёт браузер). */
export async function uploadAdminProductImages(files: FileList | readonly File[]): Promise<string[]> {
const list = Array.from(files)
for (const f of list) {
if (f.size > ADMIN_UPLOAD_IMAGE_MAX_BYTES) {
throw new Error(
`Файл «${f.name}» слишком большой (максимум ${formatAdminImageMaxSizeHint()} на одно изображение).`,
)
}
}
const fd = new FormData()
for (const f of list) {
fd.append('files', f, f.name)
}
const token = localStorage.getItem('craftshop_auth_token')
const base = apiBaseURL.replace(/\/$/, '')
const res = await fetch(`${base}/admin/uploads`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: fd,
})
const payload = (await res.json().catch(() => ({}))) as { urls?: string[]; error?: string }
if (!res.ok) {
if (res.status === 413) {
throw new Error(
'Сервер отклонил файл как слишком большой (413). На проде часто лимит nginx: добавьте client_max_body_size для /api/ (см. docs/nginx-upload-limit.md). Проверьте также MAX_UPLOAD_BODY_BYTES в .env на сервере.',
)
}
throw new Error(typeof payload.error === 'string' ? payload.error : `Ошибка загрузки (${res.status})`)
}
if (!Array.isArray(payload.urls)) {
throw new Error('Некорректный ответ сервера')
}
return payload.urls
}
@@ -1,7 +1,5 @@
import type { Category, Product } from '@/entities/product/model/types'
import { apiClient } from '@/shared/api/client'
import { apiBaseURL } from '@/shared/config'
import { ADMIN_UPLOAD_IMAGE_MAX_BYTES, formatAdminImageMaxSizeHint } from '@/shared/constants/upload-limits'
export type PublicProductsResponse = {
items: Product[]
@@ -42,107 +40,3 @@ export async function fetchCategories(): Promise<Category[]> {
const { data } = await apiClient.get<Category[]>('categories')
return data
}
export async function fetchAdminProducts(): Promise<Product[]> {
const { data } = await apiClient.get<Product[]>('admin/products')
return data
}
export async function createProduct(body: {
title: string
slug?: string
shortDescription?: string | null
description?: string | null
quantity: number
materials?: string[]
priceCents: number
imageUrl?: string | null
imageUrls?: string[]
published: boolean
categoryId: string
}): Promise<Product> {
const { data } = await apiClient.post<Product>('admin/products', body)
return data
}
export async function updateProduct(
id: string,
body: Partial<{
title: string
slug: string
shortDescription: string | null
description: string | null
quantity: number
materials: string[]
priceCents: number
imageUrl: string | null
imageUrls: string[]
published: boolean
categoryId: string
}>,
): Promise<Product> {
const { data } = await apiClient.patch<Product>(`admin/products/${id}`, body)
return data
}
export async function deleteProduct(id: string): Promise<void> {
await apiClient.delete(`admin/products/${id}`)
}
export async function createCategory(body: { name: string; slug?: string; sort?: number }): Promise<Category> {
const { data } = await apiClient.post<Category>('admin/categories', body)
return data
}
export async function fetchAdminCategories(): Promise<Category[]> {
const { data } = await apiClient.get<{ items: Category[] }>('admin/categories')
return data.items
}
export async function updateAdminCategory(
id: string,
body: Partial<{ name: string; slug: string; sort: number }>,
): Promise<Category> {
const { data } = await apiClient.patch<Category>(`admin/categories/${id}`, body)
return data
}
export async function deleteAdminCategory(id: string): Promise<void> {
await apiClient.delete(`admin/categories/${id}`)
}
/** FormData: не задавать Content-Type вручную (boundary задаёт браузер). */
export async function uploadAdminProductImages(files: FileList | readonly File[]): Promise<string[]> {
const list = Array.from(files)
for (const f of list) {
if (f.size > ADMIN_UPLOAD_IMAGE_MAX_BYTES) {
throw new Error(
`Файл «${f.name}» слишком большой (максимум ${formatAdminImageMaxSizeHint()} на одно изображение).`,
)
}
}
const fd = new FormData()
for (const f of list) {
fd.append('files', f, f.name)
}
const token = localStorage.getItem('craftshop_auth_token')
const base = apiBaseURL.replace(/\/$/, '')
const res = await fetch(`${base}/admin/uploads`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: fd,
})
const payload = (await res.json().catch(() => ({}))) as { urls?: string[]; error?: string }
if (!res.ok) {
if (res.status === 413) {
throw new Error(
'Сервер отклонил файл как слишком большой (413). На проде часто лимит nginx: добавьте client_max_body_size для /api/ (см. docs/nginx-upload-limit.md). Проверьте также MAX_UPLOAD_BODY_BYTES в .env на сервере.',
)
}
throw new Error(typeof payload.error === 'string' ? payload.error : `Ошибка загрузки (${res.status})`)
}
if (!Array.isArray(payload.urls)) {
throw new Error('Некорректный ответ сервера')
}
return payload.urls
}
@@ -1,68 +0,0 @@
import { apiClient } from '@/shared/api/client'
import { OTHER_UPLOAD_MAX_FILE_BYTES, formatOtherUploadMaxSizeHint } from '@/shared/constants/upload-limits'
export async function postProductReview(
productId: string,
body: { rating: number; text?: string | null; imageUrl?: string | null },
): Promise<void> {
await apiClient.post(`products/${productId}/reviews`, body)
}
export async function uploadReviewImage(file: File): Promise<{ url: string }> {
if (file.size > OTHER_UPLOAD_MAX_FILE_BYTES) {
throw new Error(`Файл «${file.name}» слишком большой (максимум ${formatOtherUploadMaxSizeHint()}).`)
}
const fd = new FormData()
fd.append('file', file, file.name)
const { data } = await apiClient.post<{ url: string }>('reviews/upload-image', fd)
return data
}
export type PublicReviewFeedItem = {
id: string
rating: number
text: string | null
imageUrl: string | null
createdAt: string
authorDisplay: string
productId: string
productTitle: string
}
export type PublicReviewsLatestResponse = {
items: PublicReviewFeedItem[]
}
export async function fetchLatestApprovedReviews(limit = 5): Promise<PublicReviewsLatestResponse> {
const { data } = await apiClient.get<PublicReviewsLatestResponse>('reviews/latest', {
params: { limit },
})
return data
}
export type PublicProductReviewItem = {
id: string
rating: number
text: string | null
imageUrl: string | null
createdAt: string
authorDisplay: string
}
export type PublicProductReviewsResponse = {
items: PublicProductReviewItem[]
total: number
page: number
pageSize: number
}
export async function fetchPublicProductReviews(
productId: string,
params?: { page?: number; pageSize?: number },
): Promise<PublicProductReviewsResponse> {
const { data } = await apiClient.get<PublicProductReviewsResponse>(`products/${productId}/reviews`, {
params: { page: params?.page, pageSize: params?.pageSize },
})
return data
}