From 3ba462d7979110b85e540312ea699bfc12da43ab Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Fri, 21 Feb 2025 19:27:54 +0700 Subject: [PATCH] add floating navbar --- .../app/protected/(admin-pages)/layout.tsx | 76 +++-- .../components/action-search-bar.tsx | 314 +++++++++--------- .../components/floating-action-search-bar.tsx | 60 ++++ 3 files changed, 257 insertions(+), 193 deletions(-) create mode 100644 sigap-website/components/floating-action-search-bar.tsx diff --git a/sigap-website/app/protected/(admin-pages)/layout.tsx b/sigap-website/app/protected/(admin-pages)/layout.tsx index 3c2785f..6e219b6 100644 --- a/sigap-website/app/protected/(admin-pages)/layout.tsx +++ b/sigap-website/app/protected/(admin-pages)/layout.tsx @@ -23,7 +23,7 @@ import { import { MoreHorizontal } from "lucide-react"; import { InboxDrawer } from "@/components/inbox-drawer"; import { ThemeSwitcher } from "@/components/theme-switcher"; -import ActionSearchBar from "@/components/action-search-bar"; +import FloatingActionSearchBar from "@/components/floating-action-search-bar"; import { Separator } from "@/components/ui/separator"; export default async function Layout({ @@ -37,47 +37,45 @@ export default async function Layout({ {/* Navigation bar with SidebarTrigger and Breadcrumbs */} - {/* Header with action bar and other controls */} -
-
- -
- -
- - - - - - - - Settings - Help - About - - -
-
+ {/* Header with other controls */} +
+ {children}
diff --git a/sigap-website/components/action-search-bar.tsx b/sigap-website/components/action-search-bar.tsx index 575e9b9..f5724e6 100644 --- a/sigap-website/components/action-search-bar.tsx +++ b/sigap-website/components/action-search-bar.tsx @@ -1,8 +1,6 @@ "use client" -import type React from "react" - -import { useState, useEffect } from "react" +import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react" import { Input } from "@/components/ui/input" import { motion, AnimatePresence } from "framer-motion" import { Search, Send, BarChart2, Globe, Video, PlaneTakeoff, AudioLines } from "lucide-react" @@ -64,171 +62,179 @@ const allActions = [ }, ] -function ActionSearchBar({ actions = allActions }: { actions?: Action[] }) { - const [query, setQuery] = useState("") - const [result, setResult] = useState(null) - const [isFocused, setIsFocused] = useState(false) - const [isTyping, setIsTyping] = useState(false) - const [selectedAction, setSelectedAction] = useState(null) - const debouncedQuery = useDebounce(query, 200) +interface ActionSearchBarProps { + actions?: Action[] + autoFocus?: boolean + isFloating?: boolean +} - useEffect(() => { - if (!isFocused) { - setResult(null) - return +const ActionSearchBar = forwardRef( + ({ actions = allActions, autoFocus = false, isFloating = false }, ref) => { + const [query, setQuery] = useState("") + const [result, setResult] = useState(null) + const [isFocused, setIsFocused] = useState(autoFocus) + const [selectedAction, setSelectedAction] = useState(null) + const debouncedQuery = useDebounce(query, 200) + const inputRef = React.useRef(null) + + useImperativeHandle(ref, () => inputRef.current as HTMLInputElement) + + useEffect(() => { + if (autoFocus && inputRef.current) { + inputRef.current.focus() + } + }, [autoFocus]) + + useEffect(() => { + if (!debouncedQuery) { + setResult({ actions: allActions }) + return + } + + const normalizedQuery = debouncedQuery.toLowerCase().trim() + const filteredActions = allActions.filter((action) => { + const searchableText = action.label.toLowerCase() + return searchableText.includes(normalizedQuery) + }) + + setResult({ actions: filteredActions }) + }, [debouncedQuery]) + + const handleInputChange = (e: React.ChangeEvent) => { + setQuery(e.target.value) } - if (!debouncedQuery) { - setResult({ actions: allActions }) - return - } - - const normalizedQuery = debouncedQuery.toLowerCase().trim() - const filteredActions = allActions.filter((action) => { - const searchableText = action.label.toLowerCase() - return searchableText.includes(normalizedQuery) - }) - - setResult({ actions: filteredActions }) - }, [debouncedQuery, isFocused]) - - const handleInputChange = (e: React.ChangeEvent) => { - setQuery(e.target.value) - setIsTyping(true) - } - - const container = { - hidden: { opacity: 0, height: 0 }, - show: { - opacity: 1, - height: "auto", - transition: { - height: { - duration: 0.4, + const container = { + hidden: { opacity: 0, height: 0 }, + show: { + opacity: 1, + height: "auto", + transition: { + height: { + duration: 0.4, + }, + staggerChildren: 0.1, }, - staggerChildren: 0.1, }, - }, - exit: { - opacity: 0, - height: 0, - transition: { - height: { + exit: { + opacity: 0, + height: 0, + transition: { + height: { + duration: 0.3, + }, + opacity: { + duration: 0.2, + }, + }, + }, + } + + const item = { + hidden: { opacity: 0, y: 20 }, + show: { + opacity: 1, + y: 0, + transition: { duration: 0.3, }, - opacity: { + }, + exit: { + opacity: 0, + y: -10, + transition: { duration: 0.2, }, }, - }, - } + } - const item = { - hidden: { opacity: 0, y: 20 }, - show: { - opacity: 1, - y: 0, - transition: { - duration: 0.3, - }, - }, - exit: { - opacity: 0, - y: -10, - transition: { - duration: 0.2, - }, - }, - } - - const handleFocus = () => { - setSelectedAction(null) - setIsFocused(true) - } - - return ( -
-
- setTimeout(() => setIsFocused(false), 200)} - className="pl-3 pr-9 py-1.5 h-9 text-sm rounded-lg focus-visible:ring-offset-0" - /> -
- - {query.length > 0 ? ( - - - - ) : ( - - - - )} - -
-
- - - {isFocused && result && !selectedAction && ( - - - {result.actions.map((action) => ( - setSelectedAction(action)} + return ( +
+
+ setIsFocused(true)} + onBlur={() => !isFloating && setTimeout(() => setIsFocused(false), 200)} + className="pl-3 pr-9 py-1.5 h-9 text-sm rounded-lg focus-visible:ring-offset-0" + /> +
+ + {query.length > 0 ? ( + -
-
- {action.icon} - {action.label} - {action.description} + + + ) : ( + + + + )} + +
+
+ + + {(isFocused || isFloating) && result && !selectedAction && ( + + + {result.actions.map((action) => ( + setSelectedAction(action)} + > +
+
+ {action.icon} + {action.label} + {action.description} +
-
-
- {action.short} - {action.end} -
- - ))} - -
-
- Press ⌘K to open commands - ESC to cancel +
+ {action.short} + {action.end} +
+ + ))} + +
+
+ Press ⌘K to open commands + ESC to cancel +
-
- - )} - -
- ) -} + + )} + +
+ ) + }, +) + +ActionSearchBar.displayName = "ActionSearchBar" export default ActionSearchBar diff --git a/sigap-website/components/floating-action-search-bar.tsx b/sigap-website/components/floating-action-search-bar.tsx new file mode 100644 index 0000000..8923364 --- /dev/null +++ b/sigap-website/components/floating-action-search-bar.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import ActionSearchBar from "@/components/action-search-bar"; + +export default function FloatingActionSearchBar() { + const [isOpen, setIsOpen] = useState(false); + const searchBarRef = useRef(null); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key === "k") { + event.preventDefault(); + setIsOpen((prev) => !prev); + } else if (event.key === "Escape") { + setIsOpen(false); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + useEffect(() => { + if (isOpen && searchBarRef.current) { + searchBarRef.current.focus(); + } + }, [isOpen]); + + return ( + + {isOpen && ( + setIsOpen(false)} + > + e.stopPropagation()} + > + + + + )} + + ); +}