├── .editorconfig ├── .env.example ├── .gitattributes ├── .github └── workflows │ └── checks.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── biome.json ├── bun.lockb ├── environment.d.ts ├── index.html ├── package.json ├── postcss.config.cjs ├── public ├── art │ ├── noise.svg │ └── waves-background.svg ├── common │ └── follow-icon.svg ├── favicon.ico ├── logo.png └── preview.png ├── reset.d.ts ├── scripts └── update-dependencies.ts ├── src ├── App.tsx ├── components │ ├── header.tsx │ └── placeholder.tsx ├── config.ts ├── fetchers.ts ├── hooks │ └── use-ens.ts ├── index.css ├── main.tsx ├── utilities.ts └── vite-env.d.ts ├── tailwind.config.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | max_line_length = 100 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | 12 | [*.{md}] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | PORT=5173 3 | API_URL="" 4 | API_VERSION="v1" 5 | 6 | VITE_API_URL="" 7 | VITE_API_VERSION="v1" 8 | 9 | ALCHEMY_ID="" 10 | LLAMAFOLIO_ID="" 11 | INFURA_ID="" 12 | 13 | VITE_ALCHEMY_ID="" 14 | VITE_LLAMAFOLIO_ID="" 15 | VITE_INFURA_ID="" 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | env: 16 | ACTIONS_RUNNER_DEBUG: true 17 | 18 | jobs: 19 | checks: 20 | if: ${{ !contains(github.event.head_commit.message, '[skip-checks]') }} 21 | timeout-minutes: 3 22 | runs-on: ['ubuntu-latest'] 23 | steps: 24 | - name: 'Checkout' 25 | uses: actions/checkout@v4.1.1 26 | 27 | - name: 'Set up Bun' 28 | uses: oven-sh/setup-bun@v1 29 | with: 30 | bun-version: 'latest' 31 | 32 | - name: 'Install Dependencies' 33 | shell: bash 34 | run: bun install 35 | 36 | - name: 'Lint' 37 | shell: bash 38 | run: bun lint 39 | 40 | - name: 'Format' 41 | shell: bash 42 | run: bun format 43 | 44 | - name: 'Typecheck' 45 | shell: bash 46 | run: bun typecheck 47 | 48 | - name: 'Build' 49 | shell: bash 50 | run: bun run build 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | node_modules 15 | dist 16 | dist-ssr 17 | *.local 18 | 19 | # Editor directories and files 20 | .idea 21 | .DS_Store 22 | _ 23 | _* 24 | 25 | # Editor directories and files 26 | .idea 27 | .DS_Store 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | .turbo 34 | out 35 | 36 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | auto-install-peers=true 3 | enable-pre-post-scripts=true 4 | strict-peer-dependencies=false 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "biomejs.biome", 4 | "mikestead.dotenv", 5 | "bradlc.vscode-tailwindcss", 6 | "EditorConfig.EditorConfig", 7 | "github.vscode-github-actions" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "typescript.preferences.importModuleSpecifier": "non-relative", 5 | "git.autofetch": true, 6 | "git.confirmSync": false, 7 | "git.enableCommitSigning": true, 8 | "editor.formatOnSave": true, 9 | "editor.codeActionsOnSave": { 10 | "source.organizeImports.biome": "explicit", 11 | "source.fixAll.biome": "explicit", 12 | "quickfix.biome": "explicit" 13 | }, 14 | "editor.defaultFormatter": "biomejs.biome", 15 | "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, 16 | "[javascript]": { "editor.defaultFormatter": "biomejs.biome" }, 17 | "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, 18 | "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, 19 | "[json]": { "editor.defaultFormatter": "biomejs.biome" }, 20 | "[jsonc]": { "editor.defaultFormatter": "biomejs.biome" }, 21 | "files.associations": { 22 | "*.css": "tailwindcss", 23 | "biome.json": "jsonc" 24 | }, 25 | "editor.quickSuggestions": { 26 | "strings": "on" 27 | }, 28 | "tailwindCSS.includeLanguages": { 29 | "plaintext": "html" 30 | }, 31 | "search.exclude": { 32 | "_": true, 33 | "public/*.js": true, 34 | "**/node_modules": true 35 | }, 36 | "[html]": { 37 | "editor.defaultFormatter": "vscode.html-language-features" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ethereum Follow Protocol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EFP Integration Demo 2 | 3 | The intended audience for this demo is EFP launch partners 4 | 5 | ## API Documentation: [docs.ethfollow.xyz](https://docs.ethfollow.xyz) 6 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.3.3/schema.json", 3 | "vcs": { 4 | "root": ".", 5 | "enabled": true, 6 | "clientKind": "git" 7 | }, 8 | "files": { 9 | "include": [ 10 | "./**/*.ts", 11 | "./**/*.js", 12 | "./**/*.cjs", 13 | "./**/*.tsx", 14 | "./**/*.d.ts", 15 | "./**/*.json", 16 | "./**/*.jsonc" 17 | ], 18 | "ignore": ["node_modules", "dist", "_"], 19 | "ignoreUnknown": true 20 | }, 21 | "organizeImports": { 22 | "enabled": false 23 | }, 24 | "formatter": { 25 | "enabled": true, 26 | "lineWidth": 100, 27 | "indentWidth": 2, 28 | "indentStyle": "space", 29 | "formatWithErrors": true, 30 | "include": [ 31 | "./**/*.ts", 32 | "./**/*.js", 33 | "./**/*.cjs", 34 | "./**/*.tsx", 35 | "./**/*.d.ts", 36 | "./**/*.json", 37 | "./**/*.jsonc" 38 | ] 39 | }, 40 | "linter": { 41 | "enabled": true, 42 | "rules": { 43 | "all": true, 44 | "style": { 45 | "useBlockStatements": "off", 46 | "useSelfClosingElements": "off", 47 | "noUnusedTemplateLiteral": "off" 48 | }, 49 | "a11y": { "noSvgWithoutTitle": "off", "useKeyWithClickEvents": "off" }, 50 | "nursery": { "noUnusedImports": "off" }, 51 | "suspicious": { "noExplicitAny": "off" }, 52 | "correctness": { "noUndeclaredVariables": "off" } 53 | } 54 | }, 55 | "json": { 56 | "parser": { 57 | "allowComments": true 58 | }, 59 | "formatter": { 60 | "enabled": true 61 | } 62 | }, 63 | "javascript": { 64 | "parser": { 65 | "unsafeParameterDecoratorsEnabled": true 66 | }, 67 | "formatter": { 68 | "enabled": true, 69 | "lineWidth": 100, 70 | "indentWidth": 2, 71 | "indentStyle": "space", 72 | "quoteStyle": "single", 73 | "trailingComma": "none", 74 | "semicolons": "asNeeded", 75 | "jsxQuoteStyle": "single", 76 | "quoteProperties": "asNeeded", 77 | "arrowParentheses": "asNeeded" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereumfollowprotocol/demo/ce2fed0f1632d41ce44133f1adc4127e6058f0a2/bun.lockb -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | interface EnvironmentVariables { 2 | readonly NODE_ENV: 'development' | 'production' | 'test' 3 | readonly PORT: string 4 | readonly API_URL: string 5 | readonly API_VERSION: string 6 | readonly LLAMAFOLIO_ID: string 7 | readonly ALCHEMY_ID: string 8 | readonly INFURA_ID: string 9 | } 10 | 11 | // type support for `import.meta.env[...] 12 | interface ImportMetaEnv extends EnvironmentVariables { 13 | readonly VITE_API_URL: string 14 | readonly VITE_API_VERSION: string 15 | readonly VITE_LLAMAFOLIO_ID: string 16 | readonly VITE_ALCHEMY_ID: string 17 | readonly VITE_INFURA_ID: string 18 | } 19 | 20 | declare namespace NodeJS { 21 | type ProcessEnv = EnvironmentVariables 22 | } 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | EFP Demo 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "efp-demo", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "preview": "vite preview", 8 | "build": "tsc && vite build", 9 | "format": "biome format . --write", 10 | "lint": "biome check --apply .", 11 | "typecheck": "tsc --project tsconfig.json --noEmit", 12 | "clean": "rm -rf node_modules dist" 13 | }, 14 | "dependencies": { 15 | "@radix-ui/themes": "^2.0.3", 16 | "@tanstack/react-query": "^5.17.15", 17 | "@tanstack/react-query-devtools": "^5.17.15", 18 | "million": "^2.6.4", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "@wagmi/core": "2.0.0-beta.1", 22 | "viem": "2.0.0-beta.4", 23 | "wagmi": "2.0.0-beta.1" 24 | }, 25 | "devDependencies": { 26 | "@biomejs/biome": "^1.5.2", 27 | "@total-typescript/ts-reset": "^0.5.1", 28 | "@types/bun": "^1.0.2", 29 | "@types/node": "^20.11.4", 30 | "@types/react": "^18.2.48", 31 | "@types/react-dom": "^18.2.18", 32 | "@vitejs/plugin-react-swc": "^3.5.0", 33 | "autoprefixer": "^10.4.16", 34 | "bun": "^1.0.23", 35 | "postcss": "^8.4.33", 36 | "tailwindcss": "^3.4.1", 37 | "typed-query-selector": "^2.11.0", 38 | "typescript": "^5.3.3", 39 | "vite": "^5.0.11", 40 | "vite-tsconfig-paths": "^4.3.1" 41 | }, 42 | "private": true, 43 | "sideEffects": false 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/art/noise.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 11 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /public/art/waves-background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/common/follow-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereumfollowprotocol/demo/ce2fed0f1632d41ce44133f1adc4127e6058f0a2/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereumfollowprotocol/demo/ce2fed0f1632d41ce44133f1adc4127e6058f0a2/public/logo.png -------------------------------------------------------------------------------- /public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereumfollowprotocol/demo/ce2fed0f1632d41ce44133f1adc4127e6058f0a2/public/preview.png -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | /* https://github.com/total-typescript/ts-reset */ 2 | import '@total-typescript/ts-reset' 3 | -------------------------------------------------------------------------------- /scripts/update-dependencies.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | /** 3 | * Update outdated dependencies 4 | */ 5 | 6 | import bun from 'bun' 7 | import packageJson from '../package.json' with { type: 'json' } 8 | 9 | const { name, version, type, scripts, dependencies, devDependencies, ...rest } = packageJson 10 | 11 | main().catch(error => { 12 | console.error(error) 13 | process.exit(1) 14 | }) 15 | 16 | async function main() { 17 | const updated = await bumpDependencies() 18 | if (updated) console.log('Dependencies updated') 19 | else console.log('Dependencies are up to date') 20 | 21 | const { stdout, success } = bun.spawnSync(['bun', 'install', '--no-cache', '--force']) 22 | console.log(`success: ${success}`, stdout.toString()) 23 | } 24 | 25 | async function bumpDependencies() { 26 | const unstableDependenciesNames = getUnstableDependencies(dependencies) 27 | const unstableDevDependenciesNames = getUnstableDependencies(devDependencies) 28 | 29 | // filter out packages whose version is beta or alpha 30 | const dependenciesNames = Object.keys(dependencies).filter( 31 | name => !Object.hasOwn(unstableDependenciesNames, name) 32 | ) 33 | const latestDependenciesVersions = await Promise.all( 34 | dependenciesNames.map(name => fetchPackageLatestVersion(name)) 35 | ) 36 | 37 | const updatedDependencies = Object.fromEntries( 38 | dependenciesNames.map((name, index) => [name, `^${latestDependenciesVersions[index]}`]) 39 | ) 40 | 41 | for (const [name, version] of Object.entries(unstableDependenciesNames)) { 42 | updatedDependencies[name] = version 43 | } 44 | 45 | const devDependenciesNames = Object.keys(devDependencies).filter( 46 | name => !Object.hasOwn(unstableDevDependenciesNames, name) 47 | ) 48 | 49 | const latestDevDependenciesVersions = await Promise.all( 50 | devDependenciesNames.map(name => fetchPackageLatestVersion(name)) 51 | ) 52 | 53 | const updatedDevDependencies = Object.fromEntries( 54 | devDependenciesNames.map((name, index) => [name, `^${latestDevDependenciesVersions[index]}`]) 55 | ) 56 | 57 | for (const [name, version] of Object.entries(unstableDevDependenciesNames)) { 58 | updatedDevDependencies[name] = version 59 | } 60 | 61 | const updatedPackageJson = { 62 | name, 63 | version, 64 | type, 65 | scripts, 66 | dependencies: updatedDependencies, 67 | devDependencies: updatedDevDependencies, 68 | ...rest 69 | } 70 | 71 | const write = await bun.write( 72 | `${import.meta.dir}/../package.json`, 73 | `${JSON.stringify(updatedPackageJson, undefined, 2)}\n` 74 | ) 75 | 76 | return Boolean(write) 77 | } 78 | 79 | async function fetchPackageLatestVersion(name: string) { 80 | const response = await fetch(`https://registry.npmjs.org/${name}/latest`) 81 | const { version } = (await response.json()) as { version: string } 82 | return version 83 | } 84 | 85 | function getUnstableDependencies(dependencies: Record) { 86 | return Object.entries(dependencies) 87 | .filter(([, version]) => /alpha|beta/.test(version)) 88 | .reduce((acc, [name, version]) => ({ ...acc, [name]: version }), {}) as Record 89 | } 90 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useQuery } from '@tanstack/react-query' 3 | import { Box, Flex, Text, Tabs, Avatar, Separator, Card, Link, Container } from '@radix-ui/themes' 4 | 5 | import { Header } from '#/components/header.tsx' 6 | import { truncateAddress } from '#/utilities.ts' 7 | import { Placeholder } from '#/components/placeholder.tsx' 8 | import { useEnsNames, useEnsProfile } from '#/hooks/use-ens.ts' 9 | import { fetchEfpUserFollowers, fetchEfpUserFollowing } from '#/fetchers.ts' 10 | 11 | // dr3a.eth wallet address 12 | const WALLET_ADDRESS = '0xeb6b293E9bB1d71240953c8306aD2c8aC523516a' 13 | 14 | export default function App() { 15 | const [activeTab, setActiveTab] = useState<'Followers' | 'Following'>('Followers') 16 | const { data: ensData, error: ensError, status: ensStatus } = useEnsProfile(WALLET_ADDRESS) 17 | 18 | const { 19 | data: followersData, 20 | error: followersError, 21 | status: followersStatus 22 | } = useQuery({ 23 | queryKey: ['efp-followers', WALLET_ADDRESS], 24 | queryFn: () => fetchEfpUserFollowers(WALLET_ADDRESS), 25 | select: ({ data, error }) => ({ 26 | data: data ? data.filter(Boolean).map(({ actor_address: address }) => address) : [], 27 | error 28 | }) 29 | }) 30 | 31 | const followersAddresses = followersData?.data || [] 32 | 33 | /* Returns tuple of address & ENS name pairs */ 34 | const { data: followersProfiles } = useEnsNames({ 35 | queryKey: 'efp-followers', 36 | addresses: followersAddresses, 37 | enabled: followersStatus === 'success' && followersAddresses.length > 0 38 | }) 39 | 40 | const { 41 | data: followingData, 42 | error: followingError, 43 | status: followingStatus 44 | } = useQuery({ 45 | queryKey: ['efp-following', WALLET_ADDRESS], 46 | queryFn: () => fetchEfpUserFollowing(WALLET_ADDRESS), 47 | select: ({ data, error }) => ({ 48 | data: data ? data.filter(Boolean).map(({ target_address: address }) => address) : [], 49 | error 50 | }) 51 | }) 52 | 53 | const followingAddresses = followingData?.data || [] 54 | 55 | /* Returns tuple of address & ENS name pairs */ 56 | const { data: followingProfiles } = useEnsNames({ 57 | queryKey: 'efp-following', 58 | addresses: followingAddresses, 59 | enabled: followingStatus === 'success' && followingAddresses.length > 0 60 | }) 61 | 62 | return ( 63 | 64 |
65 |
66 | 67 | 68 | } 74 | /> 75 | 76 | 77 | 78 | dr3a.eth 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |

