Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Commit

Permalink
add username for author fid
Browse files Browse the repository at this point in the history
  • Loading branch information
kevoconnell committed Aug 27, 2024
1 parent 8d152db commit 8a28e9d
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 18 deletions.
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@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-popover": "^1.1.1",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"axios": "^1.6.8",
Expand Down
148 changes: 130 additions & 18 deletions src/components/CastSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { NeynarCastCard } from '@neynar/react';
import { useRouter } from 'next/navigation';
import useSearchParamsWithoutSuspense from '@/hooks/useSearchParamsWithoutSuspense';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';

interface User {
fid: number;
username: string;
display_name: string;
pfp_url: string;
}

const CastSearch = ({ query }: { query: string }) => {
const params = useSearchParamsWithoutSuspense();
const router = useRouter();

const [username, setUsername] = useState('');
const [authorFid, setAuthorFid] = useState('');
const [channelId, setChannelId] = useState('');
const [casts, setCasts] = useState<any[]>([]);
const [cursor, setCursor] = useState('');
const [loading, setLoading] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const [showUserPopover, setShowUserPopover] = useState(false);

const usernameInputRef = useRef<HTMLInputElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);

// Initialize filters from URL params
useEffect(() => {
Expand All @@ -24,6 +42,52 @@ const CastSearch = ({ query }: { query: string }) => {
}
}, [params]);

const fetchUsers = useCallback(async (username: string) => {
if (username.length < 1) {
setUsers([]);
return;
}

try {
const response = await fetch(
`https://api.neynar.com/v2/farcaster/user/search?q=${username}`,
{
headers: {
Accept: 'application/json',
api_key: process.env.NEXT_PUBLIC_NEYNAR_API_KEY || '',
},
}
);

if (!response.ok) throw new Error('Network response was not ok');

const data = await response.json();
setUsers(data.result.users);
setShowUserPopover(true);
} catch (error) {
console.error('Error fetching users:', error);
}
}, []);

const handleUsernameChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newUsername = e.target.value;
setUsername(newUsername);
fetchUsers(newUsername);
},
[fetchUsers]
);

const handleUserSelect = useCallback((user: User) => {
setAuthorFid(user.fid.toString());
setUsername(user.username);
setShowUserPopover(false);
const newParams = new URLSearchParams();
if (channelId) newParams.append('channelId', channelId);
newParams.append('authorFid', user.fid.toString());
router.push(`/${query}?${newParams.toString()}`);
}, []);

const fetchCasts = useCallback(
async (newSearch: boolean = false) => {
setLoading(true);
Expand Down Expand Up @@ -79,16 +143,20 @@ const CastSearch = ({ query }: { query: string }) => {
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);

const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
const newParams = new URLSearchParams();
if (authorFid) newParams.append('authorFid', authorFid);
if (channelId) newParams.append('channelId', channelId);
router.push(`/${query}?${newParams.toString()}`);
setCasts([]);
setCursor('');
fetchCasts(true);
};
const handleSearch = useCallback(
(e: any) => {
e.preventDefault();
const newParams = new URLSearchParams();

if (authorFid) newParams.append('authorFid', authorFid);
if (channelId) newParams.append('channelId', channelId);
router.push(`/${query}?${newParams.toString()}`);
setCasts([]);
setCursor('');
fetchCasts(true);
},
[authorFid, channelId, query, router, fetchCasts]
);

// Initial fetch when component mounts
useEffect(() => {
Expand All @@ -97,17 +165,61 @@ const CastSearch = ({ query }: { query: string }) => {
}
}, []); // Empty dependency array ensures this only runs once on mount

// Handle click outside of popover
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node)
) {
setShowUserPopover(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

return (
<div className="w-full flex-1 items-center flex flex-col justify-center">
<form onSubmit={handleSearch} className="w-full max-w-4xl space-y-4 mb-8">
<div className="flex flex-row space-x-2">
<Input
type="number"
value={authorFid}
onChange={(e) => setAuthorFid(e.target.value)}
placeholder="Author FID (optional)"
className="w-full"
/>
<div className="relative w-full">
<Input
type="text"
value={username}
onChange={handleUsernameChange}
placeholder="author username (optional)"
className="w-full"
ref={usernameInputRef}
onFocus={() => setShowUserPopover(true)}
/>
{showUserPopover && (
<div
ref={popoverRef}
className="absolute z-10 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg"
>
<ul className="max-h-[300px] overflow-auto">
{users.map((user) => (
<li
key={user.fid}
className="p-2 hover:bg-gray-100 cursor-pointer"
onClick={() => handleUserSelect(user)}
>
<img
src={user.pfp_url}
alt={user.display_name}
className="w-8 h-8 rounded-full inline-block mr-2"
/>
{user.display_name} (@{user.username})
</li>
))}
</ul>
</div>
)}
</div>
<Input
type="text"
value={channelId}
Expand Down
31 changes: 31 additions & 0 deletions src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';

import { cn } from '@/lib/utils';

const Popover = PopoverPrimitive.Root;

const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverContent };

0 comments on commit 8a28e9d

Please sign in to comment.