Skip to content

Commit

Permalink
Infinite scroll and additional updates (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivntsng authored Oct 31, 2024
1 parent b447479 commit 220f9d1
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 177 deletions.
9 changes: 8 additions & 1 deletion frontend/src/components/listing/ListingDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,7 +57,7 @@ export const RenderDescription = ({
),
a: ({ children, href }) => (
<a
href={href}
href={transformUrl(href || "")}
target="_blank"
rel="noreferrer"
className="text-blue-500"
Expand Down
18 changes: 12 additions & 6 deletions frontend/src/components/listings/ListingGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { paths } from "@/gen/api";
import { useAlertQueue } from "@/hooks/useAlertQueue";
import { useAuthentication } from "@/hooks/useAuth";

import ListingGridSkeleton from "./ListingGridSkeleton";

type ListingInfo = {
id: string;
username: string;
Expand Down Expand Up @@ -64,13 +66,17 @@ const ListingGrid = (props: ListingGridProps) => {
{listingInfos.map((info) => (
<Link
to={`/item/${info.username}/${info.slug || info.id}`}
key={info.id}
key={`${info.username}-${info.id}`}
>
<ListingGridCard
listingId={info.id}
listing={listingDetails?.[info.id]}
showDescription={true}
/>
{listingDetails === null ? (
<ListingGridSkeleton />
) : (
<ListingGridCard
listingId={info.id}
listing={listingDetails[info.id]}
showDescription={true}
/>
)}
</Link>
))}
</div>
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/components/listings/ListingGridSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const ListingGridSkeleton = () => {
return (
<div
role="status"
className="p-4 border border-gray-200 rounded shadow animate-pulse dark:border-gray-700"
>
<div className="flex items-center justify-center h-48 mb-4 bg-gray-300 rounded dark:bg-gray-700">
<svg
className="w-10 h-10 text-gray-200 dark:text-gray-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 16 20"
>
<path d="M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM10.5 6a1.5 1.5 0 1 1 0 2.999A1.5 1.5 0 0 1 10.5 6Zm2.221 10.515a1 1 0 0 1-.858.485h-8a1 1 0 0 1-.9-1.43L5.6 10.039a.978.978 0 0 1 .936-.57 1 1 0 0 1 .9.632l1.181 2.981.541-1a.945.945 0 0 1 .883-.522 1 1 0 0 1 .879.529l1.832 3.438a1 1 0 0 1-.031.988Z" />
<path d="M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z" />
</svg>
</div>
<div className="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4"></div>
<div className="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div className="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div className="h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
<span className="sr-only">Loading...</span>
</div>
);
};

export default ListingGridSkeleton;
130 changes: 61 additions & 69 deletions frontend/src/components/pages/Browse.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,54 +24,86 @@ type ListingInfo =

const Browse = () => {
const auth = useAuthentication();
const [listingIds, setListingIds] = useState<string[] | null>(null);
const [moreListings, setMoreListings] = useState<boolean>(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<ListingInfo[]>([]);

const query = searchParams.get("query");
const [searchQuery, setSearchQuery] = useState(query || "");
const debouncedSearch = useDebounce(searchQuery, 300);
const [sortOption, setSortOption] = useState<SortOption>("most_upvoted");
const [listingInfos, setListingInfos] = useState<ListingInfo[] | null>(null);

const observerTarget = useRef<HTMLDivElement>(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" },
Expand Down Expand Up @@ -141,57 +172,18 @@ const Browse = () => {
</Button>
</div>
</div>

<div className="flex justify-between">
<Button
variant="default"
onClick={() => navigate(`/browse/?page=${pageNumber - 1}`)}
disabled={!prevButton}
>
<div className="flex items-center">
<FaChevronLeft className="text-xs mr-2" />
Previous
</div>
</Button>
<Button
variant="default"
onClick={() => navigate(`/browse/?page=${pageNumber + 1}`)}
disabled={!nextButton}
>
<div className="flex items-center">
Next
<FaChevronRight className="text-xs ml-2" />
</div>
</Button>
</div>
</div>

<ListingGrid listingInfos={listingInfos} />

{listingIds && (
<div className="flex justify-between mt-2">
<Button
variant="default"
onClick={() => navigate(`/browse/?page=${pageNumber - 1}`)}
disabled={!prevButton}
>
<div className="flex items-center">
<FaChevronLeft className="text-xs mr-2" />
Previous
</div>
</Button>
<Button
variant="default"
onClick={() => navigate(`/browse/?page=${pageNumber + 1}`)}
disabled={!nextButton}
>
<div className="flex items-center">
Next
<FaChevronRight className="text-xs ml-2" />
</div>
</Button>
</div>
)}
<div ref={observerTarget} className="py-8 text-center">
{isLoading && (
<div className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-12"></div>
<span>Loading more listings...</span>
</div>
)}
</div>
</>
);
};
Expand Down
Loading

0 comments on commit 220f9d1

Please sign in to comment.