Skip to content

Commit fb8029c

Browse files
committed
Adding is_favorited, changing response messages
1 parent 477f429 commit fb8029c

6 files changed

Lines changed: 41 additions & 49 deletions

File tree

backend/market/serializers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ class ListingSerializer(ListingTypeMixin, ModelSerializer):
133133
seller = UserSerializer(read_only=True)
134134
listing_type = SerializerMethodField()
135135
additional_data = SerializerMethodField()
136+
is_favorited = SerializerMethodField()
136137
external_link = URLField(required=False, allow_blank=True, allow_null=True)
137138
negotiable = BooleanField(required=False, default=True)
138139
expires_at = DateTimeField(required=False, allow_null=True)
@@ -155,6 +156,7 @@ class Meta:
155156
"images",
156157
"listing_type",
157158
"additional_data",
159+
"is_favorited",
158160
]
159161
read_only_fields = [
160162
"id",
@@ -196,6 +198,12 @@ def validate(self, attrs):
196198

197199
return super().validate(attrs)
198200

201+
def get_is_favorited(self, obj):
202+
request = self.context.get("request")
203+
if not request or not request.user or not request.user.is_authenticated:
204+
return False
205+
return request.user.listings_favorited.filter(id=obj.id).exists()
206+
199207
def validate_title(self, value):
200208
if self.contains_profanity(value):
201209
raise ValidationError("The title contains inappropriate language.")
@@ -327,6 +335,7 @@ def _update_sublet(self, instance, additional_data):
327335
class ListingSerializerPublic(ListingTypeMixin, ModelSerializer):
328336
buyer_count = SerializerMethodField()
329337
favorite_count = SerializerMethodField()
338+
is_favorited = SerializerMethodField()
330339
tags = SlugRelatedField(many=True, slug_field="name", queryset=Tag.objects.all())
331340
images = ListingImageURLSerializer(many=True)
332341
seller = UserSerializer(read_only=True)
@@ -350,6 +359,7 @@ class Meta:
350359
"favorite_count",
351360
"listing_type",
352361
"additional_data",
362+
"is_favorited",
353363
]
354364
read_only_fields = fields
355365

@@ -359,6 +369,12 @@ def get_buyer_count(self, obj):
359369
def get_favorite_count(self, obj):
360370
return obj.favorites.count()
361371

372+
def get_is_favorited(self, obj):
373+
request = self.context.get("request")
374+
if not request or not request.user or not request.user.is_authenticated:
375+
return False
376+
return request.user.listings_favorited.filter(id=obj.id).exists()
377+
362378

363379
# Read-only serializer for use when pulling all listings /etc
364380
class ListingSerializerList(ListingTypeMixin, ModelSerializer):

backend/market/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,25 +262,25 @@ def create(self, request, *args, **kwargs):
262262
favorites = request.user.listings_favorited
263263
if favorites.filter(id=listing_id).exists():
264264
return Response(
265-
{"favorited": True, "detail": "Favorite already exists"},
265+
{"liked": True, "detail": "User has already liked the listing"},
266266
status=status.HTTP_200_OK,
267267
)
268268
listing = get_object_or_404(Listing, id=listing_id)
269269
favorites.add(listing)
270-
return Response({"favorited": True}, status=status.HTTP_201_CREATED)
270+
return Response({"liked": True}, status=status.HTTP_201_CREATED)
271271

272272
def destroy(self, request, *args, **kwargs):
273273
listing_id = int(self.kwargs["listing_id"])
274274
listing = get_object_or_404(Listing, id=listing_id)
275275

276276
if listing not in request.user.listings_favorited.all():
277277
return Response(
278-
{"favorited": False, "detail": "Favorite does not exist"},
278+
{"liked": False, "detail": "User hasn't liked the listing yet"},
279279
status=status.HTTP_404_OK,
280280
)
281281

282282
request.user.listings_favorited.remove(listing)
283-
return Response({"favorited": False}, status=status.HTTP_200_OK)
283+
return Response({"liked": False}, status=status.HTTP_200_OK)
284284

285285

286286
class Offers(viewsets.ModelViewSet):

frontend/app/items/[id]/page.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { ListingDetail } from "@/components/listings/detail/ListingDetail";
2-
import { getListing, getUsersFavorites } from "@/lib/actions";
2+
import { getListing } from "@/lib/actions";
33

44
export default async function ItemPage({ params }: { params: Promise<{ id: string }> }) {
55
const { id } = await params;
66
const item = await getListing(id);
7-
const favorites = await getUsersFavorites().catch(() => null);
87

9-
return <ListingDetail listing={item} initialFavorites={favorites} />;
8+
return <ListingDetail listing={item} initialIsFavorited={item.is_favorited ?? false} />;
109
}

frontend/app/sublets/[id]/page.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { ListingDetail } from "@/components/listings/detail/ListingDetail";
2-
import { getListing, getUsersFavorites } from "@/lib/actions";
2+
import { getListing } from "@/lib/actions";
33

