├── .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 |
--------------------------------------------------------------------------------
/public/art/waves-background.svg:
--------------------------------------------------------------------------------
1 |
115 |
--------------------------------------------------------------------------------
/public/common/follow-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
25 |
26 |
32 |
42 |
43 |
49 |
59 |
60 |
66 |
73 |
74 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/placeholder.tsx:
--------------------------------------------------------------------------------
1 | export function Placeholder() {
2 | return (
3 |
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 |
--------------------------------------------------------------------------------