Skip to content

Commit

Permalink
charity donations loader
Browse files Browse the repository at this point in the history
  • Loading branch information
ap-justin committed Oct 15, 2024
1 parent 9ed2717 commit 0b491f0
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 89 deletions.
4 changes: 2 additions & 2 deletions src/App/root-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async function getBookmarks(user: UserV2): Promise<EndowmentBookmark[]> {
const source = new URL(APIs.aws);
source.pathname = `${v(1)}/bookmarks`;
const req = new Request(source);
req.headers.set("authorization", `Bearer ${user.idToken}`);
req.headers.set("authorization", user.idToken);

const res = await fetch(req);
if (!res.ok) return [];
Expand All @@ -83,6 +83,6 @@ async function useEndows(user: UserV2): Promise<UserEndow[]> {
const source = new URL(APIs.aws);
source.pathname = `${v(3)}/users/${user.email}/endowments`;
const req = new Request(source);
req.headers.set("authorization", `Bearer ${user.idToken}`);
req.headers.set("authorization", user.idToken);
return cacheGet(req).then<UserEndow[]>((res) => (res.ok ? res.json() : []));
}
8 changes: 6 additions & 2 deletions src/layout/DashboardLayout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ErrorBoundary from "errors/ErrorBoundary";
import type { ReactNode } from "react";
import { Outlet } from "react-router-dom";
import { Outlet, useLocation } from "react-router-dom";
import Sidebar, { SidebarOpener } from "./Sidebar";
import type { LinkGroup } from "./Sidebar/types";

Expand All @@ -14,6 +15,7 @@ export default function Layout({
sidebarHeader,
rootRoute,
}: DashboardLayoutProps) {
const { key } = useLocation();
return (
<div className="grid max-md:content-start md:grid-cols-[auto_1fr]">
{/** sidebar */}
Expand All @@ -29,7 +31,9 @@ export default function Layout({
/>
{/** views */}
<div className="px-6 py-8 md:p-10 @container">
<Outlet />
<ErrorBoundary key={key}>
<Outlet />
</ErrorBoundary>
</div>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion src/pages/Admin/Charity/Donations/Donations.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { adminRoutes } from "constants/routes";
import { useLoaderData } from "react-router-dom";
import Seo from "../Seo";
import DonationsTable from "./DonationsTable";
import type { Page } from "./types";

export default function Donations() {
const page1 = useLoaderData() as Page;
return (
<div>
<Seo title="Donations" url={adminRoutes.donations} />
<h2 className="text-[2rem] font-bold mb-4">Donations</h2>
<DonationsTable />
<DonationsTable firstPage={page1} />
</div>
);
}
157 changes: 76 additions & 81 deletions src/pages/Admin/Charity/Donations/DonationsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,91 @@
import CsvExporter from "components/CsvExporter";
import Icon from "components/Icon";
import QueryLoader from "components/QueryLoader";
import { Info } from "components/Status";
import { replaceWithEmptyString as fill, humanize } from "helpers";
import usePaginatedDonationRecords from "services/aws/usePaginatedDonations";
import { useEffect, useState } from "react";
import { useFetcher, useSearchParams } from "react-router-dom";
import type { Donation } from "types/aws";
import type { Ensure } from "types/utils";
import { useAdminContext } from "../../Context";
import Table from "./Table";
import type { Page } from "./types";

export default function DonationsTable({ classes = "" }) {
const { id: endowmentId } = useAdminContext();
interface Props {
classes?: string;
firstPage: Page;
}

export default function DonationsTable({ classes = "", firstPage }: Props) {
const fetcher = useFetcher<Page>(); //initially undefined
const [params] = useSearchParams();
const [items, setItems] = useState(firstPage.Items);

useEffect(() => {
if (!fetcher.data || fetcher.state === "loading") return;
if (fetcher.data) {
setItems((prev) => [...prev, ...(fetcher.data?.Items || [])]);
}
}, [fetcher.data, fetcher.state]);

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

const nextPage = fetcher.data?.nextPage ?? firstPage.nextPage;

const {
data,
isLoading,
isFetching,
isError,
hasMore,
loadNextPage,
isLoadingNextPage,
} = usePaginatedDonationRecords({
endowmentId: endowmentId.toString(),
});
function loadNext() {
if (!nextPage) throw `should not call load when there's no next page`;
const n = new URLSearchParams(params);
n.set("page", nextPage.toString());
fetcher.load(`?${n.toString()}`);
}

const isLoadingOrError = isLoading || isLoadingNextPage || isError;
return (
<div className={classes}>
<QueryLoader
queryState={{
data: data?.Items,
isLoading,
isFetching,
isError,
}}
messages={{
loading: "Fetching donations",
error: "Failed to get donations",
empty: "No donations found",
}}
>
{(donations) => (
<>
<div className="grid w-full sm:flex items-center sm:justify-end mb-2 gap-2">
<CsvExporter
classes="border border-blue text-blue-d1 hover:border-blue-l2 hover:text-blue rounded px-4 py-2 text-sm"
headers={csvHeaders}
data={donations
.filter(
(d): d is Ensure<Donation.Record, "donorDetails"> =>
!!d.donorDetails
)
.map<Donation.Record | Donation.KYC>(
({ donorDetails: donor, ...d }) => {
return fill({
date: new Date(d.date).toLocaleDateString(),
programName: d.programName,
appUsed:
d.appUsed === "bg-widget"
? "Donation Form"
: "Marketplace",
paymentMethod: d.paymentMethod,
isRecurring: d.isRecurring ? "Yes" : "No",
symbol: d.symbol,
initAmount: humanize(d.initAmount, 2),
finalAmountUsd: humanize(d.finalAmountUsd ?? 0, 2),
id: d.id,
receipt: donor.address?.country ? "Yes" : "No",
fullName: donor.fullName,
kycEmail: donor.kycEmail,
...donor.address,
});
}
)}
filename="received_donations.csv"
>
<Icon type="FileCSV" size={17} className="text-2xl" />
Donation Records
</CsvExporter>
</div>
<div className="grid w-full sm:flex items-center sm:justify-end mb-2 gap-2">
<CsvExporter
classes="border border-blue text-blue-d1 hover:border-blue-l2 hover:text-blue rounded px-4 py-2 text-sm"
headers={csvHeaders}
data={items
.filter(
(d): d is Ensure<Donation.Record, "donorDetails"> =>
!!d.donorDetails
)
.map<Donation.Record | Donation.KYC>(
({ donorDetails: donor, ...d }) => {
return fill({
date: new Date(d.date).toLocaleDateString(),
programName: d.programName,
appUsed:
d.appUsed === "bg-widget" ? "Donation Form" : "Marketplace",
paymentMethod: d.paymentMethod,
isRecurring: d.isRecurring ? "Yes" : "No",
symbol: d.symbol,
initAmount: humanize(d.initAmount, 2),
finalAmountUsd: humanize(d.finalAmountUsd ?? 0, 2),
id: d.id,
receipt: donor.address?.country ? "Yes" : "No",
fullName: donor.fullName,
kycEmail: donor.kycEmail,
...donor.address,
});
}
)}
filename="received_donations.csv"
>
<Icon type="FileCSV" size={17} className="text-2xl" />
Donation Records
</CsvExporter>
</div>

<div className="overflow-x-auto">
<Table
donations={donations}
hasMore={hasMore}
onLoadMore={loadNextPage}
disabled={isLoadingOrError}
isLoading={isLoadingNextPage}
/>
</div>
</>
)}
</QueryLoader>
<div className="overflow-x-auto">
<Table
donations={items}
hasMore={!!nextPage}
onLoadMore={loadNext}
disabled={fetcher.state === "loading"}
isLoading={fetcher.state === "loading"}
/>
</div>
</div>
);
}
Expand Down
35 changes: 35 additions & 0 deletions src/pages/Admin/Charity/Donations/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
import { loadAuth } from "auth/load-auth";
import { APIs } from "constants/urls";
import { cacheGet } from "helpers/cache-get";
import type { LoaderFunction } from "react-router-dom";
import { version as ver } from "services/helpers";
import * as v from "valibot";

export { default as Component } from "./Donations";

const id = v.pipe(
v.string(),
v.transform((x) => +x),
v.number(),
v.integer(),
v.minValue(1),
v.transform((x) => x.toString())
);

export const loader: LoaderFunction = async ({ params, request }) => {
const from = new URL(request.url);
const page = from.searchParams.get("page") ?? "1";

const auth = await loadAuth();
if (!auth) throw `user must have been authenticated at this point`;

const url = new URL(APIs.aws);
url.pathname = `${ver(2)}/donations`;
url.searchParams.set("asker", v.parse(id, params.id));
url.searchParams.set("status", "final");
url.searchParams.set("page", page);

const req = new Request(url);
req.headers.set("authorization", auth.idToken);

return cacheGet(req);
};
3 changes: 3 additions & 0 deletions src/pages/Admin/Charity/Donations/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Donation } from "types/aws";

export type Page = { Items: Donation.Record[]; nextPage?: number };
2 changes: 0 additions & 2 deletions src/pages/Admin/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ export function Context({
}: PropsWithChildren<{ user: UserV2 }>) {
const { id } = useParams<AdminParams>();

console.log({ user, id });

if (!user.endowments.includes(idParamToNum(id))) {
return (
<div className="grid content-start place-items-center pt-40 pb-20">
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Registration/Welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const loader: LoaderFunction = async ({ request }) => {
method: "POST",
body: JSON.stringify(payload),
});
req.headers.set("authorization", auth!.accessToken);
req.headers.set("authorization", auth.idToken);

api.search = "";
const cacheKey =
Expand Down

0 comments on commit 0b491f0

Please sign in to comment.