├── .commitlintrc.json ├── .eslintrc.json ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── public ├── CyberGraph-Icon.png ├── CyberGraph-logo.png ├── CyberGraph-screenshot.jpg ├── Sample_User_Icon.png ├── blue.png ├── brown.png ├── favicon.ico ├── green.png ├── grey.png ├── icons │ ├── context.png │ ├── eth.png │ ├── etherscan.ico │ ├── foundation.png │ ├── opensea.png │ ├── rarible.png │ └── twitter.png ├── red.jpg ├── vercel.svg └── white.png ├── src ├── components │ ├── Graph │ │ ├── FocusGraph.module.css │ │ ├── FocusGraph.tsx │ │ └── FocusGraphWrapper.tsx │ ├── ListModal │ │ ├── index.module.css │ │ └── index.tsx │ ├── NavBar │ │ ├── index.module.css │ │ └── index.tsx │ ├── NftListItem │ │ ├── index.module.css │ │ └── index.tsx │ ├── NftModal │ │ ├── index.module.css │ │ └── index.tsx │ ├── PoapModal │ │ ├── index.module.css │ │ └── index.tsx │ ├── SearchBar │ │ ├── index.module.css │ │ └── index.tsx │ ├── UserPanel │ │ ├── NftSections.tsx │ │ ├── index.module.css │ │ └── index.tsx │ └── WalletConnectButton │ │ ├── index.module.css │ │ └── index.tsx ├── config │ └── config.ts ├── context │ ├── GraphContext.tsx │ └── web3Context.tsx ├── graphql │ ├── client.tsx │ └── queries │ │ ├── get_connections.tsx │ │ ├── get_identity.tsx │ │ └── get_recommendation.tsx ├── pages │ ├── _app.tsx │ ├── api │ │ └── get_balance_api.ts │ └── index.tsx ├── types │ ├── AllSocialConnections.ts │ └── identity.ts └── utils │ └── helper.ts ├── styles ├── Home.module.css └── globals.css ├── tsconfig.json └── yarn.lock /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["next", "prettier"], 4 | "root": true, 5 | "plugins": ["@typescript-eslint"], 6 | "rules": { 7 | "jsx-a11y/alt-text": "off", 8 | "@next/next/no-page-custom-font": "off", 9 | "@next/next/no-img-element": "off", 10 | "react/jsx-no-target-blank": "off", 11 | "no-unused-vars": "off", 12 | "@typescript-eslint/no-unused-vars": "error", 13 | "no-console": ["error", { "allow": ["warn", "error"] }], 14 | "prefer-const": ["error"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,react,webstorm,visualstudiocode,node,git 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,react,webstorm,visualstudiocode,node,git 4 | 5 | ### Git ### 6 | # Created by git for backups. To disable backups in Git: 7 | # $ git config --global mergetool.keepBackup false 8 | *.orig 9 | 10 | # Created by git when using merge tools for conflicts 11 | *.BACKUP.* 12 | *.BASE.* 13 | *.LOCAL.* 14 | *.REMOTE.* 15 | *_BACKUP_*.txt 16 | *_BASE_*.txt 17 | *_LOCAL_*.txt 18 | *_REMOTE_*.txt 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Node ### 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* 57 | .pnpm-debug.log* 58 | 59 | # Diagnostic reports (https://nodejs.org/api/report.html) 60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | *.lcov 74 | 75 | # nyc test coverage 76 | .nyc_output 77 | 78 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # Bower dependency directory (https://bower.io/) 82 | bower_components 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (https://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules/ 92 | jspm_packages/ 93 | 94 | # Snowpack dependency directory (https://snowpack.dev/) 95 | web_modules/ 96 | 97 | # TypeScript cache 98 | *.tsbuildinfo 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Optional stylelint cache 107 | .stylelintcache 108 | 109 | # Microbundle cache 110 | .rpt2_cache/ 111 | .rts2_cache_cjs/ 112 | .rts2_cache_es/ 113 | .rts2_cache_umd/ 114 | 115 | # Optional REPL history 116 | .node_repl_history 117 | 118 | # Output of 'npm pack' 119 | *.tgz 120 | 121 | # Yarn Integrity file 122 | .yarn-integrity 123 | 124 | # dotenv environment variable files 125 | .env 126 | .env.development.local 127 | .env.test.local 128 | .env.production.local 129 | .env.local 130 | 131 | # parcel-bundler cache (https://parceljs.org/) 132 | .cache 133 | .parcel-cache 134 | 135 | # Next.js build output 136 | .next 137 | out 138 | 139 | # Nuxt.js build / generate output 140 | .nuxt 141 | dist 142 | 143 | # Gatsby files 144 | .cache/ 145 | # Comment in the public line in if your project uses Gatsby and not Next.js 146 | # https://nextjs.org/blog/next-9-1#public-directory-support 147 | # public 148 | 149 | # vuepress build output 150 | .vuepress/dist 151 | 152 | # vuepress v2.x temp and cache directory 153 | .temp 154 | 155 | # Docusaurus cache and generated files 156 | .docusaurus 157 | 158 | # Serverless directories 159 | .serverless/ 160 | 161 | # FuseBox cache 162 | .fusebox/ 163 | 164 | # DynamoDB Local files 165 | .dynamodb/ 166 | 167 | # TernJS port file 168 | .tern-port 169 | 170 | # Stores VSCode versions used for testing VSCode extensions 171 | .vscode-test 172 | 173 | # yarn v2 174 | .yarn/cache 175 | .yarn/unplugged 176 | .yarn/build-state.yml 177 | .yarn/install-state.gz 178 | .pnp.* 179 | 180 | ### Node Patch ### 181 | # Serverless Webpack directories 182 | .webpack/ 183 | 184 | # Optional stylelint cache 185 | 186 | # SvelteKit build / generate output 187 | .svelte-kit 188 | 189 | ### react ### 190 | .DS_* 191 | **/*.backup.* 192 | **/*.back.* 193 | 194 | node_modules 195 | 196 | *.sublime* 197 | 198 | psd 199 | thumb 200 | sketch 201 | 202 | ### VisualStudioCode ### 203 | .vscode/* 204 | !.vscode/settings.json 205 | !.vscode/tasks.json 206 | !.vscode/launch.json 207 | !.vscode/extensions.json 208 | !.vscode/*.code-snippets 209 | 210 | # Local History for Visual Studio Code 211 | .history/ 212 | 213 | # Built Visual Studio Code Extensions 214 | *.vsix 215 | 216 | ### VisualStudioCode Patch ### 217 | # Ignore all local history of files 218 | .history 219 | .ionide 220 | 221 | # Support for Project snippet scope 222 | 223 | ### WebStorm ### 224 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 225 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 226 | 227 | .idea 228 | 229 | # User-specific stuff 230 | .idea/**/workspace.xml 231 | .idea/**/tasks.xml 232 | .idea/**/usage.statistics.xml 233 | .idea/**/dictionaries 234 | .idea/**/shelf 235 | 236 | # AWS User-specific 237 | .idea/**/aws.xml 238 | 239 | # Generated files 240 | .idea/**/contentModel.xml 241 | 242 | # Sensitive or high-churn files 243 | .idea/**/dataSources/ 244 | .idea/**/dataSources.ids 245 | .idea/**/dataSources.local.xml 246 | .idea/**/sqlDataSources.xml 247 | .idea/**/dynamic.xml 248 | .idea/**/uiDesigner.xml 249 | .idea/**/dbnavigator.xml 250 | 251 | # Gradle 252 | .idea/**/gradle.xml 253 | .idea/**/libraries 254 | 255 | # Gradle and Maven with auto-import 256 | # When using Gradle or Maven with auto-import, you should exclude module files, 257 | # since they will be recreated, and may cause churn. Uncomment if using 258 | # auto-import. 259 | # .idea/artifacts 260 | # .idea/compiler.xml 261 | # .idea/jarRepositories.xml 262 | # .idea/modules.xml 263 | # .idea/*.iml 264 | # .idea/modules 265 | # *.iml 266 | # *.ipr 267 | 268 | # CMake 269 | cmake-build-*/ 270 | 271 | # Mongo Explorer plugin 272 | .idea/**/mongoSettings.xml 273 | 274 | # File-based project format 275 | *.iws 276 | 277 | # IntelliJ 278 | out/ 279 | 280 | # mpeltonen/sbt-idea plugin 281 | .idea_modules/ 282 | 283 | # JIRA plugin 284 | atlassian-ide-plugin.xml 285 | 286 | # Cursive Clojure plugin 287 | .idea/replstate.xml 288 | 289 | # SonarLint plugin 290 | .idea/sonarlint/ 291 | 292 | # Crashlytics plugin (for Android Studio and IntelliJ) 293 | com_crashlytics_export_strings.xml 294 | crashlytics.properties 295 | crashlytics-build.properties 296 | fabric.properties 297 | 298 | # Editor-based Rest Client 299 | .idea/httpRequests 300 | 301 | # Android studio 3.1+ serialized cache file 302 | .idea/caches/build_file_checksums.ser 303 | 304 | ### WebStorm Patch ### 305 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 306 | 307 | # *.iml 308 | # modules.xml 309 | # .idea/misc.xml 310 | # *.ipr 311 | 312 | # Sonarlint plugin 313 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 314 | .idea/**/sonarlint/ 315 | 316 | # SonarQube Plugin 317 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 318 | .idea/**/sonarIssues.xml 319 | 320 | # Markdown Navigator plugin 321 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 322 | .idea/**/markdown-navigator.xml 323 | .idea/**/markdown-navigator-enh.xml 324 | .idea/**/markdown-navigator/ 325 | 326 | # Cache file creation bug 327 | # See https://youtrack.jetbrains.com/issue/JBR-2257 328 | .idea/$CACHE_FILE$ 329 | 330 | # CodeStream plugin 331 | # https://plugins.jetbrains.com/plugin/12206-codestream 332 | .idea/codestream.xml 333 | 334 | # End of https://www.toptal.com/developers/gitignore/api/macos,react,webstorm,visualstudiocode,node,git -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": true 5 | } 6 | }, 7 | "[typescriptreact]": { 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CyberGraph 2 | 3 | ![`CyberGraph`](/public/CyberGraph-screenshot.jpg) 4 | 5 | 6 | 7 | ## [`Live Site`](https://cyber-graph.vercel.app/) 8 | 9 | ## About this project 10 | 11 | CyberGraph is a 3D-graph based, user based social connection explorer. It has some cool features like 3d node graph, dynamic loading bar, immersive user experience, cyber mode(10-hops friendship network display) and focus mode(aggregated connection display). 12 | 13 | This project is inspired from Gitcoin [Scheduling Point Virtual Hackathon](https://gitcoin.co/issue/cyberconnecthq/explorer-and-cyberconnected-dapps/1/100027517) contest submissions which is making a social explorer with cyberconnect on Web3. 14 | 15 |
16 | 17 | ## Technologies We Used 18 | 19 | - [`Next.js`](https://nextjs.org/) 20 |
21 | - [`TypeScript `](https://www.typescriptlang.org/) 22 |
23 | - [`MUI library`](https://mui.com/) 24 |
25 | - [`Cyber Connect`](https://docs.cyberconnect.me/) 26 |
27 | - [`Apollo Client`](https://www.apollographql.com/docs/) 28 |
29 | - [`3D Force-Directed Graph`](https://github.com/vasturiano/3d-force-graph) 30 | 31 |
32 | 33 | ## Run the App 34 | 35 | Set the following variables in `.env` 36 | 37 | - `NEXT_PUBLIC_ALCHEMY_ID = `[`Alchemy NFT API`](https://docs.alchemy.com/alchemy/enhanced-apis/nft-api) 38 | 39 | Then run: 40 | 41 | ``` 42 | yarn install 43 | 44 | yarn run dev 45 | ``` 46 | 47 | Open the browser, open [Localhost](https://localhost:3000/) and you will see the UI of the project. 48 | 49 | 81 | 82 | ## Follow Us 83 | 84 | - [Twitter](https://twitter.com/CyberConnectHQ) 85 | - [Discord](https://discord.com/invite/bYJ3cB7bbC) 86 | - [Website](https://cyberconnect.me/) 87 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack: (config, options) => { 5 | config.module.rules.push({ 6 | test: /\.glsl$/, 7 | loader: "webpack-glsl-loader", 8 | }); 9 | 10 | return config; 11 | }, 12 | }; 13 | 14 | module.exports = nextConfig; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cybergraph", 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 | "prepare": "husky install", 11 | "release": "standard-version" 12 | }, 13 | "dependencies": { 14 | "@apollo/client": "^3.5.9", 15 | "@cyberconnect/react-follow-button": "^2.1.3", 16 | "@cyberlab/cyberconnect": "^4.3.0", 17 | "@emotion/react": "^11.8.1", 18 | "@emotion/styled": "^11.8.1", 19 | "@mui/icons-material": "^5.4.2", 20 | "@mui/lab": "^5.0.0-alpha.70", 21 | "@mui/material": "^5.4.2", 22 | "@walletconnect/web3-provider": "^1.7.5", 23 | "@web3auth/web3auth": "^0.6.3", 24 | "ethers": "^5.5.4", 25 | "graphql": "^16.3.0", 26 | "magic-sdk": "7.0.0", 27 | "moralis": "^1.5.4", 28 | "next": "12.1.0", 29 | "react": "^17.0.2", 30 | "react-dom": "^17.0.2", 31 | "react-force-graph-3d": "^1.21.12", 32 | "react-loading": "^2.0.3", 33 | "react-moralis": "^1.3.2", 34 | "three": "^0.138.0", 35 | "three-spritetext": "^1.6.5", 36 | "web3modal": "^1.9.5" 37 | }, 38 | "devDependencies": { 39 | "@commitlint/cli": "^16.2.1", 40 | "@commitlint/config-conventional": "^16.2.1", 41 | "@types/node": "17.0.18", 42 | "@types/react": "17.0.39", 43 | "@types/three": "^0.137.0", 44 | "@typescript-eslint/eslint-plugin": "^5.14.0", 45 | "@typescript-eslint/parser": "^5.14.0", 46 | "eslint": "^8.9.0", 47 | "eslint-config-airbnb": "^19.0.4", 48 | "eslint-config-next": "12.1.0", 49 | "eslint-config-prettier": "^8.4.0", 50 | "eslint-plugin-import": "^2.25.4", 51 | "eslint-plugin-jsx-a11y": "^6.5.1", 52 | "eslint-plugin-react": "^7.28.0", 53 | "eslint-plugin-react-hooks": "^4.3.0", 54 | "husky": "^7.0.4", 55 | "prettier": "^2.5.1", 56 | "standard-version": "^9.3.2", 57 | "typescript": "^4.6.2" 58 | }, 59 | "lint-staged": { 60 | "*.{js,json,yml,yaml,css,scss,ts,tsx,md}": [ 61 | "prettier --write" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/CyberGraph-Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/CyberGraph-Icon.png -------------------------------------------------------------------------------- /public/CyberGraph-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/CyberGraph-logo.png -------------------------------------------------------------------------------- /public/CyberGraph-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/CyberGraph-screenshot.jpg -------------------------------------------------------------------------------- /public/Sample_User_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/Sample_User_Icon.png -------------------------------------------------------------------------------- /public/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/blue.png -------------------------------------------------------------------------------- /public/brown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/brown.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/favicon.ico -------------------------------------------------------------------------------- /public/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/green.png -------------------------------------------------------------------------------- /public/grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/grey.png -------------------------------------------------------------------------------- /public/icons/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/icons/context.png -------------------------------------------------------------------------------- /public/icons/eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/icons/eth.png -------------------------------------------------------------------------------- /public/icons/etherscan.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/icons/etherscan.ico -------------------------------------------------------------------------------- /public/icons/foundation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/icons/foundation.png -------------------------------------------------------------------------------- /public/icons/opensea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/icons/opensea.png -------------------------------------------------------------------------------- /public/icons/rarible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/icons/rarible.png -------------------------------------------------------------------------------- /public/icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/icons/twitter.png -------------------------------------------------------------------------------- /public/red.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/red.jpg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberconnecthq/GraphVis/892f855e7d54e4d0075d595e8ee8d25b90c9756b/public/white.png -------------------------------------------------------------------------------- /src/components/Graph/FocusGraph.module.css: -------------------------------------------------------------------------------- 1 | .goToYouButton { 2 | position: absolute; 3 | right: 24px; 4 | bottom: 24px; 5 | color: white; 6 | background-color: rgb(0, 0, 0); 7 | border: solid white 2px; 8 | font-size: x-large; 9 | border-radius: 10px; 10 | } 11 | 12 | .goToYouButton:hover { 13 | background-color: #555; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Graph/FocusGraph.tsx: -------------------------------------------------------------------------------- 1 | // src\components\Graph\FocusGraph.tsx 2 | 3 | // import dynamic from "next/dynamic"; 4 | import { GraphLink, useGraph } from "@/context/GraphContext"; 5 | import { formatAddress } from "@/utils/helper"; 6 | import React, { useCallback, useEffect, useRef, useState } from "react"; 7 | import ForceGraph3D, { ForceGraphMethods } from "react-force-graph-3d"; 8 | import * as THREE from "three"; 9 | import { Vector2 } from "three"; 10 | import SpriteText from "three-spritetext"; 11 | import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js"; 12 | 13 | const FocusGraph = () => { 14 | const fgRef = useRef(); 15 | const { graphData, setSelectAddress, graphAddress } = useGraph(); 16 | 17 | const [highlightNodes, setHighlightNodes] = useState(new Set()); 18 | const [highlightLinks, setHighlightLinks] = useState(new Set()); 19 | 20 | useEffect(() => { 21 | const bloomPass = new UnrealBloomPass(new Vector2(256, 256), 1, 1, 0.1); 22 | const fg = fgRef.current; 23 | fg?.postProcessingComposer().addPass(bloomPass); 24 | return () => { 25 | fg?.postProcessingComposer().removePass(bloomPass); 26 | }; 27 | }, []); 28 | 29 | const handleClick = useCallback( 30 | (node) => { 31 | highlightNodes.clear(); 32 | highlightLinks.clear(); 33 | if (node) { 34 | highlightNodes.add(node.id); 35 | node.neighbors.forEach((neighbor: string) => 36 | highlightNodes.add(neighbor) 37 | ); 38 | node.links.forEach((link: GraphLink) => 39 | highlightLinks.add(link) 40 | ); 41 | } 42 | updateHighlight(); 43 | 44 | const distance = 90; 45 | const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z); 46 | if (fgRef.current) { 47 | fgRef.current.cameraPosition( 48 | { 49 | x: node.x * distRatio, 50 | y: node.y * distRatio, 51 | z: node.z * distRatio, 52 | }, 53 | node, 54 | 3000 55 | ); 56 | } 57 | setSelectAddress(node.id); 58 | }, 59 | [fgRef, setSelectAddress, highlightLinks, highlightNodes] 60 | ); 61 | 62 | const updateHighlight = () => { 63 | setHighlightNodes(highlightNodes); 64 | setHighlightLinks(highlightLinks); 65 | }; 66 | 67 | function getRandomInt(max: number) { 68 | return Math.floor(Math.random() * max); 69 | } 70 | 71 | const getLinkLabel = useCallback((link: any) => { 72 | const categories = { 73 | 0: "Others", 74 | 1: "Followings", 75 | 2: "Followers", 76 | 3: "Friends", 77 | }; 78 | // @ts-ignore 79 | return categories[link.value]; 80 | }, []); 81 | 82 | const getNodeThreeObject = useCallback( 83 | (node: any) => { 84 | const localImgs = [ 85 | "/red.jpg", 86 | "/blue.png", 87 | "/brown.png", 88 | "/green.png", 89 | "/grey.png", 90 | ]; 91 | 92 | const imgTexture = new THREE.TextureLoader().load( 93 | node.img || localImgs[getRandomInt(localImgs.length)] 94 | // Randomly give one 95 | ); 96 | 97 | const geometry = new THREE.SphereGeometry(2, 6, 6); 98 | 99 | if (node.id === graphAddress) { 100 | const sprite = new SpriteText(formatAddress(graphAddress)); 101 | sprite.textHeight = 8; 102 | sprite.backgroundColor = "#000000"; 103 | return sprite; 104 | } 105 | 106 | const material = new THREE.MeshBasicMaterial({ 107 | map: imgTexture, 108 | }); 109 | 110 | return new THREE.Mesh(geometry, material); 111 | }, 112 | [graphAddress] 113 | ); 114 | 115 | return ( 116 | 124 | highlightLinks.has(link) ? "#ec407a" : "#5f9ea0" 125 | } 126 | linkWidth={(link) => (highlightLinks.has(link) ? 1.5 : 1.0)} 127 | linkDirectionalParticles={2} 128 | linkDirectionalParticleSpeed={0.001} 129 | linkDirectionalParticleWidth={(link) => 130 | highlightLinks.has(link) ? 1.0 : 0.0 131 | } 132 | backgroundColor="#000000" 133 | nodeThreeObject={getNodeThreeObject} 134 | /> 135 | ); 136 | }; 137 | 138 | export default FocusGraph; 139 | -------------------------------------------------------------------------------- /src/components/Graph/FocusGraphWrapper.tsx: -------------------------------------------------------------------------------- 1 | // src\components\Graph\FocusGraphWrapper.tsx 2 | 3 | import dynamic from "next/dynamic"; 4 | 5 | const FocusGraph = dynamic(() => import("./FocusGraph"), { 6 | ssr: false, 7 | }); 8 | 9 | export default FocusGraph; 10 | -------------------------------------------------------------------------------- /src/components/ListModal/index.module.css: -------------------------------------------------------------------------------- 1 | .userName { 2 | display: flex; 3 | flex-direction: column; 4 | margin-left: 5px; 5 | justify-content: center; 6 | } 7 | .avatarSection { 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | .avatar { 13 | border-radius: 50px; 14 | margin-top: 5px; 15 | background-color: white; 16 | width: 50px; 17 | height: 50px; 18 | } 19 | .userInfoSection { 20 | display: flex; 21 | align-items: center; 22 | padding: 10px; 23 | margin: 10px 5px; 24 | width: fit-content; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ListModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { GET_ADDR_CONNECTION_QUERY } from "@/graphql/queries/get_connections"; 2 | import { useQuery } from "@apollo/client"; 3 | import { Typography } from "@mui/material"; 4 | import Box from "@mui/material/Box"; 5 | import Modal from "@mui/material/Modal"; 6 | import * as React from "react"; 7 | import { useEffect } from "react"; 8 | import styles from "./index.module.css"; 9 | 10 | interface Props { 11 | open: boolean; 12 | changeOpen: React.Dispatch>; 13 | address: string; 14 | listType: boolean; 15 | } 16 | 17 | export const ListModal = ({ open, changeOpen, address, listType }: Props) => { 18 | useEffect(() => { 19 | refetch(); 20 | }); 21 | const handleClose = () => changeOpen(false); 22 | 23 | const { data, refetch, loading, error } = useQuery( 24 | GET_ADDR_CONNECTION_QUERY, 25 | { 26 | variables: { 27 | address: address, 28 | first: 50, 29 | after: "-1", 30 | }, 31 | } 32 | ); 33 | 34 | if (loading) return null; 35 | if (error) return null; 36 | 37 | const modalType = listType 38 | ? data.identity.followings.list 39 | : data.identity.followers.list; 40 | 41 | return ( 42 | <> 43 | 44 | 74 | 85 | Top 50 {listType ? "Following" : "Followers"} 86 | 87 | 88 | {modalType.map( 89 | ( 90 | value: { 91 | avatar: string; 92 | address: string; 93 | ens: string; 94 | }, 95 | index: number 96 | ) => { 97 | return ( 98 |
102 |
103 | {value.avatar ? ( 104 | 112 | {""} 117 | 118 | ) : ( 119 | 127 | {""} 134 | 135 | )} 136 |
137 |
138 | {value.ens ? ( 139 | 148 | {value.ens} 149 | 150 | ) : ( 151 | 152 | )} 153 | 162 | {value?.address} 163 | 164 |
165 |
166 | ); 167 | } 168 | )} 169 |
170 |
171 | 172 | ); 173 | }; 174 | -------------------------------------------------------------------------------- /src/components/NavBar/index.module.css: -------------------------------------------------------------------------------- 1 | /* src\components\NavBar\index.module.css */ 2 | 3 | .container { 4 | display: flex; 5 | padding: 20px; 6 | position: fixed; 7 | z-index: 100; 8 | background-color: transparent; 9 | width: 100vw; 10 | display: flex; 11 | justify-content: space-between; 12 | } 13 | 14 | .image { 15 | display: flex; 16 | margin: 10px 30px; 17 | } 18 | 19 | .logo { 20 | height: auto; 21 | cursor: pointer; 22 | } 23 | 24 | .rightPanel { 25 | display: flex; 26 | margin-right: 20px; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | // src\components\NavBar\index.tsx 2 | 3 | import { AppMode, useGraph } from "@/context/GraphContext"; 4 | import QuestionMarkIcon from "@mui/icons-material/QuestionMark"; 5 | import SettingsIcon from "@mui/icons-material/Settings"; 6 | import { 7 | Box, 8 | Button, 9 | Link, 10 | Menu, 11 | MenuItem, 12 | Modal, 13 | Typography, 14 | } from "@mui/material"; 15 | import Image from "next/image"; 16 | import { useRouter } from "next/router"; 17 | import React, { useState } from "react"; 18 | import { SearchBar } from "../SearchBar"; 19 | import { WalletConnectButton } from "../WalletConnectButton"; 20 | import styles from "./index.module.css"; 21 | 22 | export const NavBar: React.FC = () => { 23 | const router = useRouter(); 24 | 25 | const [anchorEl, setAnchorEl] = React.useState(null); 26 | const open = Boolean(anchorEl); 27 | const handleClick = (event: React.MouseEvent) => { 28 | setAnchorEl(event.currentTarget); 29 | }; 30 | const handleClose = () => { 31 | setAnchorEl(null); 32 | }; 33 | 34 | const [modalOpen, setModalOpen] = useState(false); 35 | const handleModalClose = () => { 36 | setModalOpen(false); 37 | }; 38 | const handleModalClick = () => { 39 | setModalOpen(true); 40 | }; 41 | 42 | const { setAppMode } = useGraph(); 43 | 44 | return ( 45 |
46 |
47 | Cyber Graph { 55 | router.push("/"); 56 | }} 57 | /> 58 |
59 | 60 |
61 | 75 | 89 | 90 | 99 | { 101 | setAnchorEl(null); 102 | setAppMode(AppMode.CyberMode); 103 | }} 104 | > 105 | Use cyber mode 106 | 107 | { 109 | setAnchorEl(null); 110 | setAppMode(AppMode.FocusMode); 111 | }} 112 | > 113 | Use focus mode 114 | 115 | 116 | 117 | 136 | {" "} 137 | 148 | Help 149 | 150 | 160 | About 161 | 162 | 171 | CyberGraph is a 3D-graph based, user based social 172 | connection explorer powered by{" "} 173 | 178 | CyberConnect 179 | 180 | .{" "} 181 | 182 | 191 | It has some cool features like 3d node graph, 192 | dynamic loading bar, immersive user experience, 193 | cyber mode(10-hops friendship network display) and 194 | focus mode(aggregated connection display) 195 | 196 | 205 | This is an open-sourced project. Please feel free to 206 | check and contribute on the{" "} 207 | 214 | GitHub 215 | {" "} 216 | page. 217 | 218 | 228 | Controls 229 | 230 | 239 | Left-click: rotate 240 | 241 | 250 | Right-click: pan 251 | 252 | 261 | Mouse-wheel: zoom 262 | 263 | 264 | 265 | 266 |
267 |
268 | ); 269 | }; 270 | 271 | NavBar.displayName = "NavBar"; 272 | -------------------------------------------------------------------------------- /src/components/NftListItem/index.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | margin-right: 12px; 3 | margin-bottom: 8px; 4 | border-radius: 50%; 5 | } 6 | 7 | .button:hover { 8 | outline: solid; 9 | outline-offset: 2px; 10 | } 11 | 12 | .image { 13 | width: 64px; 14 | height: 64px; 15 | border-radius: 50%; 16 | } 17 | 18 | .tooltipTitle { 19 | font-family: "Outfit"; 20 | text-align: center; 21 | font-weight: 600; 22 | font-size: x-large; 23 | } 24 | 25 | .tooltipSubtitle { 26 | font-family: "Outfit"; 27 | text-align: center; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/NftListItem/index.tsx: -------------------------------------------------------------------------------- 1 | import ImageNotSupportedIcon from "@mui/icons-material/ImageNotSupported"; 2 | import { ButtonBase, Tooltip, Typography } from "@mui/material"; 3 | import styles from "./index.module.css"; 4 | 5 | type Props = { 6 | name: string; 7 | imageUrl: string | null; 8 | onClick: () => void; 9 | }; 10 | 11 | export const NftListItem = ({ name, imageUrl, onClick }: Props) => { 12 | return ( 13 | 18 | 19 | {name} 20 | 21 | 22 | Click for more details 23 | 24 | 25 | } 26 | > 27 | 28 | {imageUrl ? ( 29 | {name} 30 | ) : ( 31 | 38 | )} 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/NftModal/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: rgba(0, 0, 0); 3 | color: white; 4 | width: 500px; 5 | height: fit-content; 6 | max-height: 100%; 7 | overflow-y: auto; 8 | padding: 30px; 9 | border: solid 3px; 10 | border-radius: 20px; 11 | position: absolute; 12 | bottom: 3rem; 13 | top: 50%; 14 | left: 50%; 15 | transform: translate(-50%, -50%); 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | 22 | .image { 23 | width: 400px; 24 | margin-bottom: 12px; 25 | } 26 | 27 | .text { 28 | margin: 4px; 29 | } 30 | 31 | .link { 32 | padding-top: 12px; 33 | font-weight: bold; 34 | font-size: large; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/NftModal/index.tsx: -------------------------------------------------------------------------------- 1 | import ImageNotSupportedIcon from "@mui/icons-material/ImageNotSupported"; 2 | import { Link, Modal, Typography } from "@mui/material"; 3 | import styles from "./index.module.css"; 4 | 5 | type Props = { 6 | name: string; 7 | description: string; 8 | imageUrl: string | null; 9 | openseaUrl: string; 10 | onClose: () => void; 11 | }; 12 | 13 | export const NftModal = ({ 14 | name, 15 | description, 16 | imageUrl, 17 | openseaUrl, 18 | onClose, 19 | }: Props) => { 20 | return ( 21 | 27 |
28 | {imageUrl ? ( 29 | {name} 30 | ) : ( 31 | 38 | )} 39 | 40 | {name} 41 | 42 | {description && ( 43 | 44 | {description} 45 | 46 | )} 47 | 53 | View on OpenSea 54 | 55 |
56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/PoapModal/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: rgba(0, 0, 0); 3 | color: white; 4 | width: 500px; 5 | height: fit-content; 6 | max-height: 100%; 7 | overflow-y: auto; 8 | padding: 30px; 9 | border: solid 3px; 10 | border-radius: 20px; 11 | position: absolute; 12 | bottom: 3rem; 13 | top: 50%; 14 | left: 50%; 15 | transform: translate(-50%, -50%); 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | 22 | .image { 23 | width: 400px; 24 | margin-bottom: 12px; 25 | } 26 | 27 | .text { 28 | margin: 4px; 29 | } 30 | 31 | .link { 32 | padding-top: 12px; 33 | font-weight: bold; 34 | font-size: large; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/PoapModal/index.tsx: -------------------------------------------------------------------------------- 1 | import ImageNotSupportedIcon from "@mui/icons-material/ImageNotSupported"; 2 | import { Modal, Typography } from "@mui/material"; 3 | import styles from "./index.module.css"; 4 | 5 | type Props = { 6 | name: string; 7 | description: string; 8 | imageUrl: string | null; 9 | onClose: () => void; 10 | }; 11 | 12 | export const PoapModal = ({ name, description, imageUrl, onClose }: Props) => { 13 | return ( 14 | 20 |
21 | {imageUrl ? ( 22 | {name} 23 | ) : ( 24 | 31 | )} 32 | 33 | {name} 34 | 35 | {description && ( 36 | 37 | {description} 38 | 39 | )} 40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.module.css: -------------------------------------------------------------------------------- 1 | /* src\components\SearchBar\index.module.css */ 2 | 3 | .textField { 4 | border: solid white 2px; 5 | border-radius: 50px; 6 | height: 60px; 7 | background-color: transparent; 8 | width: 500px; 9 | padding: 0 30px; 10 | color: white; 11 | font-size: large; 12 | font-family: "Outfit"; 13 | } 14 | 15 | .textField::placeholder { 16 | color: white; 17 | } 18 | 19 | .userAddress { 20 | font-family: "Outfit"; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | // src\components\SearchBar\index.tsx 2 | 3 | import { useGraph } from "@/context/GraphContext"; 4 | import { useWeb3 } from "@/context/web3Context"; 5 | import { isValidAddr } from "@/utils/helper"; 6 | import styles from "./index.module.css"; 7 | 8 | export const SearchBar: React.FC = () => { 9 | const { setGraphAddress, setSelectAddress } = useGraph(); 10 | const { getAddressByEns } = useWeb3(); 11 | 12 | const handleInputChange = async (e: { target: { value: string } }) => { 13 | const newValue = e.target.value; 14 | 15 | if (newValue.slice(-3) === "eth") { 16 | try { 17 | const ensAddr = await getAddressByEns(newValue); 18 | if (ensAddr) { 19 | setGraphAddress(ensAddr); 20 | setSelectAddress(ensAddr); 21 | } 22 | } catch (error) {} 23 | } else if (isValidAddr(newValue)) { 24 | setGraphAddress(newValue); 25 | setSelectAddress(newValue); 26 | } 27 | }; 28 | 29 | return ( 30 | 35 | ); 36 | }; 37 | 38 | SearchBar.displayName = "SearchBar"; 39 | -------------------------------------------------------------------------------- /src/components/UserPanel/NftSections.tsx: -------------------------------------------------------------------------------- 1 | import { useGraph } from "@/context/GraphContext"; 2 | import { CircularProgress, Typography } from "@mui/material"; 3 | import { useEffect, useState } from "react"; 4 | 5 | import { NftListItem } from "../NftListItem"; 6 | import { NftModal } from "../NftModal"; 7 | import { PoapModal } from "../PoapModal"; 8 | import styles from "./index.module.css"; 9 | 10 | export const NftSections: React.FC = () => { 11 | const { selectAddress } = useGraph(); 12 | 13 | const [poaps, setPoaps] = useState(); 14 | const [nfts, setNfts] = useState(); 15 | const [isLoading, setIsLoading] = useState(true); 16 | const [nftModalInfo, setNftModalInfo] = useState(null); 17 | const [poapModalInfo, setPoapModalInfo] = useState(null); 18 | 19 | useEffect(() => { 20 | (async () => { 21 | setIsLoading(true); 22 | const res = await fetch( 23 | `https://eth-mainnet.alchemyapi.io/v2/${process.env.NEXT_PUBLIC_ALCHEMY_ID}/getNFTs/?owner=${selectAddress}` 24 | ); 25 | let response; 26 | if (res.status === 200) { 27 | response = await res.json(); 28 | } 29 | setNfts(response.ownedNfts); 30 | 31 | setIsLoading(false); 32 | })(); 33 | }, [selectAddress]); 34 | 35 | useEffect(() => { 36 | (async () => { 37 | setIsLoading(true); 38 | const res = await fetch( 39 | `https://api.poap.xyz/actions/scan/${selectAddress}` 40 | ); 41 | let response; 42 | if (res.status === 200) { 43 | response = await res.json(); 44 | } 45 | setPoaps(response); 46 | setIsLoading(false); 47 | })(); 48 | }, [selectAddress]); 49 | 50 | const getName = (nft: any) => nft.metadata?.name || nft.name || null; 51 | 52 | const getImageUrl = (nft: any) => { 53 | let imgUrl = nft.metadata?.image_url || nft.metadata?.image || null; 54 | // convert ipfs urls to use gateway 55 | if (imgUrl?.startsWith("ipfs://ipfs/")) { 56 | imgUrl = imgUrl.replace("ipfs://", "https://ipfs.io/"); 57 | } else if (imgUrl?.startsWith("ipfs://")) { 58 | imgUrl = imgUrl.replace("ipfs://", "https://ipfs.io/ipfs/"); 59 | } 60 | return imgUrl; 61 | }; 62 | 63 | const getOpenseaUrl = (nft: any) => 64 | `https://opensea.io/assets/${nft.contract.address}/${ 65 | nft.metadata?.tokenId || 66 | nft.metadata?.token_id || 67 | nft.metadata?.id || 68 | nft.metadata?.edition || 69 | nft.metadata?.name?.substring(nft.metadata?.name?.indexOf("#") + 1) 70 | }`; 71 | 72 | const getDescription = (nft: any) => nft.metadata?.description || null; 73 | 74 | const handleClickNft = (nft: any) => { 75 | setNftModalInfo({ 76 | name: getName(nft), 77 | imageUrl: getImageUrl(nft), 78 | openseaUrl: getOpenseaUrl(nft), 79 | tokenAddress: nft.contract.address, 80 | description: getDescription(nft), 81 | }); 82 | }; 83 | 84 | const handleClickPoap = (poap: any) => { 85 | setPoapModalInfo({ 86 | name: poap.event.name, 87 | imageUrl: poap.event.image_url, 88 | description: poap.event.description, 89 | }); 90 | }; 91 | 92 | return ( 93 | <> 94 |
95 | 96 | POAPs 97 | 98 |
99 | {isLoading ? ( 100 | 101 | ) : poaps?.length ? ( 102 | poaps.map((poap: any) => ( 103 | handleClickPoap(poap)} 108 | /> 109 | )) 110 | ) : ( 111 | 112 | No POAPS :( 113 | 114 | )} 115 |
116 |
117 |
118 | 119 | NFTs Gallery 120 | 121 |
122 | {isLoading ? ( 123 | 124 | ) : nfts?.length ? ( 125 | nfts.map((nft: any) => ( 126 | handleClickNft(nft)} 131 | /> 132 | )) 133 | ) : ( 134 | 135 | No NFTs :( 136 | 137 | )} 138 |
139 |
140 | {nftModalInfo && ( 141 | setNftModalInfo(null)} 143 | name={nftModalInfo.name} 144 | description={nftModalInfo.description} 145 | imageUrl={nftModalInfo.imageUrl} 146 | openseaUrl={nftModalInfo.openseaUrl} 147 | /> 148 | )} 149 | {poapModalInfo && ( 150 | setPoapModalInfo(null)} 152 | name={poapModalInfo.name} 153 | description={poapModalInfo.description} 154 | imageUrl={poapModalInfo.imageUrl} 155 | /> 156 | )} 157 | 158 | ); 159 | }; 160 | -------------------------------------------------------------------------------- /src/components/UserPanel/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: rgba(0, 0, 0, 0.699); 3 | color: white; 4 | width: 700px; 5 | max-height: 40rem; 6 | margin-left: 2rem; 7 | position: fixed; 8 | z-index: 99; 9 | border: solid 3px; 10 | border-radius: 11px; 11 | position: absolute; 12 | bottom: 3rem; 13 | overflow-y: scroll; 14 | } 15 | 16 | .container::-webkit-scrollbar-track { 17 | box-shadow: "inset 0 0 6px #000"; 18 | -webkit-box-shadow: "inset 0 0 6px #000"; 19 | background: rgb(87, 87, 87); 20 | margin-right: 10px; 21 | } 22 | 23 | .container::-webkit-scrollbar { 24 | width: 6px; 25 | } 26 | 27 | .container::-webkit-scrollbar-thumb { 28 | width: 22px; 29 | height: 22px; 30 | background-color: rgb(208, 208, 208); 31 | border-radius: 50px; 32 | } 33 | 34 | .userInfoSection { 35 | display: flex; 36 | padding: 10px; 37 | margin: 10px 20px; 38 | border-bottom: #535353 solid 1px; 39 | } 40 | 41 | .userName { 42 | display: flex; 43 | flex-direction: column; 44 | margin-left: 15px; 45 | justify-content: center; 46 | } 47 | 48 | .avatarSection { 49 | display: flex; 50 | flex-direction: column; 51 | align-items: center; 52 | height: 120px; 53 | width: 100px; 54 | padding-top: 15px; 55 | } 56 | 57 | .avatar { 58 | border-radius: 50px; 59 | margin-top: 5px; 60 | background-color: white; 61 | } 62 | 63 | .followSection { 64 | display: flex; 65 | border-bottom: #272727 solid 1px; 66 | padding: 10px; 67 | margin: 10px 20px; 68 | justify-content: center; 69 | } 70 | 71 | .follow { 72 | padding: 5px 50px; 73 | text-align: center; 74 | } 75 | 76 | .followList { 77 | color: #cecece; 78 | background-color: transparent; 79 | text-decoration: underline; 80 | } 81 | 82 | .balanceSection { 83 | margin: 20px 20px; 84 | border-bottom: #272727 solid 1px; 85 | } 86 | 87 | .twitter { 88 | display: flex; 89 | align-items: center; 90 | } 91 | 92 | .twitterButton { 93 | background-color: #1da1f2; 94 | color: white; 95 | width: 280px; 96 | height: 60px; 97 | margin: 10px 5px; 98 | border-radius: 55px; 99 | } 100 | 101 | .twitterButton:hover { 102 | background-color: #116ca5; 103 | } 104 | 105 | .social { 106 | display: flex; 107 | justify-content: center; 108 | align-items: center; 109 | padding: 20px 10px; 110 | } 111 | 112 | .socialIcon { 113 | margin: 2px; 114 | border: solid 2px white; 115 | border-radius: 100%; 116 | width: 50px; 117 | } 118 | 119 | .exploreButton { 120 | border: solid white 3px; 121 | color: white; 122 | width: 600px; 123 | height: 70px; 124 | font-size: x-large; 125 | border-radius: 20px; 126 | margin-left: 50px; 127 | margin-bottom: 28px; 128 | } 129 | 130 | .nftSection { 131 | margin: 20px 20px; 132 | border-bottom: #272727 solid 1px; 133 | } 134 | 135 | .noNftsInSection { 136 | text-align: center; 137 | width: 100%; 138 | padding-bottom: 8px; 139 | } 140 | 141 | .nftList { 142 | display: flex; 143 | flex-wrap: wrap; 144 | margin: 12px 10px; 145 | padding: 6px; 146 | overflow-y: auto; 147 | height: 84px; 148 | } 149 | .nftList::-webkit-scrollbar { 150 | width: 6px; 151 | } 152 | 153 | .nftList::-webkit-scrollbar-thumb { 154 | width: 12px; 155 | height: 2px; 156 | background-color: rgb(208, 208, 208); 157 | } 158 | 159 | .nftList::-webkit-scrollbar-track { 160 | background: rgb(87, 87, 87); 161 | } 162 | -------------------------------------------------------------------------------- /src/components/UserPanel/index.tsx: -------------------------------------------------------------------------------- 1 | // src\components\UserPanel\index.tsx 2 | 3 | import { useGraph } from "@/context/GraphContext"; 4 | import { useWeb3 } from "@/context/web3Context"; 5 | import { 6 | Blockchain, 7 | Env, 8 | FollowButton, 9 | } from "@cyberconnect/react-follow-button"; 10 | import { LoadingButton } from "@mui/lab"; 11 | import { Button, Typography } from "@mui/material"; 12 | import { useEffect, useState } from "react"; 13 | import { useNFTBalances } from "react-moralis"; 14 | import { ListModal } from "../ListModal"; 15 | import styles from "./index.module.css"; 16 | import { NftSections } from "./NftSections"; 17 | 18 | export const UserPanel: React.FC = () => { 19 | const { selectAddress, identity, setGraphAddress } = useGraph(); 20 | const { address } = useWeb3(); 21 | 22 | const [showList, setShowList] = useState(false); 23 | 24 | const [listType, setListType] = useState(false); 25 | 26 | const { getNFTBalances, isLoading } = useNFTBalances(); 27 | 28 | //fetch the user ether balance from ehterscan API 29 | 30 | useEffect(() => { 31 | getNFTBalances({ params: { address: selectAddress, chain: "eth" } }); 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, [selectAddress]); 34 | 35 | if (!identity) return null; //only shows UserPanel if all data has loaded 36 | if (isLoading) return null; 37 | 38 | return ( 39 | <> 40 |
41 | {/* userInfoSection */} 42 |
43 | {/* User Avatar from ENS */} 44 |
45 | {identity.avatar ? ( 46 | 54 | {""} 61 | 62 | ) : ( 63 | 71 | {""} 78 | 79 | )} 80 | {address && address !== selectAddress && ( 81 | 89 | )} 90 |
91 | {/* User Name from ENS or therir address*/} 92 |
93 | {identity.ens ? ( 94 | 101 | {identity.ens} 102 | 103 | ) : ( 104 | 105 | )} 106 | 111 | {identity?.address} 112 | 113 |
114 |
115 | {/* Following & Followers Section */} 116 |
117 |
118 | [ 127 | setListType(false), //sets list modal to show followers 128 | setShowList(true), 129 | ]} 130 | > 131 | {identity.followerCount} 132 | 133 | Followers 134 |
135 |
136 | [ 145 | setListType(true), //sets list modal to show following 146 | setShowList(true), 147 | ]} 148 | > 149 | {identity.followingCount} 150 | 151 | Following 152 |
153 |
154 | 155 | {/* POAPs and NFTs */} 156 | 157 | 158 | {/*Follower/followings list*/} 159 | 165 | 166 | {/* Social Section */} 167 |
168 | 169 | External Links 170 | 171 |
172 | {identity.social.twitter && ( 173 | 189 | )} 190 | 191 | 196 | {""} 201 | 202 | 203 | 210 | {""} 215 | 216 | 221 | {""} 226 | 227 | 232 | {""} 237 | 238 | 246 | {""} 251 | 252 |
253 |
254 | {/* Explore me button */} 255 | 256 | setGraphAddress(selectAddress)} 260 | sx={{ 261 | ":hover": { 262 | bgcolor: "#555", 263 | }, 264 | fontFamily: "Outfit", 265 | }} 266 | > 267 | EXPLORE THIS ADDRESS! 268 | 269 |
270 | 271 | ); 272 | }; 273 | 274 | UserPanel.displayName = "UserPanel"; 275 | -------------------------------------------------------------------------------- /src/components/WalletConnectButton/index.module.css: -------------------------------------------------------------------------------- 1 | /* src\components\WalletConnectButton\index.module.css */ 2 | .walletInfo { 3 | font-weight: bold; 4 | padding: 1rem; 5 | color: white; 6 | text-align: center; 7 | display: flex; 8 | align-items: center; 9 | } 10 | 11 | .connectWalletButton { 12 | background-color: #fff; 13 | border-radius: 20px; 14 | box-sizing: border-box; 15 | color: #000; 16 | font-size: 16px; 17 | font-weight: 600; 18 | padding: 16px 36px; 19 | text-align: center; 20 | transition: all 300ms cubic-bezier(0.23, 1, 0.32, 1); 21 | width: 250px; 22 | box-sizing: border-box; 23 | border: 2px solid white; 24 | font-family: "Outfit"; 25 | } 26 | 27 | .connectWalletButton:hover { 28 | transform: translateY(-2px); 29 | color: #ffffff; 30 | background-color: rgb(0, 0, 0); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/WalletConnectButton/index.tsx: -------------------------------------------------------------------------------- 1 | //src\components\WalletConnectButton\index.tsx 2 | 3 | import { useWeb3 } from "@/context/web3Context"; 4 | import { formatAddress } from "@/utils/helper"; 5 | import LoadingButton from "@mui/lab/LoadingButton"; 6 | import { useCallback, useState } from "react"; 7 | import styles from "./index.module.css"; 8 | 9 | export const WalletConnectButton: React.FC = () => { 10 | //get user logged in wallet address/ens, get connect wallet function 11 | const { connectWallet, address, ens } = useWeb3(); 12 | 13 | const [loading, setLoading] = useState(false); 14 | 15 | const connect = useCallback(async () => { 16 | setLoading(true); 17 | await connectWallet(); 18 | setLoading(false); 19 | }, [connectWallet]); 20 | 21 | //if user didn't successfully logged in, we shows the wallet connect button 22 | //if user logged in, we show the logged in user's ens or edted address 23 | return ( 24 | <> 25 | {!address ? ( 26 | 36 | Connect Wallet 37 | 38 | ) : ( 39 |
40 | {formatAddress(address)} 41 |

42 | {ens || null} 43 |
44 | )} 45 | 46 | ); 47 | }; 48 | 49 | WalletConnectButton.displayName = "WalletConnectButton"; 50 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | // src\config\config.ts 2 | 3 | //where we fetch the cyberconnect connections data 4 | export const CYBERCONNECT_ENDPOINT: string = 5 | "https://api.cybertino.io/connect/"; 6 | 7 | //which we used for our default graph address 8 | export const DEFAULT_ADDRESS: string = 9 | "0x148d59faf10b52063071eddf4aaf63a395f2d41c"; 10 | -------------------------------------------------------------------------------- /src/context/GraphContext.tsx: -------------------------------------------------------------------------------- 1 | // src\context\GraphContext.tsx 2 | 3 | import { GET_ADDR_CONNECTION_QUERY } from "@/graphql/queries/get_connections"; 4 | import { GET_IDENTITY } from "@/graphql/queries/get_identity"; 5 | import { GET_RECOMMENDATION } from "@/graphql/queries/get_recommendation"; 6 | import { 7 | AllRecommendations, 8 | AllSocialConnections, 9 | SocialConnection, 10 | } from "@/types/AllSocialConnections"; 11 | import { useQuery } from "@apollo/client"; 12 | import { 13 | createContext, 14 | useCallback, 15 | useContext, 16 | useEffect, 17 | useState, 18 | } from "react"; 19 | import { Identity } from "../types/identity"; 20 | import { useWeb3 } from "./web3Context"; 21 | 22 | export type GraphNode = { 23 | id: string; 24 | img: string; 25 | group: number; 26 | neighbors: string[]; 27 | links: GraphLink[]; 28 | }; 29 | 30 | export type GraphLink = { 31 | source: string; 32 | target: string; 33 | value: number; 34 | }; 35 | 36 | export type GraphData = { 37 | nodes: GraphNode[]; 38 | links: GraphLink[]; 39 | }; 40 | 41 | export enum AppMode { 42 | CyberMode = 1, 43 | FocusMode, 44 | } 45 | 46 | export const DEFAULT_QUOTA = 10; 47 | 48 | interface GraphContextInterface { 49 | graphAddress: string; //the address which generate the 3d graph based on it 50 | selectAddress: string; //the address which to be shown in the User Panel 51 | graphData: GraphData | undefined; //the data which to create the graph based on graphAddress 52 | graphLoading: boolean; //graph loading status 53 | identity: Identity | null; //user indentity info including the ens, avatar, twitter etc. 54 | appMode: AppMode; //for changing the app mode between cyber mode or focus mode 55 | count: number; // 56 | 57 | setGraphAddress: (address: string) => void; 58 | setSelectAddress: (address: string) => void; 59 | setGraphData: (data: GraphData | undefined) => void; 60 | setGraphLoading: (loading: boolean) => void; 61 | setAppMode: (mode: AppMode) => void; 62 | } 63 | 64 | export const GraphContext = createContext({ 65 | graphAddress: "", 66 | selectAddress: "", 67 | graphData: undefined, 68 | graphLoading: true, 69 | identity: null, 70 | appMode: AppMode.CyberMode, 71 | count: 0, 72 | 73 | setGraphAddress: async () => undefined, 74 | setSelectAddress: async () => undefined, 75 | setGraphData: async () => undefined, 76 | setGraphLoading: async () => undefined, 77 | setAppMode: async () => undefined, 78 | }); 79 | let count = 0; 80 | 81 | export const GraphContextProvider: React.FC = ({ children }) => { 82 | const { address } = useWeb3(); 83 | 84 | // Cyberlab.eth default address 85 | const [graphAddress, setGraphAddress] = useState( 86 | "0x148d59faf10b52063071eddf4aaf63a395f2d41c" 87 | ); 88 | const [selectAddress, setSelectAddress] = useState( 89 | "0x148d59faf10b52063071eddf4aaf63a395f2d41c" 90 | ); 91 | const [graphData, setGraphData] = useState( 92 | undefined 93 | ); 94 | const [graphLoading, setGraphLoading] = useState(true); 95 | const [identity, setIdentity] = useState(null); 96 | const [appMode, setAppMode] = useState(AppMode.CyberMode); 97 | 98 | //Fetch IdentityData: followers following num 99 | const identityData = useQuery(GET_IDENTITY, { 100 | variables: { 101 | address: selectAddress, 102 | }, 103 | }).data; 104 | 105 | useEffect(() => { 106 | if (identityData) { 107 | setIdentity(identityData.identity); 108 | } 109 | }, [identityData]); 110 | 111 | const { fetchMore } = useQuery(GET_ADDR_CONNECTION_QUERY, { 112 | variables: { 113 | address: graphAddress, 114 | first: 50, 115 | after: "-1", 116 | }, 117 | }); 118 | 119 | const { fetchMore: fetchMoreRecommendation } = useQuery( 120 | GET_RECOMMENDATION, 121 | { 122 | variables: { 123 | address: graphAddress, 124 | }, 125 | } 126 | ); 127 | 128 | // Fetch Recommendations 129 | const fetchRecommendations = useCallback( 130 | async (targetAddr: string) => { 131 | const { data: recommendationData } = await fetchMoreRecommendation({ 132 | variables: { address: targetAddr }, 133 | updateQuery: (prev: any, { fetchMoreResult }) => { 134 | return fetchMoreResult; 135 | }, 136 | }); 137 | const recommendationList = ( 138 | recommendationData as AllRecommendations 139 | ).recommendations.data?.list; 140 | if (!recommendationList) { 141 | return { nodes: [], links: [] }; 142 | } 143 | // Return recommendation list as GraphNode[] 144 | const retGraphData: GraphData = { nodes: [], links: [] }; 145 | retGraphData.nodes = [ 146 | ...[...recommendationList].map((item: SocialConnection) => { 147 | return { 148 | id: item.address, 149 | img: item.avatar, 150 | group: 4, 151 | neighbors: [], 152 | links: [], 153 | }; 154 | }), 155 | ]; 156 | 157 | function getRandomInt(max: number) { 158 | return Math.floor(Math.random() * max); 159 | } 160 | 161 | for (let i = 0; i < retGraphData.nodes.length / 2; i++) { 162 | retGraphData.links.push({ 163 | source: retGraphData.nodes[ 164 | getRandomInt(retGraphData.nodes.length) 165 | ].id, 166 | target: retGraphData.nodes[ 167 | getRandomInt(retGraphData.nodes.length) 168 | ].id, 169 | value: 0, 170 | }); 171 | } 172 | 173 | return retGraphData; 174 | }, 175 | [fetchMoreRecommendation] 176 | ); 177 | 178 | // Fetch friends, followings, followers 179 | const fetch3Fs = useCallback( 180 | async (targetAddr: string, isFocusMode: boolean) => { 181 | const hasNextPage = true, 182 | after = "-1"; 183 | let followerList: SocialConnection[], 184 | followingList: SocialConnection[], 185 | friendList: SocialConnection[]; 186 | followerList = []; 187 | followingList = []; 188 | friendList = []; 189 | 190 | let allData; 191 | // TODO: Paginated fetching 192 | // Currently only load one batch 193 | while (hasNextPage) { 194 | const { data } = await fetchMore({ 195 | variables: { 196 | address: targetAddr, 197 | first: 50, 198 | after, 199 | }, 200 | updateQuery: (prev: any, { fetchMoreResult }) => { 201 | return fetchMoreResult; 202 | }, 203 | }); 204 | 205 | // Process Followers 206 | followerList = (data as AllSocialConnections).identity.followers 207 | .list; 208 | followingList = (data as AllSocialConnections).identity 209 | .followings.list; 210 | friendList = (data as AllSocialConnections).identity.friends 211 | .list; 212 | allData = data; 213 | break; 214 | } 215 | 216 | // Three lists filter out redundant elements 217 | const friendAddrList = friendList.map((item) => item.address); 218 | followingList = followingList.filter( 219 | (item) => !friendAddrList.includes(item.address) 220 | ); 221 | followerList = followerList.filter( 222 | (item) => !friendAddrList.includes(item.address) 223 | ); 224 | 225 | // Construct Returning GraphData 226 | let retGraphData: GraphData; 227 | if (isFocusMode) { 228 | retGraphData = { 229 | nodes: [ 230 | { 231 | id: targetAddr, 232 | img: (allData as AllSocialConnections).identity 233 | .avatar, 234 | group: 0, 235 | neighbors: [], 236 | links: [], 237 | }, 238 | { 239 | id: "Followings", 240 | img: "", 241 | group: 1, 242 | neighbors: [], 243 | links: [], 244 | }, 245 | { 246 | id: "Followers", 247 | img: "https://www.google.com/url?sa=i&url=https%3A%2F%2Fhotpot.ai%2Fcolorize-picture&psig=AOvVaw0LGfAtY4jm1qGvtp93Wh59&ust=1646331326777000&source=images&cd=vfe&ved=0CAsQjRxqFwoTCMivj5-EqPYCFQAAAAAdAAAAABAF", 248 | group: 2, 249 | neighbors: [], 250 | links: [], 251 | }, 252 | { 253 | id: "Friends", 254 | img: "", 255 | group: 3, 256 | neighbors: [], 257 | links: [], 258 | }, 259 | ...[...followingList].map((item: SocialConnection) => { 260 | return { 261 | id: item.address, 262 | img: item.avatar, 263 | group: 1, 264 | neighbors: [], 265 | links: [], 266 | }; 267 | }), 268 | ...[...followerList].map((item: SocialConnection) => { 269 | return { 270 | id: item.address, 271 | img: item.avatar, 272 | group: 2, 273 | neighbors: [], 274 | links: [], 275 | }; 276 | }), 277 | ...[...friendList].map((item: SocialConnection) => { 278 | return { 279 | id: item.address, 280 | img: item.avatar, 281 | group: 3, 282 | neighbors: [], 283 | links: [], 284 | }; 285 | }), 286 | ], 287 | links: [ 288 | ...[...followingList].map((item: SocialConnection) => { 289 | return { 290 | source: item.address, 291 | target: "Followings", 292 | value: 1, 293 | }; 294 | }), 295 | ...[...followerList].map((item: SocialConnection) => { 296 | return { 297 | source: item.address, 298 | target: "Followers", 299 | value: 2, 300 | }; 301 | }), 302 | ...[...friendList].map((item: SocialConnection) => { 303 | return { 304 | source: item.address, 305 | target: "Friends", 306 | value: 3, 307 | }; 308 | }), 309 | { 310 | source: targetAddr, 311 | target: "Friends", 312 | value: 0, 313 | }, 314 | { 315 | source: targetAddr, 316 | target: "Followings", 317 | value: 0, 318 | }, 319 | { 320 | source: targetAddr, 321 | target: "Followers", 322 | value: 0, 323 | }, 324 | ], 325 | }; 326 | } else { 327 | retGraphData = { 328 | nodes: [ 329 | { 330 | id: targetAddr, 331 | img: (allData as AllSocialConnections).identity 332 | .avatar, 333 | group: 0, 334 | neighbors: [], 335 | links: [], 336 | }, 337 | ...[...followingList].map((item: SocialConnection) => { 338 | return { 339 | id: item.address, 340 | img: item.avatar, 341 | group: 1, 342 | neighbors: [], 343 | links: [], 344 | }; 345 | }), 346 | ...[...followerList].map((item: SocialConnection) => { 347 | return { 348 | id: item.address, 349 | img: item.avatar, 350 | group: 2, 351 | neighbors: [], 352 | links: [], 353 | }; 354 | }), 355 | ...[...friendList].map((item: SocialConnection) => { 356 | return { 357 | id: item.address, 358 | img: item.avatar, 359 | group: 3, 360 | neighbors: [], 361 | links: [], 362 | }; 363 | }), 364 | ], 365 | links: [ 366 | ...[...followingList].map((item: SocialConnection) => { 367 | return { 368 | source: item.address, 369 | target: targetAddr, 370 | value: 1, 371 | neighbors: [], 372 | links: [], 373 | }; 374 | }), 375 | ...[...followerList].map((item: SocialConnection) => { 376 | return { 377 | source: item.address, 378 | target: targetAddr, 379 | value: 2, 380 | neighbors: [], 381 | links: [], 382 | }; 383 | }), 384 | ...[...friendList].map((item: SocialConnection) => { 385 | return { 386 | source: item.address, 387 | target: targetAddr, 388 | value: 3, 389 | neighbors: [], 390 | links: [], 391 | }; 392 | }), 393 | ], 394 | }; 395 | } 396 | return retGraphData; 397 | }, 398 | [fetchMore] 399 | ); 400 | 401 | const loadGraphConnections = useCallback(async () => { 402 | // queue is to do BFS, set is a hashMap record appeared addresses 403 | const bfsQueue = []; 404 | const set = new Set(); 405 | let retGraphData: GraphData = { 406 | nodes: [], 407 | links: [], 408 | }; 409 | 410 | bfsQueue.push(graphAddress); 411 | set.add(graphAddress); 412 | count = 0; 413 | 414 | while (count < DEFAULT_QUOTA) { 415 | const headAddr = bfsQueue.shift(); 416 | if (headAddr == undefined) { 417 | continue; 418 | } 419 | const recommendGD = await fetchRecommendations(headAddr); 420 | const threeFsGD = await fetch3Fs(headAddr, false); 421 | retGraphData = { 422 | nodes: [ 423 | ...retGraphData.nodes, 424 | ...recommendGD.nodes, 425 | ...threeFsGD.nodes, 426 | ], 427 | links: [ 428 | ...retGraphData.links, 429 | ...recommendGD.links, 430 | ...threeFsGD.links, 431 | ], 432 | }; 433 | 434 | // Expand BFS search 435 | for (const toNode of retGraphData.nodes) { 436 | if (!set.has(toNode.id)) { 437 | set.add(toNode.id); 438 | bfsQueue.push(toNode.id); 439 | } 440 | } 441 | count++; 442 | } 443 | 444 | return retGraphData; 445 | }, [fetch3Fs, fetchRecommendations, graphAddress]); 446 | 447 | const preprocessGraphNeighbors = (graphData: GraphData) => { 448 | graphData.links.forEach((link) => { 449 | const aNodes = graphData.nodes.filter( 450 | (node) => node.id === link.source 451 | ); 452 | const bNodes = graphData.nodes.filter( 453 | (node) => node.id === link.target 454 | ); 455 | for (const bNode of bNodes) { 456 | for (const aNode of aNodes) { 457 | aNode.neighbors.push(bNode.id); 458 | bNode.neighbors.push(aNode.id); 459 | 460 | aNode.links.push(link); 461 | bNode.links.push(link); 462 | } 463 | } 464 | }); 465 | 466 | return graphData; 467 | }; 468 | 469 | // For Cyber Mode 470 | const loadCyberModeConnections = useCallback(async () => { 471 | await setGraphLoading(true); 472 | await setGraphData({ nodes: [], links: [] }); 473 | const newGraphData = await loadGraphConnections(); 474 | const processedGraphData = preprocessGraphNeighbors(newGraphData); 475 | await setGraphData(processedGraphData); 476 | await setGraphLoading(false); 477 | }, [loadGraphConnections]); 478 | 479 | // For Focus Mode 480 | const loadFocusModeConnections = useCallback(async () => { 481 | await setGraphLoading(true); 482 | await setGraphData({ nodes: [], links: [] }); 483 | const recommendGD = await fetchRecommendations(graphAddress); 484 | const threeFsGD = await fetch3Fs(graphAddress, true); 485 | const newGraphData = { 486 | nodes: [...recommendGD.nodes, ...threeFsGD.nodes], 487 | links: [...recommendGD.links, ...threeFsGD.links], 488 | }; 489 | const processedGraphData = preprocessGraphNeighbors(newGraphData); 490 | await setGraphData(processedGraphData); 491 | await setGraphLoading(false); 492 | }, [graphAddress, fetch3Fs, fetchRecommendations]); 493 | 494 | // Using when mode or graphAddress changed 495 | useEffect(() => { 496 | if (appMode == AppMode.CyberMode) { 497 | loadCyberModeConnections(); 498 | } else { 499 | loadFocusModeConnections(); 500 | } 501 | }, [ 502 | graphAddress, 503 | appMode, 504 | loadCyberModeConnections, 505 | loadFocusModeConnections, 506 | ]); 507 | 508 | useEffect(() => { 509 | if (address) { 510 | setSelectAddress(address); 511 | setGraphAddress(address); 512 | } 513 | }, [address]); 514 | 515 | return ( 516 | 534 | {children} 535 | 536 | ); 537 | }; 538 | 539 | export const useGraph = () => { 540 | const graph = useContext(GraphContext); 541 | return graph; 542 | }; 543 | -------------------------------------------------------------------------------- /src/context/web3Context.tsx: -------------------------------------------------------------------------------- 1 | import CyberConnect from "@cyberlab/cyberconnect"; 2 | import { ethers } from "ethers"; 3 | import React, { useCallback, useContext, useState } from "react"; 4 | import Web3Modal from "web3modal"; 5 | 6 | interface Web3ContextInterface { 7 | connectWallet: () => Promise; 8 | address: string; 9 | ens: string | null; 10 | cyberConnect: CyberConnect | null; 11 | getAddressByEns: (ens: string) => Promise; 12 | } 13 | 14 | // create Context to pass data to different components 15 | export const Web3Context = React.createContext({ 16 | connectWallet: async () => undefined, //connect wallet function 17 | address: "", // user's signed in address 18 | ens: "", //user's signed in ens 19 | cyberConnect: null, //to interact with cyberconnect, e.g. follow/unfollow a address, optional here since we didn't add follow/unfollow button 20 | getAddressByEns: async () => null, //get Address from ens function 21 | }); 22 | 23 | export const Web3ContextProvider: React.FC = ({ children }) => { 24 | const [address, setAddress] = useState(""); 25 | const [ens, setEns] = useState(""); 26 | const [cyberConnect, setCyberConnect] = useState(null); 27 | 28 | const initCyberConnect = useCallback((provider: any) => { 29 | const cyberConnect = new CyberConnect({ 30 | provider, 31 | namespace: "CyberGraph", 32 | }); 33 | 34 | setCyberConnect(cyberConnect); 35 | }, []); 36 | 37 | // connectWallet fuction to use Web3Modal configuration for enabling wallet access 38 | const connectWallet = useCallback(async () => { 39 | // init Web3Modal 40 | const web3Modal = new Web3Modal({ 41 | network: "mainnet", 42 | cacheProvider: true, 43 | providerOptions: {}, 44 | }); 45 | 46 | const instance = await web3Modal.connect(); 47 | const provider = new ethers.providers.Web3Provider(instance); 48 | const signer = provider.getSigner(); 49 | // get the address which user used to sign in 50 | const address = await signer.getAddress(); 51 | // get the ens which user address associated with 52 | const ens = await getEnsByAddress(provider, address); 53 | 54 | setAddress(address); 55 | setEns(ens); 56 | initCyberConnect(provider.provider); 57 | }, [initCyberConnect]); 58 | 59 | // the function to get users' address from their ens 60 | async function getAddressByEns(ens: string) { 61 | const address = await ethers.providers 62 | .getDefaultProvider() 63 | .resolveName(ens); 64 | return address; 65 | } 66 | 67 | // the function to get users' ens from their address 68 | async function getEnsByAddress( 69 | provider: ethers.providers.Web3Provider, 70 | address: string 71 | ) { 72 | const ens = await provider.lookupAddress(address); 73 | return ens; 74 | } 75 | 76 | return ( 77 | 86 | {children} 87 | 88 | ); 89 | }; 90 | 91 | export const useWeb3 = () => { 92 | const web3 = useContext(Web3Context); 93 | return web3; 94 | }; 95 | -------------------------------------------------------------------------------- /src/graphql/client.tsx: -------------------------------------------------------------------------------- 1 | // src\graphql\client.tsx 2 | 3 | import { ApolloClient, InMemoryCache } from "@apollo/client"; 4 | import { CYBERCONNECT_ENDPOINT } from "../config/config"; 5 | 6 | const client = new ApolloClient({ 7 | uri: CYBERCONNECT_ENDPOINT, 8 | cache: new InMemoryCache(), 9 | }); 10 | 11 | export default client; 12 | -------------------------------------------------------------------------------- /src/graphql/queries/get_connections.tsx: -------------------------------------------------------------------------------- 1 | // src\graphql\queries\get_connections.tsx 2 | 3 | import { gql } from "@apollo/client"; 4 | 5 | export const GET_ADDR_CONNECTION_QUERY = gql` 6 | query identity( 7 | $address: String! 8 | $first: Int 9 | $after: String 10 | $namespace: String 11 | ) { 12 | identity(address: $address, network: ETH) { 13 | avatar 14 | followings( 15 | namespace: $namespace 16 | type: FOLLOW 17 | first: $first 18 | after: $after 19 | ) { 20 | pageInfo { 21 | hasNextPage 22 | } 23 | list { 24 | address 25 | avatar 26 | ens 27 | } 28 | } 29 | followers( 30 | namespace: $namespace 31 | type: FOLLOW 32 | first: $first 33 | after: $after 34 | ) { 35 | pageInfo { 36 | hasNextPage 37 | } 38 | list { 39 | address 40 | avatar 41 | ens 42 | } 43 | } 44 | friends( 45 | namespace: $namespace 46 | type: FOLLOW 47 | first: $first 48 | after: $after 49 | ) { 50 | pageInfo { 51 | hasNextPage 52 | } 53 | list { 54 | address 55 | avatar 56 | ens 57 | } 58 | } 59 | } 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /src/graphql/queries/get_identity.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const GET_IDENTITY = gql` 4 | query GetIdentity($address: String!) { 5 | identity(address: $address) { 6 | address 7 | domain 8 | ens 9 | social { 10 | twitter 11 | } 12 | avatar 13 | joinTime 14 | followerCount 15 | followingCount 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /src/graphql/queries/get_recommendation.tsx: -------------------------------------------------------------------------------- 1 | // src\graphql\queries\get_recommendation.tsx 2 | 3 | import { gql } from "@apollo/client"; 4 | 5 | export const GET_RECOMMENDATION = gql` 6 | query QueryRec($address: String!) { 7 | recommendations(address: $address) { 8 | result 9 | data { 10 | pageInfo { 11 | startCursor 12 | endCursor 13 | hasNextPage 14 | hasPreviousPage 15 | } 16 | list { 17 | address 18 | domain 19 | ens 20 | avatar 21 | recommendationReason 22 | followerCount 23 | } 24 | } 25 | } 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | // src\pages\_app.tsx 2 | 3 | import { GraphContextProvider } from "@/context/GraphContext"; 4 | import { Web3ContextProvider } from "@/context/web3Context"; 5 | import client from "@/graphql/client"; 6 | import { ApolloProvider } from "@apollo/client"; 7 | import { StyledEngineProvider } from "@mui/material"; 8 | import type { AppProps } from "next/app"; 9 | import Script from "next/script"; 10 | import { MoralisProvider } from "react-moralis"; 11 | import "../../styles/globals.css"; 12 | 13 | function MyApp({ Component, pageProps }: AppProps) { 14 | return ( 15 |
16 | 31 | 32 | 33 | 34 | 35 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | 52 | export default MyApp; 53 | -------------------------------------------------------------------------------- /src/pages/api/get_balance_api.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | name: string; 6 | }; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: "John Doe" }); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | // import { useRouter } from "next/router"; 2 | import FocusGraph from "@/components/Graph/FocusGraphWrapper"; 3 | import { NavBar } from "@/components/NavBar"; 4 | import { UserPanel } from "@/components/UserPanel"; 5 | import { DEFAULT_QUOTA, useGraph } from "@/context/GraphContext"; 6 | import { useWeb3 } from "@/context/web3Context"; 7 | import { LoadingButton } from "@mui/lab"; 8 | import type { NextPage } from "next"; 9 | import Head from "next/head"; 10 | import { useEffect, useState } from "react"; 11 | import ReactLoading from "react-loading"; 12 | import styles from "../../styles/Home.module.css"; 13 | import client from "../graphql/client"; 14 | 15 | const Home: NextPage = () => { 16 | const [exploreMode, setExploreMode] = useState(false); 17 | const { address } = useWeb3(); 18 | const { graphLoading, count } = useGraph(); 19 | 20 | useEffect(() => { 21 | if (address) { 22 | setExploreMode(true); 23 | } 24 | }, [address]); 25 | 26 | useEffect(() => { 27 | client; 28 | }, []); 29 | 30 | // const router = useRouter(); 31 | return ( 32 |
33 | 34 | CyberGraph 35 | 39 | 40 | 41 | 42 |
43 | {count != 10 && ( 44 | 49 | )} 50 |
57 | {" "} 58 | Loading {(100 * count) / DEFAULT_QUOTA} % 59 |
60 |
61 | {!exploreMode ? ( 62 |
63 |

CYBERCONNECTED IN

64 |

THE METAVERSE

65 | 66 | setExploreMode(true)} 70 | > 71 | Let's jump in! 72 | 73 |
74 | ) : ( 75 | 76 | )} 77 | {!graphLoading && ( 78 | <> 79 | 80 | 81 | )} 82 |
83 | ); 84 | }; 85 | 86 | export default Home; 87 | -------------------------------------------------------------------------------- /src/types/AllSocialConnections.ts: -------------------------------------------------------------------------------- 1 | // src\types\AllSocialConnections.ts 2 | 3 | export type SocialConnection = { 4 | address: string; 5 | alias: string; 6 | avatar: string; 7 | domain: string; 8 | ens: string; 9 | }; 10 | 11 | export type SocialConnectionsPaginated = { 12 | pageInfo: { 13 | hasNextPage: boolean; 14 | hasPreviousPage: boolean; 15 | startCursor: string; 16 | endCursor: string; 17 | }; 18 | list: SocialConnection[]; 19 | }; 20 | 21 | export type AllSocialConnections = { 22 | identity: { 23 | avatar: string; 24 | followers: SocialConnectionsPaginated; 25 | followings: SocialConnectionsPaginated; 26 | friends: SocialConnectionsPaginated; 27 | }; 28 | }; 29 | 30 | // Recommendation Data 31 | 32 | export type RecommendationData = { 33 | pageInfo: { 34 | hasNextPage: boolean; 35 | hasPreviousPage: boolean; 36 | startCursor: string; 37 | endCursor: string; 38 | }; 39 | list: SocialConnection[]; 40 | }; 41 | 42 | export type AllRecommendations = { 43 | recommendations: { 44 | data: RecommendationData; 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/types/identity.ts: -------------------------------------------------------------------------------- 1 | export type Identity = { 2 | address: string; 3 | domain: string; 4 | ens: string; 5 | social: { 6 | twitter: string; 7 | }; 8 | avatar: string; 9 | joinTime: number; 10 | followerCount: number; 11 | followingCount: number; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | //src\utils\helper.ts 2 | 3 | //format the address display 4 | export const formatAddress = (address: string) => { 5 | const len = address.length; 6 | return address.substr(0, 5) + "..." + address.substring(len - 4, len); 7 | }; 8 | //check if the address is valid 9 | export const isValidAddr = (address: string) => { 10 | const re = /^0x[a-fA-F0-9]{40}$/; 11 | return address.match(re); 12 | }; 13 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | /* styles\Home.module.css */ 2 | 3 | .container { 4 | height: 100vh; 5 | display: flex; 6 | position: relative; 7 | } 8 | 9 | .main { 10 | min-height: 20rem; 11 | margin-top: 30rem; 12 | padding: 4rem 4rem; 13 | position: fixed; 14 | z-index: 2; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | } 19 | 20 | .title { 21 | margin: 0; 22 | line-height: 1.15; 23 | font-size: 4rem; 24 | color: white; 25 | } 26 | 27 | .jumpButton { 28 | color: rgb(0, 0, 0); 29 | background: white; 30 | width: 270px; 31 | height: 70px; 32 | font-size: x-large; 33 | border-radius: 20px; 34 | margin: 20px 5px; 35 | font-family: "Outfit"; 36 | } 37 | 38 | .jumpButton:hover { 39 | color: white; 40 | background-color: rgb(0, 0, 0); 41 | border: solid white 2px; 42 | } 43 | 44 | .loadingSection { 45 | position: fixed; 46 | display: flex; 47 | padding: 80px 50px; 48 | } 49 | 50 | .loadingIcon { 51 | margin-right: 10px; 52 | } 53 | 54 | .loadingText { 55 | margin-top: 10px; 56 | color: "white"; 57 | font-size: 20px; 58 | } 59 | 60 | .footer { 61 | color: white; 62 | font-weight: 300; 63 | margin-left: 6px; 64 | margin-top: 30rem; 65 | } 66 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | /* styles\globals.css */ 2 | 3 | @import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,600;1,700;1,800;1,900&family=Outfit:wght@100;200;300;400;500;600;700;800;900&family=Poppins:wght@300&display=swap"); 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: "Outfit", sans-serif; 10 | max-height: 100vh; 11 | background-color: black; 12 | } 13 | 14 | a { 15 | color: inherit; 16 | text-decoration: none; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } 22 | -------------------------------------------------------------------------------- /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 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["src/*"], 20 | "@/components/*": ["src/components/*"] 21 | } 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | --------------------------------------------------------------------------------