diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..823f8b6 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68b5624 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/cache +/out diff --git a/.gitmodules b/.gitmodules index 36c2b57..7c0e3b1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "smart_contracts/lib/forge-std"] - path = smart_contracts/lib/forge-std - url = https://github.com/foundry-rs/forge-std + path = smart_contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std [submodule "smart_contracts/lib/openzeppelin-contracts"] - path = smart_contracts/lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts + path = smart_contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.gitmodules.orig b/.gitmodules.orig new file mode 100644 index 0000000..7c0e3b1 --- /dev/null +++ b/.gitmodules.orig @@ -0,0 +1,6 @@ +[submodule "smart_contracts/lib/forge-std"] + path = smart_contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "smart_contracts/lib/openzeppelin-contracts"] + path = smart_contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/README.md b/README.md index 3619819..64403eb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Ceptor Club Tech Team - Constellation - A CHAINLINK HACKATHON | NOV 8 – DEC 10 Todo: -- [ ] Provide a quick start guide for judges (update, based on ETHChicago readme) + +- [x] Provide a quick start guide for devs and judges - [x] Explain the 3 teams (Art + Tech + Games) and how they work together -- [ ] High level overview of Ceptor Club and our goal for this hackathon / future -- [ ] Onboard all 3 teams to this repo +- [x] High level [overview of Ceptor Club and our goal for this hackathon](https://docs.google.com/document/d/1tzb8Nj6E7Tyr8YrIWLrJBskozBuCecXe3IotgcpygKo/edit?usp=sharing) / future +- [x] Onboard all 3 teams to this repo - [ ] Add Art, Tech, and Games pages to the navbar and front-end. ### We are providing a unified codebase for all 3 teams and clear documentation for the judges: @@ -19,7 +20,7 @@ Each team will have their own narrative, demo video, internal page and readme li - Tippi (Product Owner and CCID/CCIP Developer) - Eman (Chainlink Developer Expert -- Functions, Automation, VRF) -- Alica (PM and Designer) +- Amy (Executive Producer) - Lena (Web3 Fullstack with a heart of gold) - Aire (Front-end support) @@ -27,10 +28,11 @@ Each team will have their own narrative, demo video, internal page and readme li > Team Art (Artour) will support overall branding and help with our demo videos/team narratives, in addition to creating an end-to-end session zero experience following character creation. They are making sure our app and images generation looks good. -- Michael Finney -- Melinda McClimans -- Spencer Kinney -- leomofthings.eth (Front End Development) +- Michael Finney (Design & Video) +- Melinda McClimans (Product & Research) +- Spencer Kinney (Engineer) +- Sam Yeh (boy wonder, and unofficial mascot -- creator of the AI prompt battler) +- Creatjve (Branding and Wizards Hat) Link to readme: @@ -38,10 +40,147 @@ Link to readme: > Team Games will make sure our overall project is fun and usable. They are building a couple small games and activities: Improved character creation and fun quiz, rollable 3D Big Dice, AI Prompt Battler, to make our app more “sticky.” -- Sam Yeh -- Jason A -- Vince -- Alan Barry -- Nadia +- leomofthings.eth (Updating the Quiz UI) +- Jason A (Team Lead and D20 Smart Contract) +- Vince Lindsey (Scheduling UI & fullstack jr dev) +- Alan Barry (Backend engineer and early-riser) +- Nadia (3D modeling of Rollable D20) -Link to readme: +[Link to GAMES readme](https://github.com/ceptor-club/Ceptor-Tech/blob/temp2/README-GAMES.md), **temp2 is Games Team's prod branch.** + +# Getting Started + +1. Clone this directory + + `git clone https://github.com/ceptor-club/Ceptor-Tech.git` + +2. It will create three folders: `backend` and `frontend` (and `smart_contracts`). + +## 🧑‍💻 Let's Setup Frontend First + +1. Go to frontend folder + + `cd frontend` + +2. Install dependencies + + `npm install` + +3. Clone .env.sample to .env + + `cp .env.sample .env` + +4. Setting up .env variables + + - Setting up Network and Chain Id + + - `NEXT_PUBLIC_NETWORK=goerli` + - `NEXT_PUBLIC_CHAIN_ID=5` + + - Setting up Alchemy API Key + + - Go to [Alchemy Dashboard](https://dashboard.alchemy.com/) and create an account(if you don't have one already) + - Create a new project based on your `NEXT_PUBLIC_NETWORK` variable and copy the API Key + - Paste the API Key in `.env` file as `NEXT_PUBLIC_ALCHEMY_KEY=https://eth-goerli.g.alchemy.com/v2/4nxF5nQMK20uGJinU_sample` + + - Setting up WALLETCONNECT_PROJECT_ID for wallet authentication - + + - Go to [WalletConnect Dashboard](https://cloud.walletconnect.com/app) and create an account(if you don't have one already) + - Create a new project and copy the API Key + - Paste the API Key in `.env` file as `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=f88d2c49_sample` + + - Setting up OPENAI_API_KEY (optional) + + - Go to [OpenAI Dashboard](https://platform.openai.com/account/api-keys) and create an account(if you don't have one already) + - Create a new project and copy the API Key + - Paste the API Key in `.env` file as `NEXT_PUBLIC_OPENAI_API_KEY=sk-<34f34qw_sample>` + + - Setting up NFT.storage api for storing images on IPFS + + - Go to [NFT.storage Dashboard](https://nft.storage/) and create an account(if you don't have one already) + - Create a new project and copy the API Key + - Paste the API Key in `.env` file as `NEXT_PUBLIC_NFT_STORAGE=eyJhbGciOiJIUzI1NiIsI_sample` + +5. Run the frontend + + `npm run dev` + +6. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result and enjoy! + + `Need to setup backend now to see the full functionality of the frontend, if already setup then skip to step 7` + +7. Check that the back-end API is functioning by uploading the fastcharacter.com sheet pdf and generating an image. + +## 🖥️ Let's Setup smart contract + +1. Go to Backend folder + + `cd smart_contracts` + +2. Install all the dependencies + + `npm install` + `forge install` + +3. Clone .env.example to .env + + `cp .env.example .env` + +4. Setting up .env variables + +- PRIVATE_KEY= +- ETHEREUM_SEPOLIA_RPC_URL= +- AVALANCHE_FUJI_RPC_URL= + +5. to compile the smart contract + `forge compile` +6. to deploy the smart contract + `forge script ./script/deployer.sol -vvv --rpc-url ethereumSepolia --broadcast --sig "deployAll()" ` + `forge script ./script/deployer2.sol -vvv --rpc-url polygonZKEVM --broadcast --sig "deployAll(uint8,uint8,uint8,address)" -- 0 2 4 0xB1eB539E7E845991F2915967f96838025277804C` + `npx hardhat deploy --network sepolia ` + `forge script ./script/deployer.sol -vvv --rpc-url polygonMumbai --broadcast --sig "postDeploy(address,address,address,address,address)" -- 0x557d4b0a566613f344E2F5a9bc4FC52F6897B372 0xB4e5136F4BADDdF23a720A14a6a0D0d60A5ee9ee 0xf965217a040cc52354d55ed6D41112bb5f9Bf9D2 0x359A9d940692e09a318C1D690eb1cEDBBBb7E03f 0x45DB01904b51857F6279FE9006De25bf6dE8d136 ` + where addresses are in the following order address prompt, address dice, address ceptor , address ccid,address reward + +## 🖥️ Let's Setup Backend + +1. Go to Backend folder + + `cd backend` + +2. Install all the dependencies + + `npm install` + +3. Clone .env.example to .env + + `cp .env.example .env` + +4. Setting up .env variables + + - For ALCHEMY_API_KEY use the same key as in frontend + - Setup API_KEY="testKey" for Socket io connections + - Setup DB_CONN_STRING=mongodb+srv://verinta:BFWmxukoOsNdIx4x@ceptorclub.rq4oohp.mongodb.net/ for db connection + +5. Renable CORS object + +6. Run the backend + + `npm run dev` + +# Learn More + +## **🛠️ MongoDB Setup: Endpoints for user creation and lookup!** + +Put the connection string: `mongodb+srv://ceptorclub:dajfMXIwMzwM8ssI@ceptor.pgtoahq.mongodb.net/?retryWrites=true&w=majority` into mongodb compass and see the db + +GET /user?wallet= will give you the user +POST /user with + +``` +{ +name: "blap", +email: "sldkfjh@klfj.com", +wallet: "0x12373fn...", +whatever: "else we want to save about users as we go (no, not images)" +} +``` diff --git a/backend/.env.example b/backend/.env.example index bc98e1c..db71212 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,14 +3,14 @@ PORT=4000 # General #============================================= # mainnet/goerli etc. -ETH_NETWORK=goerli +ETH_NETWORK=sepolia FRONTEND_URL=http://localhost:3000 API_KEY= ALCHEMY_API_KEY= -SD_API_ENDPOINT= +SD_API_ENDPOINT=https://stable-diffusion10.p.rapidapi.com/generate-image #============================================= @@ -18,5 +18,5 @@ SD_API_ENDPOINT= #============================================= DB_CONN_STRING= -DB_COLLECTION=users +DB_COLLECTION=users, characterData, submission DB_NAME=CeptorClub \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index cb9b4b4..176f30c 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -20,4 +20,6 @@ npm-debug.log* yarn-debug.log* -yarn-error.log* \ No newline at end of file +yarn-error.log* + +package-lock.json diff --git a/backend/README.md b/backend/README.md index 998ee5d..25ac798 100644 --- a/backend/README.md +++ b/backend/README.md @@ -8,13 +8,13 @@ frontend with sockets in context ready to be used: https://github.com/DnDnDiffus > interested in seeing what parts of the front-end are connecting to the sockets? > -> See the front-end above for relevant code in **utils/socketContext** and **components/GenerateButton.jsx** +> See the front-end above for relevant code in **utils/socketContext** and **components/Buttons/GenerateButton.jsx** ## installation 1. npm install -2. create a .env with the proper variables -3. reenable the cors object +2. create a .env with the proper variables (SD_ENDPOINT: https://stable-diffusion10.p.rapidapi.com/generate-image) _thanks to Team Art (Spencer!)_ +3. reenable the cors object (maybe) 4. npm run dev ## other stuff @@ -27,7 +27,7 @@ You need to run the backend in conjunction with the frontend to get this to work ## Now also has endpoints for user creation and lookup! -you can put `mongodb+srv://ceptorclub:dajfMXIwMzwM8ssI@ceptor.pgtoahq.mongodb.net/?retryWrites=true&w=majority` into mongodb compass and see the db +you can put `mongodb+srv://verinta:BFWmxukoOsNdIx4x@ceptorclub.rq4oohp.mongodb.net/` into mongodb compass and see the db GET /user?wallet= will give you the user POST /user with diff --git a/backend/src/app.ts b/backend/src/app.ts index 1fcffa7..c269a44 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -73,4 +73,4 @@ io.on("connection", (socket: any) => { server.listen(process.env.PORT, () => console.log(`Server Running on ${process.env.PORT}`) -); +); \ No newline at end of file diff --git a/backend/src/utils/getImages.ts b/backend/src/utils/getImages.ts index 542fbbf..b68a266 100644 --- a/backend/src/utils/getImages.ts +++ b/backend/src/utils/getImages.ts @@ -41,4 +41,4 @@ export async function getImages(data: any) { return error; }); return imagesData; -} +} \ No newline at end of file diff --git a/backend/src/utils/mongo.ts b/backend/src/utils/mongo.ts index 4a55bb9..c0708de 100644 --- a/backend/src/utils/mongo.ts +++ b/backend/src/utils/mongo.ts @@ -28,4 +28,4 @@ export async function getUser(wallet: string) { console.error(error) return error } -} +} \ No newline at end of file diff --git a/frontend/.env.sample b/frontend/.env.sample index 05f88eb..92db150 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -1,12 +1,20 @@ +# IMAGE GENERATION KEYS SD_API_ENDPOINT= - OPENAI_API_KEY= +# NEXT KEYS NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= - NEXT_PUBLIC_NFT_STORAGE= +NEXT_PUBLIC_API_BASE_URL=https://ceptor-backend-staging.up.railway.app +# NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +NEXT_PUBLIC_NETWORK_ID=5 +NEXT_PUBLIC_NETWORK=sepolia +NEXT_PUBLIC_API_KEY= +PUBLIC_IMAGE_URL=https://github.com/ceptor-club/Ceptor-Tech/tree/tech-dev/frontend/public/images/avatars/ +# API KEYS ALCHEMY_API_KEY= - -NEXT_PUBLIC_NETWORK_ID= -NEXT_PUBLIC_NETWORK= \ No newline at end of file +ALCHEMY_SEPOLIA_API_KEY= +ALCHEMY_GOERLI_API_KEY= +ALCHEMY_POLYGON_ZKEVM_API_KEY= +AVALANCHE_API_KEY=https://api.avax.network/ext/bc/C/rpc diff --git a/frontend/.gitignore b/frontend/.gitignore index 8972206..933721c 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -38,4 +38,6 @@ notes next-env.d.ts .vscode -.history \ No newline at end of file +.history +package-lock.json + diff --git a/frontend/components/Buttons/AdvancedButton.jsx b/frontend/components/Buttons/AdvancedButton.jsx new file mode 100644 index 0000000..a659c1b --- /dev/null +++ b/frontend/components/Buttons/AdvancedButton.jsx @@ -0,0 +1,7 @@ +import { CLIENT_STATIC_FILES_RUNTIME_AMP } from 'next/dist/shared/lib/constants'; + +const AdvancedButton = ({ advanced, setAdvanced }) => { + return <>; +}; + +export default AdvancedButton; diff --git a/frontend/components/Buttons/CopyButton.jsx b/frontend/components/Buttons/CopyButton.jsx new file mode 100644 index 0000000..63d5533 --- /dev/null +++ b/frontend/components/Buttons/CopyButton.jsx @@ -0,0 +1,76 @@ +import IImage from "next/image"; + +const CopyButton = ({ selectedImage }) => { + const grayDisable = selectedImage + ? "grayscale-0 cursor-pointer" + : "grayscale opacity-50"; + + function copyImageToClipboard(selectedImage) { + // Create a new image element + const img = new Image(); + // Set the source of the image + img.src = selectedImage; + + // When the image is loaded + img.onload = () => { + // const imageBlob = new Blob([selectedImage], { type: "image/png" }); + // Use the Clipboard API to write the image data to the clipboard + // console.log("image blob", imageBlob); + navigator.clipboard.write([ + new ClipboardItem({ + // [imageBlob.type]: imageBlob, + [selectedImage.type]: selectedImage, + }), + ]); + + // Alert the user that the image has been copied + alert("Image copied to clipboard!"); + }; + } + + return ( + <> + { + try { + // navigator.clipboard.write([ + // // clipboard item with the selected image + + // new ClipboardItem({ + // selectedImage: pngImageBlob, + // }), + // ]); + copyImageToClipboard(selectedImage); + } catch (error) { + console.error(error); + } + }} + className={`${grayDisable} grid grid-cols-1 grid-rows-2 text-black text-4xl mt-6`} + > + +