44
export default async function SubletPage({ params }: { params: Promise<{ id: string }> }) {
55
const { id } = await params;
66
const sublet = await getListing(id);
7-
const favorites = await getUsersFavorites().catch(() => null);
87

9-
return <ListingDetail listing={sublet} initialFavorites={favorites} />;
8+
return <ListingDetail listing={sublet} initialIsFavorited={sublet.is_favorited ?? false} />;
109
}

frontend/components/listings/detail/ListingDetail.tsx

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,33 @@
11
"use client";
22

3-
import { useMemo } from "react";
3+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4+
import { addToUsersFavorites, deleteFromUsersFavorites } from "@/lib/actions";
45
import { Heart, Share } from "lucide-react";
5-
import { Item, PaginatedResponse, Sublet } from "@/lib/types";
6+
import { Item, Sublet } from "@/lib/types";
67
import { ListingActions } from "@/components/listings/detail/ListingActions";
78
import { ListingImageGallery } from "@/components/listings/detail/ListingImageGallery";
89
import { ListingInfo } from "@/components/listings/detail/ListingInfo";
910
import { UserCard } from "@/components/listings/detail/UserCard";
1011
import { BackButton } from "@/components/listings/detail/BackButton";
11-
import { addToUsersFavorites, deleteFromUsersFavorites, getUsersFavorites } from "@/lib/actions";
12-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1312

1413
interface Props {
1514
listing: Item | Sublet;
16-
initialFavorites: PaginatedResponse<Item | Sublet> | null;
15+
initialIsFavorited: boolean;
1716
}
1817

19-
export const ListingDetail = ({ listing, initialFavorites }: Props) => {
18+
export const ListingDetail = ({ listing, initialIsFavorited }: Props) => {
2019
const listingType = listing.listing_type;
2120
const priceLabel = listingType === "sublet" ? "/mo" : undefined;
2221
const listingOwnerLabel = listingType === "item" ? "Seller" : "Owner";
2322
const queryClient = useQueryClient();
2423
const favoritesQuery = useQuery({
25-
queryKey: ["favorites"],
26-
queryFn: getUsersFavorites,
27-
initialData: initialFavorites ?? undefined,
28-
enabled: initialFavorites !== null,
24+
queryKey: ["favorite", listing.id],
25+
queryFn: async () => initialIsFavorited,
26+
initialData: initialIsFavorited,
27+
staleTime: Infinity,
2928
});
3029

31-
const isInsideFavorites = useMemo(
32-
() => !!favoritesQuery.data?.results?.some((favorite) => favorite.id === listing.id),
33-
[favoritesQuery.data, listing.id]
34-
);
30+
const isInsideFavorites = favoritesQuery.data ?? false;
3531

3632
const toggleFavoriteMutation = useMutation({
3733
mutationFn: async (shouldFavorite: boolean) => {
@@ -42,36 +38,16 @@ export const ListingDetail = ({ listing, initialFavorites }: Props) => {
4238
}
4339
},
4440
onMutate: async (shouldFavorite: boolean) => {
45-
await queryClient.cancelQueries({ queryKey: ["favorites"] });
46-
const previous = queryClient.getQueryData<PaginatedResponse<Item | Sublet>>(["favorites"]);
47-
48-
if (previous) {
49-
const exists = previous.results?.some((favorite) => favorite.id === listing.id);
50-
let results = previous.results ?? [];
51-
52-
if (shouldFavorite && !exists) {
53-
results = [...results, listing];
54-
}
55-
if (!shouldFavorite && exists) {
56-
results = results.filter((favorite) => favorite.id !== listing.id);
57-
}
58-
59-
queryClient.setQueryData<PaginatedResponse<Item | Sublet>>(["favorites"], {
60-
...previous,
61-
results,
62-
});
63-
}
64-
41+
await queryClient.cancelQueries({ queryKey: ["favorite", listing.id] });
42+
const previous = queryClient.getQueryData<boolean>(["favorite", listing.id]);
43+
queryClient.setQueryData(["favorite", listing.id], shouldFavorite);
6544
return { previous };
6645
},
6746
onError: (_error, _shouldFavorite, context) => {
68-
if (context?.previous) {
69-
queryClient.setQueryData(["favorites"], context.previous);
47+
if (context?.previous !== undefined) {
48+
queryClient.setQueryData(["favorite", listing.id], context.previous);
7049
}
7150
},
72-
onSettled: () => {
73-
queryClient.invalidateQueries({ queryKey: ["favorites"] });
74-
},
7551
});
7652

7753
const handleToggleFavorite = async () => {
@@ -86,6 +62,7 @@ export const ListingDetail = ({ listing, initialFavorites }: Props) => {
8662
<Share className="h-5 w-5" />
8763
<button
8864
type="button"
65+
className="cursor-pointer"
8966
onClick={handleToggleFavorite}
9067
aria-pressed={isInsideFavorites}
9168
aria-label={isInsideFavorites ? "Remove from favorites" : "Add to favorites"}

frontend/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type BaseListing = {
6161
images: string[];
6262
tags: string[];
6363
favorite_count: number;
64+
is_favorited?: boolean;
6465
seller: User;
6566
};
6667

0 commit comments

Comments
 (0)