diff --git a/frontend/src/components/listings/ListingGridCard.tsx b/frontend/src/components/listings/ListingGridCard.tsx
index b475943d..3329e770 100644
--- a/frontend/src/components/listings/ListingGridCard.tsx
+++ b/frontend/src/components/listings/ListingGridCard.tsx
@@ -32,7 +32,11 @@ const ListingGridCard = (props: Props) => {
onMouseLeave={() => setHovering(false)}
onClick={() => navigate(`/item/${listingId}`)}
>
-
+ {listing?.image_url ? (
+
+ ) : (
+
+ )}
@@ -43,7 +47,7 @@ const ListingGridCard = (props: Props) => {
)}
-
+
{listing ? (
listing?.description && (
diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts
index 5b548b80..5d5ad739 100644
--- a/frontend/src/gen/api.ts
+++ b/frontend/src/gen/api.ts
@@ -478,6 +478,8 @@ export interface components {
description: string | null;
/** Child Ids */
child_ids: string[];
+ /** Image Url */
+ image_url: string | null;
};
/** NewListingRequest */
NewListingRequest: {
diff --git a/store/app/crud/artifacts.py b/store/app/crud/artifacts.py
index 8934a5ea..66721e01 100644
--- a/store/app/crud/artifacts.py
+++ b/store/app/crud/artifacts.py
@@ -49,6 +49,10 @@ def _crop_image(self, image: Image.Image, size: tuple[int, int]) -> io.BytesIO:
lower = upper + new_height
image_resized = image.crop((left, upper, right, lower))
+ # Resize the image to the desired size.
+ image_resized = image_resized.resize(size, resample=Image.Resampling.BICUBIC)
+
+ # Save the image to a byte stream.
image_resized.save(image_bytes, format="PNG", optimize=True, quality=settings.image.quality)
image_bytes.seek(0)
return image_bytes
@@ -167,6 +171,9 @@ async def remove_artifact(self, artifact: Artifact, user_id: str) -> None:
async def get_listing_artifacts(self, listing_id: str) -> list[Artifact]:
return await self._get_items_from_secondary_index("listing_id", listing_id, Artifact)
+ async def get_listings_artifacts(self, listing_ids: list[str]) -> list[list[Artifact]]:
+ return await self._get_items_from_secondary_index_batch("listing_id", listing_ids, Artifact)
+
async def edit_artifact(
self,
artifact_id: str,
diff --git a/store/app/crud/base.py b/store/app/crud/base.py
index e5bdce62..403b4755 100644
--- a/store/app/crud/base.py
+++ b/store/app/crud/base.py
@@ -7,7 +7,7 @@
from typing import Any, AsyncContextManager, BinaryIO, Callable, Literal, Self, TypeVar, overload
import aioboto3
-from boto3.dynamodb.conditions import Key
+from boto3.dynamodb.conditions import Attr, Key
from botocore.exceptions import ClientError
from types_aiobotocore_dynamodb.service_resource import DynamoDBServiceResource
from types_aiobotocore_s3.service_resource import S3ServiceResource
@@ -226,8 +226,14 @@ async def _get_item_batch(
chunk = item_ids[i : i + chunk_size]
keys = [{"id": item_id} for item_id in chunk]
response = await self.db.batch_get_item(RequestItems={TABLE_NAME: {"Keys": keys}})
+
+ # Maps the items to their IDs to return them in the correct order.
+ item_ids_to_items: dict[str, T] = {}
for item in response["Responses"][TABLE_NAME]:
- items.append(self._validate_item(item, item_class))
+ item_impl = self._validate_item(item, item_class)
+ item_ids_to_items[item_impl.id] = item_impl
+ items += [item_ids_to_items[item_id] for item_id in chunk]
+
return items
async def _get_items_from_secondary_index(
@@ -245,6 +251,40 @@ async def _get_items_from_secondary_index(
items = item_dict["Items"]
return [self._validate_item(item, item_class) for item in items]
+ async def _get_items_from_secondary_index_batch(
+ self,
+ secondary_index_name: str,
+ secondary_index_values: list[str],
+ item_class: type[T],
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
+ ) -> list[list[T]]:
+ items: list[list[T]] = []
+ table = await self.db.Table(TABLE_NAME)
+
+ for i in range(0, len(secondary_index_values), chunk_size):
+ chunk = secondary_index_values[i : i + chunk_size]
+ response = await table.scan(
+ IndexName=self.get_gsi_index_name(secondary_index_name),
+ FilterExpression=(Attr(secondary_index_name).is_in(chunk) & Attr("type").eq(item_class.__name__)),
+ )
+
+ # Maps the items to their IDs.
+ chunk_items = [self._validate_item(item, item_class) for item in response["Items"]]
+ chunk_ids_to_items: dict[str, list[T]] = {}
+ for item in chunk_items:
+ item_id = getattr(item, secondary_index_name)
+ if item_id in chunk_ids_to_items:
+ chunk_ids_to_items[item_id].append(item)
+ else:
+ chunk_ids_to_items[item_id] = [item]
+
+ print(chunk_ids_to_items)
+
+ # Adds the items to the list.
+ items += [chunk_ids_to_items.get(id, []) for id in chunk]
+
+ return items
+
@overload
async def _get_unique_item_from_secondary_index(
self,
diff --git a/store/app/routers/listings.py b/store/app/routers/listings.py
index 3ee970eb..37b009b8 100644
--- a/store/app/routers/listings.py
+++ b/store/app/routers/listings.py
@@ -8,7 +8,7 @@
from pydantic import BaseModel
from store.app.db import Crud
-from store.app.model import Listing, User
+from store.app.model import Listing, User, get_artifact_url
from store.app.routers.users import (
get_session_user_with_read_permission,
get_session_user_with_write_permission,
@@ -41,6 +41,7 @@ class ListingInfoResponse(BaseModel):
name: str
description: str | None
child_ids: list[str]
+ image_url: str | None
class GetBatchListingsResponse(BaseModel):
@@ -52,7 +53,10 @@ async def get_batch_listing_info(
crud: Annotated[Crud, Depends(Crud.get)],
ids: list[str] = Query(description="List of part ids"),
) -> GetBatchListingsResponse:
- listings = await crud._get_item_batch(ids, Listing)
+ listings, artifacts = await asyncio.gather(
+ crud._get_item_batch(ids, Listing),
+ crud.get_listings_artifacts(ids),
+ )
return GetBatchListingsResponse(
listings=[
ListingInfoResponse(
@@ -60,8 +64,16 @@ async def get_batch_listing_info(
name=listing.name,
description=listing.description,
child_ids=listing.child_ids,
+ image_url=next(
+ (
+ get_artifact_url(artifact.id, "image", "small")
+ for artifact in artifacts
+ if artifact.artifact_type == "image"
+ ),
+ None,
+ ),
)
- for listing in listings
+ for listing, artifacts in zip(listings, artifacts)
]
)