import { Link, useNavigate } from "@tanstack/react-router"; import { ArrowRight, Search, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { place } from "streamplace"; import useAvatars from "../hooks/use-avatars"; import { useSession } from "../lib/session"; import { useStore } from "../lib/store"; import { usePDSAgent, useSiteTitle, useUserProfile } from "../lib/store/hooks"; import StreamplaceSvg from "./svg/streamplace-bw"; import { Button } from "./ui/button"; import { SidebarTrigger } from "./ui/sidebar"; interface SearchResult { did: string; handle: string; displayName?: string; } export default function Header() { const { t } = useTranslation("common"); const { state } = useSession(); const userProfile = useUserProfile(); const siteTitle = useSiteTitle(); useAvatars(state.status === "authenticated" ? [state.session.did] : []); const did = state.status === "authenticated" ? state.session.did : null; const avatar = userProfile?.avatar; const handle = userProfile?.handle; const displayName = userProfile?.displayName || handle; const agent = usePDSAgent(); const navigate = useNavigate(); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [open, setOpen] = useState(false); const [highlightIndex, setHighlightIndex] = useState(-1); const timeoutRef = useRef | null>(null); const inputRef = useRef(null); const resultsRef = useRef(null); const containerRef = useRef(null); const resultDids = results.map((r) => r.did); const avatars = useAvatars(resultDids); const searchActors = useCallback( async (q: string) => { if (!agent || !q.trim()) { setResults([]); return; } try { setSearching(true); const response = await agent.client.call( place.stream.live.searchActorsTypeahead, { q, limit: 8, }, ); setResults( response.actors.map((a: any) => ({ did: a.did, handle: a.handle, displayName: a.displayName, })), ); } catch (error) { console.error("Search failed:", error); setResults([]); } finally { setSearching(false); } }, [agent], ); const handleChange = (value: string) => { setQuery(value); setHighlightIndex(-1); if (timeoutRef.current) clearTimeout(timeoutRef.current); if (value.trim()) { setOpen(true); timeoutRef.current = setTimeout(() => searchActors(value), 250); } else { setResults([]); setOpen(false); } }; const selectResult = useCallback( (actor: SearchResult) => { setQuery(""); setResults([]); setOpen(false); navigate({ to: "/$user", params: { user: actor.handle } }); }, [navigate], ); const handleKeyDown = (e: React.KeyboardEvent) => { if (!open) return; if (e.key === "ArrowDown") { e.preventDefault(); setHighlightIndex((i) => Math.min(i + 1, results.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setHighlightIndex((i) => Math.max(i - 1, -1)); } else if (e.key === "Enter") { e.preventDefault(); if (highlightIndex >= 0 && results[highlightIndex]) { selectResult(results[highlightIndex]); } } else if (e.key === "Escape") { setOpen(false); inputRef.current?.blur(); } }; // Close on outside click. useEffect(() => { if (!open) return; const onClick = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { setOpen(false); } }; document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); }, [open]); // Scroll highlighted item into view. useEffect(() => { if (highlightIndex < 0 || !resultsRef.current) return; const el = resultsRef.current.children[highlightIndex] as HTMLElement; el?.scrollIntoView({ block: "nearest" }); }, [highlightIndex]); const clearSearch = () => { setQuery(""); setResults([]); setOpen(false); inputRef.current?.focus(); }; return ( <>
{/* logo */}

{siteTitle}

{/* search bar */}
handleChange(e.target.value)} onKeyDown={handleKeyDown} onFocus={() => { if (query.trim() && results.length > 0) setOpen(true); }} placeholder={t("search-placeholder")} className="focus:ring-ring h-8 w-full rounded-md border border-(--color-border) bg-(--color-bg) pr-8 pl-8 text-sm text-(--color-fg) placeholder:text-(--color-fg-muted) focus:ring-1 focus:outline-none" /> {query && ( )}
{/* results dropdown */} {open && (results.length > 0 || searching) && (
{searching && results.length === 0 && (
)} {results.map((actor, i) => ( ))}
setOpen(false)} className="flex items-center gap-2 py-2 text-center text-sm text-(--color-accent) transition-colors hover:bg-(--color-bg-overlay)" > {t("more-results")}{" "}
)}
); } export function UserProfile({ did, displayName, t, handle, avatar, }: { did: string | null; displayName: string | undefined; t: any; handle: string | undefined; avatar: string | undefined; }) { return (
); }