generated from scaffold-eth/scaffold-eth-2
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5197a5a
commit 2b28aaf
Showing
6 changed files
with
244 additions
and
16 deletions.
There are no files selected for viewing
148 changes: 148 additions & 0 deletions
148
packages/nextjs/app/admin/_components/EditGrantModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { ChangeEvent, forwardRef, useState } from "react"; | ||
import { useSWRConfig } from "swr"; | ||
import useSWRMutation from "swr/mutation"; | ||
import { useAccount, useNetwork, useSignTypedData } from "wagmi"; | ||
import { GrantDataWithBuilder } from "~~/services/database/schema"; | ||
import { EIP_712_DOMAIN, EIP_712_TYPES__EDIT_GRANT } from "~~/utils/eip712"; | ||
import { getParsedError, notification } from "~~/utils/scaffold-eth"; | ||
import { patchMutationFetcher } from "~~/utils/swr"; | ||
|
||
type EditGrantModalProps = { | ||
grant: GrantDataWithBuilder; | ||
closeModal: () => void; | ||
}; | ||
|
||
type ReqBody = { | ||
title?: string; | ||
description?: string; | ||
askAmount?: number; | ||
signature?: `0x${string}`; | ||
signer?: string; | ||
}; | ||
|
||
export const EditGrantModal = forwardRef<HTMLDialogElement, EditGrantModalProps>(({ grant, closeModal }, ref) => { | ||
const [formData, setFormData] = useState({ | ||
title: grant.title, | ||
description: grant.description, | ||
askAmount: grant.askAmount.toString(), | ||
}); | ||
|
||
const { address } = useAccount(); | ||
const { chain: connectedChain } = useNetwork(); | ||
const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); | ||
|
||
const { trigger: editGrant, isMutating } = useSWRMutation(`/api/grants/${grant.id}`, patchMutationFetcher<ReqBody>); | ||
const { mutate } = useSWRConfig(); | ||
|
||
const isLoading = isSigningMessage || isMutating; | ||
|
||
const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | ||
const { name, value } = e.target; | ||
setFormData(prevFormData => ({ | ||
...prevFormData, | ||
[name]: value, | ||
})); | ||
}; | ||
|
||
const handleEditGrant = async () => { | ||
if (!address || !connectedChain) { | ||
notification.error("Please connect your wallet"); | ||
return; | ||
} | ||
|
||
let notificationId: string | undefined; | ||
try { | ||
const signature = await signTypedDataAsync({ | ||
domain: EIP_712_DOMAIN, | ||
types: EIP_712_TYPES__EDIT_GRANT, | ||
primaryType: "Message", | ||
message: { | ||
grantId: grant.id, | ||
title: formData.title, | ||
description: formData.description, | ||
// Converting this to number with parseFloat and again to string (similar to backend), | ||
// if not it generates different signature with .23 and 0.23 | ||
askAmount: parseFloat(formData.askAmount).toString(), | ||
}, | ||
}); | ||
notificationId = notification.loading("Updating grant"); | ||
await editGrant({ | ||
signer: address, | ||
signature, | ||
...formData, | ||
askAmount: parseFloat(formData.askAmount), | ||
}); | ||
await mutate("/api/grants/review"); | ||
notification.remove(notificationId); | ||
notification.success(`Successfully updated grant ${grant.id}`); | ||
closeModal(); | ||
} catch (error) { | ||
console.error("Error editing grant", error); | ||
const errorMessage = getParsedError(error); | ||
notification.error(errorMessage); | ||
} finally { | ||
if (notificationId) notification.remove(notificationId); | ||
} | ||
}; | ||
|
||
return ( | ||
<dialog id="edit_grant_modal" className="modal" ref={ref}> | ||
<div className="modal-box flex flex-col space-y-3"> | ||
<form method="dialog"> | ||
{/* if there is a button in form, it will close the modal */} | ||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button> | ||
</form> | ||
<div className="flex justify-between items-center"> | ||
<p className="font-bold text-lg m-0"> | ||
Edit grant | ||
<span className="text-sm text-gray-500 ml-2">({grant.id})</span> | ||
</p> | ||
</div> | ||
<div className="w-full flex-col gap-1"> | ||
<p className="m-0 font-semibold text-base">Title</p> | ||
<input | ||
type="text" | ||
placeholder="title" | ||
name="title" | ||
value={formData.title} | ||
className="input input-sm input-bordered w-full" | ||
onChange={handleInputChange} | ||
/> | ||
</div> | ||
<div className="w-full flex-col gap-1"> | ||
<p className="m-0 font-semibold text-base">Description</p> | ||
<textarea | ||
name="description" | ||
placeholder="description" | ||
value={formData.description} | ||
className="textarea textarea-md textarea-bordered w-full" | ||
rows={5} | ||
onChange={handleInputChange} | ||
/> | ||
</div> | ||
<div className="w-full flex-col gap-1"> | ||
<p className="m-0 font-semibold text-base">Amount</p> | ||
<input | ||
type="number" | ||
name="askAmount" | ||
disabled={grant.status === "submitted"} | ||
placeholder="ask amount" | ||
value={formData.askAmount} | ||
className="input input-sm input-bordered w-full" | ||
onChange={handleInputChange} | ||
/> | ||
</div> | ||
<button | ||
className={`btn btn-md btn-success ${isLoading ? "opacity-50" : ""}`} | ||
onClick={handleEditGrant} | ||
disabled={isLoading} | ||
> | ||
{isLoading && <span className="loading loading-spinner"></span>} | ||
Submit | ||
</button> | ||
</div> | ||
</dialog> | ||
); | ||
}); | ||
|
||
EditGrantModal.displayName = "EditGrantModal"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { NextRequest, NextResponse } from "next/server"; | ||
import { recoverTypedDataAddress } from "viem"; | ||
import { updateGrant } from "~~/services/database/grants"; | ||
import { findUserByAddress } from "~~/services/database/users"; | ||
import { EIP_712_DOMAIN, EIP_712_TYPES__EDIT_GRANT } from "~~/utils/eip712"; | ||
|
||
type ReqBody = { | ||
title?: string; | ||
description?: string; | ||
askAmount?: number; | ||
signature?: `0x${string}`; | ||
signer?: string; | ||
}; | ||
|
||
export async function PATCH(req: NextRequest, { params }: { params: { grantId: string } }) { | ||
try { | ||
const { grantId } = params; | ||
const { title, description, signature, signer, askAmount } = (await req.json()) as ReqBody; | ||
|
||
if (!title || !description || !askAmount || typeof askAmount !== "number" || !signature || !signer) { | ||
return NextResponse.json({ error: "Invalid form details submited" }, { status: 400 }); | ||
} | ||
|
||
const recoveredAddress = await recoverTypedDataAddress({ | ||
domain: EIP_712_DOMAIN, | ||
types: EIP_712_TYPES__EDIT_GRANT, | ||
primaryType: "Message", | ||
message: { title, description, askAmount: askAmount.toString(), grantId }, | ||
signature: signature, | ||
}); | ||
if (recoveredAddress !== signer) { | ||
return NextResponse.json({ error: "Recovered address did not match signer" }, { status: 401 }); | ||
} | ||
|
||
// Only admins can edit grant | ||
const signerData = await findUserByAddress(signer); | ||
if (signerData.data?.role !== "admin") { | ||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
} | ||
|
||
await updateGrant(grantId, { title, description, askAmount }); | ||
return NextResponse.json({ success: true }); | ||
} catch (e) { | ||
console.error(e); | ||
return NextResponse.json({ error: "Error editing grant" }, { status: 500 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters