Skip to content

Commit

Permalink
pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
ap-justin committed Oct 12, 2024
1 parent 80c9c63 commit c04a787
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 84 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"framer-motion": "11.2.10",
"fuse.js": "7.0.0",
"gsap": "3.12.5",
"lodash": "4.17.21",
"lucide-react": "0.436.0",
"nprogress": "0.2.0",
"qrcode.react": "3.1.0",
Expand Down Expand Up @@ -80,6 +81,7 @@
"@testing-library/jest-dom": "6.4.6",
"@testing-library/react": "16.0.0",
"@testing-library/user-event": "14.5.2",
"@types/lodash": "4.17.10",
"@types/node": "18.18.13",
"@types/nprogress": "^0",
"@types/react": "18.2.62",
Expand Down
7 changes: 7 additions & 0 deletions src/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ const rootRoutes: RO[] = [
export const routes: RO[] = [
{
element: <RootLayout />,
loader: async () => {
/** reset all cache */
const cache = await caches.open("bg");
const keys = await cache.keys();
await Promise.all(keys.map((k) => cache.delete(k)));
return null;
},
children: rootRoutes,
ErrorBoundary: RouterErrorBoundary,
},
Expand Down
83 changes: 38 additions & 45 deletions src/pages/Marketplace/Cards/index.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,46 @@
import QueryLoader from "components/QueryLoader";
import { Info } from "components/Status";
import { useNavigation, useSearchParams } from "react-router-dom";
import type { EndowListPaginatedAWSQueryRes, EndowmentCard } from "types/aws";
import Card from "./Card";
import useCards from "./useCards";

export default function Cards({ classes = "" }: { classes?: string }) {
const {
hasMore,
isLoading,
isFetching,
isLoadingNextPage,
loadNextPage,
data,
isError,
} = useCards();
interface Props {
classes?: string;
page: EndowListPaginatedAWSQueryRes<EndowmentCard[]>;
}

export default function Cards({ classes = "", page }: Props) {
const navigation = useNavigation();
const [params, setParams] = useSearchParams();
const { Items: endowments, NumOfPages, Page } = page;

const hasMore = Page < NumOfPages;

if (endowments.length === 0) {
return <Info>No organisations found</Info>;
}

return (
<QueryLoader
queryState={{
data: data?.Items || [],
isLoading,
isFetching,
isError,
}}
messages={{
error: "Failed to get organisations",
loading: "Getting organisations..",
empty: "No organisations found",
}}
classes={{
container: `${classes} dark:text-white`,
}}
<div
className={`${classes} w-full grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 content-start`}
>
{(endowments) => (
<div
className={`${classes} w-full grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 content-start`}
>
{endowments.map((endow) => (
<Card {...endow} key={endow.id} />
))}
{endowments.map((endow) => (
<Card {...endow} key={endow.id} />
))}

{hasMore && (
<button
className="col-span-full btn-blue rounded-md p-2 text-sm w-full mt-6"
onClick={loadNextPage}
disabled={isLoading || isLoadingNextPage}
>
Load more organizations
</button>
)}
</div>
{hasMore && (
<button
type="button"
disabled={navigation.state === "loading"}
className="col-span-full btn-blue rounded-md p-2 text-sm w-full mt-6"
onClick={() => {
const n = new URLSearchParams(params);
n.set("page", String(Page + 1));
setParams(n, { replace: true, preventScrollReset: true });
}}
>
Load more organizations
</button>
)}
</QueryLoader>
</div>
);
}
73 changes: 40 additions & 33 deletions src/pages/Marketplace/Toolbar/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,53 @@
import Icon from "components/Icon";
import { useState } from "react";
import debounce from "lodash/debounce";
import { type ChangeEventHandler, useEffect, useRef } from "react";
import { useSearchParams } from "react-router-dom";

export default function Search({ classes = "" }: { classes?: string }) {
const ref = useRef<ReturnType<typeof debounce>>();
const inputRef = useRef<HTMLInputElement>(null);
const [params, setParams] = useSearchParams();
const q = params.get("query") || "";
const [search, setSearch] = useState(q);
const q = decodeURIComponent(params.get("query") || "");
const _id = params.get("_f") || "";

const onChange: ChangeEventHandler<HTMLInputElement> = (e) => {
const n = new URLSearchParams(params);
n.set("query", encodeURIComponent(e.target.value));
n.set("page", "1");
n.set("_f", "search");
setParams(n, {
replace: true,
preventScrollReset: true,
});
};

useEffect(() => {
if (!_id) return;
inputRef.current?.focus();
}, [_id]);

useEffect(() => {
return () => {
ref.current?.cancel();
};
}, []);

return (
<form
onSubmit={(e) => {
e.preventDefault();
const n = new URLSearchParams(params);
n.set("query", e.currentTarget.query.value);
setParams(n, { replace: true, preventScrollReset: true });
}}
className={`${classes} flex gap-2 items-center`}
<div
className={`${classes} flex gap-2 items-center border border-gray-l4 rounded-lg relative`}
>
<Icon
type="Search"
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2"
/>
<input
onChange={(e) => {
const s = e.target.value;
//user deleted all text
setSearch(s);
if (!s && q) {
const n = new URLSearchParams(params);
n.delete("query");
setParams(n, { replace: true, preventScrollReset: true });
}
}}
type="search"
name="query"
value={search}
className="w-full h-full p-3 placeholder:text-navy-l3 text-navy-d4 font-medium font-heading border border-gray-l4 rounded-lg outline-blue-d1 outline-offset-4"
ref={inputRef}
defaultValue={q}
onChange={debounce(onChange, 500)}
className="w-full h-full p-3 pl-10 placeholder:text-navy-l3 text-navy-d4 font-medium font-heading rounded-lg outline-blue-d1 outline-offset-4"
placeholder={q || "Search organizations..."}
/>
<button
disabled={!search}
type="submit"
className="rounded-lg border border-gray-l4 h-full p-3 enabled:hover:border-blue-d1 enabled:hover:text-blue-d1 disabled:text-gray"
>
<Icon type="Search" size={20} />
</button>
</form>
</div>
);
}
8 changes: 5 additions & 3 deletions src/pages/Marketplace/Toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export default function Toolbar({ classes = "" }: { classes?: string }) {
className={`${classes} grid grid-cols-2 md:grid-cols-[auto_1fr] gap-3`}
>
<button
onClick={() =>
onClick={() => {
// remove focus persistor from <Search/>
params.delete("_f");
navigate(
{ pathname: "filter", search: params.toString() },
{ replace: true, preventScrollReset: true }
)
}
);
}}
className="btn-blue justify-start justify-self-start rounded-lg px-3 py-2 text-sm"
>
<Icon type="Filter" size={16} className="mr-2" />
Expand Down
85 changes: 83 additions & 2 deletions src/pages/Marketplace/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,91 @@
import { Outlet } from "react-router-dom";
import { APIs } from "constants/urls";
import {
type LoaderFunctionArgs,
Outlet,
redirect,
useLoaderData,
} from "react-router-dom";
import { version as v } from "services/helpers";
import type { EndowListPaginatedAWSQueryRes, EndowmentCard } from "types/aws";
import ActiveFilters from "./ActiveFilters";
import Cards from "./Cards";
import Hero from "./Hero";
import Toolbar from "./Toolbar";

type Page = EndowListPaginatedAWSQueryRes<EndowmentCard[]>;

/**@param number - >= 2 */
function restPages(num: number) {
const result = [];
for (let i = num; i >= 2; i--) {
result.push(i);
}
return result;
}

async function pageRes(
search: URLSearchParams,
page: number,
cache: Cache
): Promise<Response | undefined> {
const n = new URLSearchParams(search);
n.set("page", page.toString());
const url = new URL(APIs.aws);
url.pathname = `${v(1)}/cloudsearch-nonprofits`;
url.search = n.toString();
const c = await cache.match(url);

if (c) return c.clone();

await cache.add(url);
const fresh = await cache.match(url);
if (fresh) return fresh.clone();
}

export const loader = async ({
request,
}: LoaderFunctionArgs): Promise<Page | Response> => {
const cache = await caches.open("bg");
const source = new URL(request.url);
// delete focus persistor from <Search/>
source.searchParams.delete("_f");

const pageNum = +(source.searchParams.get("page") ?? "1");

const pagesRes: Response[] = [];
const firstPageRes = await pageRes(source.searchParams, 1, cache);

if (firstPageRes) pagesRes.push(firstPageRes.clone());

const maxPage = await (async (r) => {
if (!r) return 1;
const c = r.clone();
const p: Page = await c.json();
return p.NumOfPages;
})(firstPageRes);

for (const page of restPages(Math.min(pageNum, maxPage)).reverse()) {
const res = await pageRes(source.searchParams, page, cache);
if (res) pagesRes.push(res);
}

if (pageNum > maxPage) {
const to = new URL(source);
to.searchParams.set("page", maxPage.toString());
return redirect(to.toString());
}

const res = await Promise.all(pagesRes.map<Page>((r) => r.json() as any));

return {
NumOfPages: res[0].NumOfPages,
Items: res.flatMap((r) => r.Items),
Page: pageNum,
};
};

export function Component() {
const page = useLoaderData() as Page;
return (
<div className="w-full grid content-start pb-16">
<div className="relative overlay bg-cover bg-left-top">
Expand All @@ -14,7 +95,7 @@ export function Component() {
<div className="grid gap-y-4 content-start padded-container min-h-screen">
<Toolbar classes="mt-10" />
<ActiveFilters />
<Cards />
<Cards page={page} />
</div>
<Outlet />
</div>
Expand Down
11 changes: 10 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3228,6 +3228,13 @@ __metadata:
languageName: node
linkType: hard

"@types/lodash@npm:4.17.10":
version: 4.17.10
resolution: "@types/lodash@npm:4.17.10"
checksum: 10/10fe24a93adc6048cb23e4135c1ed1d52cc39033682e6513f4f51b74a9af6d7a24fbea92203c22dc4e01e35f1ab3aa0fd0a2b487e8a4a2bbdf1fc05970094066
languageName: node
linkType: hard

"@types/mute-stream@npm:^0.0.4":
version: 0.0.4
resolution: "@types/mute-stream@npm:0.0.4"
Expand Down Expand Up @@ -3446,6 +3453,7 @@ __metadata:
"@testing-library/jest-dom": "npm:6.4.6"
"@testing-library/react": "npm:16.0.0"
"@testing-library/user-event": "npm:14.5.2"
"@types/lodash": "npm:4.17.10"
"@types/node": "npm:18.18.13"
"@types/nprogress": "npm:^0"
"@types/react": "npm:18.2.62"
Expand All @@ -3461,6 +3469,7 @@ __metadata:
gsap: "npm:3.12.5"
jsdom: "npm:24.1.0"
lefthook: "npm:1.6.15"
lodash: "npm:4.17.21"
lucide-react: "npm:0.436.0"
msw: "npm:2.3.1"
nprogress: "npm:0.2.0"
Expand Down Expand Up @@ -5015,7 +5024,7 @@ __metadata:
languageName: node
linkType: hard

"lodash@npm:^4.17.21":
"lodash@npm:4.17.21, lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532
Expand Down

0 comments on commit c04a787

Please sign in to comment.