+ COPY +

+
+
+ +
+

COPY TO CLIPBOARD

+
+
+ + ); +}; + +export default CopyButton; diff --git a/frontend/components/Buttons/GenerateButton.jsx b/frontend/components/Buttons/GenerateButton.jsx new file mode 100644 index 0000000..a277598 --- /dev/null +++ b/frontend/components/Buttons/GenerateButton.jsx @@ -0,0 +1,152 @@ +import { useEffect, useContext } from "react"; +import { SocketContext } from "../../utils/socketContext"; +import Image from "next/image"; +import { CONSTANTS } from "../../utils/CONSTANTS"; +import { useRouter } from "next/router"; +import Tooltip from "../ToolTip"; +import InfoIcon from "../InfoIcon"; + +const generateButtonTT = + "Generate your avatar based on the configuration above. Don't worry, you can always re-roll."; +const rerollButtonTT = + "Didn’t get quite what you wanted? Try again. Remember to save or mint first, lest your incredible art be lost to the ether!"; + +const GenerateButton = ({ + setConditionalCreate, + prompt, + pdfData, + setImageProcessing, + setError, + setImageResult, + imageResult, + imageProcessing, +}) => { + const socket = useContext(SocketContext); + const router = useRouter(); + + // Attempting to scroll to the results section whenever imageResult is updated + useEffect(() => { + if (imageResult) { + const resultSection = document.getElementById("results"); + resultSection.scrollIntoView({ behavior: "smooth" }); + } + }, [imageResult]); + + useEffect(() => { + if (socket) { + socket.on("imageResponse", (result) => { + console.log("result: ", result); + setImageProcessing(false); + if (result.error) { + return setError(result.error); + } + setImageResult(result); + }); + + return () => { + socket.off("message"); + }; + } + }, [socket]); + + const generateImages = async () => { + console.log("Generating images... for ", prompt); + alert("Generating images... for " + prompt); + setError(false); + setImageProcessing(true); + setConditionalCreate("results"); + + // const result = { images: [CONSTANTS.testBase64Image] }; // <------------- THIS IS FOR TESTING + // setImageResult(result); + socket.emit("imageRequest", { prompt: prompt }); + }; + + return ( + <> +
+ + + +
+ {imageResult ? ( +
+

REROLL

+ +
+ +
+
+
+ ) : imageProcessing ? ( +

+ images loading... +

+ ) : pdfData ? ( +
+

Generate

+ + +
+ ) : ( +
+

Generate

+ +
+ +
+
+
+ )} +
+
+ {imageResult ? ( +

+ Regenerate your avatar with the EXACT same prompt or{" "} + make changes below in ADVANCED and then REROLL. +

+ ) : ( +

Character Images

+ )} + + {imageResult ? ( +

router.reload("/")} + className="my-24 cursor-pointer bg-black bg-opacity-30 rounded-lg p-5" + > + << BACK TO START +

+ ) : null} + + ); +}; + +export default GenerateButton; diff --git a/frontend/components/Buttons/MintButton.jsx b/frontend/components/Buttons/MintButton.jsx new file mode 100644 index 0000000..82bfbc0 --- /dev/null +++ b/frontend/components/Buttons/MintButton.jsx @@ -0,0 +1,217 @@ +import Image from "next/image"; +import React from "react"; +import { avatarNFTSTORAGE } from "../../utils/web3utils"; +import { + useContractWrite, + usePrepareContractWrite, + useWaitForTransaction, +} from "wagmi"; +import { CONSTANTS } from "../../utils/CONSTANTS"; +import { useEffect, useState } from "react"; +import { useAccount, useDisconnect } from "wagmi"; +import { useWeb3Modal } from "@web3modal/react"; +import SuccessModal from "../SuccessModal"; + +const MintButton = ({ + selectedImage, + pdfData, + setIsMinting, + prompt, + isMinting, + setModalOpen, + setModalMessage, + setSuccessTxnHash, + setStoredNFTImage, + storedNFTImage, +}) => { + const [metadataUrl, setMetadataUrl] = useState(null); //url + const [mintError, setMintError] = useState(null); //error + + const grayDisable = selectedImage + ? "grayscale-0 cursor-pointer" + : "grayscale opacity-50"; + + const { data, isLoading, isSuccess, write, writeAsync, isError, config } = + useContractWrite({ + mode: "recklesslyUnprepared", + address: CONSTANTS.ceptorAddress, + abi: CONSTANTS.ceptorABI, + functionName: "mint", + // args: [], + /* chainId: process.env.NEXT_PUBLIC_NETWORK_ID, */ + }); + + /* const contractWrite = useContractWrite(config); */ + const { isSuccess: txnSuccess, isLoading: txnPending } = + useWaitForTransaction({ + hash: data?.hash, + onSuccess(dataSuccess) { + console.log("Success", dataSuccess); + console.log("Hash passed to useWaitForTransaction", data?.hash); + }, + }); + + if (data?.hash) { + console.log("Hash passed to useWaitForTransaction in the IF", data?.hash); + } + + // wait for actual txn success + // if true, run waitFor + useEffect(() => { + if (txnSuccess) { + console.log("HEY IT WORKED, TXN SUCCESSFUL", data?.hash); + + console.log(storedNFTImage); + + setModalOpen(true); + } + if (metadataUrl) { + console.log(metadataUrl); + console.log("this is going to be image data", storedNFTImage); + } + }, [txnSuccess]); + + const { address, isConnected } = useAccount(); + const { open, isOpen, close } = useWeb3Modal(); + const { disconnect } = useDisconnect(); + + useEffect(() => { + console.log("ISCONNECTED: ", isConnected); + }, [isConnected]); + + const mintAvatar = async () => { + if (!selectedImage) { + alert("no image selected"); + return; + } + + try { + if (!isConnected) { + open(); + } else if (isConnected) { + console.log("wallet is connected"); + } + setModalMessage(`You've minted an avatar!`); + setSuccessTxnHash(data?.hash); + + setIsMinting(true); + console.log("Minting avatar..."); + + const _metadataUrl = await avatarNFTSTORAGE( + selectedImage, + prompt, + pdfData + ); //returns url of metadata json + console.log("metadata url: ", _metadataUrl); + setMetadataUrl(_metadataUrl); + + //IPFS Image Link + if (typeof _metadataUrl === "string") { + const prefix = "https://gateway.ipfs.io/ipfs/"; + const cleanedJson = _metadataUrl.replace("ipfs://", ""); + const smash = prefix + cleanedJson; + console.log("cleanedJSON", smash); + fetch(smash) + .then((response) => response.json()) + .then((data) => { + console.log("smash object", data); + const cleaner = data.image; + const cleanedSmash = cleaner.replace("ipfs://", ""); + + setStoredNFTImage(prefix + cleanedSmash); + }) + .catch((error) => console.error(error)); + } else { + console.log("ITS NOT A STRING"); + } + + //mint nft + const mintResult = await writeAsync({ + recklesslySetUnpreparedArgs: [_metadataUrl], + }); + console.log("mintResult: ", mintResult); //this one isn't async, so it will never have the right data + + //check for error + // setIsMinting(false) + } catch (error) { + console.log("MINT ERROR CATCH: ", error.reason); + setMintError(error.reason); + } + }; + + useEffect(() => { + data && console.log("MINT DATA: ", data); + }, [data]); + + useEffect(() => { + if (isError) { + console.log("MINT ERROR: ", isError); + setIsMinting(false); + } + }, [isError, setIsMinting]); + + useEffect(() => { + if (isSuccess) { + console.log("MINT SUCCESS: ", isSuccess); + setIsMinting(false); + } + }, [isSuccess, setIsMinting]); + + // Just for testing + useEffect(() => { + console.log("this is where the image is", storedNFTImage); + }, [storedNFTImage]); + + return ( + <> + + button-image + {mintError && ( +

+ {mintError} +

+ )} + {txnPending ? ( +
+ spinner icon +

+ MINTING +

+
+ ) : ( +

+ MINT +

+ )} + +
+
+ button-image +
+

