-
Notifications
You must be signed in to change notification settings - Fork 0
Favorites functionality #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
2a2ed16
18f80b4
dbe6d5b
165bee5
c9ba984
5cadf68
477f429
bb7c7f7
fb8029c
45dfcd7
febbd34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| "use client"; | ||
|
|
||
| import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||
| import { addToUsersFavorites, deleteFromUsersFavorites } from "@/lib/actions"; | ||
| import { Heart, Share } from "lucide-react"; | ||
| import { Item, Sublet } from "@/lib/types"; | ||
| import { ListingActions } from "@/components/listings/detail/ListingActions"; | ||
|
|
@@ -8,20 +12,65 @@ import { BackButton } from "@/components/listings/detail/BackButton"; | |
|
|
||
| interface Props { | ||
| listing: Item | Sublet; | ||
| initialIsFavorited: boolean; | ||
| } | ||
|
|
||
| export const ListingDetail = ({ listing }: Props) => { | ||
| export const ListingDetail = ({ listing, initialIsFavorited }: Props) => { | ||
| const listingType = listing.listing_type; | ||
| const priceLabel = listingType === "sublet" ? "/mo" : undefined; | ||
| const listingOwnerLabel = listingType === "item" ? "Seller" : "Owner"; | ||
| const queryClient = useQueryClient(); | ||
| const favoritesQuery = useQuery({ | ||
| queryKey: ["favorite", listing.id], | ||
| queryFn: async () => initialIsFavorited, | ||
| initialData: initialIsFavorited, | ||
| staleTime: Infinity, | ||
| }); | ||
|
|
||
| const isInsideFavorites = favoritesQuery.data ?? false; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe |
||
|
|
||
| const toggleFavoriteMutation = useMutation({ | ||
| mutationFn: async (shouldFavorite: boolean) => { | ||
| if (shouldFavorite) { | ||
| await addToUsersFavorites(listing.id); | ||
| } else { | ||
| await deleteFromUsersFavorites(listing.id); | ||
| } | ||
| }, | ||
| onMutate: async (shouldFavorite: boolean) => { | ||
| await queryClient.cancelQueries({ queryKey: ["favorite", listing.id] }); | ||
| const previous = queryClient.getQueryData<boolean>(["favorite", listing.id]); | ||
| queryClient.setQueryData(["favorite", listing.id], shouldFavorite); | ||
| return { previous }; | ||
| }, | ||
| onError: (_error, _shouldFavorite, context) => { | ||
| if (context?.previous !== undefined) { | ||
| queryClient.setQueryData(["favorite", listing.id], context.previous); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| const handleToggleFavorite = async () => { | ||
| toggleFavoriteMutation.mutate(!isInsideFavorites); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="mx-auto flex w-full max-w-[96rem] flex-col p-8 px-4 sm:px-12"> | ||
| <div className="mb-4 flex items-center justify-between"> | ||
| <BackButton /> | ||
| <div className="flex items-center gap-3"> | ||
| <Share className="h-5 w-5" /> | ||
| <Heart className="h-5 w-5" /> | ||
| <button | ||
| type="button" | ||
| className="cursor-pointer" | ||
| onClick={handleToggleFavorite} | ||
| aria-pressed={isInsideFavorites} | ||
| aria-label={isInsideFavorites ? "Remove from favorites" : "Add to favorites"} | ||
| > | ||
| <Heart | ||
| className={isInsideFavorites ? "h-5 w-5 fill-red-500 text-red-500" : "h-5 w-5"} | ||
| /> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <div className="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -210,6 +210,25 @@ export async function verifyPhoneCode(phoneNumber: string, code: string) { | |
| body: JSON.stringify({ phone_number: phoneNumber, code }), | ||
| }); | ||
| } | ||
| // ------------------------------------------------------------ | ||
| // adding and removing listings from favorites | ||
| // ------------------------------------------------------------ | ||
|
|
||
| export async function addToUsersFavorites(listingId: number) { | ||
| const res = await serverFetch<void>(`/market/listings/${listingId}/favorites/`, { | ||
| method: "POST", | ||
| }); | ||
| return res; | ||
| } | ||
| export async function deleteFromUsersFavorites(listingId: number) { | ||
| return await serverFetch<void>(`/market/listings/${listingId}/favorites/`, { | ||
| method: "DELETE", | ||
| }); | ||
| } | ||
|
|
||
| export async function getUsersFavorites() { | ||
| return await serverFetch<PaginatedResponse<Item | Sublet>>("/market/favorites/"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im just noticing this, but why are you using Also, this is my b for also just bringing this up, but ideally, we should not fetch the entire list of user's favorites just to see if one current item/sublet is favorited (this is inefficient). Don't remove this function (since we'll prob use it later), but ideally, we should be able to do this on a single listing object. One way to do this is computing and returning If you decide to go with this approach, your What that means in practical terms:
So at this point, the query isn’t being used to “fetch” the data anymore - it’s just holding the current value in the cache. Then when favorite/unfavorite mutation happens:
So the flow becomes:
If you have questions, we can talk about it tmrw during gbm |
||
| } | ||
|
|
||
| // ------------------------------------------------------------ | ||
| // creating new listings | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to see if 200 makes sense here and if not, maybe use a better status code