├── .eslintrc.json ├── .gitignore ├── README.md ├── abi ├── ERC20.json └── ProfileNFT.json ├── apollo └── index.ts ├── components ├── Buttons │ ├── CollectBtn.tsx │ ├── PostBtn.tsx │ ├── SetEssenceBtn.tsx │ ├── SetSubscribeBtn.tsx │ ├── SigninBtn.tsx │ ├── SignupBtn.tsx │ └── SubscribeBtn.tsx ├── Cards │ ├── AccountCard.tsx │ ├── EssenceMwCard.tsx │ ├── PostCard.tsx │ ├── PrimaryProfileCard.tsx │ ├── ProfileCard.tsx │ ├── SubscribeMwCard.tsx │ └── SuggestedProfileCard.tsx ├── Forms │ ├── EssenceMwForm.tsx │ ├── PostForm.tsx │ ├── SignupForm.tsx │ └── SubscribeMwForm.tsx ├── Loader │ └── index.tsx ├── Modal │ └── index.tsx ├── Navbar │ └── index.tsx └── Panel │ └── index.tsx ├── context ├── auth.tsx └── modal.tsx ├── graphql ├── Accounts.ts ├── Address.ts ├── CreateCollectEssenceTypedData.ts ├── CreateRegisterEssenceTypedData.ts ├── CreateSetEssenceDataTypedData.ts ├── CreateSetSubscribeDataTypedData.ts ├── CreateSubscribeTypedData.ts ├── EssencesByFilter.tsx ├── LoginGetMessage.ts ├── LoginVerify.ts ├── PrimaryProfile.ts ├── PrimaryProfileEssences.ts ├── ProfilesByIds.tsx ├── Relay.ts ├── RelayActionStatus.ts └── index.ts ├── helpers ├── constants.ts └── functions.ts ├── hooks ├── useCancellableQuery.tsx └── useModal.tsx ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── index.tsx ├── posts.tsx └── settings.tsx ├── public ├── assets │ ├── avatar-placeholder.svg │ └── essence-placeholder.svg ├── favicon.ico └── vercel.svg ├── styles └── globals.css ├── tsconfig.json ├── types.ts └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CyberConnect Content app 2 | 3 | ## Introduction 4 | 5 | **CyberConnect Protocol** is a decentralized social graph protocol that helps Web3 applications bootstrap network effects. It empowers users to truly own their social identities, contents, and connections in a social network and provides developers with a rich set of tools to build applications with meaningful social experiences. 6 | 7 | ## Project 8 | 9 | The repo contains the full code for the **How to Build Content app** guide from the [CyberConnect Developer Center](https://docs.cyberconnect.me/). 10 | 11 | The project was built to help developer with the basic functionalities necessary to develop a content application by taking advantage of the full power of CyberConnect APIs. 12 | 13 | This example contains all the steps described in the docs: 14 | 15 | 1. [Create a Profile](https://docs.cyberconnect.me/how-to/build-content-app/create-a-profile) 16 | 2. [Authentication](https://docs.cyberconnect.me/how-to/build-content-app/authentication) 17 | 3. [Subscribe to Profile](https://docs.cyberconnect.me/how-to/build-content-app/subscribe-to-profile) 18 | 4. [Create a Post](https://docs.cyberconnect.me/how-to/build-content-app/create-a-post) 19 | 5. [Collect a Post](https://docs.cyberconnect.me/how-to/build-content-app/collect-a-post) 20 | 6. [Middleware for Subscribe](https://docs.cyberconnect.me/how-to/build-content-app/middleware-for-subscribe) 21 | 7. [Middleware for Post](https://docs.cyberconnect.me/how-to/build-content-app/middleware-for-post) 22 | 23 | ## Prerequisites 24 | 25 | Make sure that you have installed [Node.js](https://nodejs.org/en/download/) on your computer and [MetaMask](https://metamask.io/) extension in your Chrome browser. 26 | 27 | ## Installation 28 | 29 | Clone the repo [https://github.com/cyberconnecthq/cc-content-app.git](https://github.com/cyberconnecthq/cc-content-app.git) and run the following command in your terminal to install all the packages that are necessary to start the development server: `npm install` or `yarn install`. 30 | 31 | ## Local Development 32 | 33 | To start the local development server run the following command and open up the browser window http://localhost:3000. Most changes are reflected live without having to restart the server: `npm run dev` or `yarn dev`. 34 | 35 | ## Contact 36 | 37 | These are our communication channels, so feel free to contact us: 38 | 39 | - [Discord](https://discord.com/invite/cUc8VRGmPs) `#developers` channel 40 | - [@CyberConnectHQ](https://twitter.com/CyberConnectHQ) on Twitter 41 | - [Github](https://github.com/cyberconnecthq/build-nft-sbt-guide/issues) for issues 42 | 43 | -------------------------------------------------------------------------------- /abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /abi/ProfileNFT.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "components": [ 6 | { 7 | "internalType": "address", 8 | "name": "to", 9 | "type": "address" 10 | }, 11 | { 12 | "internalType": "string", 13 | "name": "handle", 14 | "type": "string" 15 | }, 16 | { 17 | "internalType": "string", 18 | "name": "avatar", 19 | "type": "string" 20 | }, 21 | { 22 | "internalType": "string", 23 | "name": "metadata", 24 | "type": "string" 25 | }, 26 | { 27 | "internalType": "address", 28 | "name": "operator", 29 | "type": "address" 30 | } 31 | ], 32 | "internalType": "struct DataTypes.CreateProfileParams", 33 | "name": "params", 34 | "type": "tuple" 35 | }, 36 | { "internalType": "bytes", "name": "preData", "type": "bytes" }, 37 | { "internalType": "bytes", "name": "postData", "type": "bytes" } 38 | ], 39 | "name": "createProfile", 40 | "outputs": [ 41 | { "internalType": "uint256", "name": "tokenID", "type": "uint256" } 42 | ], 43 | "stateMutability": "payable", 44 | "type": "function" 45 | }, 46 | { 47 | "inputs": [ 48 | { "internalType": "string", "name": "handle", "type": "string" } 49 | ], 50 | "name": "getProfileIdByHandle", 51 | "outputs": [ 52 | { "internalType": "uint256", "name": "", "type": "uint256" } 53 | ], 54 | "stateMutability": "view", 55 | "type": "function" 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /apollo/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client"; 2 | import { setContext } from "@apollo/client/link/context"; 3 | 4 | const httpLink = createHttpLink({ 5 | uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, 6 | }); 7 | 8 | const authLink = setContext((_, { headers }) => { 9 | const token = localStorage.getItem("accessToken"); 10 | 11 | return { 12 | headers: { 13 | ...headers, 14 | Authorization: token ? `bearer ${token}` : "", 15 | "X-API-KEY": process.env.NEXT_PUBLIC_CYBERCONNECT_API_KEY, 16 | }, 17 | }; 18 | }); 19 | 20 | export const apolloClient = new ApolloClient({ 21 | link: authLink.concat(httpLink), 22 | cache: new InMemoryCache(), 23 | }); 24 | -------------------------------------------------------------------------------- /components/Buttons/CollectBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { CREATE_COLLECT_ESSENCE_TYPED_DATA, RELAY } from "../../graphql"; 4 | import { AuthContext } from "../../context/auth"; 5 | import { ModalContext } from "../../context/modal"; 6 | import { ethers, BigNumber } from "ethers"; 7 | import ERC20ABI from "../../abi/ERC20.json"; 8 | 9 | function CollectBtn({ 10 | profileID, 11 | essenceID, 12 | isCollectedByMe, 13 | collectMw, 14 | }: { 15 | profileID: number; 16 | essenceID: number; 17 | isCollectedByMe: boolean; 18 | collectMw: Record; 19 | }) { 20 | const { accessToken, connectWallet, checkNetwork } = useContext(AuthContext); 21 | const { handleModal } = useContext(ModalContext); 22 | const [createCollectEssenceTypedData] = useMutation( 23 | CREATE_COLLECT_ESSENCE_TYPED_DATA 24 | ); 25 | const [relay] = useMutation(RELAY); 26 | const [stateCollect, setStateCollect] = useState(isCollectedByMe); 27 | 28 | const handleOnClick = async () => { 29 | try { 30 | /* Check if the user logged in */ 31 | if (!accessToken) { 32 | throw Error("You need to Sign in."); 33 | } 34 | 35 | /* Connect wallet and get provider */ 36 | const provider = await connectWallet(); 37 | 38 | /* Check if the network is the correct one */ 39 | await checkNetwork(provider); 40 | 41 | /* Get the signer from the provider */ 42 | const signer = provider.getSigner(); 43 | 44 | // const ERC20Contract = new ethers.Contract( 45 | // "0x326C977E6efc84E512bB9C30f76E30c160eD06FB", 46 | // ERC20ABI, 47 | // signer 48 | // ); 49 | 50 | /* Get the address from the provider */ 51 | const address = await signer.getAddress(); 52 | 53 | // if (collectMw) { 54 | // console.log("collectMw", collectMw.contractAddress); 55 | // const allowance = await ERC20Contract.allowance( 56 | // "0x370CA01D7314e3EEa59d57E343323bB7e9De24C6", 57 | // collectMw.contractAddress 58 | // ); 59 | // console.log("allowance", allowance.toString()); 60 | // let needApprove = allowance.gte(BigNumber.from("10000000000000000000")); 61 | // console.log("needApprove", needApprove); 62 | // } 63 | // return; 64 | // 65 | // await ERC20Contract.approve( 66 | // "0x415648c28adb31629418498264f55d54e4c324db", 67 | // "10000000000000000000" 68 | // ); 69 | // 70 | /* Get the network from the provider */ 71 | const network = await provider.getNetwork(); 72 | 73 | /* Create typed data in a readable format */ 74 | const typedDataResult = await createCollectEssenceTypedData({ 75 | variables: { 76 | input: { 77 | collector: address, 78 | profileID: profileID, 79 | essenceID: essenceID, 80 | }, 81 | }, 82 | }); 83 | 84 | const typedData = 85 | typedDataResult.data?.createCollectEssenceTypedData?.typedData; 86 | const message = typedData.data; 87 | const typedDataID = typedData.id; 88 | 89 | /* Get the signature for the message signed with the wallet */ 90 | const params = [address, message]; 91 | const method = "eth_signTypedData_v4"; 92 | const signature = await signer.provider.send(method, params); 93 | 94 | /* Call the relay to broadcast the transaction */ 95 | const relayResult = await relay({ 96 | variables: { 97 | input: { 98 | typedDataID: typedDataID, 99 | signature: signature, 100 | }, 101 | }, 102 | }); 103 | const txHash = relayResult.data?.relay?.relayTransaction?.txHash; 104 | 105 | /* Log the transation hash */ 106 | console.log("~~ Tx hash ~~"); 107 | console.log(txHash); 108 | 109 | /* Set the state to true */ 110 | setStateCollect(true); 111 | 112 | /* Display success message */ 113 | handleModal("success", "Post was collected!"); 114 | } catch (error) { 115 | /* Display error message */ 116 | const message = error.message as string; 117 | handleModal("error", message); 118 | } 119 | }; 120 | 121 | return ( 122 | 129 | ); 130 | } 131 | 132 | export default CollectBtn; 133 | -------------------------------------------------------------------------------- /components/Buttons/PostBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { useMutation, useLazyQuery } from "@apollo/client"; 3 | import { pinJSONToIPFS, getEssenceSVGData } from "../../helpers/functions"; 4 | import { 5 | CREATE_REGISTER_ESSENCE_TYPED_DATA, 6 | RELAY, 7 | RELAY_ACTION_STATUS, 8 | } from "../../graphql"; 9 | import { IEssenceMetadata, IPostInput } from "../../types"; 10 | import { randPhrase } from "@ngneat/falso"; 11 | import { AuthContext } from "../../context/auth"; 12 | import { ModalContext } from "../../context/modal"; 13 | import { v4 as uuidv4 } from "uuid"; 14 | 15 | function PostBtn({ nftImageURL, content, middleware }: IPostInput) { 16 | const { 17 | accessToken, 18 | primaryProfile, 19 | indexingPosts, 20 | setIndexingPosts, 21 | connectWallet, 22 | checkNetwork, 23 | } = useContext(AuthContext); 24 | const { handleModal } = useContext(ModalContext); 25 | const [createRegisterEssenceTypedData] = useMutation( 26 | CREATE_REGISTER_ESSENCE_TYPED_DATA 27 | ); 28 | const [getRelayActionStatus] = useLazyQuery(RELAY_ACTION_STATUS); 29 | const [relay] = useMutation(RELAY); 30 | 31 | const handleOnClick = async () => { 32 | try { 33 | /* Check if the user logged in */ 34 | if (!accessToken) { 35 | throw Error("You need to Sign in."); 36 | } 37 | 38 | /* Check if the has signed up */ 39 | if (!primaryProfile?.profileID) { 40 | throw Error("Youn need to mint a profile."); 41 | } 42 | 43 | /* Connect wallet and get provider */ 44 | const provider = await connectWallet(); 45 | 46 | /* Check if the network is the correct one */ 47 | await checkNetwork(provider); 48 | 49 | /* Function to render the svg data for the NFT */ 50 | /* (default if the user doesn't pass a image url) */ 51 | const svg_data = getEssenceSVGData(); 52 | 53 | /* Construct the metadata object for the Essence NFT */ 54 | const metadata: IEssenceMetadata = { 55 | metadata_id: uuidv4(), 56 | version: "1.0.0", 57 | app_id: "cyberconnect-bnbt", 58 | lang: "en", 59 | issue_date: new Date().toISOString(), 60 | content: content || randPhrase(), 61 | media: [], 62 | tags: [], 63 | image: nftImageURL ? nftImageURL : "", 64 | image_data: !nftImageURL ? svg_data : "", 65 | name: `@${primaryProfile?.handle}'s post`, 66 | description: `@${primaryProfile?.handle}'s post on CyberConnect Content app`, 67 | animation_url: "", 68 | external_url: "", 69 | attributes: [], 70 | }; 71 | 72 | /* Upload metadata to IPFS */ 73 | const ipfsHash = await pinJSONToIPFS(metadata); 74 | 75 | /* Get the signer from the provider */ 76 | const signer = provider.getSigner(); 77 | 78 | /* Get the address from the provider */ 79 | const address = await signer.getAddress(); 80 | 81 | /* Get the network from the provider */ 82 | const network = await provider.getNetwork(); 83 | 84 | /* Create typed data in a readable format */ 85 | const typedDataResult = await createRegisterEssenceTypedData({ 86 | variables: { 87 | input: { 88 | /* The profile id under which the Essence is registered */ 89 | profileID: primaryProfile?.profileID, 90 | /* Name of the Essence */ 91 | name: "Post", 92 | /* Symbol of the Essence */ 93 | symbol: "POST", 94 | /* URL for the json object containing data about content and the Essence NFT */ 95 | tokenURI: `https://cyberconnect.mypinata.cloud/ipfs/${ipfsHash}`, 96 | /* Middleware that allows users to collect the Essence NFT for free */ 97 | middleware: 98 | middleware === "free" 99 | ? { collectFree: true } 100 | : { 101 | collectPaid: { 102 | /* Address that will receive the amount */ 103 | recipient: address, 104 | /* Number of times the Essence can be collected */ 105 | totalSupply: "1000", 106 | /* Amount that needs to be paid to collect essence */ 107 | amount: "1000000000000000000", 108 | /* The currency for the amount. Chainlink token contract on Goerli */ 109 | currency: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB", 110 | /* If it require that the collector is also subscribed */ 111 | subscribeRequired: false, 112 | }, 113 | }, 114 | /* Set if the Essence should be transferable or not */ 115 | transferable: true, 116 | }, 117 | }, 118 | }); 119 | 120 | const typedData = 121 | typedDataResult.data?.createRegisterEssenceTypedData?.typedData; 122 | const message = typedData.data; 123 | const typedDataID = typedData.id; 124 | 125 | /* Get the signature for the message signed with the wallet */ 126 | const fromAddress = await signer.getAddress(); 127 | const params = [fromAddress, message]; 128 | const method = "eth_signTypedData_v4"; 129 | const signature = await signer.provider.send(method, params); 130 | 131 | /* Call the relay to broadcast the transaction */ 132 | const relayResult = await relay({ 133 | variables: { 134 | input: { 135 | typedDataID: typedDataID, 136 | signature: signature, 137 | }, 138 | }, 139 | }); 140 | 141 | const relayActionId = relayResult.data.relay.relayActionId; 142 | 143 | /* Close Post Modal */ 144 | handleModal(null, ""); 145 | 146 | const relayingPost = { 147 | createdBy: { 148 | handle: primaryProfile?.handle, 149 | avatar: primaryProfile?.avatar, 150 | metadata: primaryProfile?.metadata, 151 | profileID: primaryProfile?.profileID, 152 | }, 153 | essenceID: 0, // Value will be updated once it's indexed 154 | tokenURI: `https://cyberconnect.mypinata.cloud/ipfs/${ipfsHash}`, 155 | isIndexed: false, 156 | isCollectedByMe: false, 157 | collectMw: undefined, 158 | relayActionId: relayActionId, 159 | }; 160 | 161 | localStorage.setItem( 162 | "relayingPosts", 163 | JSON.stringify([...indexingPosts, relayingPost]) 164 | ); 165 | /* Set the indexingPosts in the state variables */ 166 | setIndexingPosts([...indexingPosts, relayingPost]); 167 | 168 | /* Display success message */ 169 | handleModal("success", "Post was created!"); 170 | } catch (error) { 171 | /* Set the indexingPosts in the state variables */ 172 | setIndexingPosts([...indexingPosts]); 173 | 174 | /* Display error message */ 175 | const message = error.message as string; 176 | handleModal("error", message); 177 | } 178 | }; 179 | 180 | return ( 181 | 184 | ); 185 | } 186 | 187 | export default PostBtn; 188 | -------------------------------------------------------------------------------- /components/Buttons/SetEssenceBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { CREATE_SET_ESSENCE_DATA_TYPED_DATA, RELAY } from "../../graphql"; 4 | import { AuthContext } from "../../context/auth"; 5 | import { ModalContext } from "../../context/modal"; 6 | 7 | function SetEssenceBtn({ 8 | essence, 9 | middleware, 10 | }: { 11 | essence: { 12 | essenceID: number; 13 | tokenURI: string; 14 | }; 15 | middleware: string; 16 | }) { 17 | const { accessToken, primaryProfile, connectWallet, checkNetwork } = 18 | useContext(AuthContext); 19 | const { handleModal } = useContext(ModalContext); 20 | const [createSetEssenceDataTypedData] = useMutation( 21 | CREATE_SET_ESSENCE_DATA_TYPED_DATA 22 | ); 23 | const [relay] = useMutation(RELAY); 24 | 25 | const handleOnClick = async () => { 26 | try { 27 | /* Check if the user logged in */ 28 | if (!accessToken) { 29 | throw Error("You need to Sign in."); 30 | } 31 | 32 | /* Check if the has signed up */ 33 | if (!primaryProfile?.profileID) { 34 | throw Error("Youn need to mint a profile."); 35 | } 36 | 37 | /* Connect wallet and get provider */ 38 | const provider = await connectWallet(); 39 | 40 | /* Check if the network is the correct one */ 41 | await checkNetwork(provider); 42 | 43 | /* Get the signer from the provider */ 44 | const signer = provider.getSigner(); 45 | 46 | /* Get the address from the provider */ 47 | const address = await signer.getAddress(); 48 | 49 | /* Get the network from the provider */ 50 | const network = await provider.getNetwork(); 51 | 52 | /* Create typed data in a readable format */ 53 | const typedDataResult = await createSetEssenceDataTypedData({ 54 | variables: { 55 | input: { 56 | /* The id of the essence the middleware is set for */ 57 | essenceId: essence.essenceID, 58 | /* The id of the profile that created the essence */ 59 | profileId: primaryProfile?.profileID, 60 | /* URL for the json object containing data about content and the Essence NFT */ 61 | tokenURI: essence.tokenURI, 62 | /* The middleware that will be set for the essence */ 63 | middleware: 64 | middleware === "free" 65 | ? { collectFree: true } 66 | : { 67 | collectPaid: { 68 | /* Address that will receive the amount */ 69 | recipient: address, 70 | /* Number of times the Essence can be collected */ 71 | totalSupply: "1000", 72 | /* Amount that needs to be paid to collect essence */ 73 | amount: "1000000000000000000", 74 | /* The currency for the amount. Chainlink token contract on Goerli */ 75 | currency: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB", 76 | /* If it require that the collector is also subscribed */ 77 | subscribeRequired: false, 78 | }, 79 | }, 80 | }, 81 | }, 82 | }); 83 | const typedData = 84 | typedDataResult.data?.createSetEssenceDataTypedData?.typedData; 85 | const message = typedData.data; 86 | const typedDataID = typedData.id; 87 | 88 | /* Get the signature for the message signed with the wallet */ 89 | const fromAddress = await signer.getAddress(); 90 | const params = [fromAddress, message]; 91 | const method = "eth_signTypedData_v4"; 92 | const signature = await signer.provider.send(method, params); 93 | 94 | /* Call the relay to broadcast the transaction */ 95 | const relayResult = await relay({ 96 | variables: { 97 | input: { 98 | typedDataID: typedDataID, 99 | signature: signature, 100 | }, 101 | }, 102 | }); 103 | const txHash = relayResult.data?.relay?.relayTransaction?.txHash; 104 | 105 | /* Log the transation hash */ 106 | console.log("~~ Tx hash ~~"); 107 | console.log(txHash); 108 | 109 | /* Display success message */ 110 | handleModal("success", "Essence middleware was set!"); 111 | } catch (error) { 112 | console.log(error); 113 | /* Display error message */ 114 | const message = error.message as string; 115 | handleModal("error", message); 116 | } 117 | }; 118 | 119 | return ( 120 | 127 | ); 128 | } 129 | 130 | export default SetEssenceBtn; 131 | -------------------------------------------------------------------------------- /components/Buttons/SetSubscribeBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { CREATE_SET_SUBSCRIBE_DATA_TYPED_DATA, RELAY } from "../../graphql"; 4 | import { AuthContext } from "../../context/auth"; 5 | import { ModalContext } from "../../context/modal"; 6 | import { getSubscriberSVGData, pinJSONToIPFS } from "../../helpers/functions"; 7 | 8 | function SetSubscribeBtn({ 9 | profileID, 10 | middleware, 11 | }: { 12 | profileID: number; 13 | middleware: string; 14 | }) { 15 | const { accessToken, primaryProfile, connectWallet, checkNetwork } = 16 | useContext(AuthContext); 17 | const { handleModal } = useContext(ModalContext); 18 | const [createSetSubscribeDataTypedData] = useMutation( 19 | CREATE_SET_SUBSCRIBE_DATA_TYPED_DATA 20 | ); 21 | const [relay] = useMutation(RELAY); 22 | 23 | const handleOnClick = async () => { 24 | try { 25 | /* Check if the user logged in */ 26 | if (!accessToken) { 27 | throw Error("You need to Sign in."); 28 | } 29 | 30 | /* Check if the has signed up */ 31 | if (!primaryProfile?.profileID) { 32 | throw Error("Youn need to mint a profile."); 33 | } 34 | 35 | /* Connect wallet and get provider */ 36 | const provider = await connectWallet(); 37 | 38 | /* Check if the network is the correct one */ 39 | await checkNetwork(provider); 40 | 41 | /* Get the signer from the provider */ 42 | const signer = provider.getSigner(); 43 | 44 | /* Get the address from the provider */ 45 | const address = await signer.getAddress(); 46 | 47 | /* Get the network from the provider */ 48 | const network = await provider.getNetwork(); 49 | 50 | /* Get the chain id from the network */ 51 | 52 | /* Construct the metadata object for the Subscribe NFT */ 53 | const metadata = { 54 | image_data: getSubscriberSVGData(), 55 | name: `@${primaryProfile?.handle}'s subscriber`, 56 | description: `@${primaryProfile.handle}'s subscriber on CyberConnect Content app`, 57 | }; 58 | 59 | /* Upload metadata to IPFS */ 60 | const ipfsHash = await pinJSONToIPFS(metadata); 61 | 62 | /* Create typed data in a readable format */ 63 | const typedDataResult = await createSetSubscribeDataTypedData({ 64 | variables: { 65 | input: { 66 | profileId: profileID, 67 | /* URL for the json object containing data about the Subscribe NFT */ 68 | tokenURI: `https://cyberconnect.mypinata.cloud/ipfs/${ipfsHash}`, 69 | middleware: 70 | middleware === "free" 71 | ? { subscribeFree: true } 72 | : { 73 | subscribePaid: { 74 | /* Address that will receive the amount */ 75 | recipient: address, 76 | /* Amount that needs to be paid to subscribe */ 77 | amount: 1, 78 | /* The currency for the amount. Chainlink token contract on Goerli */ 79 | currency: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB", 80 | /* If it require the subscriber to hold a NFT */ 81 | nftRequired: false, 82 | /* The contract of the NFT that the subscriber needs to hold */ 83 | nftAddress: "0x0000000000000000000000000000000000000000", 84 | }, 85 | }, 86 | }, 87 | }, 88 | }); 89 | const typedData = 90 | typedDataResult.data?.createSetSubscribeDataTypedData?.typedData; 91 | const message = typedData.data; 92 | const typedDataID = typedData.id; 93 | 94 | /* Get the signature for the message signed with the wallet */ 95 | const fromAddress = await signer.getAddress(); 96 | const params = [fromAddress, message]; 97 | const method = "eth_signTypedData_v4"; 98 | const signature = await signer.provider.send(method, params); 99 | 100 | /* Call the relay to broadcast the transaction */ 101 | const relayResult = await relay({ 102 | variables: { 103 | input: { 104 | typedDataID: typedDataID, 105 | signature: signature, 106 | }, 107 | }, 108 | }); 109 | const txHash = relayResult.data?.relay?.relayTransaction?.txHash; 110 | 111 | /* Log the transation hash */ 112 | console.log("~~ Tx hash ~~"); 113 | console.log(txHash); 114 | 115 | /* Display success message */ 116 | handleModal("success", "Subscribe middleware was set!"); 117 | } catch (error) { 118 | /* Display error message */ 119 | const message = error.message as string; 120 | handleModal("error", message); 121 | } 122 | }; 123 | 124 | return ( 125 | 132 | ); 133 | } 134 | 135 | export default SetSubscribeBtn; 136 | -------------------------------------------------------------------------------- /components/Buttons/SigninBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { LOGIN_GET_MESSAGE, LOGIN_VERIFY } from "../../graphql"; 4 | import { DOMAIN } from "../../helpers/constants"; 5 | import { AuthContext } from "../../context/auth"; 6 | import { ModalContext } from "../../context/modal"; 7 | 8 | function SigninBtn() { 9 | const { setAccessToken, connectWallet, checkNetwork } = 10 | useContext(AuthContext); 11 | const { handleModal } = useContext(ModalContext); 12 | const [loginGetMessage] = useMutation(LOGIN_GET_MESSAGE); 13 | const [loginVerify] = useMutation(LOGIN_VERIFY); 14 | 15 | const handleOnClick = async () => { 16 | try { 17 | /* Connect wallet and get provider */ 18 | const provider = await connectWallet(); 19 | 20 | /* Check if the network is the correct one */ 21 | await checkNetwork(provider); 22 | 23 | /* Get the signer from the provider */ 24 | const signer = provider.getSigner(); 25 | 26 | /* Get the address from the provider */ 27 | const account = await signer.getAddress(); 28 | 29 | /* Get the network from the provider */ 30 | const network = await provider.getNetwork(); 31 | 32 | /* Get the chain id from the network */ 33 | 34 | /* Get the message from the server */ 35 | const messageResult = await loginGetMessage({ 36 | variables: { 37 | input: { 38 | address: account, 39 | domain: DOMAIN, 40 | }, 41 | }, 42 | }); 43 | const message = messageResult?.data?.loginGetMessage?.message; 44 | 45 | /* Get the signature for the message signed with the wallet */ 46 | const signature = await signer.signMessage(message); 47 | 48 | /* Verify the signature on the server and get the access token */ 49 | const accessTokenResult = await loginVerify({ 50 | variables: { 51 | input: { 52 | address: account, 53 | domain: DOMAIN, 54 | signature: signature, 55 | }, 56 | }, 57 | }); 58 | const accessToken = accessTokenResult?.data?.loginVerify?.accessToken; 59 | 60 | /* Log the access token */ 61 | console.log("~~ Access token ~~"); 62 | console.log(accessToken); 63 | 64 | /* Save the access token in local storage */ 65 | localStorage.setItem("accessToken", accessToken); 66 | 67 | /* Set the access token in the state variable */ 68 | setAccessToken(accessToken); 69 | 70 | /* Display success message */ 71 | handleModal("success", "You are now logged in!"); 72 | } catch (error) { 73 | /* Display error message */ 74 | const message = error.message as string; 75 | handleModal("error", message); 76 | } 77 | }; 78 | 79 | return ( 80 | 83 | ); 84 | } 85 | 86 | export default SigninBtn; 87 | -------------------------------------------------------------------------------- /components/Buttons/SignupBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useContext } from "react"; 3 | import { ethers } from "ethers"; 4 | import ProfileNFTABI from "../../abi/ProfileNFT.json"; 5 | import { 6 | PROFILE_NFT_CONTRACT, 7 | PROFILE_NFT_OPERATOR, 8 | } from "../../helpers/constants"; 9 | import { pinJSONToIPFS } from "../../helpers/functions"; 10 | import { 11 | randUserName, 12 | randAvatar, 13 | randPhrase, 14 | randFullName, 15 | } from "@ngneat/falso"; 16 | import { IProfileMetadata, ISignupInput } from "../../types"; 17 | import { AuthContext } from "../../context/auth"; 18 | import { ModalContext } from "../../context/modal"; 19 | 20 | function SignupBtn({ handle, avatar, name, bio, operator }: ISignupInput) { 21 | const router = useRouter(); 22 | const { indexingProfiles, setIndexingProfiles, connectWallet, checkNetwork } = 23 | useContext(AuthContext); 24 | const { handleModal } = useContext(ModalContext); 25 | 26 | const handleOnClick = async () => { 27 | try { 28 | /* Connect wallet and get provider */ 29 | const provider = await connectWallet(); 30 | 31 | /* Check if the network is the correct one */ 32 | await checkNetwork(provider); 33 | 34 | const profileName = name || randFullName(); 35 | const profileHandle = handle || randUserName(); 36 | const profileAvatar = avatar || randAvatar(); 37 | const profileBio = bio || randPhrase(); 38 | 39 | /* Construct metadata schema */ 40 | const metadata: IProfileMetadata = { 41 | name: profileName, 42 | bio: profileBio, 43 | handle: profileHandle, 44 | version: "1.0.0", 45 | }; 46 | 47 | /* Upload metadata to IPFS */ 48 | const ipfsHash = await pinJSONToIPFS(metadata); 49 | 50 | /* Get the signer from the provider */ 51 | const signer = provider.getSigner(); 52 | 53 | /* Get the address from the provider */ 54 | const address = await signer.getAddress(); 55 | 56 | /* Get the contract instance */ 57 | const contract = new ethers.Contract( 58 | PROFILE_NFT_CONTRACT, 59 | ProfileNFTABI, 60 | signer 61 | ); 62 | 63 | /* Call the createProfile function to create the profile */ 64 | const tx = await contract.createProfile( 65 | /* CreateProfileParams */ 66 | { 67 | to: address, 68 | handle: handle || randUserName(), 69 | avatar: avatar || randAvatar({ size: 200 }), 70 | metadata: ipfsHash, 71 | operator: operator || PROFILE_NFT_OPERATOR, 72 | }, 73 | /* preData */ 74 | 0x0, 75 | /* postData */ 76 | 0x0 77 | ); 78 | 79 | /* Close Signup Modal */ 80 | handleModal(null, ""); 81 | 82 | /* Call the getProfileIdByHandle function to get the profile id */ 83 | const profileID = await contract.getProfileIdByHandle(handle); 84 | 85 | /* Set the indexingProfiles in the state variables */ 86 | setIndexingProfiles([ 87 | ...indexingProfiles, 88 | { 89 | profileID: Number(profileID), 90 | handle: profileHandle, 91 | avatar: profileAvatar, 92 | metadata: ipfsHash, 93 | isIndexed: false, 94 | }, 95 | ]); 96 | 97 | /* Wait for the transaction to be executed */ 98 | await tx.wait(); 99 | 100 | /* Log the transaction hash */ 101 | console.log("~~ Tx hash ~~"); 102 | console.log(tx.hash); 103 | 104 | /* Display success message */ 105 | handleModal("success", "Profile was created!"); 106 | } catch (error) { 107 | /* Set the indexingProfiles in the state variables */ 108 | setIndexingProfiles([...indexingProfiles]); 109 | 110 | /* Display error message */ 111 | const message = error.message as string; 112 | handleModal("error", message); 113 | } 114 | }; 115 | 116 | return ( 117 | 123 | ); 124 | } 125 | 126 | export default SignupBtn; 127 | -------------------------------------------------------------------------------- /components/Buttons/SubscribeBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { CREATE_SUBSCRIBE_TYPED_DATA, RELAY } from "../../graphql"; 4 | import { AuthContext } from "../../context/auth"; 5 | import { ModalContext } from "../../context/modal"; 6 | 7 | function SubscribeBtn({ 8 | profileID, 9 | isSubscribedByMe, 10 | }: { 11 | profileID: number; 12 | isSubscribedByMe: boolean; 13 | }) { 14 | const { accessToken, connectWallet, checkNetwork } = useContext(AuthContext); 15 | const { handleModal } = useContext(ModalContext); 16 | const [createSubscribeTypedData] = useMutation(CREATE_SUBSCRIBE_TYPED_DATA); 17 | const [relay] = useMutation(RELAY); 18 | const [stateSubscribe, setStateSubscribe] = useState(false); 19 | 20 | const handleOnClick = async () => { 21 | try { 22 | /* Check if the user logged in */ 23 | if (!accessToken) { 24 | throw Error("You need to Sign in."); 25 | } 26 | 27 | /* Connect wallet and get provider */ 28 | const provider = await connectWallet(); 29 | 30 | /* Check if the network is the correct one */ 31 | await checkNetwork(provider); 32 | 33 | /* Get the signer from the provider */ 34 | const signer = provider.getSigner(); 35 | 36 | /* Get the address from the provider */ 37 | const address = await signer.getAddress(); 38 | 39 | /* Get the network from the provider */ 40 | const network = await provider.getNetwork(); 41 | 42 | /* Get the chain id from the network */ 43 | 44 | /* Create typed data in a readable format */ 45 | const typedDataResult = await createSubscribeTypedData({ 46 | variables: { 47 | input: { 48 | profileIDs: [profileID], 49 | }, 50 | }, 51 | }); 52 | const typedData = 53 | typedDataResult.data?.createSubscribeTypedData?.typedData; 54 | const message = typedData.data; 55 | const typedDataID = typedData.id; 56 | 57 | /* Get the signature for the message signed with the wallet */ 58 | const fromAddress = address; 59 | const params = [fromAddress, message]; 60 | const method = "eth_signTypedData_v4"; 61 | const signature = await signer.provider.send(method, params); 62 | 63 | /* Call the relay to broadcast the transaction */ 64 | const relayResult = await relay({ 65 | variables: { 66 | input: { 67 | typedDataID: typedDataID, 68 | signature: signature, 69 | }, 70 | }, 71 | }); 72 | const txHash = relayResult.data?.relay?.relayTransaction?.txHash; 73 | 74 | /* Log the transation hash */ 75 | console.log("~~ Tx hash ~~"); 76 | console.log(txHash); 77 | 78 | /* Set the state to true */ 79 | setStateSubscribe(true); 80 | 81 | /* Display success message */ 82 | handleModal("success", "Subscribed to profile!"); 83 | } catch (error) { 84 | /* Display error message */ 85 | const message = error.message as string; 86 | handleModal("error", message); 87 | } 88 | }; 89 | 90 | return ( 91 | 98 | ); 99 | } 100 | 101 | export default SubscribeBtn; 102 | -------------------------------------------------------------------------------- /components/Cards/AccountCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Image from "next/image"; 3 | import { IAccountCard } from "../../types"; 4 | import { parseURL } from "../../helpers/functions"; 5 | import Loader from "../Loader"; 6 | 7 | const AccountCard = ({ profileID, handle, avatar, metadata, isPrimary, isIndexed }: IAccountCard) => { 8 | const [src, setSrc] = useState(parseURL(avatar)); 9 | const [data, setData] = useState({ 10 | name: "", 11 | bio: "" 12 | }); 13 | 14 | useEffect(() => { 15 | if (!metadata) return; 16 | (async () => { 17 | setData({ 18 | name: "", 19 | bio: "" 20 | }); 21 | try { 22 | const res = await fetch(parseURL(metadata)); 23 | if (res.status === 200) { 24 | const data = await res.json(); 25 | setData(data); 26 | } 27 | } catch (error) { 28 | console.error(error); 29 | } 30 | })(); 31 | }, [metadata]); 32 | 33 | return ( 34 |
35 |
36 | avatar setSrc("/assets/avatar-placeholder.svg")} 42 | placeholder="blur" 43 | blurDataURL="/assets/avatar-placeholder.svg" 44 | /> 45 |
46 |
47 |
48 |
{data.name} •
49 |
@{handle}
50 |
51 |
Profile ID: {profileID}
52 |
53 |
54 | { 55 | !isIndexed 56 | ? 57 | :
58 | { 59 | isPrimary 60 | ?
Primary
61 | :
62 | } 63 |
64 | } 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default AccountCard; 71 | -------------------------------------------------------------------------------- /components/Cards/EssenceMwCard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { IEssenceMwCard } from "../../types"; 3 | import { parseURL } from "../../helpers/functions"; 4 | 5 | const EssenceMwCard = ({ 6 | essence, 7 | setSelectedEssence, 8 | setSelectedEssenceContent, 9 | setShowDropdown 10 | }: IEssenceMwCard) => { 11 | const [content, setContent] = useState(""); 12 | 13 | useEffect(() => { 14 | if (!essence?.tokenURI) return; 15 | (async () => { 16 | setContent(""); 17 | try { 18 | const res = await fetch(parseURL(essence?.tokenURI)); 19 | if (res.status === 200) { 20 | const data = await res.json(); 21 | setContent(data?.content); 22 | } 23 | } catch (error) { 24 | console.error(error); 25 | } 26 | })(); 27 | }, [essence]); 28 | 29 | const handleOnClick = () => { 30 | setSelectedEssence(essence); 31 | setSelectedEssenceContent(content); 32 | setShowDropdown(false); 33 | } 34 | 35 | return ( 36 |
{content}
40 | ); 41 | }; 42 | 43 | export default EssenceMwCard; -------------------------------------------------------------------------------- /components/Cards/PostCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Image from "next/image"; 3 | import CollectBtn from "../Buttons/CollectBtn"; 4 | import { IPostCard } from "../../types"; 5 | import { parseURL, timeSince } from "../../helpers/functions"; 6 | import Loader from "../Loader"; 7 | 8 | const PostCard = ({ essenceID, tokenURI, createdBy, isCollectedByMe, isIndexed, collectMw }: IPostCard) => { 9 | const { avatar, handle, profileID, metadata } = createdBy; 10 | const [name, setName] = useState(""); 11 | const [data, setData] = useState({ 12 | image: "", 13 | image_data: "", 14 | content: "", 15 | issue_date: "", 16 | attributes: [], 17 | }); 18 | const [avatarSrc, setAvatarSrc] = useState(parseURL(avatar)); 19 | const [nftSrc, setNftSrc] = useState(data.image ? parseURL(data.image) : data.image_data); 20 | 21 | useEffect(() => { 22 | if (!tokenURI) return; 23 | (async () => { 24 | setData({ 25 | image: "", 26 | image_data: "", 27 | content: "", 28 | issue_date: "", 29 | attributes: [], 30 | }); 31 | try { 32 | const res = await fetch(parseURL(tokenURI)); 33 | if (res.status === 200) { 34 | const data = await res.json(); 35 | setData(data); 36 | } 37 | } catch (error) { 38 | console.error(error); 39 | } 40 | })(); 41 | }, [tokenURI]); 42 | 43 | useEffect(() => { 44 | if (!metadata) return; 45 | (async () => { 46 | setName(""); 47 | try { 48 | const res = await fetch(parseURL(metadata)); 49 | if (res.status === 200) { 50 | const data = await res.json(); 51 | setName(data?.name); 52 | } 53 | } catch (error) { 54 | console.error(error); 55 | } 56 | })(); 57 | }, [metadata]); 58 | 59 | return ( 60 | <> 61 | { 62 | data?.attributes?.length === 0 && 63 |
64 |
65 | avatar setAvatarSrc("/assets/avatar-placeholder.svg")} 71 | placeholder="blur" 72 | blurDataURL="/assets/avatar-placeholder.svg" 73 | /> 74 |
75 |
76 |
77 |
{name}
78 |
@{handle} •
79 |
{timeSince(new Date(data.issue_date))}
80 |
81 |
{JSON.stringify(data.content)}
82 |
83 |
84 | nft setNftSrc("/assets/essence-placeholder.svg")} 90 | placeholder="blur" 91 | blurDataURL="/assets/essence-placeholder.svg" 92 | /> 93 |
94 |
95 | { 96 | isIndexed 97 | ? 103 | : 104 | } 105 |
106 |
107 | } 108 | 109 | ); 110 | }; 111 | 112 | export default PostCard; 113 | -------------------------------------------------------------------------------- /components/Cards/PrimaryProfileCard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useContext } from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { IPrimaryProfileCard } from "../../types"; 5 | import { parseURL } from "../../helpers/functions"; 6 | import { AuthContext } from "../../context/auth"; 7 | 8 | const PrimaryProfileCard = ({ handle, avatar, metadata }: IPrimaryProfileCard) => { 9 | const { address } = useContext(AuthContext); 10 | const [src, setSrc] = useState(parseURL(avatar)); 11 | const [data, setData] = useState({ 12 | name: "", 13 | bio: "" 14 | }); 15 | 16 | useEffect(() => { 17 | if (!metadata) return; 18 | (async () => { 19 | setData({ 20 | name: "", 21 | bio: "" 22 | }); 23 | try { 24 | const res = await fetch(parseURL(metadata)); 25 | if (res.status === 200) { 26 | const data = await res.json(); 27 | setData(data); 28 | } 29 | } catch (error) { 30 | console.error(error); 31 | } 32 | })(); 33 | }, [metadata]); 34 | 35 | return ( 36 |
37 |
38 | 39 |
40 | avatar setSrc("/assets/avatar-placeholder.svg")} 46 | placeholder="blur" 47 | blurDataURL="/assets/avatar-placeholder.svg" 48 | /> 49 |
50 | 51 | { 52 | address && 53 |
54 |
{`${address.slice(0, 6)}..`}
55 |
56 |
57 | } 58 |
59 |
60 |
{data.name}
61 |
@{handle}
62 |
63 |

64 |
{data.bio}
65 |
66 | ); 67 | }; 68 | 69 | export default PrimaryProfileCard; 70 | -------------------------------------------------------------------------------- /components/Cards/ProfileCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Image from "next/image"; 3 | import SubscribeBtn from "../Buttons/SubscribeBtn"; 4 | import { IProfileCard } from "../../types"; 5 | import { parseURL } from "../../helpers/functions"; 6 | 7 | const ProfileCard = ({ 8 | handle, 9 | avatar, 10 | metadata, 11 | profileID, 12 | isSubscribedByMe, 13 | }: any) => { 14 | const [src, setSrc] = useState(parseURL(avatar)); 15 | const [data, setData] = useState({ 16 | name: "", 17 | bio: "", 18 | }); 19 | 20 | useEffect(() => { 21 | if (!metadata) return; 22 | (async () => { 23 | setData({ 24 | name: "", 25 | bio: "", 26 | }); 27 | try { 28 | const res = await fetch(parseURL(metadata)); 29 | if (res.status === 200) { 30 | const data = await res.json(); 31 | setData(data); 32 | } 33 | } catch (error) { 34 | console.error(error); 35 | } 36 | })(); 37 | }, [metadata]); 38 | 39 | return ( 40 |
41 |
42 |
43 | avatar setSrc("/assets/avatar-placeholder.svg")} 49 | placeholder="blur" 50 | blurDataURL="/assets/avatar-placeholder.svg" 51 | /> 52 |
53 | 57 |
58 |
59 |
{data.name}
60 |
@{handle}
61 |
{data.bio}
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default ProfileCard; 68 | -------------------------------------------------------------------------------- /components/Cards/SubscribeMwCard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { IProfileMwCard } from "../../types"; 3 | import { parseURL } from "../../helpers/functions"; 4 | 5 | const SubscribeMwCard = ({ 6 | profileID, 7 | metadata, 8 | setSelectedProfileId, 9 | setSelectedProfileHandle, 10 | setShowDropdown 11 | }: IProfileMwCard) => { 12 | const [handle, setHandle] = useState(""); 13 | 14 | useEffect(() => { 15 | if (!metadata) return; 16 | (async () => { 17 | setHandle(""); 18 | try { 19 | const res = await fetch(parseURL(metadata)); 20 | if (res.status === 200) { 21 | const data = await res.json(); 22 | setHandle(data?.handle); 23 | } 24 | } catch (error) { 25 | console.error(error); 26 | } 27 | })(); 28 | }, [metadata]); 29 | 30 | const handleOnClick = () => { 31 | setSelectedProfileId(profileID); 32 | setSelectedProfileHandle(handle); 33 | setShowDropdown(false); 34 | } 35 | 36 | return ( 37 |
@{handle}
41 | ); 42 | }; 43 | 44 | export default SubscribeMwCard; -------------------------------------------------------------------------------- /components/Cards/SuggestedProfileCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Image from "next/image"; 3 | import SubscribeBtn from "../Buttons/SubscribeBtn"; 4 | import { IProfileCard } from "../../types"; 5 | import { parseURL } from "../../helpers/functions"; 6 | 7 | const SuggestedProfileCard = ({ 8 | handle, 9 | avatar, 10 | metadata, 11 | profileID, 12 | isSubscribedByMe, 13 | }: any) => { 14 | const [src, setSrc] = useState(parseURL(avatar)); 15 | const [data, setData] = useState({ 16 | name: "", 17 | bio: "", 18 | }); 19 | 20 | useEffect(() => { 21 | if (!metadata) return; 22 | (async () => { 23 | setData({ 24 | name: "", 25 | bio: "", 26 | }); 27 | try { 28 | const res = await fetch(parseURL(metadata)); 29 | if (res.status === 200) { 30 | const data = await res.json(); 31 | setData(data); 32 | } 33 | } catch (error) { 34 | console.error(error); 35 | } 36 | })(); 37 | }, [metadata]); 38 | 39 | return ( 40 |
41 |
42 | avatar setSrc("/assets/avatar-placeholder.svg")} 48 | placeholder="blur" 49 | blurDataURL="/assets/avatar-placeholder.svg" 50 | /> 51 |
52 |
53 |
{data.name}
54 |
@{handle}
55 |
56 | 57 |
58 | ); 59 | }; 60 | 61 | export default SuggestedProfileCard; 62 | -------------------------------------------------------------------------------- /components/Forms/EssenceMwForm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useState, 3 | MouseEvent, 4 | ChangeEvent, 5 | useContext, 6 | useEffect, 7 | } from "react"; 8 | import { AuthContext } from "../../context/auth"; 9 | import { useLazyQuery } from "@apollo/client"; 10 | import { ADDRESS } from "../../graphql"; 11 | import { IEssenceMwCard } from "../../types"; 12 | import EssenceMwCard from "../Cards/EssenceMwCard"; 13 | import SetEssenceBtn from "../Buttons/SetEssenceBtn"; 14 | import { BsCaretUpFill, BsFillCaretDownFill } from "react-icons/bs"; 15 | 16 | const EssenceMwForm = () => { 17 | const { address, accessToken } = useContext(AuthContext); 18 | const [essenceMw, setEssenceMw] = useState("free"); 19 | 20 | /* State variable to store the essences */ 21 | const [essences, setEssences] = useState([]); 22 | 23 | /* State variable to store the selected essence */ 24 | const [selectedEssence, setSelectedEssence] = useState({ 25 | essenceID: 0, 26 | tokenURI: "", 27 | }); 28 | 29 | /* State variable to store the selected essence */ 30 | const [selectedEssenceContent, setSelectedEssenceContent] = 31 | useState(""); 32 | 33 | /* Query to get user information by wallet address */ 34 | const [getAddress] = useLazyQuery(ADDRESS); 35 | 36 | const [showDropdown, setShowDropdown] = useState(false); 37 | 38 | useEffect(() => { 39 | if (!(address && accessToken)) return; 40 | 41 | (async () => { 42 | /* Get the primary profile for the wallet address */ 43 | const res = await getAddress({ 44 | variables: { 45 | address: address, 46 | }, 47 | }); 48 | const primaryProfile = res?.data?.address?.wallet?.primaryProfile; 49 | const essences = 50 | primaryProfile?.essences?.edges?.map((edge: any) => edge?.node) || []; 51 | 52 | /* Set the essences */ 53 | setEssences(essences); 54 | })(); 55 | }, [address, accessToken, getAddress]); 56 | 57 | const handleOnChange = (event: ChangeEvent) => { 58 | const value = event.target.value; 59 | setEssenceMw(value); 60 | }; 61 | 62 | const handleOnClick = (event: MouseEvent) => { 63 | const target = event.target as HTMLDivElement; 64 | if (target.className !== "dropdown-select-btn") { 65 | setShowDropdown(false); 66 | } 67 | }; 68 | 69 | return ( 70 |
71 |

Set middleware for Post

72 | 73 |
74 |
75 |
76 | {!selectedEssence ? "Select post" : `${selectedEssenceContent}`} 77 |
78 |
79 | {showDropdown ? : } 80 |
81 | 85 |
86 | {showDropdown && ( 87 |
88 | {essences.map((essence: any, index: number) => ( 89 | 97 | ))} 98 |
99 | )} 100 |
101 |
102 |
Middleware
103 |
104 | 114 | 123 |
124 |
125 |
126 | Note: You will set the middleware for the selected 127 | post. 128 |
129 | 130 |
131 | ); 132 | }; 133 | 134 | export default EssenceMwForm; 135 | -------------------------------------------------------------------------------- /components/Forms/PostForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState, ChangeEvent } from "react"; 2 | import { IPostInput } from "../../types"; 3 | import PostBtn from "../Buttons/PostBtn"; 4 | 5 | const PostForm = () => { 6 | const [postInput, setPostInput] = useState({ 7 | nftImageURL: "", 8 | content: "", 9 | middleware: "free" 10 | }); 11 | 12 | const handleOnChange = (event: ChangeEvent) => { 13 | const name = event.target.name; 14 | const value = event.target.value; 15 | setPostInput({ 16 | ...postInput, 17 | [name]: value 18 | }); 19 | }; 20 | 21 | return ( 22 |
23 |

Create post

24 |
25 | 26 | 32 |
33 |
34 | 35 | 41 |
42 |
43 |
Middleware
44 |
45 | 48 | 51 |
52 |
53 |
Note: For empty fields we will randomly generate values.
54 | 55 |
56 | ); 57 | }; 58 | 59 | export default PostForm; 60 | -------------------------------------------------------------------------------- /components/Forms/SignupForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState, ChangeEvent } from "react"; 2 | import { ISignupInput } from "../../types"; 3 | import SignupBtn from "../../components/Buttons/SignupBtn"; 4 | 5 | const SignupForm = () => { 6 | const [signupInput, setSignupInput] = useState({ 7 | handle: "", 8 | name: "", 9 | bio: "", 10 | avatar: "", 11 | operator: "", 12 | }); 13 | 14 | const handleOnChange = (event: ChangeEvent) => { 15 | const name = event.target.name; 16 | const value = event.target.value; 17 | setSignupInput({ 18 | ...signupInput, 19 | [name]: value 20 | }); 21 | }; 22 | 23 | return ( 24 |
25 |

Create profile

26 |
27 | 28 | 33 |
34 |
35 | 36 | 42 |
43 |
44 | 45 | 50 |
51 |
52 | 53 | 58 |
59 |
60 | 61 | 67 |
68 |
Note: For empty fields we will randomly generate values.
69 | 70 |
71 | ); 72 | }; 73 | 74 | export default SignupForm; 75 | -------------------------------------------------------------------------------- /components/Forms/SubscribeMwForm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useState, 3 | MouseEvent, 4 | ChangeEvent, 5 | useContext, 6 | useEffect, 7 | } from "react"; 8 | import { AuthContext } from "../../context/auth"; 9 | import { useLazyQuery } from "@apollo/client"; 10 | import { ADDRESS } from "../../graphql"; 11 | import { IProfileMwCard } from "../../types"; 12 | import SubscribeMwCard from "../Cards/SubscribeMwCard"; 13 | import SetSubscribeBtn from "../Buttons/SetSubscribeBtn"; 14 | import { BsCaretUpFill, BsFillCaretDownFill } from "react-icons/bs"; 15 | 16 | const SubscribeMwForm = () => { 17 | const { address, accessToken } = useContext(AuthContext); 18 | const [subscribeMw, setSubscribeMw] = useState("free"); 19 | 20 | /* State variable to store the profiles */ 21 | const [profiles, setProfiles] = useState([]); 22 | 23 | /* State variable to store the selected profile id */ 24 | const [selectedProfileId, setSelectedProfileId] = useState(0); 25 | 26 | /* State variable to store the selected handle */ 27 | const [selectedProfileHandle, setSelectedProfileHandle] = 28 | useState(""); 29 | 30 | /* Query to get user information by wallet address */ 31 | const [getAddress] = useLazyQuery(ADDRESS); 32 | 33 | const [showDropdown, setShowDropdown] = useState(false); 34 | 35 | useEffect(() => { 36 | if (!(address && accessToken)) return; 37 | 38 | (async () => { 39 | /* Get all profile for the wallet address */ 40 | const res = await getAddress({ 41 | variables: { 42 | address: address, 43 | }, 44 | }); 45 | const edges = res?.data?.address?.wallet?.profiles?.edges; 46 | const profiles = edges?.map((edge: any) => edge?.node) || []; 47 | 48 | /* Set the profile profiles */ 49 | setProfiles(profiles); 50 | })(); 51 | }, [address, accessToken, getAddress]); 52 | 53 | const handleOnChange = (event: ChangeEvent) => { 54 | const value = event.target.value; 55 | setSubscribeMw(value); 56 | }; 57 | 58 | const handleOnClick = (event: MouseEvent) => { 59 | const target = event.target as HTMLDivElement; 60 | if (target.className !== "dropdown-select-btn") { 61 | setShowDropdown(false); 62 | } 63 | }; 64 | 65 | return ( 66 |
67 |

Set middleware for Profile

68 | 69 |
70 |
71 |
72 | {selectedProfileId === 0 73 | ? "Select profile" 74 | : `@${selectedProfileHandle}`} 75 |
76 |
77 | {showDropdown ? : } 78 |
79 | 83 |
84 | {showDropdown && ( 85 |
86 | {profiles.map((profile: IProfileMwCard) => ( 87 | 96 | ))} 97 |
98 | )} 99 |
100 |
101 |
Middleware
102 |
103 | 113 | 122 |
123 |
124 |
125 | Note: You will set the middleware for the selected 126 | profile. 127 |
128 | 129 |
130 | ); 131 | }; 132 | 133 | export default SubscribeMwForm; 134 | -------------------------------------------------------------------------------- /components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loader = () => { 4 | return ( 5 |
6 |
7 |
indexing
8 |
9 | ); 10 | }; 11 | 12 | export default Loader; 13 | -------------------------------------------------------------------------------- /components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, MouseEvent } from "react"; 2 | import { ModalContext } from "../../context/modal"; 3 | import { BsFillCheckCircleFill } from "react-icons/bs"; 4 | import { TiWarning } from "react-icons/ti"; 5 | import SignupForm from "../Forms/SignupForm"; 6 | import PostForm from "../Forms/PostForm"; 7 | import EssenceMwForm from "../Forms/EssenceMwForm"; 8 | import SubscribeMwForm from "../Forms/SubscribeMwForm"; 9 | 10 | const Modal = () => { 11 | const { modal, modalType, modalText, handleModal } = useContext(ModalContext); 12 | 13 | const handleOnClick = (event: MouseEvent) => { 14 | const target = event.target as HTMLDivElement; 15 | if (target.className === "modal") { 16 | handleModal(null, ""); 17 | } 18 | } 19 | 20 | const render = (type: string | null): any => { 21 | switch (type) { 22 | case "success": 23 | return ( 24 |
25 | 26 |
Success: {modalText}
27 |
28 | ); 29 | case "error": 30 | return ( 31 |
32 | 33 |
Error: {modalText}
34 |
35 | ); 36 | case "signup": 37 | return ( 38 |
39 | 40 |
41 | ); 42 | case "post": 43 | return ( 44 |
45 | 46 |
47 | ); 48 | case "essence-mw": 49 | return ( 50 |
51 | 52 |
53 | ); 54 | case "subscribe-mw": 55 | return ( 56 |
57 | 58 |
59 | ); 60 | default: 61 | return null; 62 | } 63 | } 64 | 65 | return ( 66 | <> 67 | { 68 | modal && 69 |
70 |
71 | 72 | {render(modalType)} 73 |
74 |
75 | } 76 | 77 | ); 78 | }; 79 | 80 | export default Modal; 81 | -------------------------------------------------------------------------------- /components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import Link from "next/link"; 3 | import { IoSparklesOutline } from "react-icons/io5"; 4 | import { FiSettings } from "react-icons/fi"; 5 | import { MdOutlineDashboard } from "react-icons/md"; 6 | import { MdHistoryEdu } from "react-icons/md"; 7 | import { useRouter } from "next/router"; 8 | import { AuthContext } from "../../context/auth"; 9 | import { ModalContext } from "../../context/modal"; 10 | 11 | const Navbar = () => { 12 | const { accessToken, primaryProfile } = useContext(AuthContext); 13 | const { handleModal } = useContext(ModalContext); 14 | const router = useRouter(); 15 | 16 | const handleOnClick = () => { 17 | if (!accessToken) { 18 | handleModal("error", "You need to Sign in."); 19 | } else if (!primaryProfile?.profileID) { 20 | handleModal("error", "You need to Mint a profile."); 21 | } else { 22 | handleModal("post", ""); 23 | } 24 | }; 25 | 26 | return ( 27 | 56 | ); 57 | }; 58 | 59 | export default Navbar; 60 | -------------------------------------------------------------------------------- /components/Panel/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState, useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { AuthContext } from "../../context/auth"; 4 | import { ModalContext } from "../../context/modal"; 5 | import SigninBtn from "../../components/Buttons/SigninBtn"; 6 | import PrimaryProfileCard from "../Cards/PrimaryProfileCard"; 7 | import { useLazyQuery } from "@apollo/client"; 8 | import { PROFILES_BY_IDS } from "../../graphql"; 9 | import { SUGGESTED_PROFILES } from "../../helpers/constants"; 10 | import SuggestedProfileCard from "../Cards/SuggestedProfileCard"; 11 | import { IProfileCard } from "../../types"; 12 | 13 | const Panel = () => { 14 | const { accessToken, primaryProfile, address } = useContext(AuthContext); 15 | const { handleModal } = useContext(ModalContext); 16 | const [getProfilesByIDs] = useLazyQuery(PROFILES_BY_IDS); 17 | const [profiles, setProfiles] = useState([]); 18 | const router = useRouter(); 19 | 20 | useEffect(() => { 21 | const getProfiles = async () => { 22 | const { data } = await getProfilesByIDs({ 23 | variables: { 24 | profileIDs: [15, 16, 44, 5, 227], 25 | myAddress: address, 26 | }, 27 | }); 28 | setProfiles([...data.profilesByIDs]); 29 | }; 30 | 31 | if (accessToken && address) { 32 | getProfiles(); 33 | } else { 34 | setProfiles(SUGGESTED_PROFILES); 35 | } 36 | }, [accessToken, address]); 37 | 38 | return ( 39 |
40 |
41 | {primaryProfile && } 42 |
43 | {!accessToken && } 44 | {!primaryProfile?.profileID && ( 45 | 51 | )} 52 |
53 |
54 |
55 |

Who to subscribe

56 | {profiles.length > 0 && 57 | profiles.map((profile) => ( 58 | 59 | ))} 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default Panel; 66 | -------------------------------------------------------------------------------- /context/auth.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, createContext, useState, useEffect } from "react"; 2 | import { Web3Provider } from "@ethersproject/providers"; 3 | import { ethers } from "ethers"; 4 | import detectEthereumProvider from "@metamask/detect-provider"; 5 | import { ExternalProvider } from "@ethersproject/providers"; 6 | import { 7 | IAuthContext, 8 | IPrimaryProfileCard, 9 | IPostCard, 10 | IAccountCard, 11 | } from "../types"; 12 | import { 13 | ACCOUNTS, 14 | PRIMARY_PROFILE, 15 | PRIMARY_PROFILE_ESSENCES, 16 | RELAY_ACTION_STATUS, 17 | } from "../graphql"; 18 | import { useCancellableQuery } from "../hooks/useCancellableQuery"; 19 | import { timeout } from "../helpers/functions"; 20 | import { useLazyQuery } from "@apollo/client"; 21 | 22 | export const AuthContext = createContext({ 23 | address: undefined, 24 | accessToken: undefined, 25 | primaryProfile: undefined, 26 | indexingProfiles: [], 27 | indexingPosts: [], 28 | profileCount: 0, 29 | postCount: 0, 30 | posts: [], 31 | profiles: [], 32 | setAddress: () => { }, 33 | setAccessToken: () => { }, 34 | setPrimaryProfile: () => { }, 35 | setIndexingProfiles: () => { }, 36 | setIndexingPosts: () => { }, 37 | setProfileCount: () => { }, 38 | setPostCount: () => { }, 39 | setPosts: () => { }, 40 | setProfiles: () => { }, 41 | connectWallet: async () => new Promise(() => { }), 42 | checkNetwork: async () => new Promise(() => { }), 43 | }); 44 | AuthContext.displayName = "AuthContext"; 45 | 46 | export const AuthContextProvider = ({ children }: { children: ReactNode }) => { 47 | /* State variable to store the provider */ 48 | const [provider, setProvider] = useState(undefined); 49 | 50 | /* State variable to store the address */ 51 | const [address, setAddress] = useState(undefined); 52 | 53 | /* State variable to store the access token */ 54 | const [accessToken, setAccessToken] = useState(undefined); 55 | 56 | /* State variable to store the primary profile */ 57 | const [primaryProfile, setPrimaryProfile] = useState< 58 | IPrimaryProfileCard | undefined 59 | >(undefined); 60 | 61 | /* State variable to store the initial number of accounts */ 62 | const [profileCount, setProfileCount] = useState(0); 63 | 64 | /* State variable to store the initial number of posts */ 65 | const [postCount, setPostCount] = useState(0); 66 | 67 | /* State variable to store indexing profiles */ 68 | const [indexingProfiles, setIndexingProfiles] = useState([]); 69 | 70 | /* State variable to store indexing posts */ 71 | const [indexingPosts, setIndexingPosts] = useState([]); 72 | 73 | /* State variable to store the posts */ 74 | const [posts, setPosts] = useState([]); 75 | 76 | /* State variable to store the profiles */ 77 | const [profiles, setProfiles] = useState([]); 78 | const [getRelayActionStatus] = useLazyQuery(RELAY_ACTION_STATUS); 79 | 80 | useEffect(() => { 81 | const fetch = async () => { 82 | try { 83 | /* Fetch primary profile posts */ 84 | let query = useCancellableQuery({ 85 | query: PRIMARY_PROFILE_ESSENCES, 86 | variables: { 87 | address: address, 88 | }, 89 | }); 90 | const res = await query; 91 | 92 | /* Get the primary profile */ 93 | const primaryProfile = res?.data?.address?.wallet?.primaryProfile; 94 | 95 | /* Get the posts */ 96 | const edges = primaryProfile?.essences?.edges; 97 | const nodes = edges?.map((edge: any) => edge?.node) || []; 98 | 99 | /* Get the total count of posts */ 100 | const count = primaryProfile?.essences?.totalCount; 101 | /* Set the initial posts */ 102 | setPosts([...nodes]); 103 | 104 | /* Set the initial number of posts */ 105 | setPostCount(count); 106 | } catch (error) { 107 | /* Display error message */ 108 | console.error(error); 109 | } 110 | }; 111 | 112 | async function sync() { 113 | indexingPosts.forEach(async (post: any) => { 114 | const res = await getRelayActionStatus({ 115 | variables: { relayActionId: post.relayActionId }, 116 | fetchPolicy: "network-only", 117 | }); 118 | 119 | console.log("res 3000", res.data.relayActionStatus); 120 | 121 | if (res.data.relayActionStatus.txStatus === "SUCCESS") { 122 | const filtered = indexingPosts.filter( 123 | (item: any) => item.relayActionId !== post.relayActionId 124 | ); 125 | 126 | setIndexingPosts(filtered); 127 | localStorage.setItem("relayingPosts", JSON.stringify(filtered)); 128 | await fetch(); 129 | } 130 | 131 | if (indexingPosts.length > 0) { 132 | await new Promise((resolve) => setTimeout(resolve, 3000)); 133 | await sync(); 134 | } 135 | }); 136 | } 137 | 138 | if (address && indexingPosts?.length) { 139 | sync(); 140 | } 141 | }, [indexingPosts, address, getRelayActionStatus]); 142 | 143 | useEffect(() => { 144 | const accessToken = localStorage.getItem("accessToken"); 145 | const address = localStorage.getItem("address"); 146 | const relayingPosts = localStorage.getItem("relayingPosts"); 147 | if (accessToken && address) { 148 | setAccessToken(accessToken); 149 | setAddress(address); 150 | if (relayingPosts?.length) { 151 | setIndexingPosts(JSON.parse(relayingPosts)); 152 | } 153 | } 154 | }, []); 155 | 156 | useEffect(() => { 157 | /* Check if the user connected with wallet */ 158 | if (!(provider && address)) return; 159 | 160 | try { 161 | /* Function to check if the network is the correct one */ 162 | checkNetwork(provider); 163 | } catch (error) { 164 | /* Display error message */ 165 | alert(error.message); 166 | } 167 | }, [provider, address]); 168 | 169 | useEffect(() => { 170 | if (!(address && accessToken)) return; 171 | let query: any; 172 | 173 | const fetch = async () => { 174 | try { 175 | /* Fetch primary profile */ 176 | query = useCancellableQuery({ 177 | query: PRIMARY_PROFILE, 178 | variables: { 179 | address: address, 180 | }, 181 | }); 182 | const res = await query; 183 | 184 | /* Get the primary profile */ 185 | const primaryProfile = res?.data?.address?.wallet?.primaryProfile; 186 | 187 | /* Set the primary profile */ 188 | setPrimaryProfile(primaryProfile); 189 | } catch (error) { 190 | /* Display error message */ 191 | console.error(error); 192 | } 193 | }; 194 | fetch(); 195 | 196 | return () => { 197 | query.cancel(); 198 | }; 199 | }, [address, accessToken]); 200 | 201 | useEffect(() => { 202 | if (!(address && accessToken)) return; 203 | 204 | let query: any; 205 | let timer: number = Date.now() + 1000 * 60 * 10; 206 | let mount = true; 207 | 208 | const fetch = async () => { 209 | try { 210 | /* Fetch all profiles */ 211 | query = useCancellableQuery({ 212 | query: ACCOUNTS, 213 | variables: { 214 | address: address, 215 | }, 216 | }); 217 | const res = await query; 218 | 219 | /* Get the profiles */ 220 | const edges = res?.data?.address?.wallet?.profiles?.edges; 221 | const nodes = edges?.map((edge: any) => edge?.node) || []; 222 | 223 | /* Get the total count of posts */ 224 | const count = nodes.length; 225 | 226 | /* Get primary profile */ 227 | const primaryProfile = nodes?.find((node: any) => node?.isPrimary); 228 | 229 | /* Set the primary profile if exists (might be the first one) */ 230 | if (primaryProfile) setPrimaryProfile(primaryProfile); 231 | 232 | if (indexingProfiles.length === 0) { 233 | /* Set the profiles */ 234 | setProfiles([...nodes]); 235 | 236 | /* Set the initial number of profiles */ 237 | setProfileCount(count); 238 | } else { 239 | if (profileCount + indexingProfiles.length === count) { 240 | /* Set the posts in the state variable */ 241 | setProfiles([...nodes]); 242 | 243 | /* Set the posts count in the state variable */ 244 | setProfileCount(count); 245 | 246 | /* Reset the indexingProfiles in the state variable */ 247 | setIndexingProfiles([]); 248 | } else { 249 | /* Fetch again after a 2s timeout */ 250 | if (Date.now() < timer) { 251 | /* Wait 2s before fetching data again */ 252 | console.log("Fetching profiles again."); 253 | await timeout(2000); 254 | if (mount) fetch(); 255 | } else { 256 | /* Reset the indexingProfiles in the state variable */ 257 | setIndexingProfiles([]); 258 | } 259 | } 260 | } 261 | } catch (error) { 262 | /* Display error message */ 263 | console.error(error); 264 | 265 | /* Reset the indexingProfiles in the state variable */ 266 | setIndexingProfiles([]); 267 | } 268 | }; 269 | fetch(); 270 | 271 | return () => { 272 | mount = false; 273 | if (query) { 274 | query.cancel(); 275 | } 276 | }; 277 | }, [address, accessToken, indexingProfiles, profileCount]); 278 | 279 | useEffect(() => { 280 | if (!(address && accessToken)) return; 281 | 282 | let query: any; 283 | let timer: number = Date.now() + 1000 * 60 * 10; 284 | let mount = true; 285 | 286 | const fetch = async () => { 287 | try { 288 | /* Fetch primary profile posts */ 289 | query = useCancellableQuery({ 290 | query: PRIMARY_PROFILE_ESSENCES, 291 | variables: { 292 | address: address, 293 | }, 294 | }); 295 | const res = await query; 296 | 297 | /* Get the primary profile */ 298 | const primaryProfile = res?.data?.address?.wallet?.primaryProfile; 299 | 300 | /* Get the posts */ 301 | const edges = primaryProfile?.essences?.edges; 302 | const nodes = edges?.map((edge: any) => edge?.node) || []; 303 | 304 | /* Get the total count of posts */ 305 | const count = primaryProfile?.essences?.totalCount; 306 | /* Set the initial posts */ 307 | setPosts([...nodes]); 308 | 309 | /* Set the initial number of posts */ 310 | setPostCount(count); 311 | } catch (error) { 312 | /* Display error message */ 313 | console.error(error); 314 | 315 | /* Reset the indexingPosts in the state variable */ 316 | setIndexingPosts([]); 317 | } 318 | }; 319 | fetch(); 320 | 321 | return () => { 322 | mount = false; 323 | if (query) { 324 | query.cancel(); 325 | } 326 | }; 327 | }, [address, accessToken, indexingPosts, postCount]); 328 | 329 | /* Function to connect with MetaMask wallet */ 330 | const connectWallet = async () => { 331 | try { 332 | /* Function to detect most providers injected at window.ethereum */ 333 | const detectedProvider = 334 | (await detectEthereumProvider()) as ExternalProvider; 335 | 336 | /* Check if the Ethereum provider exists */ 337 | if (!detectedProvider) { 338 | throw new Error("Please install MetaMask!"); 339 | } 340 | 341 | /* Ethers Web3Provider wraps the standard Web3 provider injected by MetaMask */ 342 | const web3Provider = new ethers.providers.Web3Provider(detectedProvider); 343 | 344 | /* Connect to Ethereum. MetaMask will ask permission to connect user accounts */ 345 | await web3Provider.send("eth_requestAccounts", []); 346 | 347 | /* Get the signer from the provider */ 348 | const signer = web3Provider.getSigner(); 349 | 350 | /* Get the address of the connected wallet */ 351 | const address = await signer.getAddress(); 352 | 353 | /* Set the providers in the state variables */ 354 | setProvider(web3Provider); 355 | 356 | /* Set the address in the state variable */ 357 | setAddress(address); 358 | localStorage.setItem("address", address); 359 | 360 | return web3Provider; 361 | } catch (error) { 362 | /* Throw the error */ 363 | throw error; 364 | } 365 | }; 366 | 367 | /* Function to check if the network is the correct one */ 368 | const checkNetwork = async (provider: Web3Provider) => { 369 | try { 370 | /* Get the network from the provider */ 371 | const network = await provider.getNetwork(); 372 | 373 | /* Check if the network is the correct one */ 374 | if (network.chainId !== (Number(process.env.NEXT_PUBLIC_CHAIN_ID) || 0)) { 375 | /* Switch network if the chain id doesn't correspond to Goerli Testnet Network */ 376 | await provider.send("wallet_switchEthereumChain", [ 377 | { 378 | chainId: 379 | "0x" + Number(process.env.NEXT_PUBLIC_CHAIN_ID)?.toString(16), 380 | }, 381 | ]); 382 | 383 | /* Trigger a page reload */ 384 | window.location.reload(); 385 | } 386 | } catch (error) { 387 | /* This error code indicates that the chain has not been added to MetaMask */ 388 | if (error.code === 4902) { 389 | await provider.send("wallet_addEthereumChain", [ 390 | { 391 | chainId: 392 | "0x" + Number(process.env.NEXT_PUBLIC_CHAIN_ID)?.toString(16), 393 | rpcUrls: ["https://goerli.infura.io/v3/"], 394 | }, 395 | ]); 396 | 397 | /* Trigger a page reload */ 398 | window.location.reload(); 399 | } else { 400 | /* Throw the error */ 401 | throw error; 402 | } 403 | } 404 | }; 405 | 406 | return ( 407 | 431 | {children} 432 | 433 | ); 434 | }; 435 | -------------------------------------------------------------------------------- /context/modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, createContext } from "react"; 2 | import useModal from "../hooks/useModal"; 3 | import ModalContainer from "../components/Modal"; 4 | import { IModalContext } from "../types"; 5 | 6 | const ModalContext = createContext({ 7 | modal: false, 8 | modalType: null, 9 | modalText: "", 10 | handleModal: () => { }, 11 | }); 12 | ModalContext.displayName = "ModalContext"; 13 | 14 | const ModalContextProvider = ({ children }: { children: ReactNode }) => { 15 | const { modal, modalType, modalText, handleModal } = useModal(); 16 | 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export { ModalContext, ModalContextProvider }; 26 | -------------------------------------------------------------------------------- /graphql/Accounts.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const ACCOUNTS = gql` 4 | query Accounts($address: AddressEVM!) { 5 | address(address: $address) { 6 | wallet { 7 | profiles { 8 | totalCount 9 | edges { 10 | node { 11 | id 12 | profileID 13 | handle 14 | metadata 15 | avatar 16 | isPrimary 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /graphql/Address.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const ADDRESS = gql` 4 | query Address($address: AddressEVM!) { 5 | address(address: $address) { 6 | wallet { 7 | profiles { 8 | totalCount 9 | edges { 10 | node { 11 | id 12 | profileID 13 | handle 14 | metadata 15 | avatar 16 | isPrimary 17 | essences { 18 | totalCount 19 | edges { 20 | node { 21 | essenceID 22 | tokenURI 23 | createdBy { 24 | handle 25 | metadata 26 | avatar 27 | profileID 28 | } 29 | } 30 | } 31 | } 32 | } 33 | cursor 34 | } 35 | } 36 | primaryProfile { 37 | id 38 | profileID 39 | handle 40 | metadata 41 | avatar 42 | isPrimary 43 | essences { 44 | totalCount 45 | edges { 46 | node { 47 | essenceID 48 | tokenURI 49 | createdBy { 50 | handle 51 | metadata 52 | avatar 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /graphql/CreateCollectEssenceTypedData.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const CREATE_COLLECT_ESSENCE_TYPED_DATA = gql` 4 | mutation CreateCollectEssenceTypedData( 5 | $input: CreateCollectEssenceTypedDataInput! 6 | ) { 7 | createCollectEssenceTypedData(input: $input) { 8 | typedData { 9 | id 10 | sender 11 | data 12 | nonce 13 | } 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /graphql/CreateRegisterEssenceTypedData.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const CREATE_REGISTER_ESSENCE_TYPED_DATA = gql` 4 | mutation CreateRegisterEssenceTypedData( 5 | $input: CreateRegisterEssenceTypedDataInput! 6 | ) { 7 | createRegisterEssenceTypedData(input: $input) { 8 | typedData { 9 | id 10 | sender 11 | data 12 | nonce 13 | } 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /graphql/CreateSetEssenceDataTypedData.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const CREATE_SET_ESSENCE_DATA_TYPED_DATA = gql` 4 | mutation CreateSetEssenceDataTypedData( 5 | $input: CreateSetEssenceDataTypedDataInput! 6 | ) { 7 | createSetEssenceDataTypedData(input: $input) { 8 | typedData { 9 | id 10 | sender 11 | data 12 | nonce 13 | } 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /graphql/CreateSetSubscribeDataTypedData.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const CREATE_SET_SUBSCRIBE_DATA_TYPED_DATA = gql` 4 | mutation CreateSetSubscribeDataTypedData( 5 | $input: CreateSetSubscribeDataTypedDataInput! 6 | ) { 7 | createSetSubscribeDataTypedData(input: $input) { 8 | typedData { 9 | id 10 | sender 11 | data 12 | nonce 13 | } 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /graphql/CreateSubscribeTypedData.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const CREATE_SUBSCRIBE_TYPED_DATA = gql` 4 | mutation CreateSubscribeTypedData($input: CreateSubscribeTypedDataInput!) { 5 | createSubscribeTypedData(input: $input) { 6 | typedData { 7 | id 8 | sender 9 | data 10 | nonce 11 | } 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /graphql/EssencesByFilter.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const ESSENCES_BY_FILTER = gql` 4 | query essencesByFilter($appID: String, $me: AddressEVM!) { 5 | essenceByFilter(appID: $appID) { 6 | essenceID 7 | tokenURI 8 | createdBy { 9 | avatar 10 | handle 11 | profileID 12 | metadata 13 | } 14 | collectMw { 15 | contractAddress 16 | type 17 | } 18 | isCollectedByMe(me: $me) 19 | } 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /graphql/LoginGetMessage.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const LOGIN_GET_MESSAGE = gql` 4 | mutation LoginGetMessage($input: LoginGetMessageInput!) { 5 | loginGetMessage(input: $input) { 6 | message 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /graphql/LoginVerify.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const LOGIN_VERIFY = gql` 4 | mutation LoginVerify($input: LoginVerifyInput!) { 5 | loginVerify(input: $input) { 6 | accessToken 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /graphql/PrimaryProfile.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const PRIMARY_PROFILE = gql` 4 | query PrimaryProfile($address: AddressEVM!) { 5 | address(address: $address) { 6 | wallet { 7 | primaryProfile { 8 | id 9 | profileID 10 | handle 11 | metadata 12 | avatar 13 | isPrimary 14 | } 15 | } 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /graphql/PrimaryProfileEssences.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const PRIMARY_PROFILE_ESSENCES = gql` 4 | query PrimaryProfileEssences($address: AddressEVM!) { 5 | address(address: $address) { 6 | wallet { 7 | primaryProfile { 8 | essences { 9 | totalCount 10 | edges { 11 | node { 12 | essenceID 13 | tokenURI 14 | createdBy { 15 | handle 16 | metadata 17 | avatar 18 | profileID 19 | } 20 | isCollectedByMe(me: $address) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /graphql/ProfilesByIds.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const PROFILES_BY_IDS = gql` 4 | query profilesByIDs($profileIDs: [ProfileID!]!, $myAddress: AddressEVM!) { 5 | profilesByIDs(profileIDs: $profileIDs) { 6 | handle 7 | profileID 8 | metadata 9 | avatar 10 | isSubscribedByMe(me: $myAddress) 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /graphql/Relay.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const RELAY = gql` 4 | mutation Relay($input: RelayInput!) { 5 | relay(input: $input) { 6 | relayActionId 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /graphql/RelayActionStatus.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const RELAY_ACTION_STATUS = gql` 4 | query RelayActionStatus($relayActionId: ID!) { 5 | relayActionStatus(relayActionId: $relayActionId) { 6 | ... on RelayActionStatusResult { 7 | txHash 8 | txStatus 9 | } 10 | ... on RelayActionError { 11 | reason 12 | lastKnownTxHash 13 | } 14 | ... on RelayActionQueued { 15 | queuedAt 16 | } 17 | } 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /graphql/index.ts: -------------------------------------------------------------------------------- 1 | export { LOGIN_GET_MESSAGE } from "./LoginGetMessage"; 2 | export { LOGIN_VERIFY } from "./LoginVerify"; 3 | export { CREATE_SUBSCRIBE_TYPED_DATA } from "./CreateSubscribeTypedData"; 4 | export { RELAY } from "./Relay"; 5 | export { ACCOUNTS } from "./Accounts"; 6 | export { CREATE_COLLECT_ESSENCE_TYPED_DATA } from "./CreateCollectEssenceTypedData"; 7 | export { CREATE_REGISTER_ESSENCE_TYPED_DATA } from "./CreateRegisterEssenceTypedData"; 8 | export { CREATE_SET_SUBSCRIBE_DATA_TYPED_DATA } from "./CreateSetSubscribeDataTypedData"; 9 | export { CREATE_SET_ESSENCE_DATA_TYPED_DATA } from "./CreateSetEssenceDataTypedData"; 10 | export { PRIMARY_PROFILE } from "./PrimaryProfile"; 11 | export { PRIMARY_PROFILE_ESSENCES } from "./PrimaryProfileEssences"; 12 | export { ADDRESS } from "./Address"; 13 | export { PROFILES_BY_IDS } from "./ProfilesByIds"; 14 | export { ESSENCES_BY_FILTER } from "./EssencesByFilter"; 15 | export { RELAY_ACTION_STATUS } from "./RelayActionStatus"; 16 | -------------------------------------------------------------------------------- /helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const PROFILE_NFT_CONTRACT = 2 | "0x57e12b7a5f38a7f9c23ebd0400e6e53f2a45f271"; // Link3 ProfileNFT contract address 3 | 4 | export const PROFILE_NFT_OPERATOR = 5 | "0xaB24749c622AF8FC567CA2b4d3EC53019F83dB8F"; 6 | 7 | export const DOMAIN = "test.com"; // Domain name 8 | 9 | export const SUGGESTED_PROFILES = [ 10 | { 11 | handle: "ccprotocol", 12 | avatar: 13 | "https://gateway.pinata.cloud/ipfs/QmNcqSpCvhiyHocUaVf7qB8qwEGerSpnELeAi567YEraYm", 14 | metadata: "QmRiyArHF4abhXo4pdKVQj3fVg6jLvcnH4DitVijuTaoyq", 15 | profileID: 15, 16 | isSubscribedByMe: false, 17 | }, 18 | { 19 | handle: "cyberlab", 20 | avatar: 21 | "https://gateway.pinata.cloud/ipfs/QmTMBsha6BjtNQqQFRjrpwQAfkt1DHpe5VTr2idw5piE47", 22 | profileID: 16, 23 | metadata: "QmfQTU5eWfG1wwfC5k6enHtTZGNgRimqU5rvvt5Qp8GCyi", 24 | isSubscribedByMe: false, 25 | }, 26 | { 27 | handle: "snowdot", 28 | avatar: 29 | "https://gateway.pinata.cloud/ipfs/QmV1ZVcyC96g1HYsxXgG6BP6Kc8xrZCBqj7PNkvxhPwLoz", 30 | profileID: 44, 31 | metadata: "QmUoU9be1DGKUiVwEjvbw9dMRrRNK4TX7A57YL4NBe4hQa", 32 | isSubscribedByMe: false, 33 | }, 34 | { 35 | handle: "satoshi", 36 | avatar: 37 | "https://gateway.pinata.cloud/ipfs/QmaGUuGqxJ29we67C7RbCHSQaPybPdfoNr8Zccd7pfw8et", 38 | profileID: 5, 39 | metadata: "QmewyA1GAKFDs7wze3KuzwrUsJV3qmUA41jcpAuJNQpqTs", 40 | isSubscribedByMe: false, 41 | }, 42 | ]; 43 | 44 | export const PROFILES = [ 45 | { 46 | avatar: 47 | "https://img.seadn.io/files/4f1c6f44419cf7a6eb0bd990fae8d5d3.png?auto=format&fit=max&w=3840", 48 | handle: "samer", 49 | isSubscribedByMe: false, 50 | metadata: "QmZbL66zhGrmyT7uYJ1ceRhigXSMbSj61HQ5UKWTgHiV7m", 51 | profileID: 2, 52 | }, 53 | { 54 | avatar: 55 | "https://gateway.pinata.cloud/ipfs/QmaGUuGqxJ29we67C7RbCHSQaPybPdfoNr8Zccd7pfw8et", 56 | handle: "satoshi", 57 | isSubscribedByMe: false, 58 | metadata: "QmewyA1GAKFDs7wze3KuzwrUsJV3qmUA41jcpAuJNQpqTs", 59 | profileID: 5, 60 | }, 61 | { 62 | avatar: "https://i.pravatar.cc/200", 63 | handle: "Hong.Patel69", 64 | isSubscribedByMe: false, 65 | metadata: "QmVcEWGUCfBq5z5y9B6DnKPhpqd4hofbJuBrAEakbfo5rG", 66 | profileID: 12, 67 | }, 68 | { 69 | avatar: "asdasda", 70 | handle: "12312312", 71 | isSubscribedByMe: false, 72 | metadata: "QmVnnxZKSPVzYCbC1TZgy33ta2Rj2xt6BXTLMgg915xWbk", 73 | profileID: 10, 74 | }, 75 | { 76 | avatar: 77 | "https://gateway.pinata.cloud/ipfs/QmNcqSpCvhiyHocUaVf7qB8qwEGerSpnELeAi567YEraYm", 78 | handle: "ccprotocol", 79 | isSubscribedByMe: false, 80 | metadata: "QmRiyArHF4abhXo4pdKVQj3fVg6jLvcnH4DitVijuTaoyq", 81 | profileID: 15, 82 | }, 83 | { 84 | avatar: 85 | "https://gateway.pinata.cloud/ipfs/QmTMBsha6BjtNQqQFRjrpwQAfkt1DHpe5VTr2idw5piE47", 86 | handle: "cyberlab", 87 | isSubscribedByMe: false, 88 | metadata: "QmfQTU5eWfG1wwfC5k6enHtTZGNgRimqU5rvvt5Qp8GCyi", 89 | profileID: 16, 90 | }, 91 | { 92 | avatar: 93 | "https://img.seadn.io/files/ac7856326ee1a80bf0cd9bfb67876c04.png?auto=format&fit=max&w=3840", 94 | handle: "Jan_Jónasdóttir", 95 | isSubscribedByMe: false, 96 | metadata: "QmPedMy5JVyR1RkjxjBcgD6U2uXHx5VTgZzWQnk5FrDz46", 97 | profileID: 77, 98 | }, 99 | ]; 100 | 101 | export const FEATURED_POSTS = [ 102 | { 103 | essenceID: 2, 104 | tokenURI: 105 | "https://cyberconnect.mypinata.cloud/ipfs/Qmd7G1BVZ3EQ3w2mNWBqgi4DaRrnkv5thy5UR1ParwM7AG", 106 | createdBy: { 107 | avatar: 108 | "https://gateway.pinata.cloud/ipfs/QmNcqSpCvhiyHocUaVf7qB8qwEGerSpnELeAi567YEraYm", 109 | handle: "ccprotocol", 110 | profileID: 15, 111 | metadata: "QmRiyArHF4abhXo4pdKVQj3fVg6jLvcnH4DitVijuTaoyq", 112 | }, 113 | isCollectedByMe: false, 114 | }, 115 | { 116 | essenceID: 3, 117 | tokenURI: 118 | "https://cyberconnect.mypinata.cloud/ipfs/QmNnAaGTy1orXMYNyAVvVr2rHoa1NE86aY6LsKACnVvGNZ", 119 | createdBy: { 120 | handle: "check12345678", 121 | metadata: "Qmbx1k1U2fDVw3gr8UHLH7AqB5yPsvg4JHCxDUbWTt7CMn", 122 | avatar: "https://i.pravatar.cc/200", 123 | profileID: 229, 124 | __typename: "Profile", 125 | }, 126 | isCollectedByMe: false, 127 | __typename: "Essence", 128 | }, 129 | ]; 130 | -------------------------------------------------------------------------------- /helpers/functions.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const apiKey = process.env.NEXT_PUBLIC_API_KEY || ""; 4 | const apiSecret = process.env.NEXT_PUBLIC_API_SECRET || ""; 5 | 6 | export const pinJSONToIPFS = async (json: { [key: string]: any }) => { 7 | const data = JSON.stringify(json); 8 | const url = "https://api.pinata.cloud/pinning/pinJSONToIPFS"; 9 | 10 | return axios 11 | .post(url, data, { 12 | headers: { 13 | "Content-Type": "application/json", 14 | pinata_api_key: apiKey, 15 | pinata_secret_api_key: apiSecret, 16 | }, 17 | }) 18 | .then((response) => response.data.IpfsHash) 19 | .catch((error) => { 20 | throw error; 21 | }); 22 | }; 23 | 24 | export const parseURL = (url: string) => { 25 | if (!url) return ""; 26 | const str = url.substring(0, 4); 27 | 28 | if (str === "http") { 29 | return url; 30 | } else { 31 | return `https://cyberconnect.mypinata.cloud/ipfs/${url}`; 32 | } 33 | }; 34 | 35 | export const getEssenceSVGData = () => { 36 | const svg = ` 37 | 38 | `; 39 | return `data:image/svg+xml;base64,${btoa(svg)}`; 40 | }; 41 | 42 | export const getSubscriberSVGData = () => { 43 | const svg = ` 44 | 45 | `; 46 | return `data:image/svg+xml;base64,${btoa(svg)}`; 47 | }; 48 | 49 | export const timeout = async (ms: number) => { 50 | return new Promise((resolve) => setTimeout(resolve, ms)); 51 | }; 52 | 53 | export const timeSince = (date: any) => { 54 | let seconds = Math.floor(((new Date() as any) - date) / 1000); 55 | let interval = seconds / 31536000; 56 | 57 | if (interval > 1) { 58 | return Math.floor(interval) + "y"; 59 | } 60 | interval = seconds / 2592000; 61 | if (interval > 1) { 62 | return Math.floor(interval) + "mo"; 63 | } 64 | interval = seconds / 86400; 65 | if (interval > 1) { 66 | return Math.floor(interval) + "d"; 67 | } 68 | interval = seconds / 3600; 69 | if (interval > 1) { 70 | return Math.floor(interval) + "h"; 71 | } 72 | interval = seconds / 60; 73 | if (interval > 1) { 74 | return Math.floor(interval) + "m"; 75 | } 76 | return Math.floor(seconds) + "s"; 77 | }; 78 | -------------------------------------------------------------------------------- /hooks/useCancellableQuery.tsx: -------------------------------------------------------------------------------- 1 | import { QueryOptions, ApolloQueryResult } from "@apollo/client"; 2 | import { apolloClient } from "../apollo"; 3 | 4 | interface CancellablePromise extends Promise { 5 | cancel: () => void 6 | } 7 | 8 | export function useCancellableQuery(options: QueryOptions): CancellablePromise> { 9 | const abortController = new AbortController() 10 | const observable = apolloClient.watchQuery({ 11 | ...options, 12 | fetchPolicy: "no-cache", 13 | context: { 14 | fetchOptions: { 15 | signal: abortController.signal, 16 | }, 17 | queryDeduplication: false 18 | } 19 | }); 20 | 21 | let subscription: ZenObservable.Subscription 22 | let promise = new Promise((resolve, reject) => { 23 | subscription = observable.subscribe( 24 | (res) => resolve(res), 25 | (err) => reject(err) 26 | ) 27 | }) as CancellablePromise> 28 | promise.cancel = () => { 29 | abortController.abort() 30 | subscription.unsubscribe() 31 | } 32 | return promise; 33 | } -------------------------------------------------------------------------------- /hooks/useModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const useModal = () => { 4 | const [modal, setModal] = useState(false); 5 | const [modalType, setModalType] = useState(null); 6 | const [modalText, setModalText] = useState(""); 7 | 8 | const handleModal = (type: string | null, text: string) => { 9 | setModal(Boolean(type)); 10 | if (type) { 11 | setModalType(type); 12 | setModalText(text); 13 | } 14 | }; 15 | 16 | return { 17 | modal, 18 | modalType, 19 | modalText, 20 | handleModal 21 | } 22 | 23 | } 24 | 25 | export default useModal; 26 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: "https", 9 | hostname: "**", 10 | }, 11 | ], 12 | }, 13 | } 14 | 15 | module.exports = nextConfig 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cc-content-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.7.0", 13 | "@ethersproject/providers": "^5.7.1", 14 | "@metamask/detect-provider": "^2.0.0", 15 | "@ngneat/falso": "^6.1.0", 16 | "@types/uuid": "^8.3.4", 17 | "@types/zen-observable": "^0.8.3", 18 | "axios": "^1.1.2", 19 | "ethers": "^5.7.1", 20 | "graphql": "^16.6.0", 21 | "next": "12.3.1", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "react-icons": "^4.4.0", 25 | "uuid": "^9.0.0", 26 | "zen-observable": "^0.8.15" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "18.8.5", 30 | "@types/react": "18.0.21", 31 | "@types/react-dom": "18.0.6", 32 | "eslint": "8.25.0", 33 | "eslint-config-next": "12.3.1", 34 | "typescript": "4.8.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { ApolloProvider } from "@apollo/client"; 4 | import { apolloClient } from "../apollo"; 5 | import { AuthContextProvider } from "../context/auth"; 6 | import { ModalContextProvider } from "../context/modal"; 7 | import React from "react"; 8 | 9 | function MyApp({ Component, pageProps }: AppProps) { 10 | React.useEffect(() => { 11 | (window as any)?.ethereum?.on("accountsChanged", function () { 12 | // Time to reload your interface with accounts[0]! 13 | localStorage.clear(); 14 | window.location.reload(); 15 | }); 16 | }, []); 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | export default MyApp; 29 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useContext, useState, useEffect } from "react"; 3 | import Navbar from "../components/Navbar"; 4 | import Panel from "../components/Panel"; 5 | import { useLazyQuery } from "@apollo/client"; 6 | import { PROFILES_BY_IDS } from "../graphql"; 7 | import { PROFILES } from "../helpers/constants"; 8 | import ProfileCard from "../components/Cards/ProfileCard"; 9 | import { IProfileCard } from "../types"; 10 | import { AuthContext } from "../context/auth"; 11 | 12 | const Home: NextPage = () => { 13 | const { accessToken, address } = useContext(AuthContext); 14 | const [getProfilesByIDs] = useLazyQuery(PROFILES_BY_IDS); 15 | const [profiles, setProfiles] = useState([]); 16 | 17 | useEffect(() => { 18 | console.log("address", address); 19 | const getProfiles = async () => { 20 | const { data } = await getProfilesByIDs({ 21 | variables: { 22 | profileIDs: [2, 5, 12, 10, 15, 16, 77], 23 | myAddress: address, 24 | }, 25 | }); 26 | setProfiles([...data.profilesByIDs]); 27 | }; 28 | 29 | if (accessToken && address) { 30 | getProfiles(); 31 | } else { 32 | setProfiles(PROFILES); 33 | } 34 | }, [accessToken, address, getProfilesByIDs]); 35 | 36 | return ( 37 |
38 | 39 |
40 |
41 |

Profiles

42 |
43 |
44 | {profiles.length > 0 && 45 | profiles.map((profile) => ( 46 | 47 | ))} 48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default Home; 59 | -------------------------------------------------------------------------------- /pages/posts.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import type { NextPage } from "next"; 3 | import { AuthContext } from "../context/auth"; 4 | import Navbar from "../components/Navbar"; 5 | import Panel from "../components/Panel"; 6 | import PostCard from "../components/Cards/PostCard"; 7 | import { IPostCard } from "../types"; 8 | import { useLazyQuery } from "@apollo/client"; 9 | import { ESSENCES_BY_FILTER } from "../graphql"; 10 | import { FEATURED_POSTS } from "../helpers/constants"; 11 | 12 | const PostPage: NextPage = () => { 13 | const { accessToken, indexingPosts, posts, address } = 14 | useContext(AuthContext); 15 | const [getEssencesByFilter] = useLazyQuery(ESSENCES_BY_FILTER); 16 | const [featuredPosts, setFeaturedPosts] = useState([]); 17 | 18 | useEffect(() => { 19 | const getEssences = async () => { 20 | const { data } = await getEssencesByFilter({ 21 | variables: { 22 | appID: "cyberconnect-bnbt", 23 | me: address, 24 | }, 25 | }); 26 | const filtered = data?.essenceByFilter || []; 27 | setFeaturedPosts(filtered); 28 | 29 | console.log("filtered", filtered); 30 | }; 31 | 32 | if (accessToken) { 33 | getEssences(); 34 | } else { 35 | setFeaturedPosts(FEATURED_POSTS); 36 | } 37 | }, [accessToken]); 38 | 39 | return ( 40 |
41 | 42 |
43 |
44 |

Posts

45 |
46 |
47 |

Featured

48 |

49 |
50 | {featuredPosts.length > 0 && 51 | featuredPosts.map((post) => ( 52 | 57 | ))} 58 |
59 |

60 |

61 |

My posts

62 |

63 | {!accessToken ? ( 64 |
65 | You need to Sign in to view your posts. 66 |
67 | ) : ( 68 |
69 | {posts.length === 0 && ( 70 |
You haven't created any posts yet.
71 | )} 72 | {posts.length > 0 && ( 73 |
74 | {posts.map((post) => ( 75 | 80 | ))} 81 |
82 | )} 83 |
84 |

Relaying Posts

85 | {indexingPosts.length > 0 && 86 | indexingPosts.map((post) => ( 87 | 91 | ))} 92 |
93 |
94 | )} 95 |
96 |
97 |
98 | 99 |
100 |
101 |
102 | ); 103 | }; 104 | 105 | export default PostPage; 106 | -------------------------------------------------------------------------------- /pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import type { NextPage } from "next"; 3 | import { AuthContext } from "../context/auth"; 4 | import { ModalContext } from "../context/modal"; 5 | import Navbar from "../components/Navbar"; 6 | import Panel from "../components/Panel"; 7 | import AccountCard from "../components/Cards/AccountCard"; 8 | import { IAccountCard } from "../types"; 9 | 10 | const SettingsPage: NextPage = () => { 11 | const { accessToken, indexingProfiles, profiles } = useContext(AuthContext); 12 | const { handleModal } = useContext(ModalContext); 13 | 14 | return ( 15 |
16 | 17 |
18 |
19 |

Settings

20 |
21 |

Account

22 |

23 | {!accessToken ? ( 24 |
25 | You need to Sign in to view details about your 26 | account. 27 |
28 | ) : ( 29 |
30 |
31 | {profiles.length === 0 && 32 | (indexingProfiles.length > 0 ? ( 33 |
34 | {indexingProfiles.map((account) => ( 35 | 39 | ))} 40 |
41 | ) : ( 42 |
You do not have a profile yet.
43 | ))} 44 | {profiles.length > 0 && ( 45 | <> 46 |
47 | The list of all accounts associated to the connected 48 | wallet. 49 |
50 |

51 | {profiles.map((account) => ( 52 | 57 | ))} 58 | {indexingProfiles.length > 0 && 59 | indexingProfiles.map((account) => ( 60 | 64 | ))} 65 | 66 | )} 67 |
68 |

69 |

70 |

Middlewares

71 |
72 |
73 |

Subscribe

74 |

75 | Set the middleware for your profile to be either{" "} 76 | FREE or PAID when users 77 | subscribe to it. 78 |

79 |
80 | 86 |
87 |
88 |
89 |

Essence

90 |

91 | Set the middleware for your posts to be either{" "} 92 | FREE or PAID when users 93 | collect them. 94 |

95 |
96 | 102 |
103 |

104 |

105 |
106 | )} 107 |
108 |
109 | 110 |
111 |
112 |
113 | ); 114 | }; 115 | 116 | export default SettingsPage; 117 | -------------------------------------------------------------------------------- /public/assets/avatar-placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/essence-placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/cc-content-app/b11fa6a03b2959a830907dc26768c86bcd694dc1/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --black: rgba(17, 17, 17, 1); 3 | --white: #FFFEFC; 4 | --gray: rgba(17, 17, 17, 0.1); 5 | --dark-gray: #00000099; 6 | --orange: #EB5757; 7 | --green: #65c819; 8 | --red: #c8192a; 9 | --font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 10 | } 11 | 12 | html { 13 | scroll-behavior: smooth; 14 | } 15 | 16 | html, 17 | body { 18 | background-color: var(--white); 19 | padding: 0; 20 | margin: 0; 21 | font-family: var(--font-family); 22 | } 23 | 24 | a { 25 | color: inherit; 26 | text-decoration: none; 27 | } 28 | 29 | * { 30 | box-sizing: border-box; 31 | } 32 | 33 | hr { 34 | border-bottom: 1px solid var(--gray); 35 | margin: 20px auto; 36 | } 37 | 38 | h1, 39 | h2 { 40 | margin: 0; 41 | padding: 0; 42 | } 43 | 44 | svg { 45 | display: block; 46 | font-size: 30px; 47 | margin: 0 auto; 48 | height: 100%; 49 | } 50 | 51 | /* GLOBAL */ 52 | .container { 53 | max-width: 1398px; 54 | margin: 0 auto; 55 | height: 100%; 56 | position: relative; 57 | } 58 | 59 | .wrapper { 60 | display: flex; 61 | align-items: flex-start; 62 | flex-direction: column-reverse; 63 | padding: 0px 20px 0px 100px; 64 | min-height: 100vh; 65 | } 66 | 67 | .wrapper-content { 68 | padding-top: 20px; 69 | min-height: 100vh; 70 | } 71 | 72 | .wrapper-details { 73 | width: 100%; 74 | padding-top: 20px; 75 | margin-bottom: 20px; 76 | } 77 | 78 | .center { 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | } 83 | 84 | .space-between { 85 | display: flex; 86 | align-items: center; 87 | justify-content: space-between; 88 | } 89 | 90 | /* NAVBAR */ 91 | .navbar { 92 | background-color: var(--white); 93 | border-right: 1px solid var(--gray); 94 | flex-direction: column; 95 | display: flex; 96 | align-items: center; 97 | justify-content: center; 98 | width: 80px; 99 | height: 100vh; 100 | padding: 20px 0px; 101 | top: 0; 102 | z-index: 1; 103 | position: fixed; 104 | } 105 | 106 | .navbar-link { 107 | display: flex; 108 | align-items: center; 109 | justify-content: center; 110 | color: #c4c4c4; 111 | cursor: pointer; 112 | width: 100%; 113 | height: 60px; 114 | text-align: center; 115 | } 116 | 117 | .navbar-link.active { 118 | color: #000000; 119 | } 120 | 121 | /* BUTTONS */ 122 | .create-btn, 123 | .signin-btn, 124 | .subscribe-btn, 125 | .collect-btn, 126 | .middleware-btn { 127 | background: #fdf5f2; 128 | border: 1px solid rgba(235, 87, 87, 0.3); 129 | box-shadow: 0px 1px 2px rgb(15 15 15 / 10%); 130 | border-radius: 50px; 131 | color: var(--orange); 132 | cursor: pointer; 133 | font-size: 18px; 134 | width: 100%; 135 | margin-bottom: 10px; 136 | padding: 10px 16px; 137 | } 138 | 139 | .connect-btn, 140 | .signup-btn, 141 | .post-btn, 142 | .set-essence-btn, 143 | .set-subscribe-btn { 144 | background: var(--orange); 145 | border: 1px solid rgba(235, 87, 87, 0.3); 146 | box-shadow: 0px 1px 2px rgb(15 15 15 / 10%); 147 | border-radius: 50px; 148 | color: var(--white); 149 | cursor: pointer; 150 | font-size: 18px; 151 | width: 100%; 152 | margin-bottom: 10px; 153 | padding: 10px 16px; 154 | } 155 | 156 | .create-btn { 157 | border-radius: 50%; 158 | width: 60px; 159 | height: 60px; 160 | margin: 0 auto; 161 | } 162 | 163 | .subscribe-btn:disabled, 164 | .collect-btn:disabled { 165 | cursor: not-allowed; 166 | } 167 | 168 | /* CARDS */ 169 | .account-card { 170 | display: grid; 171 | grid-template-columns: 54px 1fr 80px; 172 | grid-gap: 20px; 173 | height: 54px; 174 | align-items: center; 175 | margin-top: 20px; 176 | } 177 | 178 | .account-card-primary { 179 | background-color: var(--orange); 180 | border-radius: 20px; 181 | color: var(--white); 182 | font-size: 12px; 183 | padding: 6px; 184 | width: 68px; 185 | text-align: center; 186 | } 187 | 188 | .account-card-info { 189 | display: flex; 190 | align-items: flex-end; 191 | } 192 | 193 | .account-card-info div:last-child { 194 | margin-left: 6px; 195 | } 196 | 197 | .account-card-id { 198 | font-size: 15px; 199 | } 200 | 201 | .profile-card { 202 | border: 1px solid var(--gray); 203 | border-radius: 8px; 204 | min-width: 270px; 205 | padding: 20px; 206 | } 207 | 208 | .profile-card-img { 209 | display: flex; 210 | align-items: flex-start; 211 | justify-content: space-between; 212 | margin-bottom: 20px; 213 | } 214 | 215 | .profile-card-img .subscribe-btn { 216 | font-size: 15px; 217 | padding: 8px 12px; 218 | max-width: 100px; 219 | } 220 | 221 | .profile-card-img img, 222 | .account-card-img img { 223 | border: 1px solid var(--gray); 224 | border-radius: 50%; 225 | margin-bottom: 10px; 226 | } 227 | 228 | .profile-card-name, 229 | .account-card-name { 230 | font-size: 18px; 231 | font-weight: 600; 232 | } 233 | 234 | .profile-card-handle, 235 | .account-card-handle { 236 | font-size: 16px; 237 | font-weight: 600; 238 | color: var(--dark-gray); 239 | } 240 | 241 | .profile-card-bio { 242 | margin-top: 10px; 243 | } 244 | 245 | .profile-card-address { 246 | display: grid; 247 | grid-template-columns: 1fr 10px; 248 | grid-gap: 10px; 249 | align-items: center; 250 | } 251 | 252 | .profile-card-address div:last-child { 253 | background-color: var(--green); 254 | border-radius: 50%; 255 | width: 10px; 256 | height: 10px; 257 | } 258 | 259 | .profiles { 260 | display: grid; 261 | grid-template-columns: 1fr; 262 | grid-gap: 20px; 263 | margin-bottom: 60px; 264 | } 265 | 266 | .profiles .profile-card-img div { 267 | border: 1px solid var(--gray); 268 | border-radius: 50%; 269 | width: 80px; 270 | height: 80px; 271 | } 272 | 273 | .post-card { 274 | border-top: 1px solid var(--gray); 275 | display: grid; 276 | grid-template-columns: 50px 1fr; 277 | align-items: flex-start; 278 | padding-top: 40px; 279 | padding-bottom: 40px; 280 | position: relative; 281 | } 282 | 283 | .post-card:last-child { 284 | border-bottom: 1px solid var(--gray); 285 | } 286 | 287 | .post-collect { 288 | position: absolute; 289 | top: 56px; 290 | right: 16px; 291 | } 292 | 293 | .post-nft { 294 | display: none; 295 | } 296 | 297 | .post-avatar img { 298 | border-radius: 50%; 299 | } 300 | 301 | .post-card .collect-btn { 302 | font-size: 15px; 303 | padding: 8px 12px; 304 | max-width: 80px; 305 | margin: 0px; 306 | } 307 | 308 | .post-profile { 309 | margin-left: 20px; 310 | margin-right: 60px; 311 | } 312 | 313 | .post-profile img { 314 | border-radius: 50%; 315 | } 316 | 317 | .post-nft { 318 | border: 1px solid var(--gray); 319 | box-shadow: 0px 1px 2px rgb(15 15 15 / 10%); 320 | } 321 | 322 | .post-nft img { 323 | min-width: 240px; 324 | min-height: 240px; 325 | } 326 | 327 | .post-profile-name { 328 | display: none; 329 | font-size: 16px; 330 | font-weight: 600; 331 | } 332 | 333 | .post-profile-handle { 334 | font-size: 16px; 335 | font-weight: 600; 336 | color: var(--dark-gray); 337 | margin-right: 4px; 338 | } 339 | 340 | .post-profile-details { 341 | display: flex; 342 | align-items: center; 343 | } 344 | 345 | .post-profile-time { 346 | font-size: 16px; 347 | color: var(--dark-gray); 348 | } 349 | 350 | .post-content { 351 | margin-top: 10px; 352 | word-break: break-all; 353 | } 354 | 355 | .posts { 356 | margin-bottom: 60px; 357 | } 358 | 359 | .posts .collect-btn { 360 | font-size: 13px; 361 | max-width: 100px; 362 | padding: 8px 16px; 363 | } 364 | 365 | .essence-mw-card, 366 | .subscribe-mw-card { 367 | cursor: pointer; 368 | padding: 8px 16px; 369 | } 370 | 371 | .essence-mw-card:hover, 372 | .subscribe-mw-card:hover { 373 | background: #fdf5f2; 374 | } 375 | 376 | /* PANEL */ 377 | .panel-profiles { 378 | border: 1px solid var(--gray); 379 | border-radius: 8px; 380 | padding: 20px; 381 | margin-top: 20px; 382 | } 383 | 384 | .panel .panel-profile-card { 385 | border: none; 386 | display: grid; 387 | grid-template-columns: 40px 1fr 90px; 388 | align-items: center; 389 | grid-gap: 12px; 390 | margin-top: 20px; 391 | padding: 0px; 392 | } 393 | 394 | .panel .panel-profile-card-img { 395 | border: 1px solid var(--gray); 396 | border-radius: 50%; 397 | width: 44px; 398 | height: 44px; 399 | } 400 | 401 | .panel .panel-profile-card-img img { 402 | border-radius: 50%; 403 | } 404 | 405 | .panel .profile-card-user div:first-child { 406 | font-size: 15px; 407 | font-weight: 500; 408 | } 409 | 410 | .panel .profile-card-user div:last-child { 411 | color: var(--dark-gray); 412 | font-size: 14px; 413 | } 414 | 415 | .panel .subscribe-btn { 416 | font-size: 13px; 417 | max-width: 100px; 418 | padding: 8px 12px; 419 | } 420 | 421 | /* SETTINGS PAGE */ 422 | .middleware { 423 | border: 1px solid rgba(235, 87, 87, 0.3); 424 | border-radius: 8px; 425 | padding: 16px 20px; 426 | margin-top: 20px; 427 | } 428 | 429 | .middleware .middleware-btn { 430 | border-radius: 2px; 431 | height: 40px; 432 | font-size: 16px; 433 | padding: 6px 10px; 434 | margin-top: 20px; 435 | } 436 | 437 | /* MODAL */ 438 | .modal { 439 | background-color: rgb(15 15 15 / 10%); 440 | position: fixed; 441 | top: 0; 442 | left: 0; 443 | width: 100%; 444 | height: 100vh; 445 | z-index: 2; 446 | } 447 | 448 | .modal-wrapper { 449 | background-color: var(--white); 450 | border-radius: 4px; 451 | width: calc(100% - 40px); 452 | max-width: 600px; 453 | height: auto; 454 | position: absolute; 455 | top: 50%; 456 | left: 50%; 457 | transform: translate(-50%, -50%); 458 | outline: none; 459 | overflow: hidden; 460 | position: relative; 461 | user-select: none; 462 | } 463 | 464 | .modal-post, 465 | .modal-signup, 466 | .modal-essence-mw, 467 | .modal-subscribe-mw { 468 | padding: 40px 50px 40px 40px; 469 | } 470 | 471 | .modal-close-btn { 472 | background-color: transparent; 473 | border: none; 474 | cursor: pointer; 475 | font-size: 24px; 476 | font-weight: bold; 477 | outline: none; 478 | position: absolute; 479 | top: 20px; 480 | right: 20px; 481 | } 482 | 483 | .modal-success, 484 | .modal-error { 485 | border-radius: 4px; 486 | display: grid; 487 | grid-template-columns: 30px 1fr; 488 | grid-gap: 16px; 489 | align-items: center; 490 | font-size: 18px; 491 | overflow: hidden; 492 | padding: 20px 60px 20px 20px; 493 | } 494 | 495 | .modal-success div, 496 | .modal-error div { 497 | overflow: hidden; 498 | } 499 | 500 | .modal-success svg, 501 | .modal-error svg { 502 | width: 30px; 503 | } 504 | 505 | .modal-success svg path { 506 | fill: var(--green); 507 | } 508 | 509 | .modal-error svg path { 510 | fill: var(--red); 511 | } 512 | 513 | .modal-post textarea { 514 | font-family: var(--font-family); 515 | width: 100%; 516 | min-height: 200px; 517 | } 518 | 519 | /* FORMS */ 520 | .form label { 521 | display: block; 522 | font-weight: 600; 523 | margin-bottom: 4px; 524 | margin-top: 16px; 525 | } 526 | 527 | .form textarea, 528 | .form input { 529 | background: #fdf5f2; 530 | border: 1px solid rgba(235, 87, 87, 0.3); 531 | accent-color: var(--orange); 532 | width: 100%; 533 | outline: none; 534 | padding: 10px 12px; 535 | } 536 | 537 | .form-note { 538 | color: var(--dark-gray); 539 | font-style: italic; 540 | font-size: 14px; 541 | margin: 40px 0px; 542 | } 543 | 544 | .form-post-middleware label { 545 | display: grid; 546 | grid-template-columns: 40px 1fr 20px; 547 | grid-gap: 10px; 548 | cursor: pointer; 549 | padding: 10px 12px; 550 | margin: 0px; 551 | } 552 | 553 | .form-post-middleware div:first-child { 554 | margin: 16px 0px 4px; 555 | font-weight: 500; 556 | } 557 | 558 | .form-post-middleware div:last-child { 559 | background: #fdf5f2; 560 | border: 1px solid rgba(235, 87, 87, 0.3); 561 | box-shadow: 0px 1px 2px rgb(15 15 15 / 10%); 562 | } 563 | 564 | .form .dropdown { 565 | background: #fdf5f2; 566 | border: 1px solid rgba(235, 87, 87, 0.3); 567 | color: var(--orange); 568 | cursor: pointer; 569 | min-width: 250px; 570 | max-width: 70%; 571 | position: relative; 572 | height: 44px; 573 | text-align: center; 574 | } 575 | 576 | .form .dropdown-options { 577 | border: 1px solid rgba(235, 87, 87, 0.3); 578 | background-color: var(--white); 579 | color: var(--dark-gray); 580 | width: calc(100% + 2px); 581 | height: 240px; 582 | overflow-y: scroll; 583 | position: absolute; 584 | top: 42px; 585 | left: -1px; 586 | text-align: left; 587 | } 588 | 589 | .form .dropdown-select { 590 | display: flex; 591 | justify-content: space-between; 592 | align-items: center; 593 | height: 100%; 594 | padding: 0px 16px; 595 | } 596 | 597 | .form .dropdown-select-post { 598 | overflow: hidden; 599 | white-space: nowrap; 600 | text-overflow: ellipsis; 601 | } 602 | 603 | .form .dropdown-select svg { 604 | width: 18px; 605 | height: 18px; 606 | } 607 | 608 | .form .dropdown-select button { 609 | background-color: transparent; 610 | border: none; 611 | cursor: pointer; 612 | outline: none; 613 | position: absolute; 614 | top: 0; 615 | left: 0; 616 | width: 100%; 617 | height: 100%; 618 | } 619 | 620 | /* LOADER */ 621 | .loader { 622 | display: flex; 623 | align-items: center; 624 | flex-direction: column; 625 | justify-content: center; 626 | } 627 | 628 | .loader-circle { 629 | width: 98px; 630 | height: 98px; 631 | border: 2.5px solid var(--black); 632 | border-bottom-color: var(--orange); 633 | border-radius: 50%; 634 | display: inline-block; 635 | box-sizing: border-box; 636 | animation: rotation 1s linear infinite; 637 | width: 20px; 638 | height: 20px; 639 | } 640 | 641 | .loader div:last-child { 642 | font-size: 10px; 643 | margin-top: 4px; 644 | } 645 | 646 | @keyframes rotation { 647 | 0% { 648 | transform: rotate(0deg); 649 | } 650 | 651 | 100% { 652 | transform: rotate(360deg); 653 | } 654 | } 655 | 656 | @media screen and (min-width: 798px) { 657 | .wrapper { 658 | flex-direction: row; 659 | justify-content: space-between; 660 | } 661 | 662 | .wrapper-content { 663 | border-right: 1px solid var(--gray); 664 | width: 100%; 665 | padding: 20px 60px 20px 40px; 666 | } 667 | 668 | .wrapper-details { 669 | display: block; 670 | padding: 20px; 671 | max-width: 360px; 672 | margin: 0px; 673 | min-height: 100vh; 674 | } 675 | 676 | .middleware { 677 | display: grid; 678 | grid-template-columns: 1fr 160px; 679 | align-items: center; 680 | grid-gap: 40px; 681 | margin-top: 20px; 682 | } 683 | 684 | .post-card { 685 | grid-template-columns: 50px 1fr 120px; 686 | } 687 | 688 | .profile-nft { 689 | display: block; 690 | width: 120px; 691 | height: 120px; 692 | } 693 | } 694 | 695 | @media screen and (min-width: 1200px) { 696 | .post-card { 697 | grid-template-columns: 50px 1fr 260px; 698 | } 699 | 700 | .profiles { 701 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 702 | } 703 | 704 | .post-nft, 705 | .post-profile-name { 706 | display: block; 707 | } 708 | 709 | .post-profile-handle { 710 | margin: 0px 10px; 711 | } 712 | } 713 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "useUnknownInCatchVariables": false 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import { Web3Provider } from "@ethersproject/providers"; 2 | 3 | export interface IAuthContext { 4 | address: string | undefined; 5 | accessToken: string | undefined; 6 | primaryProfile: IPrimaryProfileCard | undefined; 7 | profileCount: number; 8 | postCount: number; 9 | posts: IPostCard[]; 10 | profiles: IAccountCard[]; 11 | indexingProfiles: IAccountCard[]; 12 | indexingPosts: IPostCard[]; 13 | setAddress: (address: string | undefined) => void; 14 | setAccessToken: (accessToken: string | undefined) => void; 15 | setPrimaryProfile: (primaryProfile: IPrimaryProfileCard | undefined) => void; 16 | setProfileCount: (profileCount: number) => void; 17 | setPostCount: (postCount: number) => void; 18 | setPosts: (posts: IPostCard[]) => void; 19 | setProfiles: (profiles: IAccountCard[]) => void; 20 | setIndexingProfiles: (indexingProfiles: IAccountCard[]) => void; 21 | setIndexingPosts: (indexingPosts: IPostCard[]) => void; 22 | connectWallet: () => Promise; 23 | checkNetwork: (provider: Web3Provider) => Promise; 24 | } 25 | 26 | export interface IModalContext { 27 | modal: boolean; 28 | modalType: string | null; 29 | modalText: string; 30 | handleModal: (type: string | null, text: string) => void; 31 | } 32 | 33 | /* Metadata schema for Profile NFT */ 34 | export interface IProfileMetadata { 35 | name: string; 36 | bio: string; 37 | handle: string; 38 | version: string; 39 | } 40 | 41 | /* Metadata schema for Essence NFT */ 42 | interface Media { 43 | /* The MIME type for the media */ 44 | media_type: string; 45 | /* The URL link for the media */ 46 | media_url: string; 47 | /* Alternative text when media can't be rendered */ 48 | alt_tag?: string; 49 | /* The preview image for the media */ 50 | preview_image_url?: string; 51 | } 52 | 53 | interface Attribute { 54 | /* Field indicating how you would like it to be displayed */ 55 | /* optional if the trait_type is string */ 56 | display_type?: string; 57 | /* Name of the trait */ 58 | trait_type: string; 59 | /* Value of the trait */ 60 | value: number | string; 61 | } 62 | 63 | export interface IEssenceMetadata { 64 | /* ~~ REQUIRED ~~ */ 65 | /* Unique id for the issued item */ 66 | metadata_id: string; 67 | 68 | /* Version of the metadata schema used for the issued item. */ 69 | version: string; 70 | 71 | /* ~~ OPTIONAL ~~ */ 72 | /* Id of the application under which the items are being minted. */ 73 | app_id?: string; 74 | 75 | /* Language of the content as a BCP47 language tag. */ 76 | lang?: string; 77 | 78 | /* Creation time of the item as ISO 8601. */ 79 | issue_date?: string; 80 | 81 | /* The content associated with the item */ 82 | content?: string; 83 | 84 | /* Media refers to any image, video, or any other MIME type attached to the content. 85 | Limited to max. 10 media objects. */ 86 | media?: Media[]; 87 | 88 | /* Field indicating the tags associated with the content. Limited to max. 5 tags. */ 89 | tags?: string[]; 90 | 91 | /* ~~ OPENSEA (optional) ~~ */ 92 | /* URL to the image of the item. */ 93 | image?: string; 94 | 95 | /* SVG image data when the image is not passed. Only use this if you're not 96 | including the image parameter. */ 97 | image_data?: string; 98 | 99 | /* Name of the item. */ 100 | name?: string; 101 | 102 | /* Description of the item. */ 103 | description?: string; 104 | 105 | /* URL to a multi-media attachment for the item. */ 106 | animation_url?: string; 107 | 108 | /* Attributes for the item. */ 109 | attributes?: Attribute[]; 110 | 111 | /* URL to the item on your site. */ 112 | external_url?: string; 113 | } 114 | 115 | export interface IProfileCard { 116 | handle: string; 117 | avatar: string; 118 | metadata: string; 119 | profileID: number; 120 | isSubscribedByMe: boolean; 121 | } 122 | 123 | export interface IPostCard { 124 | createdBy: { 125 | handle: string; 126 | avatar: string; 127 | metadata: string; 128 | profileID: number; 129 | }; 130 | essenceID: number; 131 | tokenURI: string; 132 | isCollectedByMe: boolean; 133 | isIndexed?: boolean; 134 | collectMw?: any; 135 | } 136 | 137 | export interface IEssenceMwCard { 138 | essence: { 139 | essenceID: number; 140 | tokenURI: string; 141 | }; 142 | selectedEssenceContent: string; 143 | setSelectedEssence: (essence: { 144 | essenceID: number; 145 | tokenURI: string; 146 | }) => void; 147 | setSelectedEssenceContent: (selectedEssenceContent: string) => void; 148 | setShowDropdown: (showDropdown: boolean) => void; 149 | } 150 | 151 | export interface IProfileMwCard { 152 | profileID: number; 153 | metadata: string; 154 | selectedProfileHandle: string; 155 | setSelectedProfileId: (profileID: number) => void; 156 | setSelectedProfileHandle: (selectedProfileHandle: string) => void; 157 | setShowDropdown: (showDropdown: boolean) => void; 158 | } 159 | 160 | export interface IAccountCard { 161 | profileID: number; 162 | handle: string; 163 | avatar: string; 164 | metadata: string; 165 | isPrimary?: boolean; 166 | isIndexed?: boolean; 167 | } 168 | 169 | export interface ISignupInput { 170 | name: string; 171 | bio: string; 172 | handle: string; 173 | avatar: string; 174 | operator: string; 175 | } 176 | 177 | export interface IPostInput { 178 | nftImageURL: string; 179 | content: string; 180 | middleware: string; 181 | } 182 | 183 | export interface IPrimaryProfileCard { 184 | profileID: number; 185 | handle: string; 186 | avatar: string; 187 | metadata: string; 188 | } 189 | --------------------------------------------------------------------------------