ERC-721 NFT

+
+
+ + ); +}; + +export default MintButton; diff --git a/frontend/components/Buttons/RainbowKitButton.tsx b/frontend/components/Buttons/RainbowKitButton.tsx new file mode 100644 index 0000000..df34187 --- /dev/null +++ b/frontend/components/Buttons/RainbowKitButton.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import Image from "next/image"; + +import { ConnectButton } from "@rainbow-me/rainbowkit"; + +// type Props = {} + +const RainbowKitButton = () => { + return ( +
+ + {({ + account, + chain, + openAccountModal, + openChainModal, + openConnectModal, + authenticationStatus, + mounted, + }) => { + // Note: If your app doesn't use authentication, you + // can remove all 'authenticationStatus' checks + const ready = mounted && authenticationStatus !== "loading"; + const connected = + ready && + account && + chain && + (!authenticationStatus || authenticationStatus === "authenticated"); + + return ( +
+ {(() => { + if (!connected) { + return ( + + ); + } + + if (chain.unsupported) { + return ( + + ); + } + + return ( +
+ +
+ ); + })()} +
+ ); + }} +
+
+ ); +}; + +export default RainbowKitButton; diff --git a/frontend/components/Buttons/SaveButton.jsx b/frontend/components/Buttons/SaveButton.jsx new file mode 100644 index 0000000..706ce05 --- /dev/null +++ b/frontend/components/Buttons/SaveButton.jsx @@ -0,0 +1,47 @@ +import Image from 'next/image'; + +const SaveButton = ({ selectedImage, setModalOpen, setModalMessage }) => { + const grayDisable = selectedImage ? 'grayscale-0' : 'grayscale opacity-50'; + + const saving = () => { + setModalMessage( + `You've downloaded an avatar! Share with a friend and start playing` + ); + setModalOpen(true); + }; + + return ( + <> + + button-image +

+ SAVE +

+
+
+ button-image +
+

PNG

+
+
+ + ); +}; + +export default SaveButton; diff --git a/frontend/components/Buttons/StartButton.jsx b/frontend/components/Buttons/StartButton.jsx new file mode 100644 index 0000000..335fba3 --- /dev/null +++ b/frontend/components/Buttons/StartButton.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import buttonImage from "../../public/images/CREATE-hero/buttonImage.png"; +import Image from "next/image"; + +const StartButton = ({ setConditionalRender }) => { + return ( +
setConditionalRender(true)} + > +
+

Press

+
+ + button image +
+

Start

+
+
+
+

GRAB A COPY OF YOUR CHARACTER SHEET AND MAKE ART

+
+
+ ); +}; + +export default StartButton; diff --git a/frontend/components/Buttons/WalletConnectButton.tsx b/frontend/components/Buttons/WalletConnectButton.tsx new file mode 100644 index 0000000..bf67138 --- /dev/null +++ b/frontend/components/Buttons/WalletConnectButton.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import Image from "next/image"; +import { ConnectButton } from "@rainbow-me/rainbowkit"; + +// type Props = {} + +const WalletConnectButton = () => { + return ( +
+ + {({ + account, + chain, + openAccountModal, + openChainModal, + openConnectModal, + authenticationStatus, + mounted, + }) => { + // Note: If your app doesn't use authentication, you + // can remove all 'authenticationStatus' checks + const ready = mounted && authenticationStatus !== "loading"; + const connected = + ready && + account && + chain && + (!authenticationStatus || authenticationStatus === "authenticated"); + + return ( +
+ {(() => { + if (!connected) { + return ( + + ); + } + + if (chain.unsupported) { + return ( + + ); + } + + return ( +
+ + + +
+ ); + })()} +
+ ); + }} +
+
+ ); +}; + +export default WalletConnectButton; diff --git a/frontend/components/Buttons/index.ts b/frontend/components/Buttons/index.ts new file mode 100644 index 0000000..1f53dcc --- /dev/null +++ b/frontend/components/Buttons/index.ts @@ -0,0 +1,8 @@ +// Export Button components +// When creating a new button, be sure to export the component from the file it is defined in and import it here. +export { default as AdvancedButton } from './AdvancedButton'; +export { default as CopyButton } from './CopyButton'; +export { default as GenerateButton } from './GenerateButton'; +export { default as MintButton } from './MintButton'; +export { default as SaveButton } from './SaveButton'; +export { default as StartButton } from './StartButton'; \ No newline at end of file diff --git a/frontend/components/CCID.jsx b/frontend/components/CCID.jsx new file mode 100644 index 0000000..fa3984f --- /dev/null +++ b/frontend/components/CCID.jsx @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import { useAccount } from "wagmi"; +import RainbowKitButton from "./Buttons/RainbowKitButton"; + +export default function CCID() { + const [isCreateIDModalOpen, setIsCreateIDModalOpen] = useState(false); + const [isLoginPopupOpen, setIsLoginPopupOpen] = useState(false); + const [isLoginSuccessPopupOpen, setIsLoginSuccessPopupOpen] = useState(false); + const [isLoginErrorPopupOpen, setIsLoginErrorPopupOpen] = useState(false); + const router = useRouter(); + const { isConnected } = useAccount(); + const [hasCCID, setHasCCID] = useState(); + + useEffect(() => { + if (isConnected) { + // Automatically close the "Your wallet is successfully connected" pop-up after 4 seconds and redirect to the uploadpage + setTimeout(() => { + setIsLoginSuccessPopupOpen(false); + router.push("/chooseUser"); + }, 4000); + } + }, [isConnected, router]); + + const handleCreateIDClick = () => { + if (!isConnected) { + // Show the "Please connect your wallet" pop-up if the wallet is not connected + setIsLoginPopupOpen(true); + } + }; + + // Define the handleLoginClick function + const handleLoginClick = () => { + setIsLoginErrorPopupOpen(true); + }; + + const handleCloseLoginErrorPopup = () => { + setIsLoginErrorPopupOpen(false); + }; + + return ( +
+
+