91 | {followersAddresses.length} 92 |

93 | 94 | Followers 95 | 96 |
97 | 98 | 99 |

100 | {followingAddresses.length} 101 |

102 | 103 | Following 104 | 105 |
106 |
107 |
108 | 109 | 110 | 111 |
setActiveTab('Followers')} 113 | className={`before:bg-transparent p-3 cursor-pointer flex items-center ${ 114 | activeTab === 'Followers' ? 'bg-white/80' : 'transparent hover:bg-white/50' 115 | } hover:rounded-full transition-colors rounded-full data-[state=active]:font-extrabold`} 116 | > 117 | 118 | Followers 119 | 120 |
121 |
setActiveTab('Following')} 123 | className={`before:bg-transparent p-3 cursor-pointer flex items-center ${ 124 | activeTab === 'Following' ? 'bg-white/80' : 'transparent hover:bg-white/50' 125 | } hover:rounded-full transition-colors rounded-full data-[state=active]:font-extrabold`} 126 | > 127 | 128 | Following 129 | 130 |
131 |
132 | 133 | 134 | {followersProfiles?.map(([address, name]) => ( 135 | 136 | 137 | } 142 | /> 143 | 144 | 145 | {name || truncateAddress(address)} 146 | 147 | 148 | 149 | 150 | ))} 151 | 152 | 153 | {followingProfiles?.map(([address, name]) => ( 154 | 155 | 156 | } 161 | /> 162 | 163 | 164 | {name || truncateAddress(address)} 165 | 166 | 167 | 168 | 169 | ))} 170 | 171 | 172 |
173 |
174 |
175 |
176 | 177 | ) 178 | } 179 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@radix-ui/themes' 2 | 3 | export function Header() { 4 | return ( 5 |
6 | 12 | 18 | 24 | 25 | 26 | 32 | 37 | 41 | 42 | 43 | 49 | 54 | 58 | 59 | 60 | 66 | 73 | 74 |
75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/components/placeholder.tsx: -------------------------------------------------------------------------------- 1 | export function Placeholder() { 2 | return ( 3 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { mainnet } from 'wagmi/chains' 2 | import { createConfig, http, fallback, createStorage } from 'wagmi' 3 | 4 | export const config = createConfig({ 5 | chains: [mainnet], 6 | transports: { 7 | [mainnet.id]: fallback([ 8 | http(`https://eth-mainnet.alchemyapi.io/v2/${import.meta.env.VITE_ALCHEMY_ID}`), 9 | http(`https://mainnet.infura.io/v3/${import.meta.env.VITE_INFURA_ID}`), 10 | http(`https://eth.llamarpc.com/rpc/${import.meta.env.VITE_LLAMAFOLIO_ID}`) 11 | ]) 12 | }, 13 | storage: createStorage({ storage: window.localStorage }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/fetchers.ts: -------------------------------------------------------------------------------- 1 | import { isAddress, type Address } from 'viem' 2 | 3 | /** 4 | * This code is intentionally verbose to make it easier to read and understand. 5 | */ 6 | 7 | const API_URL = import.meta.env.VITE_API_URL 8 | const API_VERSION = import.meta.env.VITE_API_VERSION 9 | 10 | export type EfpResponse = { data?: T; error?: string } 11 | 12 | export interface EfpUserStats { 13 | followersCount: number 14 | followingCount: number 15 | } 16 | 17 | export interface EfpUserFollowing { 18 | target_address: Address 19 | action_timestamp: string 20 | } 21 | 22 | export interface EfpUserFollower { 23 | actor_address: Address 24 | action_timestamp: string 25 | } 26 | 27 | export interface EfpUser extends EfpUserStats { 28 | followers: (EfpUserFollower | undefined)[] 29 | following: (EfpUserFollowing | undefined)[] 30 | } 31 | 32 | /** 33 | * If you just need the number of followers and following for an address 34 | */ 35 | export async function fetchEfpUserStats(address: Address): Promise> { 36 | if (!isAddress(address)) throw new Error(`${address} is not a valid address`) 37 | const response = await fetch(`${API_URL}/${API_VERSION}/stats/${address}`, { 38 | method: 'GET' 39 | }) 40 | if (!response.ok) { 41 | throw new Error(`Invalid response for ${address}: ${response.statusText}`) 42 | } 43 | return (await response.json()) as EfpResponse 44 | } 45 | 46 | export async function fetchEfpUserFollowers( 47 | address: Address 48 | ): Promise> { 49 | if (!isAddress(address)) throw new Error(`${address} is not a valid address`) 50 | const response = await fetch(`${API_URL}/${API_VERSION}/followers/${address}`, { 51 | method: 'GET' 52 | }) 53 | if (!response.ok) { 54 | throw new Error(`Error for ${address}: ${response.statusText}`) 55 | } 56 | return (await response.json()) as EfpResponse 57 | } 58 | 59 | export async function fetchEfpUserFollowing( 60 | address: Address 61 | ): Promise> { 62 | if (!isAddress(address)) throw new Error(`${address} is not a valid address`) 63 | const response = await fetch(`${API_URL}/${API_VERSION}/following/${address}`, { 64 | method: 'GET' 65 | }) 66 | if (!response.ok) { 67 | throw new Error(`Error for ${address}: ${response.statusText}`) 68 | } 69 | return (await response.json()) as EfpResponse 70 | } 71 | 72 | export async function fetchEfpUser(address: Address): Promise> { 73 | if (!isAddress(address)) throw new Error(`${address} is not a valid address`) 74 | const response = await fetch(`${API_URL}/${API_VERSION}/all/${address}`) 75 | if (!response.ok) { 76 | throw new Error(`Error for ${address}: ${response.statusText}`) 77 | } 78 | return (await response.json()) as EfpResponse 79 | } 80 | -------------------------------------------------------------------------------- /src/hooks/use-ens.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from 'viem/ens' 2 | import { useQuery } from '@tanstack/react-query' 3 | import type { Address, GetEnsNameReturnType } from 'viem' 4 | import { getEnsAvatar, getEnsName, getPublicClient } from '@wagmi/core' 5 | 6 | import { config } from '#/config.ts' 7 | 8 | export function useEnsProfile(address: Address) { 9 | return useQuery({ 10 | queryKey: ['ens', address], 11 | queryFn: async () => { 12 | const name = await getEnsName(config, { address }) 13 | if (!name) return { address } 14 | const avatar = await getEnsAvatar(config, { name: normalize(name) }) 15 | return { address, avatar, name } 16 | }, 17 | staleTime: Infinity, 18 | select: data => data 19 | }) 20 | } 21 | 22 | export function useEnsNames({ 23 | queryKey, 24 | enabled = true, 25 | addresses 26 | }: { 27 | queryKey: string 28 | enabled?: boolean 29 | addresses: Array
30 | }) { 31 | return useQuery>({ 32 | enabled, 33 | queryKey: ['ens-names', queryKey], 34 | queryFn: async () => { 35 | const client = getPublicClient(config) 36 | const names = await Promise.all(addresses.map(address => client.getEnsName({ address }))) 37 | return addresses.map((address, index) => [address, names[index]]) 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .radix-theme { 6 | --default-font-family: 'Roboto', sans-serif; 7 | } 8 | 9 | /* Box sizing rules */ 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | /* Prevent font size inflation */ 15 | html { 16 | -moz-text-size-adjust: none; 17 | -webkit-text-size-adjust: none; 18 | text-size-adjust: none; 19 | } 20 | 21 | html, 22 | body, 23 | #root { 24 | height: 100%; 25 | width: 100%; 26 | min-height: 100%; 27 | min-width: 100%; 28 | margin: 0; 29 | padding: 0; 30 | background: white; 31 | background-image: url('../public/art/waves-background.svg'); 32 | background-repeat: no-repeat; 33 | background-position: -45vw 4vw; 34 | background-size: 200vw 100%; 35 | font-family: "Roboto", sans-serif !important; 36 | } 37 | 38 | @media screen and (max-width: 640px) { 39 | html, body, #root { 40 | background-repeat: no-repeat; 41 | background-size: 120vw; 42 | background-attachment: fixed; 43 | background-position: -20vw; 44 | } 45 | } 46 | 47 | /* A elements that don't have a class get default styles */ 48 | a:not([class]) { 49 | text-decoration-skip-ink: auto; 50 | color: currentColor; 51 | } 52 | 53 | /* Make images easier to work with */ 54 | img, 55 | picture { 56 | max-width: 100%; 57 | display: block; 58 | } 59 | 60 | /* Inherit fonts for inputs and buttons */ 61 | input, 62 | button, 63 | textarea, 64 | select { 65 | font: inherit; 66 | } 67 | 68 | /* Make sure textareas without a rows attribute are not tiny */ 69 | textarea:not([rows]) { 70 | min-height: 10em; 71 | } 72 | 73 | 74 | /** scrollbar */ 75 | *::-webkit-scrollbar { 76 | width: 0; 77 | } 78 | 79 | *::-webkit-scrollbar-track { 80 | -ms-overflow-style: none; 81 | overflow: -moz-scrollbars-none; 82 | } 83 | 84 | *::-webkit-scrollbar-thumb { 85 | -ms-overflow-style: none; 86 | overflow: -moz-scrollbars-none; 87 | } 88 | 89 | @supports (scrollbar-gutter: stable) { 90 | html { 91 | overflow-y: auto; 92 | } 93 | } 94 | 95 | ::selection { 96 | background-color: #ff9eb1 !important; 97 | color: #f9f9f9; 98 | } 99 | 100 | .glass-card { 101 | background: linear-gradient( 102 | to right bottom, 103 | rgba(246, 246, 246, 0.2), 104 | rgba(246, 246, 246, 0.1) 105 | ), url('/common/noise.svg'); 106 | background-repeat: no-repeat; 107 | background-size: cover; 108 | background-blend-mode: saturation; 109 | backdrop-filter: blur(1.25rem); 110 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import '@radix-ui/themes/styles.css' 3 | import { Theme } from '@radix-ui/themes' 4 | import * as ReactDOM from 'react-dom/client' 5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools' 6 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 7 | 8 | import '#/index.css' 9 | import App from '#/App.tsx' 10 | 11 | const root = document.querySelector('div#root') 12 | if (!root) throw new Error('Root element not found') 13 | 14 | export const queryClient = new QueryClient() 15 | 16 | ReactDOM.createRoot(root).render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | export function truncateAddress(address: string) { 2 | return `${address.slice(0, 6)}...${address.slice(-4)}` 3 | } 4 | 5 | export function raise(error: unknown): never { 6 | throw typeof error === 'string' ? new Error(error) : error 7 | } 8 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | import * as defaultTheme from 'tailwindcss/defaultTheme' 3 | 4 | export default { 5 | content: ['./index.html', './src/**/*.{js,ts,tsx}'], 6 | darkMode: 'class', 7 | theme: { 8 | fontFamily: { 9 | serif: ['Roboto', ...defaultTheme.fontFamily.serif] 10 | }, 11 | extend: { 12 | fontFamily: {} 13 | } 14 | }, 15 | plugins: [] 16 | } satisfies Config 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "noEmit": true, 7 | "useDefineForClassFields": true, 8 | "resolveJsonModule": true, 9 | "resolvePackageJsonExports": true, 10 | "resolvePackageJsonImports": true, 11 | "allowImportingTsExtensions": true, 12 | "noUncheckedIndexedAccess": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noPropertyAccessFromIndexSignature": true, 15 | "alwaysStrict": true, 16 | "strictNullChecks": true, 17 | "verbatimModuleSyntax": true, 18 | "strict": true, 19 | "allowJs": true, 20 | "checkJs": true, 21 | "types": ["node", "bun","typed-query-selector/strict"], 22 | "skipLibCheck": true, 23 | "allowSyntheticDefaultImports": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 26 | "jsx": "react-jsx", 27 | "isolatedModules": true, 28 | "paths": { 29 | "#/*": ["./src/*"] 30 | }, 31 | // TODO: set to true 32 | "noUnusedLocals": false, 33 | "noUnusedParameters": false, 34 | }, 35 | "include": ["src", "scripts"], 36 | "files": [ 37 | "biome.json", 38 | "reset.d.ts", 39 | "vite.config.ts", 40 | "environment.d.ts", 41 | "tailwind.config.ts", 42 | "postcss.config.cjs" 43 | ], 44 | "references": [{ "path": "./tsconfig.node.json" }] 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"], 10 | "files": ["package.json"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import million from 'million/compiler' 3 | import react from '@vitejs/plugin-react-swc' 4 | import tsconfigPaths from 'vite-tsconfig-paths' 5 | 6 | export default defineConfig({ 7 | server: { 8 | port: Number(process.env.PORT || 3_034) 9 | }, 10 | plugins: [million.vite({ auto: true }), react(), tsconfigPaths()] 11 | }) 12 | --------------------------------------------------------------------------------