From 220f9d19c5434ab6beb8516da1c9033feb6bcb66 Mon Sep 17 00:00:00 2001 From: Ivan <45982459+ivntsng@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:48:58 +0800 Subject: [PATCH] Infinite scroll and additional updates (#520) --- .../components/listing/ListingDescription.tsx | 9 +- .../src/components/listings/ListingGrid.tsx | 18 +- .../listings/ListingGridSkeleton.tsx | 28 +++ frontend/src/components/pages/Browse.tsx | 130 ++++++------ .../src/components/products/ProductPage.tsx | 190 ++++++++---------- 5 files changed, 198 insertions(+), 177 deletions(-) create mode 100644 frontend/src/components/listings/ListingGridSkeleton.tsx diff --git a/frontend/src/components/listing/ListingDescription.tsx b/frontend/src/components/listing/ListingDescription.tsx index 572b5bb9..8c9bc6f9 100644 --- a/frontend/src/components/listing/ListingDescription.tsx +++ b/frontend/src/components/listing/ListingDescription.tsx @@ -14,6 +14,13 @@ interface RenderDescriptionProps { onImageClick?: (src: string, alt: string) => void; } +const transformUrl = (url: string) => { + if (url.startsWith("http://") || url.startsWith("https://")) { + return url; + } + return `https://${url}`; +}; + export const RenderDescription = ({ description, onImageClick, @@ -50,7 +57,7 @@ export const RenderDescription = ({ ), a: ({ children, href }) => ( { {listingInfos.map((info) => ( - + {listingDetails === null ? ( + + ) : ( + + )} ))} diff --git a/frontend/src/components/listings/ListingGridSkeleton.tsx b/frontend/src/components/listings/ListingGridSkeleton.tsx new file mode 100644 index 00000000..860ed733 --- /dev/null +++ b/frontend/src/components/listings/ListingGridSkeleton.tsx @@ -0,0 +1,28 @@ +const ListingGridSkeleton = () => { + return ( +
+
+ +
+
+
+
+
+ Loading... +
+ ); +}; + +export default ListingGridSkeleton; diff --git a/frontend/src/components/pages/Browse.tsx b/frontend/src/components/pages/Browse.tsx index f3d81ba5..1c73b0e8 100644 --- a/frontend/src/components/pages/Browse.tsx +++ b/frontend/src/components/pages/Browse.tsx @@ -1,6 +1,5 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { FaTimes } from "react-icons/fa"; -import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; import { useNavigate, useSearchParams } from "react-router-dom"; import ListingGrid from "@/components/listings/ListingGrid"; @@ -25,54 +24,86 @@ type ListingInfo = const Browse = () => { const auth = useAuthentication(); - const [listingIds, setListingIds] = useState(null); - const [moreListings, setMoreListings] = useState(false); const { addErrorAlert } = useAlertQueue(); const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); - // Gets the current page number and makes sure it is valid. - const page = searchParams.get("page"); - const query = searchParams.get("query"); - const pageNumber = parseInt(page || "1", 10); - if (isNaN(pageNumber) || pageNumber < 0) { - navigate("/404"); - } + const [page, setPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + + const [listingInfos, setListingInfos] = useState([]); + const query = searchParams.get("query"); const [searchQuery, setSearchQuery] = useState(query || ""); const debouncedSearch = useDebounce(searchQuery, 300); const [sortOption, setSortOption] = useState("most_upvoted"); - const [listingInfos, setListingInfos] = useState(null); + + const observerTarget = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !isLoading) { + setPage((prev) => prev + 1); + } + }, + { threshold: 0.1 }, + ); + + if (observerTarget.current) { + observer.observe(observerTarget.current); + } + + return () => observer.disconnect(); + }, [hasMore, isLoading]); + + useEffect(() => { + handleSearch(true); + }, [debouncedSearch, sortOption]); useEffect(() => { handleSearch(); - }, [debouncedSearch, pageNumber, sortOption]); + }, [page]); + + const handleSearch = async (resetSearch: boolean = false) => { + if (isLoading) return; + + setIsLoading(true); + const currentPage = resetSearch ? 1 : page; - const handleSearch = async () => { - setListingIds(null); - setListingInfos(null); + if (resetSearch) { + setListingInfos([]); + setPage(1); + setHasMore(true); + } const { data, error } = await auth.client.GET("/listings/search", { params: { query: { - page: pageNumber, + page: currentPage, search_query: searchQuery, sort_by: sortOption, include_user_vote: true, }, }, }); + if (error) { addErrorAlert(error); } else { - setMoreListings(data.has_next); - setListingInfos(data.listings); + const newListings = resetSearch + ? data.listings + : [...listingInfos, ...data.listings]; + const uniqueListings = Array.from( + new Map(newListings.map((listing) => [listing.id, listing])).values(), + ); + setListingInfos(uniqueListings); + setHasMore(data.has_next); } + setIsLoading(false); }; - const prevButton = pageNumber > 1; - const nextButton = moreListings; - const options: { value: SortOption; label: string }[] = [ { value: "most_upvoted", label: "Most Upvoted" }, { value: "most_viewed", label: "Most Viewed" }, @@ -141,57 +172,18 @@ const Browse = () => { - -
- - -
- {listingIds && ( -
- - -
- )} +
+ {isLoading && ( +
+
+ Loading more listings... +
+ )} +
); }; diff --git a/frontend/src/components/products/ProductPage.tsx b/frontend/src/components/products/ProductPage.tsx index f75304b1..62f384e7 100644 --- a/frontend/src/components/products/ProductPage.tsx +++ b/frontend/src/components/products/ProductPage.tsx @@ -22,6 +22,23 @@ import { convertToDecimal } from "@/lib/utils/priceFormat"; const FALLBACK_IMAGE = "https://flowbite.com/docs/images/examples/image-1@2x.jpg"; +const MARKDOWN_PLACEHOLDER = [ + "# Heading 1", + "## Heading 2", + "**Bold text**", + "*Italic text*", + "", + "- Bullet point", + "- Another point", + "", + "1. Numbered list", + "2. Second item", + "", + "[Link text](https://example.com)", + "", + "![Image alt text](image-url.jpg)", +].join("\n"); + interface ProductPageProps { productId: string; checkoutLabel: string; @@ -174,10 +191,8 @@ const ProductPage: React.FC = ({ } setDeletingImageIndex(index); - console.log("Attempting to delete image:", imageUrl); // Debug log try { - // Get the artifacts data to find the correct artifact_id const { data, error: fetchError } = await auth.client.GET( "/artifacts/list/{listing_id}", { @@ -194,7 +209,6 @@ const ProductPage: React.FC = ({ return; } - // Find the artifact that matches our image URL const artifact = data.artifacts.find( (a: { urls: { large: string } }) => a.urls.large === imageUrl, ); @@ -206,8 +220,6 @@ const ProductPage: React.FC = ({ return; } - console.log("Found artifact ID:", artifact.artifact_id); // Debug log - const { error } = await auth.client.DELETE( "/artifacts/delete/{artifact_id}", { @@ -218,7 +230,6 @@ const ProductPage: React.FC = ({ ); if (error) { - console.error("Delete request failed:", error); // Debug log addErrorAlert(error); setDeletingImageIndex(null); } else { @@ -234,7 +245,6 @@ const ProductPage: React.FC = ({ } } } catch (err) { - console.error("Error in handleDeleteImage:", err); // Debug log addErrorAlert(humanReadableError(err)); setDeletingImageIndex(null); } @@ -406,8 +416,6 @@ const ProductPage: React.FC = ({ return; } - console.log("Attempting to set main image:", imageUrl); // Debug log - try { const { data, error: fetchError } = await auth.client.GET( "/artifacts/list/{listing_id}", @@ -435,9 +443,6 @@ const ProductPage: React.FC = ({ return; } - console.log("Found artifact ID:", artifact.artifact_id); // Debug log - console.log("Listing ID:", productId); // Additional debug log - const { error } = await auth.client.PUT( "/artifacts/list/{listing_id}/main_image", { @@ -449,10 +454,8 @@ const ProductPage: React.FC = ({ ); if (error) { - console.error("Main image update request failed:", error); // Debug log addErrorAlert(error); } else { - console.log("Main image update successful, updating UI state"); // Debug log const newImages = [...currentImages]; newImages.splice(index, 1); newImages.unshift(imageUrl); @@ -464,7 +467,6 @@ const ProductPage: React.FC = ({ addAlert("Main image updated successfully", "success"); } } catch (err) { - console.error("Error in handleSetMainImage:", err); // Debug log addErrorAlert(humanReadableError(err)); } }; @@ -916,20 +918,7 @@ const ProductPage: React.FC = ({ setDescriptionHasChanged(true); }} className="min-h-[300px] font-mono text-sm p-4" - placeholder="# Heading 1 - ## Heading 2 - **Bold text** - *Italic text* - - - Bullet point - - Another point - - 1. Numbered list - 2. Second item - - [Link text](https://example.com) - - ![Image alt text](image-url.jpg)" + placeholder={MARKDOWN_PLACEHOLDER} autoFocus /> @@ -995,86 +984,85 @@ const ProductPage: React.FC = ({ - {currentImages.length > 0 && ( -
-
- {creatorInfo?.can_edit && ( -
- -
- {isUploadingImage ? ( - - ) : ( - <> -
+
-

- {isDragActive ? "Drop image here" : "Upload Image"} -

- - )} -
+ {/* Replace the current images section with this updated version */} +
+
+ {creatorInfo?.can_edit && ( +
+ +
+ {isUploadingImage ? ( + + ) : ( + <> +
+
+

+ {isDragActive ? "Drop image here" : "Upload Image"} +

+ + )}
- )} - {currentImages.map((image, index) => ( -
openModal(image)} - > - {`Product - {creatorInfo?.can_edit && ( -
- {index !== 0 && ( - - )} +
+ )} + {currentImages.map((image, index) => ( +
openModal(image)} + > + {`Product + {creatorInfo?.can_edit && ( +
+ {index !== 0 && ( -
- )} - {index === 0 && ( -
- - Main Image - -
- )} -
- ))} -
+ )} + +
+ )} + {index === 0 && ( +
+ + Main Image + +
+ )} +
+ ))}
- )} +
{shouldShowCheckout && isFixed && (