+ hackathon! +

+

0.11.30

+
+
+

Welcome to the Ceptor Club

+

+ Create D&D character and storyline onchain! +

+
+
+ + + + + {isLoginPopupOpen && ( +
+
+ +
+

+ Please connect your wallet and choose the network to mint your + CCID (Ceptor Club ID) +

+ +
+
+
+ )} + + {isLoginSuccessPopupOpen && ( +
+
+

+ Your wallet is successfully connected +

+
+
+ )} + + {isLoginErrorPopupOpen && ( +
+
+ +

+ Please create the CCID first +

+
+
+ )} +
+
+ ); +} diff --git a/frontend/components/CharacterModule/CharacterBackstory.jsx b/frontend/components/CharacterModule/CharacterBackstory.jsx new file mode 100644 index 0000000..9b5dfe7 --- /dev/null +++ b/frontend/components/CharacterModule/CharacterBackstory.jsx @@ -0,0 +1,51 @@ +import React from "react" +import { useState } from "react" +import { CONSTANTS } from "../../utils/CONSTANTS" + +export const CharacterBackstory = ({ pdfData }) => { + const [backstory, setBackstory] = useState("") + const [generating, setGenerating] = useState(false) + + const generateBackstory = async () => { + setGenerating(true) + delete pdfData.playerName + delete pdfData.level + delete pdfData.xp + delete pdfData.profBonus + + console.log("generating backstory from pdfData: ", pdfData) + const gptResult = await fetch("/api/gpt", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + prefix: CONSTANTS.gptPrefixDescription, + data: pdfData, + }), + }) + const result = await gptResult.json() + console.log("result: ", result.output.text) + setBackstory(result.output.text) + setGenerating(false) + } + + return ( +
+

Generate a backstory!

+ {backstory ? ( +

{backstory}

+ ) : generating ? ( +

Generating...

+ ) : pdfData ? ( + + ) : ( +

Upload a character sheet to generate a backstory!

+ )} + {/* */} +
+ ) +} diff --git a/frontend/components/CharacterModule/CharacterContext.jsx b/frontend/components/CharacterModule/CharacterContext.jsx new file mode 100644 index 0000000..48ce16e --- /dev/null +++ b/frontend/components/CharacterModule/CharacterContext.jsx @@ -0,0 +1,70 @@ +import { createContext, useState } from "react"; + +const CharacterContext = createContext(); + +const CharacterProvider = ({ children }) => { + const [characterData, setCharacterData] = useState({ + playerName: "Ceptor", + charName: "", + level: "", + gender: "", + weapon: "", + interest: "", + partyPos: "", + species: "", + background: "", + strength: "", + dexterity: "", + constitution: "", + intelligence: "", + wisdom: "", + charisma: "", + hitPoints: "", + myClass: "", + proficiencyBonus: "", + XP: "", + myAlignment: "", + //updating names of the keys to make it easier for others to read + + strengthMod: "", + dexterityMod: "", + constitutionMod: "", + intelligenceMod: "", + wisdomMod: "", + charismaMod: "", + + savingThrowStrength: "", + savingThrowDexterity: "", + savingThrowConstitution: "", + savingThrowIntelligence: "", + savingThrowWisdom: "", + savingThrowCharisma: "", + + acrobatics: "", + animalHandling: "", + arcana: "", + athletics: "", + deception: "", + history: "", + insight: "", + intimidation: "", + investigate: "", + medicine: "", + nature: "", + perception: "", + performance: "", + persuasion: "", + religion: "", + sleightOfHand: "", + stealth: "", + survival: "", + }); + + return ( + + {children} + + ); +}; + +export { CharacterContext, CharacterProvider }; diff --git a/frontend/components/CharacterModule/CharacterStatsFull.jsx b/frontend/components/CharacterModule/CharacterStatsFull.jsx new file mode 100644 index 0000000..a36aa2c --- /dev/null +++ b/frontend/components/CharacterModule/CharacterStatsFull.jsx @@ -0,0 +1,22 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { CharacterContext } from './CharacterContext'; + +export const CharacterStatsFull = () => { + const { characterData } = useContext(CharacterContext); + const [fullCharacterData, setFullCharacterData] = useState(characterData); + + useEffect(() => { + setFullCharacterData(characterData); + }, [characterData]); + + return ( +
+

Full Character Stats

+ {Object.entries(fullCharacterData).map(([key, value]) => ( +
+ {key}: {value} +
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/frontend/components/CharacterModule/RandomName.jsx b/frontend/components/CharacterModule/RandomName.jsx new file mode 100644 index 0000000..9e067f7 --- /dev/null +++ b/frontend/components/CharacterModule/RandomName.jsx @@ -0,0 +1,130 @@ +// Randomly generates a name for the user + +function RandomName() { + + let ranPart1 = Math.floor(Math.random() * 18) + 1; + let ranPart2 = Math.floor(Math.random() * 9) + 1; + let ranPart3 = Math.floor(Math.random() * 7) + 1; + + let part1 = ""; + let part2 = ""; + let part3 = ""; + + //First Part of Name + switch (ranPart1) { + case 1: + part1 = "Tom" + break + case 2: + part1 = "Gor" + break + case 3: + part1 = "Sam" + break + case 4: + part1 = "Dor" + break + case 5: + part1 = "Ban" + break + case 6: + part1 = "Ar" + break + case 7: + part1 = "Bong" + break + case 8: + part1 = "Ram" + break + case 9: + part1 = "Pang" + break + case 10: + part1 = "Zap" + break + case 11: + part1 = "Jay" + break + case 12: + part1 = "Garg" + break + case 13: + part1 = "Quar" + break + case 14: + part1 = "Qyn" + break + case 15: + part1 = "Foon" + break + case 16: + part1 = "Flyn" + break + case 17: + part1 = "Van" + break + default: + part1 = "Von" + } + + //Second Part of Name + switch (ranPart2) { + case 1: + part2 = "lin" + break + case 2: + part2 = "an" + break + case 3: + part2 = "ing" + break + case 4: + part2 = "un" + break + case 5: + part2 = "on" + break + case 6: + part2 = "ort" + break + case 7: + part2 = "art" + break + case 8: + part2 = "ar" + break + default: + part2 = "er" + } + + //Third Part of Name + switch (ranPart3) { + case 1: + part3 = "" + break + case 2: + part3 = "eus" + break + case 3: + part3 = "o" + break + case 4: + part3 = "ite" + break + case 5: + part3 = "ing" + break + case 6: + part3 = "ang" + break + default: + part3 = "" + } + + //Put it all together + let randomName = part1 + part2 + part3 + return randomName + +} + +module.exports = { RandomName } \ No newline at end of file diff --git a/frontend/components/CharacterModule/index.ts b/frontend/components/CharacterModule/index.ts new file mode 100644 index 0000000..af15ba9 --- /dev/null +++ b/frontend/components/CharacterModule/index.ts @@ -0,0 +1,4 @@ +// Export CharacterModule components +// export { default as CharacterBackstory } from './CharacterBackstory'; +// export { default as CharacterContext } from './CharacterContext'; +export { default as CharacterStats } from './CharacterStats'; diff --git a/frontend/components/CharacterStats.jsx b/frontend/components/CharacterStats.jsx index 53a0123..ffcad26 100644 --- a/frontend/components/CharacterStats.jsx +++ b/frontend/components/CharacterStats.jsx @@ -1,18 +1,16 @@ - -import React from 'react'; -import { useEffect, useState, useContext } from 'react'; -import { createPrompt } from '../utils/promptGen'; -import AdvancedButton from './AdvancedButton'; -import { CONSTANTS } from '../utils/CONSTANTS'; -import Image from 'next/image'; -import { CharacterContext } from './CharacterContext'; +import React from "react"; +import { useEffect, useState, useContext } from "react"; +import { createPrompt } from "../utils/promptGen"; +import AdvancedButton from "./AdvancedButton"; +import { CONSTANTS } from "../utils/CONSTANTS"; +import Image from "next/image"; +import { CharacterContext } from "./CharacterContext"; import Tooltip from "./ToolTip"; import InfoIcon from "./InfoIcon"; const characterStatsTT = "Here's what we're working with from your stat sheet. You can customize it prior to generating, or edit it later in \"Advanced\""; - const CharacterStats = ({ pdfData, prompt, @@ -22,34 +20,34 @@ const CharacterStats = ({ imageResult, advanced, }) => { - const { characterData } = useContext(CharacterContext) + const { characterData } = useContext(CharacterContext); useEffect(() => { if (!advanced && pdfData) { - console.log("pdfData: ", pdfData); + console.log("pdfData: ", pdfData); //create text prompt using pdfData and other data const prompt = createPrompt(pdfData); setPrompt(prompt); setError(null); } if (characterData.myClass !== pdfData.class) { - setPdfData({ ...pdfData, class: characterData.myClass }) + setPdfData({ ...pdfData, class: characterData.myClass }); } if (characterData.background !== pdfData.background) { - setPdfData({ ...pdfData, background: characterData.background }) + setPdfData({ ...pdfData, background: characterData.background }); } if (characterData.myAlignment !== pdfData.alignment) { - setPdfData({...pdfData, alignment: characterData.myAlignment}) + setPdfData({ ...pdfData, alignment: characterData.myAlignment }); } - if(characterData.gender !== pdfData.gender) { - if (characterData.gender === "He" && pdfData.gender === '') { - setPdfData({...pdfData, gender: "He / Him"}) - } else if (characterData.gender === "She" && pdfData.gender === '') { - setPdfData({...pdfData, gender: "She / Hers"}) - } else if (characterData.gender === "They" && pdfData.gender === '') { - setPdfData({...pdfData, gender: "They / Them"}) + if (characterData.gender !== pdfData.gender) { + if (characterData.gender === "He" && pdfData.gender === "") { + setPdfData({ ...pdfData, gender: "male" }); + } else if (characterData.gender === "She" && pdfData.gender === "") { + setPdfData({ ...pdfData, gender: "female" }); + } else if (characterData.gender === "They" && pdfData.gender === "") { + setPdfData({ ...pdfData, gender: "nonbinary" }); } } - console.log(pdfData) + console.log(pdfData); }); const handleClassSelect = (e) => { @@ -64,6 +62,16 @@ const CharacterStats = ({ } }; + const handleSpeciesSelect = (e) => { + const input = document.getElementById("speciesInput"); + if (input.value !== "") { + setPdfData({ ...pdfData, race: input.value }); + } else { + setPdfData({ ...pdfData, race: "" }); + } + }; + + const handleArmorSelect = (e) => { const input = document.getElementById("armorInput"); if ( @@ -168,7 +176,15 @@ const CharacterStats = ({ readOnly disabled={imageResult} > -

(LOCKED) RACE: Dragonborn

+

SPECIES of CEPTOR:

+
CLASS:

@@ -258,12 +273,11 @@ const CharacterStats = ({ - {CONSTANTS.colors.map((colors) => ( @@ -340,9 +353,8 @@ const CharacterStats = ({

EDIT THIS LATER IN ADVANCED

- - - + + ) : null} diff --git a/frontend/components/Countdown.tsx b/frontend/components/Countdown.tsx new file mode 100644 index 0000000..9dc9804 --- /dev/null +++ b/frontend/components/Countdown.tsx @@ -0,0 +1,50 @@ +import { useCallback, useEffect, useState } from "react"; + +export const Countdown = ({ deadline }: { deadline: number }) => { + // const [deadline, setDeadline] = useState(); + const [countdown, setCountdown] = useState(""); + + // countdown until next powt is + const updateCountdown = useCallback((deadline: number) => { + const now = new Date(); + const timeDifference = deadline - now.getTime(); + + if (timeDifference <= 0) { + // The deadline has passed. + document.getElementById("deadline")!.textContent = "Time's up!"; + } else { + const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + const hours = Math.floor( + (timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) + ); + const minutes = Math.floor( + (timeDifference % (1000 * 60 * 60)) / (1000 * 60) + ); + const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000); + + const countdownText = `${days}d ${hours}h ${minutes}m ${seconds}s`; + setCountdown(countdownText); + + // Update the countdown every 1 second (1000 milliseconds) + setTimeout(() => { + updateCountdown(deadline); + }, 1000); + } + }, []); + + useEffect(() => { + // Start the countdown initially. + updateCountdown(deadline); + }, [updateCountdown, deadline]); + + return ( +
+

+ Deadline +

+

+ {countdown} +

+
+ ); +}; diff --git a/frontend/components/Create.jsx b/frontend/components/Create.jsx index f15e1a1..367ea89 100644 --- a/frontend/components/Create.jsx +++ b/frontend/components/Create.jsx @@ -1,4 +1,3 @@ - import React from "react"; import Link from 'next/link'; import { useEffect, useState, useContext } from "react"; @@ -78,7 +77,7 @@ export const Create = () => { const [userDice, setUserDice] = useState([0, 0, 0, 0, 0, 0]); //dice balance const [pdfData, setPdfData] = useState({ - race: "DnDDragonbornGeneral", + race: "", class: "", armorWorn: "", background: "", @@ -225,6 +224,19 @@ export const Create = () => {
{imageProcessing ? ( <> +
+
The Creator is evolving. If you don't see your artwork in the first 30 seconds please retry.
+ 44 + {/* retry button (back to Create start) */} + + + +
) : !imageProcessing ? ( @@ -248,13 +260,14 @@ export const Create = () => {
*/} {!imageResult && (
- + /> */} + ORCS Coming Soon
)} { + const { chain, chains } = useNetwork(); + const [diceBalance, setDiceBalance] = useState({ 0: null, 1: null, @@ -22,19 +26,19 @@ const DiceDashboard = ({}) => { 4: null, 5: null, }); - const [userTimer, setUserTimer] = useState(''); + const [userTimer, setUserTimer] = useState(""); const { address, isConnected } = useAccount(); const diceContract = { - address: CONSTANTS.diceAddress, - abi: CONSTANTS.diceABI, + address: addresses[chain?.network]?.ceptorDice, + abi: ceptorDiceABI, }; const { data: userTimerRead } = useContractRead({ - address: CONSTANTS.ceptorAddress, - abi: CONSTANTS.ceptorABI, - functionName: 'userTimers', + address: addresses[chain?.network]?.ceptors, + abi: ceptorABI, + functionName: "userTimers", args: [address], watch: true, }); @@ -56,47 +60,48 @@ const DiceDashboard = ({}) => { contracts: [ { ...diceContract, - functionName: 'balanceOf', + functionName: "balanceOf", args: [address, 0], }, { ...diceContract, - functionName: 'balanceOf', + functionName: "balanceOf", args: [address, 1], }, { ...diceContract, - functionName: 'balanceOf', + functionName: "balanceOf", args: [address, 2], }, { ...diceContract, - functionName: 'balanceOf', + functionName: "balanceOf", args: [address, 3], }, { ...diceContract, - functionName: 'balanceOf', + functionName: "balanceOf", args: [address, 4], }, { ...diceContract, - functionName: 'balanceOf', + functionName: "balanceOf", args: [address, 5], }, ], // allowFailure: true, }); + // TODO: Move Logic into the burn and buy page after the hackathon // Type and amount of dice to mint, Will be dynamic, just for testing const _ids = [5]; const _amounts = [1]; // Config for minting dice const { config: configMint } = usePrepareContractWrite({ - address: CONSTANTS.diceAddress, - abi: CONSTANTS.diceABI, - functionName: 'mintBatch', + address: diceContract.address, + abi: diceContract.abi, + functionName: "mintBatch", args: [_ids, _amounts], }); @@ -114,9 +119,9 @@ const DiceDashboard = ({}) => { if (!isConnected) { open(); } else if (isConnected) { - console.log('wallet is connected'); + console.log("wallet is connected"); } - console.log('mint dice'); + console.log("mint dice"); writeMint(); } catch (error) { @@ -131,9 +136,9 @@ const DiceDashboard = ({}) => { // Config for burning dice const { config: configBurn } = usePrepareContractWrite({ - address: CONSTANTS.diceAddress, - abi: CONSTANTS.diceABI, - functionName: 'timerBurn', + address: addresses[chain?.network]?.ceptorDice, + abi: ceptorDiceABI, + functionName: "timerBurn", args: [address, _idsBurn, _amountsBurn], }); @@ -151,9 +156,9 @@ const DiceDashboard = ({}) => { if (!isConnected) { open(); } else if (isConnected) { - console.log('wallet is connected'); + console.log("wallet is connected"); } - console.log('burn dice'); + console.log("burn dice"); writeBurn(); } catch (error) { @@ -163,13 +168,13 @@ const DiceDashboard = ({}) => { useEffect(() => { if (isSuccessMinting || isSuccessBurn) { - console.log('success burn', isSuccessBurn); + console.log("success burn", isSuccessBurn); refetch(); } if (isConnected && diceData) { const newDiceBalance = {}; diceData.forEach((balance, i) => { - console.log('balance: ', i, parseInt(balance)); + console.log("balance: ", i, parseInt(balance)); newDiceBalance[i] = parseInt(balance); }); @@ -186,39 +191,32 @@ const DiceDashboard = ({}) => { return ( <> -
+ <> {address ? ( - <> -

Dice Balance

-

0: {diceBalance[0]}

-

1: {diceBalance[1]}

-

2: {diceBalance[2]}

-

3: {diceBalance[3]}

-

4: {diceBalance[4]}

-

5: {diceBalance[5]}

- - - {userTimer && ( - <> -
Time Remaining:
- - - )} - +
+
+ + +
+ <> + + + +
) : ( -

Connect to see dice balance

+

Connect to see dice balance

)} -
+ ); }; diff --git a/frontend/components/Explorer.tsx b/frontend/components/Explorer.tsx new file mode 100644 index 0000000..d7a4fd8 --- /dev/null +++ b/frontend/components/Explorer.tsx @@ -0,0 +1,181 @@ +import React, { useState, useEffect, useCallback } from "react"; +import NftCard from "./NftCard"; +import NftModal from "./NftModal"; +import { Alchemy, Network, Nft } from "alchemy-sdk"; +import { SubmitData } from "../utils/types"; + +const Explorer = ({ + ALCHEMY_GOERLI_API_KEY, + ALCHEMY_SEPOLIA_API_KEY, + ALCHEMY_POLYGON_ZKEVM_API_KEY, + PUBLIC_IMAGE_URL, + nftList, +}: { + ALCHEMY_GOERLI_API_KEY: string; + ALCHEMY_SEPOLIA_API_KEY: string; + ALCHEMY_POLYGON_ZKEVM_API_KEY: string; + PUBLIC_IMAGE_URL: string; + nftList: SubmitData[]; +}) => { + const [alchemy, setAlchemy] = useState(null); + const [nfts, setNfts] = useState([]); + const cardsPerPage = 20; + const [displayedCards, setDisplayedCards] = useState(cardsPerPage); + const [modalOpen, setModalOpen] = useState(true); + const [selectedNft, setSelectedNft] = useState(null); + + useEffect(() => { + const initializeAlchemy = async () => { + let newAlchemy: any = {}; + + if (ALCHEMY_GOERLI_API_KEY) { + const goerliSettings = { + apiKey: ALCHEMY_GOERLI_API_KEY, + network: Network.ETH_GOERLI, + }; + newAlchemy.goerli = new Alchemy(goerliSettings); + } + + if (ALCHEMY_SEPOLIA_API_KEY) { + const sepoliaSettings = { + apiKey: ALCHEMY_SEPOLIA_API_KEY, + network: Network.ETH_SEPOLIA, + }; + newAlchemy.sepolia = new Alchemy(sepoliaSettings); + } + + if (ALCHEMY_POLYGON_ZKEVM_API_KEY) { + const polygonZKEVMSettings = { + apiKey: ALCHEMY_POLYGON_ZKEVM_API_KEY, + network: Network.POLYGONZKEVM_TESTNET, + }; + newAlchemy.polygonZKEVM = new Alchemy(polygonZKEVMSettings); + } + + // Sets alchemy state only after both network + // instances have been created. + setAlchemy(newAlchemy); + }; + initializeAlchemy(); + }, [ + ALCHEMY_GOERLI_API_KEY, + ALCHEMY_SEPOLIA_API_KEY, + ALCHEMY_POLYGON_ZKEVM_API_KEY, + ]); + + useEffect(() => { + window.addEventListener("scroll", handleScroll); + + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + const handleScroll = () => { + const scrollTop = window.pageYOffset; + const scrollHeight = document.documentElement.scrollHeight; + const clientHeight = window.innerHeight; + + if (scrollTop + clientHeight >= scrollHeight - 50) { + setDisplayedCards((prev) => prev + cardsPerPage); + } + }; + + const updateNfts = (newNfts) => { + setNfts(newNfts); + }; + + const getNfts = useCallback(async () => { + // if (!alchemy) { + // return; + // } + + // either nftList is input or standard collection is queried + if (nftList?.length > 0) { + updateNfts(nftList); + return; + } + + // const exploreCeptor = [ + // { + // network: "goerli", + // address: "0x4379044facb5f0879de15e70b45afd495a197674", + // }, + // { + // network: "goerli", + // address: "0x60fAF5FAe9F2EA10504d505B50104E783bf505B1", + // }, + // { + // network: "sepolia", //Sep Ceptors + // address: "0x4dBe3E96d429b9fE5F2Bb89728E39138aC4F817A", + // }, + // { + // network: "sepolia", //Sep Dice + // address: "0xEd1dbc1f6E5e9f4066AAa341c87e157Ad40328A9", + // }, + // ]; + + // const invalidTokenIds = [4, 5, 8, 18]; // Token IDs in targetAddress with no image + // // Diffuse Contract on Goerli + // const targetAddress = "0x4379044facb5f0879de15e70b45afd495a197674"; + + // let allNfts = []; + + // for (const { network, address } of exploreCeptor) { + // const currentAlchemy = alchemy[network]; + + // const response = await currentAlchemy?.nft.getNftsForContract(address); + + // let nfts = response.nfts; + + // // Filter is only for the targetAddress + // if (address === targetAddress) { + // nfts = nfts.filter( + // (nft) => !invalidTokenIds.includes(parseInt(nft.tokenId)) + // ); + // } + + // allNfts = allNfts.concat(nfts); + // } + + // updateNfts(allNfts); + }, [alchemy, nftList]); + + useEffect(() => { + getNfts(); + }, [getNfts]); + + const handleCardClick = (nft) => { + setSelectedNft(nft); + setModalOpen(true); + }; + + const closeModal = () => { + setSelectedNft(null); + setModalOpen(false); + }; + + return ( +
+ + {Array.isArray(nfts) && + nfts.slice(0, displayedCards).map((nft) => { + return ( + handleCardClick(nft)} + PUBLIC_IMAGE_URL={PUBLIC_IMAGE_URL} + /> + ); + })} +
+ ); +}; + +export default Explorer; diff --git a/frontend/components/GenerateButton.jsx b/frontend/components/GenerateButton.jsx index 854d108..ce7eaf3 100644 --- a/frontend/components/GenerateButton.jsx +++ b/frontend/components/GenerateButton.jsx @@ -55,9 +55,11 @@ const GenerateButton = ({ setImageProcessing(true); setConditionalCreate("results"); - // const result = { images: [CONSTANTS.testBase64Image] }; // <------------- THIS IS FOR TESTING + // TODO: replace testimage with backend response + const result = { images: [CONSTANTS.testBase64Image] }; // <------------- THIS IS FOR TESTING - socket.emit("imageRequest", { prompt: prompt }); + setImageResult(result); + // socket.emit("imageRequest", { prompt: prompt }); }; return ( @@ -109,11 +111,11 @@ const GenerateButton = ({ ) : pdfData ? (

Generate

- - - - - + +
) : (
diff --git a/frontend/components/Layout.jsx b/frontend/components/Layout.jsx deleted file mode 100644 index 0e80b30..0000000 --- a/frontend/components/Layout.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -const Layout = ({ children }) => { - return ( - <> -
{children}
- - ); -}; - -export default Layout; diff --git a/frontend/components/Layout.tsx b/frontend/components/Layout.tsx new file mode 100644 index 0000000..c48ee91 --- /dev/null +++ b/frontend/components/Layout.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import Meta from "./Meta"; +import { NavbarMdUp, NavbarSmUp } from "./NavBar/index"; + +const Layout = ({ children }) => { + const isBrowser = typeof window !== "undefined"; + + return ( + <> + + + + +
{children}
+
+ + ); +}; + +export default Layout; diff --git a/frontend/components/Meta.jsx b/frontend/components/Meta.jsx index 96ff77e..91ab719 100644 --- a/frontend/components/Meta.jsx +++ b/frontend/components/Meta.jsx @@ -1,5 +1,5 @@ -import Head from 'next/head'; -import Script from 'next/script'; +import Head from "next/head"; +import Script from "next/script"; const Meta = () => ( // const Head = ({ title, desc, canonical, image, props }) => ( @@ -21,12 +21,28 @@ const Meta = () => ( */} + + + +