forked from passportxyz/passport-scorer
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add three sample apps (passportxyz#214)
* Add three sample apps * Add tooltip * Update denied and dashboard styles * Update AirDrop styles * Add admin address table * Deploy * Fix quotes * Fix quotes again * Add ability for admin to manual add to airdrop * Fix admin add * Remove console logs * Consolidate env files and remove console logs * Add info cards to admin dashboard * Add pagination * Style pagination * Add configure theme * Commit to rebuild * Add keys * Replace a with Link * Allow for admin theme customization * Style configure theme page * Style configure link * Add knex * Change knex configuration * Add ssl * Add env vars to knexfile * Fix getServerSideProps for admin dashboard
- Loading branch information
Showing
94 changed files
with
30,099 additions
and
0 deletions.
There are no files selected for viewing
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,3 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
} |
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,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
.pnpm-debug.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
.env | ||
|
||
dev.sqlite3 |
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,54 @@ | ||
# AirDrop | ||
|
||
## Introduction | ||
|
||
This sample app connects to a user's wallet, then fetches their passport score from the passport scorer API and uses it to determine if they are allowed to claim an airdrop. | ||
|
||
## Getting Started | ||
|
||
### Create your API key and Scorer | ||
|
||
1. Create your API key by going to [Gitcoin Passport Scorer](https://scorer.gitcoin.co) and clicking on the "API Keys" section. | ||
Then create a `.env.local` file and copy the contents of the `example.local.env` file into it. | ||
Replace `SCORER_API_KEY` with your API key. | ||
|
||
![Create api key](./screenshots/crete_api_key.png) | ||
|
||
2. Create a Scorer, by clicking on the "Scorer" section. | ||
Replace `NEXT_PUBLIC_SCORER_ID` with your Scorer ID in `.env.local`. | ||
|
||
![Create scorer](./screenshots/create_scorer.png) | ||
|
||
3. Run migrations `npx knex migrate:latest` | ||
|
||
### Start the app | ||
|
||
Now you can start the app by running: | ||
|
||
```bash | ||
cd examples/airdrop && npm install | ||
``` | ||
|
||
then | ||
|
||
```bash | ||
npm run dev | ||
``` | ||
|
||
Finally, you can navigate to `http://localhost:3000` to view the sample app. | ||
|
||
Navigate to `http://localhost:3000/admin/dashboard` to view the admin dashboard. | ||
|
||
### Layout | ||
|
||
Most of the logic for connecting to the passport scorer API can be found in `/components/AirDrop.js` | ||
|
||
Inside this component we: | ||
|
||
1. Fetch a message and nonce from the scorer API. | ||
2. Prompt the user to sign the message. | ||
3. Submit the user's passport for scoring. | ||
4. Fetch the user's passport score. | ||
5. Use the score to determine if they are eligible for the airdrop. | ||
|
||
All of the interaction with the scorer API is proxied through our endpoints in `/pages/api`. This is done to prevent our `SCORER_API_KEY` env variable from being exposed to the frontend. |
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,38 @@ | ||
import styles from "@/styles/Dashboard.module.css"; | ||
import { useState } from "react"; | ||
|
||
export default function AddAirdrop({ cancel, add }) { | ||
const [address, setAddress] = useState(""); | ||
|
||
return ( | ||
<div | ||
style={{ | ||
marginTop: "21px", | ||
display: "flex", | ||
justifyContent: "flex-end", | ||
}} | ||
> | ||
<input | ||
onChange={(e) => setAddress(e.target.value)} | ||
style={{ | ||
padding: "7px 5px", | ||
borderRadius: "10px", | ||
marginRight: "10px", | ||
}} | ||
placeholder="0x..." | ||
type="text" | ||
/> | ||
<button onClick={() => add(address)} className={styles.btn} type="submit"> | ||
Add | ||
</button> | ||
<button | ||
onClick={cancel} | ||
style={{ marginLeft: "10px" }} | ||
className={styles.btn} | ||
type="button" | ||
> | ||
Cancel | ||
</button> | ||
</div> | ||
); | ||
} |
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,186 @@ | ||
import { useState, useEffect } from "react"; | ||
import axios from "axios"; | ||
import { verifyMessage } from "ethers/lib/utils"; | ||
import { useAccount, useSignMessage } from "wagmi"; | ||
import styles from "@/styles/AirDrop.module.css"; | ||
|
||
export default function AirDrop() { | ||
useEffect(() => { | ||
setIsMounted(true); | ||
}, []); | ||
|
||
const { address } = useAccount({ | ||
onDisconnect() { | ||
reset(); | ||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
reset(); | ||
// Query the airdrop_addresses table to check if this user is already in the aidrop list. | ||
// If they are there is no need to connect to the scorer API, we can display their passport score immediately. | ||
async function checkIfAddressAlreadyInAirdropList() { | ||
const resp = await axios.post(`/api/airdrop/check/${address}`); | ||
if (resp?.data?.address) { | ||
setChecked(true); | ||
setPassportScore(resp.data.score); | ||
} | ||
} | ||
checkIfAddressAlreadyInAirdropList(); | ||
}, [address]); | ||
|
||
async function addToAirdrop() { | ||
setNonce(""); | ||
setPassportScore(0); | ||
// Step #1 (Optional, only required if using the "signature" param when submitting a user's passport. See https://docs.passport.gitcoin.co/building-with-passport/scorer-api/endpoint-definition#submit-passport) | ||
// We call our /api/scorer-message endpoint (/pages/api/scorer-message.js) which internally calls /registry/signing-message | ||
// on the scorer API. Instead of calling /registry/signing-message directly, we call it via our api endpoint so we do not | ||
// expose our scorer API key to the frontend. | ||
// This will return a response like: | ||
// { | ||
// message: "I hereby agree to submit my address in order to score my associated Gitcoin Passport from Ceramic.", | ||
// nonce: "b7e3b0f86820744b9242dd99ce91465f10c961d98aa9b3f417f966186551" | ||
// } | ||
const scorerMessageResponse = await axios.get("/api/scorer-message"); | ||
if (scorerMessageResponse.status !== 200) { | ||
console.error("failed to fetch scorer message"); | ||
return; | ||
} | ||
setNonce(scorerMessageResponse.data.nonce); | ||
|
||
// Step #2 (Optional, only required if using the "signature" param when submitting a user's passport.) | ||
// Have the user sign the message that was returned from the scorer api in Step #1. | ||
signMessage({ message: scorerMessageResponse.data.message }); | ||
} | ||
|
||
const { signMessage } = useSignMessage({ | ||
async onSuccess(data, variables) { | ||
// Verify signature when sign message succeeds | ||
const address = verifyMessage(variables.message, data); | ||
|
||
// Step #3 | ||
// Now that we have the signature from the user, we can submit their passport for scoring | ||
// We call our /api/submit-passport endpoint (/pages/api/submit-passport.js) which | ||
// internally calls /registry/submit-passport on the scorer API. | ||
// This will return a response like: | ||
// { | ||
// address: "0xabc", | ||
// error: null, | ||
// evidence: null, | ||
// last_score_timestamp: "2023-03-26T15:17:03.393567+00:00", | ||
// score: null, | ||
// status: "PROCESSING" | ||
// } | ||
const submitResponse = await axios.post("/api/submit-passport", { | ||
address: address, // Required: The user's address you'd like to score. | ||
community: process.env.NEXT_PUBLIC_SCORER_ID, // Required: get this from one of your scorers in the Scorer API dashboard https://scorer.gitcoin.co/ | ||
signature: data, // Optional: The signature of the message returned in Step #1 | ||
nonce: nonce, // Optional: The nonce returned in Step #1 | ||
}); | ||
|
||
// Step #4 | ||
// Finally, we can attempt to add the user to the airdrop list. | ||
// We call our /api/airdrop/{scorer_id}/{address} endpoint (/pages/api/airdrop/[scorer_id]/[address].js) which internally calls | ||
// /registry/score/{scorer_id}/{address} | ||
// This will return a response like: | ||
// { | ||
// address: "0xabc", | ||
// error: null, | ||
// evidence: null, | ||
// last_score_timestamp: "2023-03-26T15:17:03.393567+00:00", | ||
// score: "1.574606692", | ||
// status: ""DONE"" | ||
// } | ||
// We check if the returned score is above our threshold to qualify for the airdrop. | ||
// If the score is above our threshold, we add the user to the airdrop list. | ||
const scoreResponse = await axios.get( | ||
`/api/airdrop/add/${process.env.NEXT_PUBLIC_SCORER_ID}/${address}` | ||
); | ||
|
||
// Make sure to check the status | ||
if (scoreResponse.data.status === "ERROR") { | ||
setPassportScore(0); | ||
alert(scoreResponse.data.error); | ||
return; | ||
} | ||
|
||
// Store the user's passport score for later use. | ||
setPassportScore(scoreResponse.data.score); | ||
setChecked(true); | ||
|
||
if (scoreResponse.data.score < 1) { | ||
alert("Sorry, you're not eligible for the airdrop."); | ||
} else { | ||
alert("You're eligible for the airdrop! Your address has been added."); | ||
} | ||
}, | ||
}); | ||
|
||
function reset() { | ||
setNonce(""); | ||
setPassportScore(0); | ||
setChecked(false); | ||
} | ||
|
||
// This isMounted check is needed to prevent hydration errors with next.js server side rendering. | ||
// See https://github.com/wagmi-dev/wagmi/issues/542 for more details. | ||
const [isMounted, setIsMounted] = useState(false); | ||
const [nonce, setNonce] = useState(""); | ||
const [passportScore, setPassportScore] = useState(0); | ||
const [checked, setChecked] = useState(false); | ||
|
||
function display() { | ||
if (isMounted && address) { | ||
if (checked) { | ||
if (passportScore < 1) { | ||
return ( | ||
<div> | ||
<p className={styles.p}> | ||
Your score isn't high enough, collect more stamps to | ||
qualify. | ||
</p> | ||
<p style={{ marginTop: "20px" }} className={styles.p}> | ||
Passport Score:{" "} | ||
<span style={{ color: "rgb(111 63 245" }}> | ||
{passportScore | 0} | ||
</span> | ||
/1 | ||
</p> | ||
<div style={{ marginTop: "10px" }}> | ||
<a | ||
className={styles.link} | ||
target="_blank" | ||
rel="noreferrer" | ||
href="https://passport.gitcoin.co" | ||
> | ||
Click here to increase your score. | ||
</a> | ||
</div> | ||
</div> | ||
); | ||
} else { | ||
return ( | ||
<div> | ||
<p className={styles.p}>You've been added to the airdrop!</p> | ||
</div> | ||
); | ||
} | ||
} else { | ||
return ( | ||
<button className={styles.btn} onClick={() => addToAirdrop(address)}> | ||
Add address to airdrop | ||
</button> | ||
); | ||
} | ||
} else { | ||
return ( | ||
<p className={styles.p}> | ||
Connect your wallet to find out if you're eligible for the | ||
airdrop. | ||
</p> | ||
); | ||
} | ||
} | ||
|
||
return <div>{display()}</div>; | ||
} |
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,16 @@ | ||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||
import styles from "@/styles/InfoCard.module.css"; | ||
|
||
export default function InfoCard({ title, icon, value }) { | ||
return ( | ||
<div className={styles.card}> | ||
<div className={styles.header}> | ||
<h3 className={styles.title}>{title}</h3> | ||
<div className={styles.iconContainer}> | ||
<FontAwesomeIcon color="rgb(108, 122, 137)" icon={icon} /> | ||
</div> | ||
</div> | ||
<p className={styles.value}>{value}</p> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.