diff --git a/package-lock.json b/package-lock.json index a65a53e..a1963b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,19 @@ "@amplitude/analytics-browser": "^2.9.3", "@farcaster/hub-nodejs": "^0.11.11", "@neynar/nodejs-sdk": "^1.27.0", - "@neynar/react": "^0.5.0", + "@neynar/react": "^0.6.1", "@pigment-css/react": "^0.0.9", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "frames.js": "^0.17.0", + "hls.js": "^1.5.13", "lucide-react": "^0.378.0", "next": "14.2.3", "next13-progressbar": "^1.2.1", @@ -31,6 +34,7 @@ "react-farcaster-embed": "^1.5.0", "react-icons": "^5.2.1", "react-loading-skeleton": "^3.4.0", + "swr": "^2.2.5", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "uuid": "^10.0.0" @@ -2570,13 +2574,15 @@ } }, "node_modules/@neynar/react": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@neynar/react/-/react-0.5.0.tgz", - "integrity": "sha512-YX4Vm59S/x4gJsTIeHzp9Pu9ShYUnr/q3YiHihu0XtG5jVNBOnELzb8kNS9RUIVFKPYWHXmW3fifQb9PL5bmYw==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@neynar/react/-/react-0.6.1.tgz", + "integrity": "sha512-eQxd+wIzEBSytH1/PBjR+tfycGSJfY4p1NJEUVpXqMiuKIOyBSnmqj9VEtGi/29yIhIbsmOfe8YY8/H4T4ZA1w==", "peerDependencies": { "@pigment-css/react": "^0.0.9", + "hls.js": "^1.5.13", "react": "^18.3.0", - "react-dom": "^18.3.0" + "react-dom": "^18.3.0", + "swr": "^2.2.5" } }, "node_modules/@noble/curves": { @@ -3016,6 +3022,41 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "license": "MIT", @@ -3054,6 +3095,34 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz", + "integrity": "sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.0", "license": "MIT", @@ -3106,6 +3175,45 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", + "integrity": "sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "license": "MIT", @@ -3201,6 +3309,36 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.1", "license": "MIT", @@ -7683,6 +7821,11 @@ "node": "*" } }, + "node_modules/hls.js": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.13.tgz", + "integrity": "sha512-xRgKo84nsC7clEvSfIdgn/Tc0NOT+d7vdiL/wvkLO+0k0juc26NRBPPG1SfB8pd5bHXIjMW/F5VM8VYYkOYYdw==" + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -10533,7 +10676,8 @@ }, "node_modules/react-icons": { "version": "5.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", "peerDependencies": { "react": "*" } @@ -11562,6 +11706,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -12221,7 +12377,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } diff --git a/package.json b/package.json index 4a57ffe..e48718c 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,19 @@ "@amplitude/analytics-browser": "^2.9.3", "@farcaster/hub-nodejs": "^0.11.11", "@neynar/nodejs-sdk": "^1.27.0", - "@neynar/react": "^0.5.0", + "@neynar/react": "^0.6.1", "@pigment-css/react": "^0.0.9", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "frames.js": "^0.17.0", + "hls.js": "^1.5.13", "lucide-react": "^0.378.0", "next": "14.2.3", "next13-progressbar": "^1.2.1", @@ -34,6 +37,7 @@ "react-farcaster-embed": "^1.5.0", "react-icons": "^5.2.1", "react-loading-skeleton": "^3.4.0", + "swr": "^2.2.5", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "uuid": "^10.0.0" diff --git a/public/arrowright.png b/public/arrowright.png new file mode 100644 index 0000000..af69dc6 Binary files /dev/null and b/public/arrowright.png differ diff --git a/public/check.png b/public/check.png new file mode 100644 index 0000000..f24609b Binary files /dev/null and b/public/check.png differ diff --git a/public/cross.png b/public/cross.png new file mode 100644 index 0000000..3c9bdf4 Binary files /dev/null and b/public/cross.png differ diff --git a/public/explorer.png b/public/explorer.png new file mode 100644 index 0000000..974fbb7 Binary files /dev/null and b/public/explorer.png differ diff --git a/public/eye.png b/public/eye.png new file mode 100644 index 0000000..31e4679 Binary files /dev/null and b/public/eye.png differ diff --git a/public/eyecross.png b/public/eyecross.png new file mode 100644 index 0000000..eea8217 Binary files /dev/null and b/public/eyecross.png differ diff --git a/public/homebackground.png b/public/homebackground.png new file mode 100644 index 0000000..426f1ca Binary files /dev/null and b/public/homebackground.png differ diff --git a/public/neynarexplorer.png b/public/neynarexplorer.png new file mode 100644 index 0000000..b506695 Binary files /dev/null and b/public/neynarexplorer.png differ diff --git a/public/searchbackground.png b/public/searchbackground.png new file mode 100644 index 0000000..6654a29 Binary files /dev/null and b/public/searchbackground.png differ diff --git a/src/app/[identifier]/page.tsx b/src/app/[identifier]/page.tsx index c142c6f..6b3284e 100644 --- a/src/app/[identifier]/page.tsx +++ b/src/app/[identifier]/page.tsx @@ -4,6 +4,16 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { hubs } from '@/constants'; import { useClipboard } from '@/hooks/useClipboard'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { extractIdentifierFromUrl, extractUsernameFromUrl, @@ -11,22 +21,23 @@ import { isValidWarpcastUrl, } from '@/lib/utils'; import { NeynarProfileCard, NeynarCastCard } from '@neynar/react'; -import { capitalizeNickname } from '@/lib/helpers'; -import { CopyCheckIcon, CopyIcon, UserIcon } from 'lucide-react'; +import { capitalizeNickname, isNumeric } from '@/lib/helpers'; +import { CopyCheckIcon, CopyIcon, UserIcon, SearchIcon } from 'lucide-react'; import Link from 'next/link'; import * as amplitude from '@amplitude/analytics-browser'; import { useEffect, useState } from 'react'; import SkeletonHeader from '@/components/skeleton-header'; +import { Input } from '@/components/ui/input'; +import { usePathname, useRouter } from 'next/navigation'; +import ActionButtons from '@/components/ActionButtons'; +import Search from '@/components/search'; interface ResponseProps { params: { identifier: string }; } -const isNumeric = (str: string): boolean => { - return !isNaN(Number(str)) && !isNaN(parseFloat(str)) && !/^0x/.test(str); -}; - export default function Page({ params }: ResponseProps) { + const router = useRouter(); let identifier = decodeURIComponent(params.identifier); const fid: number | null = isNumeric(identifier) ? Number(identifier) : null; let hash = fid ? null : identifier; @@ -40,14 +51,13 @@ export default function Page({ params }: ResponseProps) { console.error('Invalid URL identifier'); } } - - const { copied, copy } = useClipboard(); const [data, setData] = useState(null); const [modalData, setModalData] = useState(null); const [modalTitle, setModalTitle] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); const [loading, setLoading] = useState(true); const [showOtherHubs, setShowOtherHubs] = useState(false); + const [clickedHeader, setClickedHeader] = useState(null); const checkWarning = (message: any) => { if (!message) return []; @@ -125,11 +135,13 @@ export default function Page({ params }: ResponseProps) { setModalTitle(title); setModalData({ ...response, missingObjects }); setIsModalOpen(true); + setClickedHeader(title); // Set the clicked header }; const closeModal = () => { setIsModalOpen(false); setModalData(null); + setClickedHeader(null); // Reset the clicked header }; const { warpcast, neynar } = data?.apiData ?? {}; @@ -152,8 +164,6 @@ export default function Page({ params }: ResponseProps) { const { author: warpcastAuthor, cast: warpcastCast } = warpcast || {}; const { author: neynarAuthor, cast: neynarCast } = neynar || {}; - const authorFidCast = - warpcastCast?.author?.fid || neynarCast?.cast?.author?.fid; const authorFid = warpcastCast?.author?.fid || neynarCast?.cast?.author?.fid || @@ -161,42 +171,58 @@ export default function Page({ params }: ResponseProps) { warpcastAuthor?.author?.fid; const username = - warpcast?.author?.username ?? - neynar?.author?.author?.username ?? - neynar?.cast?.cast?.author?.username ?? - warpcast?.cast?.author?.username ?? + warpcastAuthor?.username ?? + neynarAuthor?.author?.username ?? + neynarCast?.cast?.author?.username ?? + warpcastCast?.cast?.author?.username ?? null; const castHash = neynar?.cast?.cast?.hash ?? warpcast?.cast?.hash ?? null; - const renderHeader = ( - label: string, - data: any | null, - missingObjects: string[] - ) => { + const renderHeader = (label: string, data: any, missingObjects: any[]) => { if (!data) { return null; } let icon = '✅'; + let backgroundColor = '#03A800'; + let hoverColor = '#028700'; // Adjust hover color + if (data?.is_server_dead) { icon = '❓'; + backgroundColor = '#FFA500'; + hoverColor = '#CC8400'; // Adjust hover color } else if (data?.error) { icon = '❌'; + backgroundColor = '#C67A7D'; + hoverColor = '#A66060'; // Adjust hover color } else if (missingObjects.length > 0) { icon = '⚠️'; + backgroundColor = '#FFD700'; + hoverColor = '#CCB300'; // Adjust hover color } + const isClicked = clickedHeader === label; + const activeColor = isClicked ? '#03039A' : backgroundColor; + return ( - ); }; @@ -209,169 +235,193 @@ export default function Page({ params }: ResponseProps) { response={modalData} title={modalTitle} /> -
-
- {fid || hash ? ( - - ) : null} - - {authorFidCast ? ( - - ) : null} -
- -
- {loading ? ( - <> - {} - {} - {} - {} - - ) : ( - <> - {renderHeader( - 'Warpcast API', - warpcastAuthor, - warpcastAuthorMissing - )} - {renderHeader('Warpcast API', warpcastCast, warpcastCastMissing)} - {renderHeader( - capitalizeNickname(hoyt.name), - hoytAuthor, - warpcastAuthorHubMissing - )} - {renderHeader( - capitalizeNickname(hoyt.name), - hoytCast, - warpcastCastHubMissing - )} - {renderHeader( - 'Neynar Hub', - neynarHubAuthor, - neynarAuthorHubMissing - )} - {renderHeader('Neynar Hub', neynarHubCast, neynarCastHubMissing)} - {renderHeader('Neynar API', neynarAuthor, neynarAuthorMissing)} - {renderHeader('Neynar API', neynarCast, neynarCastMissing)} - - )} -
-
- {hash && !extractUsernameFromUrl(hash) ? ( - - ) : authorFid ? ( - - ) : null} -
- -
- {!showOtherHubs ? ( - - ) : ( - - )} - {loading ? null : castHash || username ? ( - <> - - - - ) : null} -
- - {showOtherHubs && ( -
- {hubs.slice(2).map((hub, index) => { - const hubData = data?.hubData?.[index + 2]; - const missingObjects = checkWarning(hubData?.author); - return ( -
- {renderHeader( - `${capitalizeNickname(hub.shortname)}`, - hubData, - missingObjects - )} +
+
+
+
+

+ showing results for: +

+
+
+ +
+
+
+ {loading ? ( + <> + {} + {} + {} + {} + + ) : ( + <> + {renderHeader( + 'Warpcast API', + warpcastAuthor, + warpcastAuthorMissing + )} + {renderHeader( + 'Warpcast API', + warpcastCast, + warpcastCastMissing + )} + {renderHeader( + capitalizeNickname(hoyt.name), + hoytAuthor, + warpcastAuthorHubMissing + )} + {renderHeader( + capitalizeNickname(hoyt.name), + hoytCast, + warpcastCastHubMissing + )} + {renderHeader( + 'Neynar Hub', + neynarHubAuthor, + neynarAuthorHubMissing + )} + {renderHeader( + 'Neynar Hub', + neynarHubCast, + neynarCastHubMissing + )} + {renderHeader( + 'Neynar API', + neynarAuthor, + neynarAuthorMissing + )} + {renderHeader('Neynar API', neynarCast, neynarCastMissing)} + {!showOtherHubs ? ( + + ) : ( + + )} + {showOtherHubs && ( +
+ {hubs.slice(2).map((hub, index) => { + const hubData = data?.hubData?.[index + 2]; + const missingObjects = checkWarning(hubData?.author); + return ( +
+ {renderHeader( + `${capitalizeNickname(hub.shortname)}`, + hubData, + missingObjects + )} +
+ ); + })} +
+ )} + + )} +
+
+ {hash && !extractUsernameFromUrl(hash) ? ( + + ) : authorFid ? ( + + ) : null} +
+
+
+ {loading ? null : castHash || username ? ( +
+ {loading ? null : castHash || username ? ( + + + {' '} + + + + { + router.push( + castHash + ? `https://warpcast.com/${username}/${castHash.slice(0, 10)}` + : `https://warpcast.com/${username}` + ); + }} + > + in Warpcast + + { + router.push( + castHash + ? `https://www.supercast.xyz/c/${castHash}` + : `https://www.supercast.xyz/${username}` + ); + }} + > + in Supercast + + + + ) : null}
- ); - })} + ) : null} +
- )} + +
+ +
+
); diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fe..974fbb7 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css index 1c241db..cdd5900 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Pixelify+Sans:wght@400..700&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6680627..8501c9a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,23 +5,24 @@ import '@neynar/react/dist/style.css'; import './globals.css'; import Link from 'next/link'; import Providers from './providers'; -import Search from '@/components/search'; -import { usePathname } from 'next/navigation'; import { useEffect } from 'react'; import { seo } from '@/constants'; -import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { DownloadIcon } from 'lucide-react'; +import { Grid2X2 } from 'lucide-react'; import { NeynarContextProvider, Theme } from '@neynar/react'; import * as amplitude from '@amplitude/analytics-browser'; import { v4 as uuidv4 } from 'uuid'; -import { NeynarAuthButton } from '@neynar/react'; +import { usePathname } from 'next/navigation'; +import HubsDataComponent from '@/components/hubs-data'; +import AuthButton from '@/components/AuthButton'; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const pathname = usePathname(); + useEffect(() => { let userId = localStorage.getItem('user_uuid'); if (!userId) { @@ -31,6 +32,13 @@ export default function RootLayout({ amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY as string, userId); }, []); + const getBackgroundImage = () => { + if (pathname === '/') { + return 'url(/homebackground.png)'; + } + return 'url(/searchbackground.png)'; + }; + return ( - +
-
-
- +
+
+ Neynar logo
- -
- -

Farcaster Explorer

+
+ + home - -
-
- Blog + blog - Github + github - Docs + docs - +
- -
- {children} -
- -
- +
{children}
+
+ +
+ +
diff --git a/src/components/ActionButtons.tsx b/src/components/ActionButtons.tsx new file mode 100644 index 0000000..4f06620 --- /dev/null +++ b/src/components/ActionButtons.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import Link from 'next/link'; // Assuming you're using Next.js for routing +import { CopyCheckIcon, CopyIcon, UserIcon, SearchIcon } from 'lucide-react'; +import * as amplitude from '@amplitude/analytics-browser'; +import { useClipboard } from '@/hooks/useClipboard'; + +const ActionButtons = ({ fid, hash, identifier }: any) => { + const { copied, copy } = useClipboard(); + return ( +
+ {fid || hash ? ( + + ) : null} +
+ ); +}; + +export default ActionButtons; diff --git a/src/components/AuthButton.tsx b/src/components/AuthButton.tsx new file mode 100644 index 0000000..ed69633 --- /dev/null +++ b/src/components/AuthButton.tsx @@ -0,0 +1,33 @@ +'use client'; +import { NeynarAuthButton, useNeynarContext } from '@neynar/react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; + +const AuthButton = () => { + const { isAuthenticated } = useNeynarContext(); + return ( + + +

+ {!isAuthenticated ? 'sign in' : 'sign out'} +

+
+ + + {isAuthenticated ? 'Sign out' : 'Sign in'} + + + + + +
+ ); +}; + +export default AuthButton; diff --git a/src/components/home.tsx b/src/components/home.tsx index d87b8db..69be10f 100644 --- a/src/components/home.tsx +++ b/src/components/home.tsx @@ -11,46 +11,13 @@ import { import Link from 'next/link'; import HubsDataComponent from './hubs-data'; import { NeynarProfileCard } from '@neynar/react'; +import Search from './search'; export default function Home() { return ( -
-
-
-

Example hash

-
- -
- -

Example FID

-
- - - -
-
-
-

Example Warpcast cast url

-
- -
- -

Example Warpcast profile url

-
- - - -
-
-
-
- +
+
+
); diff --git a/src/components/hubs-data.tsx b/src/components/hubs-data.tsx index 67e4c70..628169b 100644 --- a/src/components/hubs-data.tsx +++ b/src/components/hubs-data.tsx @@ -42,18 +42,18 @@ const HubsDataComponent = () => { }, []); return ( -
-
+
+
{loading ? // Skeleton loaders for loading state hubs.slice(0, 3).map((item) => ( -
+

- +

- +

@@ -65,26 +65,25 @@ const HubsDataComponent = () => { ).toFixed(2); return ( -
-

- Number of Messages on {capitalizeNickname(hub?.nickname)} -

+
0 + ? '#355E2B' + : '#C67A7D', + }} + className="space-y-2 p-2 w-26 md:w-full" + key={index} + >
{hub.dbStats.numMessages !== null ? ( -

- {hub?.dbStats?.numMessages.toLocaleString()}{' '} - 0 - ? 'green' - : 'red', - }} - > - ({percentageDifference}%) +

+ {capitalizeNickname(hub?.nickname)}  + + ({percentageDifference > 0 ? '+' : ''} + {percentageDifference}%)

) : ( diff --git a/src/components/modal-component.tsx b/src/components/modal-component.tsx index 40ee85c..ea8a84d 100644 --- a/src/components/modal-component.tsx +++ b/src/components/modal-component.tsx @@ -81,19 +81,19 @@ const Modal: React.FC = ({ return (
-
+
-

{title}

{missingObjects && missingObjects.length > 0 && ( -
+
Missing: {missingObjects.join(', ')}
)} -
+
 {
+    setIdentifier('');
+  };
+
   return (
     
{ e.preventDefault(); amplitude.track('Search made', { @@ -30,15 +32,27 @@ export default function Search() { router.push(`/${encodeURIComponent(identifier)}`); }} > - setIdentifier(e.currentTarget.value)} - /> - +
+ setIdentifier(e.currentTarget.value)} + /> + {identifier && ( + + )} +
+
); } diff --git a/src/components/skeleton-header.tsx b/src/components/skeleton-header.tsx index 4ea5f5d..82d2858 100644 --- a/src/components/skeleton-header.tsx +++ b/src/components/skeleton-header.tsx @@ -3,16 +3,8 @@ import { Card, CardContent, CardHeader } from './ui/card'; export default function SkeletonHeader() { return ( - -
- -
-
-
- -
-
-
-
+
+
+
); } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..b552952 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..283467c --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +'use client'; + +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { Check, ChevronRight, Circle } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index ffe9c64..6b6fac4 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -10,3 +10,7 @@ export const calculatePercentageDifference = ( if (median === 0) return 0; return ((current - median) / median) * 100; }; + +export const isNumeric = (str: string): boolean => { + return !isNaN(Number(str)) && !isNaN(parseFloat(str)) && !/^0x/.test(str); +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1cf6cfb..bc14c32 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { hubs, tokenBearer } from '@/constants'; +import { isNumeric } from './helpers'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } @@ -500,6 +501,16 @@ export const fetchWarpcastAuthor = async (identifier: string | null) => { try { let url, params; + if (identifier && isNumeric(identifier)) { + const response = await axios.get( + `https://api.warpcast.com/v2/user?fid=${identifier}`, + { + headers: { 'Content-Type': 'application/json' }, + } + ); + return response.data?.result?.user || null; + } + // Extract username if identifier is a URL if (identifier && identifier.includes('https://www.supercast.xyz/')) { const username = identifier.split('/').pop(); diff --git a/tailwind.config.ts b/tailwind.config.ts index 8deeaa7..36826fa 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -72,6 +72,10 @@ const config = { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', }, + fontFamily: { + pixelify: ['Pixelify Sans', 'sans-serif'], + jetbrains: ['JetBrains Mono', 'monospace'], + }, }, }, plugins: [require('tailwindcss-animate')],