Skip to content

Commit

Permalink
user donations
Browse files Browse the repository at this point in the history
  • Loading branch information
ap-justin committed Oct 19, 2024
1 parent ae87b29 commit eaf0c69
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 241 deletions.
4 changes: 2 additions & 2 deletions src/pages/Admin/Charity/Donations/Donations.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useLoaderData } from "react-router-dom";
import type { DonationsPage } from "types/aws";
import DonationsTable from "./DonationsTable";
import type { Page } from "./types";

export default function Donations() {
const page1 = useLoaderData() as Page;
const page1 = useLoaderData() as DonationsPage;
return (
<div>
<h2 className="text-[2rem] font-bold mb-4">Donations</h2>
Expand Down
7 changes: 3 additions & 4 deletions src/pages/Admin/Charity/Donations/DonationsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ import { Info } from "components/Status";
import { replaceWithEmptyString as fill, humanize } from "helpers";
import { useEffect, useRef, useState } from "react";
import { useFetcher, useSearchParams } from "react-router-dom";
import type { Donation } from "types/aws";
import type { Donation, DonationsPage } from "types/aws";
import type { Ensure } from "types/utils";
import Table from "./Table";
import type { Page } from "./types";

interface Props {
classes?: string;
firstPage: Page;
firstPage: DonationsPage;
}

export default function DonationsTable({ classes = "", firstPage }: Props) {
const { data, state, load } = useFetcher<Page>(); //initially undefined
const { data, state, load } = useFetcher<DonationsPage>(); //initially undefined
const [params] = useSearchParams();
const [items, setItems] = useState(firstPage.Items);
const pageRef = useRef(1);
Expand Down
3 changes: 0 additions & 3 deletions src/pages/Admin/Charity/Donations/types.ts

This file was deleted.

57 changes: 27 additions & 30 deletions src/pages/UserDashboard/Donations/Filter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,63 @@ import { Popover, PopoverButton } from "@headlessui/react";
import { yupResolver } from "@hookform/resolvers/yup";
import Icon, { DrawerIcon } from "components/Icon";
import { dateToFormFormat } from "components/form";
import { cleanObject } from "helpers/cleanObject";
import { weeksAgo } from "helpers/weeksAgo";
import { type FormEventHandler, useRef } from "react";
import { FormProvider, useForm } from "react-hook-form";
import type { DonationsQueryParams } from "types/aws";
import { useSearchParams } from "react-router-dom";
import Form from "./Form";
import { schema } from "./schema";
import type { FormValues as FV } from "./types";

type Props = {
classes?: string;
setParams: React.Dispatch<React.SetStateAction<DonationsQueryParams>>;
asker: string;
isDisabled: boolean;
};

export default function Filter({
setParams,
classes = "",
isDisabled,
asker,
}: Props) {
export default function Filter({ classes = "", isDisabled }: Props) {
const [params, setParams] = useSearchParams();
const buttonRef = useRef<HTMLButtonElement | null>(null);

const start = params.get("startDate");
const end = params.get("endDate");

const methods = useForm<FV>({
mode: "onChange",
reValidateMode: "onChange",
resolver: yupResolver(schema),
defaultValues: {
//set default value so empty can be tagged as invalid
startDate: dateToFormFormat(weeksAgo("now", 5)),
endDate: dateToFormFormat(new Date()),
startDate: dateToFormFormat(start ? new Date(start) : weeksAgo("now", 5)),
endDate: dateToFormFormat(end ? new Date(end) : new Date()),
network: { label: "Select network...", value: "" },
currency: { label: "Select currency...", value: "" },
},
});

const { handleSubmit, reset } = methods;
const { handleSubmit } = methods;

async function submit(data: FV) {
setParams((prev) => ({
asker,
status: prev.status,
...cleanObject({
startDate: data.startDate ? new Date(data.startDate).toISOString() : "",
endDate: data.endDate ? new Date(data.endDate).toISOString() : "",
viaId: data.network.value,
symbol: data.currency.value,
}),
}));
buttonRef.current?.click();
const p = new URLSearchParams(params);
if (data.startDate) {
p.set("startDate", new Date(data.startDate).toISOString());
} else {
p.delete("startDate");
}

if (data.endDate) {
p.set("endDate", new Date(data.endDate).toISOString());
} else {
p.delete("endDate");
}

setParams(p);
}

const onReset: FormEventHandler<HTMLFormElement> = () => {
reset();
setParams((prev) => ({
asker,
status: prev.status,
}));
buttonRef.current?.click();
const p = new URLSearchParams(params);
p.delete("startDate");
p.delete("endDate");
setParams(p);
};
return (
<Popover className={`${classes} flex relative items-center`}>
Expand Down
34 changes: 34 additions & 0 deletions src/pages/UserDashboard/Donations/donations-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { loadAuth, redirectToAuth } from "auth";
import { APIs } from "constants/urls";
import { cacheGet } from "helpers/cache-get";
import type { LoaderFunction } from "react-router-dom";
import { version as v } from "services/helpers";
import type { UserV2 } from "types/auth";
import type { DonationsPage } from "types/aws";

export interface DonationsData extends DonationsPage {
user: UserV2;
}

export const loader: LoaderFunction = async ({ request }) => {
const from = new URL(request.url);
const { page = "1", status = "final" } = Object.fromEntries(
from.searchParams.entries()
);

const auth = await loadAuth();
if (!auth) return redirectToAuth(request);

const url = new URL(APIs.aws);
url.pathname = `${v(2)}/donations`;
url.searchParams.set("asker", auth.email);
url.searchParams.set("page", page);
url.searchParams.set("status", status);

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

return cacheGet(req)
.then((res) => res.json())
.then((data) => ({ ...data, user: auth }));
};
145 changes: 67 additions & 78 deletions src/pages/UserDashboard/Donations/index.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,54 @@
import CsvExporter from "components/CsvExporter";
import Icon from "components/Icon";
import QueryLoader from "components/QueryLoader";
import { replaceWithEmptyString as fill, humanize, isEmpty } from "helpers";
import usePaginatedDonationRecords from "services/aws/usePaginatedDonations";
import { replaceWithEmptyString as fill, humanize } from "helpers";
import { useEffect, useState } from "react";
import { useFetcher, useLoaderData, useSearchParams } from "react-router-dom";
import type { Donation } from "types/aws";
import { useUser } from "../use-user";
import Filter from "./Filter";
import MobileTable from "./MobileTable";
import NoDonations from "./NoDonations";
import StatusTabs from "./StatusTabs";
import Table from "./Table";
import type { DonationsData } from "./donations-loader";

export default function Donations() {
const user = useUser();
const queryState = usePaginatedDonationRecords({
email: user.email,
});
export { loader } from "./donations-loader";

const {
data,
hasMore,
isError,
error,
isLoading,
isFetching,
isLoadingNextPage,
query,
loadNextPage,
onQueryChange,
setParams,
status,
setStatus,
} = queryState;
export function Component() {
const [params, setParams] = useSearchParams();
const { load, data, state } = useFetcher<DonationsData>();
const { user, ...firstPage } = useLoaderData() as DonationsData;
const [items, setItems] = useState<Donation.Record[]>(firstPage.Items);

const isLoadingOrError =
isLoading || isFetching || isLoadingNextPage || isError;
const [query, setQuery] = useState("");

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

const nextPage = data ? data.nextPage : firstPage.nextPage;
function loadNextPage() {
if (!nextPage) return;
const p = new URLSearchParams(params);
p.set("page", nextPage.toString());
load(`?${p.toString()}`);
}

const status: Donation.Status = (params.get("status") ??
"final") as Donation.Status;
const isLoading = state !== "idle";

return (
<div className="grid grid-cols-[1fr_auto] content-start gap-y-4 @5xl:gap-y-8 @5xl:gap-x-3 relative">
<h1 className="text-3xl text-center @5xl:text-left col-span-full @5xl:col-span-1 mb-4 @5xl:mb-0">
My Donations
</h1>
<CsvExporter
aria-disabled={isLoadingOrError || isEmpty(data?.Items ?? [])}
aria-disabled={isLoading || items.length === 0}
classes="row-start-5 @5xl:row-auto col-span-full @5xl:col-span-1 @5xl:justify-self-end btn-blue px-8 py-3"
headers={csvHeaders}
data={
data?.Items.map((item) => {
items.map((item) => {
return fill({
recipientName: item.recipientName,
date: new Date(item.date).toLocaleDateString(),
Expand Down Expand Up @@ -75,67 +77,54 @@ export default function Donations() {
className="text-navy-d4 dark:text-navy-l2 absolute top-1/2 -translate-y-1/2 left-3"
/>
<input
disabled={isError}
className="p-3 pl-10 placeholder:text-navy-l1 dark:placeholder:text-navy-l2 bg-transparent w-full outline-none disabled:bg-gray-l3 dark:disabled:bg-navy-d3"
type="text"
placeholder="Search donations..."
value={query}
onChange={(e) => onQueryChange(e.target.value)}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<Filter
isDisabled={isLoading || isLoadingNextPage}
setParams={setParams}
asker={user.email}
isDisabled={isLoading}
classes="col-span-full @5xl:col-span-1 w-full @5xl:w-auto"
/>
<div className="grid col-span-full overflow-x-auto">
<StatusTabs status={status} changeStatus={setStatus} />

<StatusTabs
status={status}
changeStatus={(s) => {
const copy = new URLSearchParams(params);
copy.set("status", s ?? "final");
setParams(copy);
}}
/>
<div className="p-5 bg-gray-l6 border border-gray-l4 rounded-b @2xl:rounded-tr grid">
<QueryLoader
queryState={{
data: data?.Items,
isLoading,
isFetching,
error: error,
isError: isError,
}}
messages={{
fetching: "Loading donations...",
loading: "Loading donations...",
error: "Failed to get donations",
empty: (
<NoDonations
classes="mt-8 place-self-center col-span-full"
status={status}
/>
),
}}
>
{(donations) => (
<div className="grid">
<Table
donations={donations}
disabled={isLoadingOrError}
hasMore={hasMore}
isLoading={isLoadingNextPage}
status={status}
onLoadMore={loadNextPage}
classes="hidden mt-4 @5xl:mt-0 @5xl:table"
/>
<MobileTable
donations={donations}
disabled={isLoadingOrError}
hasMore={hasMore}
status={status}
isLoading={isLoadingNextPage}
onLoadMore={loadNextPage}
classes="@5xl:hidden my-4 @5xl:my-0"
/>
</div>
)}
</QueryLoader>
{items.length === 0 ? (
<NoDonations
classes="mt-8 place-self-center col-span-full"
status={status}
/>
) : (
<div className="grid">
<Table
donations={items}
disabled={isLoading}
hasMore={!!nextPage}
isLoading={isLoading}
status={status}
onLoadMore={loadNextPage}
classes="hidden mt-4 @5xl:mt-0 @5xl:table"
/>
<MobileTable
donations={items}
disabled={isLoading}
hasMore={!!nextPage}
status={status}
isLoading={isLoading}
onLoadMore={loadNextPage}
classes="@5xl:hidden my-4 @5xl:my-0"
/>
</div>
)}
</div>
</div>
</div>
Expand Down
3 changes: 1 addition & 2 deletions src/pages/UserDashboard/UserDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
type RouteObject,
useLoaderData,
} from "react-router-dom";
import Donations from "./Donations";
import EditProfile from "./EditProfile";
import Settings from "./Settings";
import { linkGroups, routes } from "./routes";
Expand Down Expand Up @@ -39,7 +38,7 @@ export const userDashboardRoute: RouteObject = {
loader,
children: [
{ path: routes.edit_profile, element: <EditProfile /> },
{ path: routes.donations, element: <Donations /> },
{ path: routes.donations, lazy: () => import("./Donations") },
{ path: routes.settings, element: <Settings /> },
{ index: true, element: <Navigate to={routes.edit_profile} /> },
],
Expand Down
Loading

0 comments on commit eaf0c69

Please sign in to comment.