diff --git a/package-lock.json b/package-lock.json
index 37a9dd0..66cd9b7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-tabs": "^1.1.0",
"axios": "^1.6.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -3439,6 +3440,36 @@
}
}
},
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz",
+ "integrity": "sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "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-presence": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-roving-focus": "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-use-callback-ref": {
"version": "1.1.0",
"license": "MIT",
diff --git a/package.json b/package.json
index 682a877..99b2516 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-tabs": "^1.1.0",
"axios": "^1.6.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
diff --git a/src/app/[identifier]/page.tsx b/src/app/[identifier]/page.tsx
index 1773cee..e20ce0b 100644
--- a/src/app/[identifier]/page.tsx
+++ b/src/app/[identifier]/page.tsx
@@ -2,6 +2,7 @@
import CastSearch from '@/components/CastSearch';
import NetworkResponse from '@/components/NetworkResponse';
import Search from '@/components/search';
+import SearchComponent from '@/components/SearchComponent';
interface ResponseProps {
params: { identifier: string };
@@ -36,11 +37,11 @@ export default function Page({ params }: ResponseProps) {
-
+
{isSearch ? (
-
+
) : (
)}
diff --git a/src/components/CastSearch.tsx b/src/components/CastSearch.tsx
index 85ee9cd..8373991 100644
--- a/src/components/CastSearch.tsx
+++ b/src/components/CastSearch.tsx
@@ -1,16 +1,7 @@
-'use client';
-import React, { useState, useEffect, useCallback, useRef } from 'react';
-import { Input } from '@/components/ui/input';
-import { NeynarCastCard, NeynarProfileCard } from '@neynar/react';
+// CastSearch.tsx
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import { NeynarCastCard } from '@neynar/react';
import { Loader2 } from 'lucide-react';
-import debounce from 'lodash/debounce';
-
-interface User {
- fid: number;
- username: string;
- display_name: string;
- pfp_url: string;
-}
interface Cast {
hash: string;
@@ -22,569 +13,137 @@ interface Cast {
};
}
-interface SearchParams {
- query: string;
- authorFid: string;
+const CastSearch = ({
+ searchQuery,
+ username,
+ channelId,
+}: {
+ searchQuery: string;
+ username: string;
channelId: string;
-}
-
-interface Channel {
- id: string;
- name: string;
- description: string;
- image_url: string;
-}
-
-const CastSearch = ({ initialQuery }: { initialQuery: string }) => {
- const [username, setUsername] = useState('');
- const [channelId, setChannelId] = useState('');
-
- const [searchParams, setSearchParams] = useState
({
- query: initialQuery,
- authorFid: '',
- channelId: '',
- });
+}) => {
const [casts, setCasts] = useState([]);
- const [searchUsers, setSearchUsers] = useState([]);
- const [inputUsers, setInputUsers] = useState([]);
- const [inputChannels, setInputChannels] = useState([]);
- const [userCursor, setUserCursor] = useState('');
- const [castCursor, setCastCursor] = useState('');
- const [loading, setLoading] = useState({
- users: false,
- casts: false,
- inputUsers: false,
- inputChannels: false,
- });
- const [showUserDropdown, setShowUserDropdown] = useState(false);
-
- const usernameInputRef = useRef(null);
- const dropdownRef = useRef(null);
- const userObserverRef = useRef(null);
- const castObserverRef = useRef(null);
- const lastUserRef = useRef(null);
- const lastCastRef = useRef(null);
- const [showChannelDropdown, setShowChannelDropdown] = useState(false);
- const channelDropdownRef = useRef(null);
-
- const handleShowMore = (identifier: string) => {
- window.open(`/${identifier}`, '_blank', 'noopener,noreferrer');
- };
-
- const fetchSearchUsers = useCallback(async (newSearch: boolean = false) => {
- if (loading.users || (!newSearch && !userCursor)) return;
-
- setLoading((prev) => ({ ...prev, users: true }));
- try {
- const userUrl = new URL(
- 'https://api.neynar.com/v2/farcaster/user/search'
- );
- userUrl.searchParams.append('q', searchParams.query);
- userUrl.searchParams.append('limit', '10');
- if (userCursor && !newSearch)
- userUrl.searchParams.append('cursor', userCursor);
-
- const userResponse = await fetch(userUrl, {
- headers: {
- Accept: 'application/json',
- api_key: process.env.NEXT_PUBLIC_NEYNAR_API_KEY || '',
- },
- });
-
- if (!userResponse.ok)
- throw new Error('User search network response was not ok');
- const userData = await userResponse.json();
- setSearchUsers((prevUsers) =>
- newSearch
- ? userData.result.users
- : [...prevUsers, ...userData.result.users]
- );
- if (userData.result.next) {
- setUserCursor(userData.result.next.cursor);
- } else {
- setUserCursor('');
- }
- } catch (error) {
- console.error('Error fetching search users:', error);
- } finally {
- setLoading((prev) => ({ ...prev, users: false }));
- }
- }, []);
-
- const fetchCasts = useCallback(
- async (newSearch: boolean = false) => {
- if (loading.casts || (!newSearch && !castCursor)) return;
-
- setLoading((prev) => ({ ...prev, casts: true }));
- try {
- let castUrl = 'https://api.neynar.com/v2/farcaster/cast/search?';
-
- // Append query parameters
- castUrl += `q=${encodeURIComponent(initialQuery)}&limit=20`;
-
- if (castCursor && !newSearch) {
- castUrl += `&cursor=${encodeURIComponent(castCursor)}`;
- }
-
- if (searchParams.authorFid) {
- castUrl += `&author_fid=${encodeURIComponent(searchParams.authorFid)}`;
+ const [cursor, setCursor] = useState(null);
+ const [hasMore, setHasMore] = useState(true);
+ const [loading, setLoading] = useState(false);
+ const observer = useRef(null);
+ const lastCastElementRef = useCallback(
+ (node: HTMLDivElement) => {
+ if (loading) return;
+ if (observer.current) observer.current.disconnect();
+ observer.current = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && hasMore) {
+ fetchCasts();
}
-
- if (searchParams.channelId) {
- castUrl += `&channel_id=${encodeURIComponent(searchParams.channelId)}`;
- }
-
- const castResponse = await fetch(castUrl, {
- headers: {
- Accept: 'application/json',
- api_key: process.env.NEXT_PUBLIC_NEYNAR_API_KEY || '',
- },
- });
-
- if (!castResponse.ok)
- throw new Error('Cast search network response was not ok');
- const castData = await castResponse.json();
- setCasts((prevCasts) =>
- newSearch
- ? castData.result.casts
- : [...prevCasts, ...castData.result.casts]
- );
- if (castData.result.next) {
- setCastCursor(castData.result.next.cursor);
- } else {
- setCastCursor('');
- }
- } catch (error) {
- console.error('Error fetching casts:', error);
- } finally {
- setLoading((prev) => ({ ...prev, casts: false }));
- }
+ });
+ if (node) observer.current.observe(node);
},
- [searchParams]
+ [loading, hasMore]
);
- const performSearch = useCallback(() => {
- setSearchUsers([]);
- setCasts([]);
- setUserCursor('');
- setCastCursor('');
- fetchSearchUsers(true);
- fetchCasts(true);
- }, [fetchSearchUsers, fetchCasts]);
+ const fetchCasts = async () => {
+ if (loading) return;
- // Debounced search function
- const debouncedSearch = useCallback(
- debounce(() => {
- performSearch();
- }, 300),
- [performSearch]
- );
-
- const fetchInputUsers = useCallback(async (inputUsername: string) => {
- if (loading.inputUsers || inputUsername.length < 1) return;
-
- setLoading((prev) => ({ ...prev, inputUsers: true }));
+ setLoading(true);
try {
- const userUrl = new URL(
- 'https://api.neynar.com/v2/farcaster/user/search'
+ const castUrl = new URL(
+ 'https://api.neynar.com/v2/farcaster/cast/search'
);
- userUrl.searchParams.append('q', inputUsername);
- userUrl.searchParams.append('limit', '5');
-
- const userResponse = await fetch(userUrl, {
- headers: {
- Accept: 'application/json',
- api_key: process.env.NEXT_PUBLIC_NEYNAR_API_KEY || '',
- },
- });
+ castUrl.searchParams.append('q', searchQuery);
+ castUrl.searchParams.append('limit', '20');
- if (!userResponse.ok)
- throw new Error('User search network response was not ok');
- const userData = await userResponse.json();
- setInputUsers(userData.result.users);
- } catch (error) {
- console.error('Error fetching input users:', error);
- } finally {
- setLoading((prev) => ({ ...prev, inputUsers: false }));
- }
- }, []);
+ if (cursor) {
+ castUrl.searchParams.append('cursor', cursor);
+ }
- const fetchInputChannels = useCallback(async (inputChannelId: string) => {
- if (loading.inputChannels || inputChannelId.length < 1) return;
+ if (username) {
+ castUrl.searchParams.append('author_fid', username);
+ }
- setLoading((prev) => ({ ...prev, inputChannels: true }));
- try {
- const channelUrl = new URL(
- 'https://api.neynar.com/v2/farcaster/channel/search'
- );
- channelUrl.searchParams.append('q', inputChannelId);
- channelUrl.searchParams.append('limit', '5');
+ if (channelId) {
+ castUrl.searchParams.append('channel_id', channelId);
+ }
- const channelResponse = await fetch(channelUrl, {
+ const castResponse = await fetch(castUrl, {
headers: {
Accept: 'application/json',
api_key: process.env.NEXT_PUBLIC_NEYNAR_API_KEY || '',
},
});
- if (!channelResponse.ok)
- throw new Error('Channel search network response was not ok');
- const channelData = await channelResponse.json();
- setInputChannels(channelData.channels);
+ if (!castResponse.ok)
+ throw new Error('Cast search network response was not ok');
+ const castData = await castResponse.json();
+ const newCasts = castData.result.casts;
+
+ setCasts((prevCasts) => [...prevCasts, ...newCasts]);
+ setCursor(castData.result.next?.cursor || null);
+ setHasMore(!!castData.result.next?.cursor);
} catch (error) {
- console.error('Error fetching input channels:', error);
+ console.error('Error fetching casts:', error);
} finally {
- setLoading((prev) => ({ ...prev, inputChannels: false }));
+ setLoading(false);
}
- }, []);
-
- const handleUsernameChange = useCallback(
- (e: React.ChangeEvent) => {
- const newUsername = e.target.value;
- setUsername(newUsername);
- if (newUsername.length > 0) {
- setShowUserDropdown(true);
- } else {
- setShowUserDropdown(false);
- setSearchParams((prev) => ({ ...prev, authorFid: '' }));
- }
- fetchInputUsers(newUsername);
- },
- [fetchInputUsers]
- );
-
- const handleUserSelect = useCallback((user: User) => {
- setUsername(user.username);
- setSearchParams((prev) => ({ ...prev, authorFid: user.fid.toString() }));
- setShowUserDropdown(false);
- }, []);
-
- const handleSearch = useCallback(
- (e: React.FormEvent) => {
- e.preventDefault();
- debouncedSearch();
- },
- [debouncedSearch]
- );
-
- // Set up Intersection Observers
- useEffect(() => {
- const userObserverOptions = {
- root: null,
- rootMargin: '0px',
- threshold: 1.0,
- };
-
- const castObserverOptions = {
- root: null,
- rootMargin: '0px',
- threshold: 1.0,
- };
-
- userObserverRef.current = new IntersectionObserver((entries) => {
- if (entries[0].isIntersecting) {
- fetchSearchUsers(false);
- }
- }, userObserverOptions);
-
- castObserverRef.current = new IntersectionObserver((entries) => {
- if (entries[0].isIntersecting) {
- fetchCasts(false);
- }
- }, castObserverOptions);
-
- return () => {
- if (userObserverRef.current) userObserverRef.current.disconnect();
- if (castObserverRef.current) castObserverRef.current.disconnect();
- };
- }, [fetchSearchUsers, fetchCasts]);
-
- // Observe last user and cast elements
- useEffect(() => {
- if (lastUserRef.current && userObserverRef.current) {
- userObserverRef.current.observe(lastUserRef.current);
- }
- if (lastCastRef.current && castObserverRef.current) {
- castObserverRef.current.observe(lastCastRef.current);
- }
- }, [searchUsers, casts]);
-
- // Trigger search when searchParams change
- useEffect(() => {
- debouncedSearch();
- }, [searchParams, debouncedSearch]);
-
- // Handle click outside of dropdown
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- dropdownRef.current &&
- !dropdownRef.current.contains(event.target as Node)
- ) {
- setShowUserDropdown(false);
- }
- };
-
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, []);
-
- const handleUsernameKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- if (inputUsers.length > 0) {
- handleUserSelect(inputUsers[0]);
- } else {
- handleSearch(e as any);
- }
- }
- },
- [inputUsers, handleUserSelect, handleSearch]
- );
-
- const handleChannelIdKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- handleSearch(e as any);
- }
- },
- [handleSearch]
- );
-
- // ... (rest of the existing functions)
-
- const handleChannelIdChange = useCallback(
- (e: React.ChangeEvent) => {
- const newChannelId = e.target.value;
- setChannelId(newChannelId);
- if (newChannelId.length > 0) {
- setShowChannelDropdown(true);
- } else {
- setShowChannelDropdown(false);
- setSearchParams((prev) => ({ ...prev, channelId: '' }));
- }
- fetchInputChannels(newChannelId);
- },
- [fetchInputChannels]
- );
-
- const handleChannelSelect = useCallback((channel: Channel) => {
- setChannelId(channel.name);
- setSearchParams((prev) => ({ ...prev, channelId: channel.id }));
- setShowChannelDropdown(false);
- }, []);
-
- // ... (rest of the existing useEffects)
+ };
- // Add a new useEffect for handling clicks outside the channel dropdown
useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- channelDropdownRef.current &&
- !channelDropdownRef.current.contains(event.target as Node)
- ) {
- setShowChannelDropdown(false);
- }
- };
+ setCasts([]);
+ setCursor(null);
+ setHasMore(true);
+ fetchCasts();
+ }, [searchQuery, username, channelId]);
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, []);
+ const handleShowMore = (identifier: string) => {
+ window.open(`/${identifier}`, '_blank', 'noopener,noreferrer');
+ };
return (
-