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
8d19074
commit 2d0bea5
Showing
12 changed files
with
356 additions
and
1 deletion.
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
packages/nextjs/app/api/builders/[builderAddress]/route.ts
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,17 @@ | ||
import { NextResponse } from "next/server"; | ||
import { findUserByAddress } from "~~/services/database/users"; | ||
|
||
export async function GET(_request: Request, { params }: { params: { builderAddress: string } }) { | ||
try { | ||
const builderAddress = params.builderAddress; | ||
const builderData = await findUserByAddress(builderAddress); | ||
return NextResponse.json(builderData); | ||
} catch (error) { | ||
return NextResponse.json( | ||
{ message: "Internal Server Error" }, | ||
{ | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
"use server"; | ||
|
||
import { verifyMessage } from "viem"; | ||
import { createGrant } from "~~/services/database/grants"; | ||
import { findUserByAddress } from "~~/services/database/users"; | ||
|
||
type SignatureAndSigner = { | ||
signature?: `0x${string}`; | ||
address?: string; | ||
}; | ||
|
||
export const submitGrantAction = async ({ signature, address }: SignatureAndSigner, form: FormData) => { | ||
try { | ||
const formData = Object.fromEntries(form.entries()); | ||
if (!formData.title || !formData.description || !formData.askAmount) { | ||
throw new Error("Invalid form data"); | ||
} | ||
|
||
if (!signature || !address) { | ||
throw new Error("Signature and address are required to submit grant"); | ||
} | ||
|
||
const constructedMessage = JSON.stringify(formData); | ||
const isMessageValid = await verifyMessage({ message: constructedMessage, signature, address }); | ||
if (!isMessageValid) { | ||
throw new Error("Invalid signature"); | ||
} | ||
|
||
// Verif if the builder is present | ||
const builder = await findUserByAddress(address); | ||
if (!builder.exists) { | ||
throw new Error("Only buidlguild builders can submit for grants"); | ||
} | ||
|
||
// Save the form data to the database | ||
const grant = await createGrant({ | ||
title: formData.title as string, | ||
description: formData.description as string, | ||
askAmount: Number(formData.askAmount), | ||
builder: address, | ||
}); | ||
|
||
return grant; | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
throw e; | ||
} | ||
|
||
throw new Error("Error processing form"); | ||
} | ||
}; |
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,88 @@ | ||
"use client"; | ||
|
||
import { useRouter } from "next/navigation"; | ||
import { submitGrantAction } from "../_actions"; | ||
import SubmitButton from "./SubmitButton"; | ||
import { useAccount, useSignMessage } from "wagmi"; | ||
import { notification } from "~~/utils/scaffold-eth"; | ||
|
||
const selectOptions = [0.1, 0.25, 0.5, 1]; | ||
|
||
const Form = () => { | ||
const { signMessageAsync } = useSignMessage(); | ||
const { address: connectedAddress } = useAccount(); | ||
const router = useRouter(); | ||
|
||
const clientFormAction = async (formData: FormData) => { | ||
try { | ||
const formState = Object.fromEntries(formData.entries()); | ||
if (formState.title === "" || formState.description === "") { | ||
notification.error("Title and description are required"); | ||
return; | ||
} | ||
|
||
const signature = await signMessageAsync({ message: JSON.stringify(formState) }); | ||
const signedMessageObject = { | ||
signature: signature, | ||
address: connectedAddress, | ||
}; | ||
|
||
// server action | ||
const submitGrantActionWithSignedMessage = submitGrantAction.bind(null, signedMessageObject); | ||
await submitGrantActionWithSignedMessage(formData); | ||
|
||
notification.success("Proposal submitted successfully!"); | ||
router.push("/"); | ||
} catch (error: any) { | ||
if (error instanceof Error) { | ||
notification.error(error.message); | ||
return; | ||
} | ||
notification.error("Something went wrong"); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="card card-compact w-96 bg-base-100 shadow-xl"> | ||
<form action={clientFormAction} className="card-body space-y-4"> | ||
<h2 className="card-title self-center text-3xl !mb-0">Submit Proposal</h2> | ||
<div className="space-y-2"> | ||
<p className="m-0 text-xl ml-2">Title</p> | ||
<div className="flex border-2 border-base-300 bg-base-200 rounded-full text-accent"> | ||
<input | ||
className="input input-ghost focus-within:border-transparent focus:outline-none focus:bg-transparent focus:text-gray-400 h-[2.2rem] min-h-[2.2rem] px-4 border w-full font-medium placeholder:text-accent/50 text-gray-400" | ||
placeholder="title" | ||
name="title" | ||
autoComplete="off" | ||
/> | ||
</div> | ||
</div> | ||
<div className="space-y-2"> | ||
<p className="m-0 text-xl ml-2">Description</p> | ||
<div className="flex border-2 border-base-300 bg-base-200 rounded-3xl text-accent"> | ||
<textarea | ||
className="input input-ghost focus-within:border-transparent focus:outline-none focus:bg-transparent focus:text-gray-400 px-4 pt-2 border w-full font-medium placeholder:text-accent/50 text-gray-400 h-28 rounded-none" | ||
placeholder="description" | ||
name="description" | ||
autoComplete="off" | ||
/> | ||
</div> | ||
</div> | ||
<div className="space-y-2"> | ||
<p className="m-0 text-xl ml-2">Ask amount</p> | ||
<select className="select bg-base-200 select-primary select-md select-bordered w-full" name="askAmount"> | ||
<option disabled>Select amount</option> | ||
{selectOptions.map(option => ( | ||
<option key={option} value={option}> | ||
{option} ETH | ||
</option> | ||
))} | ||
</select> | ||
</div> | ||
<SubmitButton /> | ||
</form> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Form; |
33 changes: 33 additions & 0 deletions
33
packages/nextjs/app/submit-grant/_component/SubmitButton.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,33 @@ | ||
"use client"; | ||
|
||
import { useFormStatus } from "react-dom"; | ||
import { useAccount } from "wagmi"; | ||
import { useBGBuilderData } from "~~/hooks/useBGBuilderData"; | ||
|
||
// To use useFormStatus we need to make sure button is child of form | ||
const SubmitButton = () => { | ||
const { pending } = useFormStatus(); | ||
const { isConnected, address: connectedAddress } = useAccount(); | ||
const { isBuilderPresent, isLoading: isFetchingBuilderData } = useBGBuilderData(connectedAddress); | ||
const isSubmitDisabled = !isConnected || isFetchingBuilderData || pending || !isBuilderPresent; | ||
|
||
return ( | ||
<div | ||
className={`flex ${(!isConnected || !isBuilderPresent) && "tooltip tooltip-bottom"}`} | ||
data-tip={`${ | ||
!isConnected | ||
? "Please connect your wallet" | ||
: !isBuilderPresent | ||
? "You should be a buidlguidl builder to submit grant" | ||
: "" | ||
}`} | ||
> | ||
<button className="btn btn-primary w-full" disabled={isSubmitDisabled} aria-disabled={isSubmitDisabled}> | ||
{(isFetchingBuilderData || pending) && <span className="loading loading-spinner loading-md"></span>} | ||
Submit | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
export default SubmitButton; |
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,12 @@ | ||
import Form from "./_component/Form"; | ||
import { NextPage } from "next"; | ||
|
||
const SubmitGrant: NextPage = () => { | ||
return ( | ||
<div className="flex items-center flex-col flex-grow pt-10"> | ||
<Form /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default SubmitGrant; |
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,46 @@ | ||
import { useEffect, useState } from "react"; | ||
import { BuilderData, BuilderDataResponse } from "~~/services/database/schema"; | ||
|
||
export const useBGBuilderData = (address?: string) => { | ||
const [data, setData] = useState<BuilderData | null>(null); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const [error, setError] = useState<Error | null>(null); | ||
const [isBuilderPresent, setIsBuilderPresent] = useState(false); | ||
|
||
useEffect(() => { | ||
if (!address) { | ||
setData(null); | ||
setError(null); | ||
setIsBuilderPresent(false); | ||
return; | ||
} | ||
|
||
const fetchBuilderData = async () => { | ||
setIsLoading(true); | ||
try { | ||
const response = await fetch(`/api/builders/${address}`); | ||
if (!response.ok) { | ||
throw new Error("An error occurred while fetching builder data"); | ||
} | ||
const jsonData: BuilderDataResponse = await response.json(); | ||
if (!jsonData.exists || !jsonData.data) { | ||
setData(null); | ||
setIsBuilderPresent(false); | ||
return; | ||
} | ||
|
||
setData(jsonData.data); | ||
setIsBuilderPresent(true); | ||
} catch (err: any) { | ||
setError(err); | ||
setIsBuilderPresent(false); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
fetchBuilderData(); | ||
}, [address]); | ||
|
||
return { isLoading, error, data, isBuilderPresent }; | ||
}; |
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,22 @@ | ||
import { getFirestoreConnector } from "./firestoreDB"; | ||
import { GrantData } from "./schema"; | ||
|
||
const firestoreDB = getFirestoreConnector(); | ||
const grantsCollection = firestoreDB.collection("grants"); | ||
// const getGrantsDoc = (id: string) => grantsCollection.doc(id); | ||
// const getGrantSnapshotById = (id: string) => getGrantsDoc(id).get(); | ||
|
||
export const createGrant = async (grantData: Omit<GrantData, "id" | "timestamp" | "status">) => { | ||
try { | ||
const timestamp = new Date().getTime(); | ||
const status = "pending"; | ||
|
||
const grantRef = await grantsCollection.add({ ...grantData, timestamp, status }); | ||
const grantSnapshot = await grantRef.get(); | ||
|
||
return { id: grantSnapshot.id, ...grantSnapshot.data() } as GrantData; | ||
} catch (error) { | ||
console.error("Error creating the grant:", error); | ||
throw error; | ||
} | ||
}; |
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,49 @@ | ||
type SocialLinks = { | ||
twitter?: string; | ||
github?: string; | ||
discord?: string; | ||
telegram?: string; | ||
instagram?: string; | ||
email?: string; | ||
}; | ||
|
||
type Build = { | ||
submittedTimestamp: number; | ||
id: string; | ||
}; | ||
|
||
type Status = { | ||
text: string; | ||
timestamp: number; | ||
}; | ||
|
||
type Graduated = { | ||
reason: string; | ||
status: boolean; | ||
}; | ||
|
||
export type BuilderData = { | ||
id: string; | ||
socialLinks?: SocialLinks; | ||
role?: string; | ||
function?: string; | ||
creationTimestamp?: number; | ||
builds?: Build[]; | ||
status?: Status; | ||
graduated?: Graduated; | ||
}; | ||
|
||
export type BuilderDataResponse = { | ||
exists: boolean; | ||
data?: BuilderData; | ||
}; | ||
|
||
export type GrantData = { | ||
id: string; | ||
title: string; | ||
description: string; | ||
askAmount: number; | ||
builder: string; | ||
timestamp: number; | ||
status: "pending" | "approved" | "completed" | "rejected"; | ||
}; |
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,21 @@ | ||
import { getFirestoreConnector } from "./firestoreDB"; | ||
import { BuilderDataResponse } from "./schema"; | ||
import { DocumentData } from "firebase-admin/firestore"; | ||
|
||
const firestoreDB = getFirestoreConnector(); | ||
const getUserDoc = (id: string) => firestoreDB.collection("users").doc(id); | ||
const getUserSnapshotById = (id: string) => getUserDoc(id).get(); | ||
|
||
export const findUserByAddress = async (builderAddress: string): Promise<BuilderDataResponse> => { | ||
try { | ||
const builderSnapshot = await getUserSnapshotById(builderAddress); | ||
if (!builderSnapshot.exists) { | ||
return { exists: false }; | ||
} | ||
const data = builderSnapshot.data() as DocumentData; | ||
return { exists: true, data: { id: builderSnapshot.id, ...data } }; | ||
} catch (error) { | ||
console.error("Error finding user by address:", error); | ||
throw error; | ||
} | ||
}; |
Oops, something went wrong.