Skip to content

Commit

Permalink
use swr for fetching and mutations (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
technophile-04 authored Feb 20, 2024
1 parent ae8d7f2 commit d8e4478
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 178 deletions.
96 changes: 96 additions & 0 deletions packages/nextjs/app/admin/_components/GrantReview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useSWRConfig } from "swr";
import useSWRMutation from "swr/mutation";
import { useAccount, useSignTypedData } from "wagmi";
import { GrantData } from "~~/services/database/schema";
import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT } from "~~/utils/eip712";
import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants";
import { notification } from "~~/utils/scaffold-eth";
import { postMutationFetcher } from "~~/utils/swr";

type ReqBody = {
signer: string;
signature: `0x${string}`;
action: ProposalStatusType;
};

export const GrantReview = ({ grant }: { grant: GrantData }) => {
const { address } = useAccount();
const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData();
const { trigger: postReviewGrant, isMutating: isPostingNewGrant } = useSWRMutation(
`/api/grants/${grant.id}/review`,
postMutationFetcher<ReqBody>,
);
const { mutate } = useSWRConfig();
const isLoading = isSigningMessage || isPostingNewGrant;

const handleReviewGrant = async (grant: GrantData, action: ProposalStatusType) => {
if (!address) {
notification.error("Please connect your wallet");
return;
}

let signature;
try {
signature = await signTypedDataAsync({
domain: EIP_712_DOMAIN,
types: EIP_712_TYPES__REVIEW_GRANT,
primaryType: "Message",
message: {
grantId: grant.id,
action: action,
},
});
} catch (e) {
console.error("Error signing message", e);
notification.error("Error signing message");
return;
}

let notificationId;
try {
notificationId = notification.loading("Submitting review");
await postReviewGrant({ signer: address, signature, action });
await mutate("/api/grants/review");
notification.remove(notificationId);
notification.success(`Grant reviewed: ${action}`);
} catch (error) {
notification.error("Error reviewing grant");
} finally {
if (notificationId) notification.remove(notificationId);
}
};

if (grant.status !== PROPOSAL_STATUS.PROPOSED && grant.status !== PROPOSAL_STATUS.SUBMITTED) return null;

const acceptStatus = grant.status === PROPOSAL_STATUS.PROPOSED ? PROPOSAL_STATUS.APPROVED : PROPOSAL_STATUS.COMPLETED;
const acceptLabel = grant.status === PROPOSAL_STATUS.PROPOSED ? "Approve" : "Complete";
return (
<div className="border p-4 my-4">
<h3 className="font-bold">
{grant.title}
<span className="text-sm text-gray-500 ml-2">({grant.id})</span>
</h3>
<p>{grant.description}</p>
<div className="mt-4">
<button
className={`bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded ${
isLoading ? "opacity-50" : ""
}`}
onClick={() => handleReviewGrant(grant, acceptStatus)}
disabled={isLoading}
>
{acceptLabel}
</button>
<button
className={`bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded ml-4 ${
isLoading ? "opacity-50" : ""
}`}
onClick={() => handleReviewGrant(grant, PROPOSAL_STATUS.REJECTED)}
disabled={isLoading}
>
Reject
</button>
</div>
</div>
);
};
113 changes: 17 additions & 96 deletions packages/nextjs/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,40 @@
"use client";

import { useEffect, useState } from "react";
import { useAccount, useSignTypedData } from "wagmi";
import { GrantReview } from "./_components/GrantReview";
import useSWR from "swr";
import { GrantData } from "~~/services/database/schema";
import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT } from "~~/utils/eip712";
import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants";
import { PROPOSAL_STATUS } from "~~/utils/grants";
import { notification } from "~~/utils/scaffold-eth";

const GrantReview = ({
grant,
reviewGrant,
}: {
grant: GrantData;
reviewGrant: (grant: GrantData, action: ProposalStatusType) => void;
}) => {
if (grant.status !== PROPOSAL_STATUS.PROPOSED && grant.status !== PROPOSAL_STATUS.SUBMITTED) return null;

const acceptStatus = grant.status === PROPOSAL_STATUS.PROPOSED ? PROPOSAL_STATUS.APPROVED : PROPOSAL_STATUS.COMPLETED;
const acceptLabel = grant.status === PROPOSAL_STATUS.PROPOSED ? "Approve" : "Complete";
return (
<div className="border p-4 my-4">
<h3 className="font-bold">
{grant.title}
<span className="text-sm text-gray-500 ml-2">({grant.id})</span>
</h3>
<p>{grant.description}</p>
<div className="mt-4">
<button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
onClick={() => reviewGrant(grant, acceptStatus)}
>
{acceptLabel}
</button>
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded ml-4"
onClick={() => reviewGrant(grant, PROPOSAL_STATUS.REJECTED)}
>
Reject
</button>
</div>
</div>
);
};

// ToDo. "Protect" with address header or PROTECT with signing the read.
// ToDo. Loading states (initial, actions, etc)
// ToDo. Refresh list after action
const AdminPage = () => {
const { address } = useAccount();
const [grants, setGrants] = useState<GrantData[]>([]);
const { signTypedDataAsync } = useSignTypedData();

useEffect(() => {
const getGrants = async () => {
try {
const response = await fetch("/api/grants/review");
const grants: GrantData[] = (await response.json()).data;
setGrants(grants);
} catch (error) {
notification.error("Error getting grants for review");
}
};

getGrants();
}, []);

const reviewGrant = async (grant: GrantData, action: ProposalStatusType) => {
let signature;
try {
signature = await signTypedDataAsync({
domain: EIP_712_DOMAIN,
types: EIP_712_TYPES__REVIEW_GRANT,
primaryType: "Message",
message: {
grantId: grant.id,
action: action,
},
});
} catch (e) {
console.error("Error signing message", e);
notification.error("Error signing message");
return;
}

try {
await fetch(`/api/grants/${grant.id}/review`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ signature, signer: address, action }),
});
notification.success(`Grant reviewed: ${action}`);
} catch (error) {
notification.error("Error reviewing grant");
}
};
// TODO: Move the response type to a shared location
const { data, isLoading } = useSWR<{ data: GrantData[] }>("/api/grants/review", {
onError: error => {
console.error("Error fetching grants", error);
notification.error("Error getting grants data");
},
});
const grants = data?.data;

const completedGrants = grants?.filter(grant => grant.status === PROPOSAL_STATUS.SUBMITTED);
const newGrants = grants.filter(grant => grant.status === PROPOSAL_STATUS.PROPOSED);
const newGrants = grants?.filter(grant => grant.status === PROPOSAL_STATUS.PROPOSED);

