diff --git a/components/Navbar.tsx b/components/Navbar.tsx index d61eb73..dccbdac 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -3,12 +3,16 @@ import Image from "next/image"; import Link from "next/link"; import { motion } from "framer-motion"; +import { useState } from "react"; +import { Menu, X } from "lucide-react"; import { useScrollHiddenNav } from "@/hooks/useScrollHiddenNav"; import ToggleButton from "@/components/ui/ToggleButton"; import ConnectBtn from "./ui/ConnectBtn"; +import { SearchModal } from "./ui/SearchModal"; export default function Navbar() { const isVisible = useScrollHiddenNav(); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); return ( -
+ +
+
+ {}} inline /> +
+
+
+ +
+ + +
+ + {isMobileMenuOpen && ( +
+
+
+
+ setIsMobileMenuOpen(false)} + mobile + /> +
+
+
+ +
+
+
+ )}
); } diff --git a/components/ui/SearchModal.tsx b/components/ui/SearchModal.tsx new file mode 100644 index 0000000..f879a98 --- /dev/null +++ b/components/ui/SearchModal.tsx @@ -0,0 +1,180 @@ +"use client"; + +import React, { useRef, useState, useCallback, useEffect } from "react"; +import { X, Search, CornerDownLeft } from "lucide-react"; +import { useRouter } from "next/navigation"; + +interface SearchModalProps { + isOpen: boolean; + onClose: () => void; + mobile?: boolean; + inline?: boolean; +} + +export function SearchModal({ + isOpen, + onClose, + mobile = false, + inline = false, +}: SearchModalProps) { + // useRef for debounce timer — no re-renders + const debounceRef = useRef | null>(null); + // useRef for input element (uncontrolled) — no re-render on every keystroke + const inputRef = useRef(null); + // Only ONE piece of state: whether input has content (drives the arrow button visibility) + const [hasValue, setHasValue] = useState(false); + const router = useRouter(); + + // Clean up timer on unmount + useEffect(() => { + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, []); + + const handleChange = useCallback(() => { + const value = inputRef.current?.value ?? ""; + + // Update arrow button visibility (single boolean — cheap) + setHasValue(value.trim().length > 0); + + // Debounce the search API call using ref, not state + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + const trimmed = value.trim(); + if (trimmed) { + console.log("Debounced search term:", trimmed); + // TODO: Add elasticsearch fetch here + } + }, 300); + }, []); + + const handleSubmit = useCallback( + (e?: React.FormEvent) => { + e?.preventDefault(); + const value = inputRef.current?.value.trim(); + if (!value) return; + router.push(`/${value}`); + onClose(); + if (inputRef.current) inputRef.current.value = ""; + setHasValue(false); + }, + [router, onClose] + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Escape") onClose(); + if (e.key === "Enter") handleSubmit(); + }, + [onClose, handleSubmit] + ); + + const searchInput = ( +
+ {/* Search icon */} + + + {/* Uncontrolled input — no value prop, no re-render per keystroke */} + + + {/* Enter / submit arrow button — visible only when input has content */} + + + ); + + // ── Inline / mobile variant ────────────────────────────────────────────── + if (mobile || inline) { + return
{searchInput}
; + } + + if (!isOpen) return null; + + // ── Modal variant ──────────────────────────────────────────────────────── + return ( +
+
e.stopPropagation()} + > + {/* Close button */} + + + {/* Header */} +
+

+ Search Identity +

+

+ Search by username or Token ID +

+
+ + {/* Search input (with built-in arrow submit button) */} + {searchInput} + + {/* Hint */} +

+ Press{" "} + + Enter + {" "} + or click{" "} + + + {" "} + to search +

+
+
+ ); +}