feat(F-058): completed feature
This commit is contained in:
@@ -157,9 +157,9 @@ export default function ProductsPage() {
|
||||
>
|
||||
<td className="px-4 py-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
{p.imageUrl ? (
|
||||
{p.images?.[0]?.url ? (
|
||||
<img
|
||||
src={p.imageUrl.replace('/uploads/', '/uploads/40/')}
|
||||
src={p.images[0].url.replace('/uploads/', '/uploads/40/')}
|
||||
alt={p.name}
|
||||
className="w-10 h-10 rounded-lg object-cover bg-gray-100 flex-shrink-0"
|
||||
/>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -203,7 +203,13 @@ export async function registerCatalogRoutes(
|
||||
const limit = parseInt((request.query as { limit?: string }).limit ?? '20', 10);
|
||||
const offset = parseInt((request.query as { offset?: string }).offset ?? '0', 10);
|
||||
const result = await repository.listAll({ limit, offset, q });
|
||||
return reply.send({ items: result.items.map((p) => serializeProduct(p)), total: result.total });
|
||||
const productIds = result.items.map((p) => p.id);
|
||||
const allImages = await images.listByProductIds(productIds);
|
||||
const imagesByProductId = groupImagesByProductId(allImages);
|
||||
return reply.send({
|
||||
items: result.items.map((p) => serializeProduct(p, imagesByProductId.get(p.id) ?? [])),
|
||||
total: result.total,
|
||||
});
|
||||
});
|
||||
|
||||
const publicProductSchema: FastifySchema = {
|
||||
@@ -270,6 +276,9 @@ export async function registerCatalogRoutes(
|
||||
const input = parseJson(searchQuerySchema, request.query);
|
||||
const startedAt = performance.now();
|
||||
const items = await searchProducts.execute(input);
|
||||
const productIds = items.map((p) => p.id);
|
||||
const allImages = await images.listByProductIds(productIds);
|
||||
const imagesByProductId = groupImagesByProductId(allImages);
|
||||
deps.logger?.info(
|
||||
{
|
||||
event: 'catalog_search',
|
||||
@@ -284,7 +293,11 @@ export async function registerCatalogRoutes(
|
||||
},
|
||||
'catalog search completed',
|
||||
);
|
||||
return reply.send({ items: items.map((product) => serializeProduct(product)) });
|
||||
return reply.send({
|
||||
items: items.map((product) =>
|
||||
serializeProduct(product, imagesByProductId.get(product.id) ?? []),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
const suggestSchema: FastifySchema = {
|
||||
@@ -665,6 +678,19 @@ function serializeImage(image: ProductImage) {
|
||||
};
|
||||
}
|
||||
|
||||
function groupImagesByProductId(allImages: ProductImage[]): Map<string, ProductImage[]> {
|
||||
const map = new Map<string, ProductImage[]>();
|
||||
for (const image of allImages) {
|
||||
const list = map.get(image.productId);
|
||||
if (list) {
|
||||
list.push(image);
|
||||
} else {
|
||||
map.set(image.productId, [image]);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function serializeVariant(variant: ProductVariant) {
|
||||
return {
|
||||
id: variant.id,
|
||||
|
||||
@@ -49,6 +49,11 @@ export interface ProductRichDataRepository {
|
||||
|
||||
export interface ProductImageRepository {
|
||||
listByProductId(productId: string, variantId?: string | null): Promise<ProductImage[]>;
|
||||
/**
|
||||
* Fetch images for many products in a single query. Returns a flat array;
|
||||
* the caller groups by productId when needed.
|
||||
*/
|
||||
listByProductIds(productIds: readonly string[]): Promise<ProductImage[]>;
|
||||
attach(productId: string, input: NewProductImage): Promise<ProductImage>;
|
||||
detach(productId: string, imageId: string): Promise<boolean>;
|
||||
reorder(productId: string, items: readonly ProductImageOrderItem[]): Promise<ProductImage[]>;
|
||||
|
||||
@@ -41,6 +41,17 @@ export class PgProductImageRepository implements ProductImageRepository {
|
||||
return result.rows.map(toImage);
|
||||
}
|
||||
|
||||
async listByProductIds(productIds: readonly string[]): Promise<ProductImage[]> {
|
||||
if (productIds.length === 0) return [];
|
||||
const result = await this.pool.query<ImageRow>(
|
||||
`SELECT * FROM catalog_product_images
|
||||
WHERE product_id = ANY($1::uuid[])
|
||||
ORDER BY product_id ASC, position ASC, created_at ASC, id ASC`,
|
||||
[productIds as string[]],
|
||||
);
|
||||
return result.rows.map(toImage);
|
||||
}
|
||||
|
||||
async attach(productId: string, input: NewProductImage): Promise<ProductImage> {
|
||||
try {
|
||||
const result = await this.pool.query<ImageRow>(
|
||||
|
||||
@@ -105,6 +105,13 @@ class FakeImageRepository implements ProductImageRepository {
|
||||
.sort((a, b) => a.position - b.position || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
async listByProductIds(productIds: readonly string[]): Promise<ProductImage[]> {
|
||||
const set = new Set(productIds);
|
||||
return this.images
|
||||
.filter((item) => set.has(item.productId))
|
||||
.sort((a, b) => a.position - b.position || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
async attach(productId: string, input: NewProductImage): Promise<ProductImage> {
|
||||
const created = image({
|
||||
id: `img-${this.images.length + 1}`,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user