return (
<div className="container mx-auto max-w-screen-md mt-12">
<h1 className="text-4xl font-bold">Admin page</h1>
{isLoading && <span className="loading loading-spinner"></span>}
{grants && (
<>
<h2 className="font-bold mt-8">Proposals submitted as completed:</h2>
{completedGrants?.length === 0 && <p>No completed grants</p>}
{completedGrants.map(grant => (
<GrantReview key={grant.id} grant={grant} reviewGrant={reviewGrant} />
{completedGrants?.map(grant => (
<GrantReview key={grant.id} grant={grant} />
))}
<h2 className="font-bold mt-8">New grant proposal:</h2>
{newGrants?.length === 0 && <p>No new grants</p>}
{newGrants.map(grant => (
<GrantReview key={grant.id} grant={grant} reviewGrant={reviewGrant} />
{newGrants?.map(grant => (
<GrantReview key={grant.id} grant={grant} />
))}
</>
)}
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/api/builders/[builderAddress]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function GET(_request: Request, { params }: { params: { builderAddr
return NextResponse.json(builderData);
} catch (error) {
return NextResponse.json(
{ message: "Internal Server Error" },
{ error: "Internal Server Error" },
{
status: 500,
},
Expand Down
34 changes: 19 additions & 15 deletions packages/nextjs/app/apply/_component/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@

import { useRouter } from "next/navigation";
import SubmitButton from "./SubmitButton";
import useSWRMutation from "swr/mutation";
import { useAccount, useSignTypedData } from "wagmi";
import { EIP_712_DOMAIN, EIP_712_TYPES__APPLY_FOR_GRANT } from "~~/utils/eip712";
import { notification } from "~~/utils/scaffold-eth";
import { postMutationFetcher } from "~~/utils/swr";

// TODO: move to a shared location
type ReqBody = {
title?: string;
description?: string;
askAmount?: string;
signature?: `0x${string}`;
signer?: string;
};

const selectOptions = [0.1, 0.25, 0.5, 1];

const Form = () => {
const { address: connectedAddress } = useAccount();
const { signTypedDataAsync } = useSignTypedData();
const router = useRouter();
const { trigger: postNewGrant } = useSWRMutation("/api/grants/new", postMutationFetcher<ReqBody>);

const clientFormAction = async (formData: FormData) => {
if (!connectedAddress) {
Expand All @@ -20,9 +32,9 @@ const Form = () => {
}

try {
const title = formData.get("title");
const description = formData.get("description");
const askAmount = formData.get("askAmount");
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const askAmount = formData.get("askAmount") as string;
if (!title || !description || !askAmount) {
notification.error("Please fill all the fields");
return;
Expand All @@ -33,21 +45,13 @@ const Form = () => {
types: EIP_712_TYPES__APPLY_FOR_GRANT,
primaryType: "Message",
message: {
title: title as string,
description: description as string,
askAmount: askAmount as string,
title: title,
description: description,
askAmount: askAmount,
},
});

const res = await fetch("/api/grants/new", {
method: "POST",
body: JSON.stringify({ title, description, askAmount, signature, signer: connectedAddress }),
});

if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Error submitting grant proposal");
}
await postNewGrant({ title, description, askAmount, signature, signer: connectedAddress });

notification.success("Proposal submitted successfully!");
router.push("/");
Expand Down
1 change: 0 additions & 1 deletion packages/nextjs/app/apply/_component/SubmitButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useBGBuilderData } from "~~/hooks/useBGBuilderData";
const SubmitButton = () => {
const { pending } = useFormStatus();
const { isConnected, address: connectedAddress } = useAccount();
// ToDo. We could use builderData from global state
const { isBuilderPresent, isLoading: isFetchingBuilderData } = useBGBuilderData(connectedAddress);
const isSubmitDisabled = !isConnected || isFetchingBuilderData || pending || !isBuilderPresent;

Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { RainbowKitCustomConnectButton } from "./scaffold-eth";
import { useAccount } from "wagmi";
import { LockClosedIcon } from "@heroicons/react/24/outline";
import { useGlobalState } from "~~/services/store/store";
import { useBGBuilderData } from "~~/hooks/useBGBuilderData";

type HeaderMenuLink = {
label: string;
Expand All @@ -26,7 +27,8 @@ export const menuLinks: HeaderMenuLink[] = [

export const HeaderMenuLinks = () => {
const pathname = usePathname();
const builderData = useGlobalState(state => state.builderData);
const { address: connectedAddress } = useAccount();
const { data: builderData } = useBGBuilderData(connectedAddress);

return (
<>
Expand Down
22 changes: 6 additions & 16 deletions packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,17 @@ import { useEffect, useState } from "react";
import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
import { useTheme } from "next-themes";
import { Toaster } from "react-hot-toast";
import { WagmiConfig, useAccount } from "wagmi";
import { SWRConfig } from "swr";
import { WagmiConfig } from "wagmi";
import { Footer } from "~~/components/Footer";
import { Header } from "~~/components/Header";
import { BlockieAvatar } from "~~/components/scaffold-eth";
import { ProgressBar } from "~~/components/scaffold-eth/ProgressBar";
import { useBGBuilderData } from "~~/hooks/useBGBuilderData";
import { useGlobalState } from "~~/services/store/store";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
import { appChains } from "~~/services/web3/wagmiConnectors";
import { fetcher } from "~~/utils/swr";

const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
const { address } = useAccount();
const { data } = useBGBuilderData(address);

const setBuilderData = useGlobalState(state => state.setBuilderData);

useEffect(() => {
if (data?.id) {
setBuilderData(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setBuilderData, data?.id]);

return (
<>
<div className="flex flex-col min-h-screen font-spaceGrotesk">
Expand Down Expand Up @@ -56,7 +44,9 @@ export const ScaffoldEthAppWithProviders = ({ children }: { children: React.Reac
avatar={BlockieAvatar}
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
>
<ScaffoldEthApp>{children}</ScaffoldEthApp>
<SWRConfig value={{ fetcher: fetcher, revalidateOnFocus: false }}>
<ScaffoldEthApp>{children}</ScaffoldEthApp>
</SWRConfig>
</RainbowKitProvider>
</WagmiConfig>
);
Expand Down
Loading

0 comments on commit d8e4478

Please sign in to comment.