From ae7a366e2f5b3676c88a0660997523b9eeca26bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ckirukib=E2=80=9D?= <“kirubeljkl679@gmail.com”> Date: Wed, 15 Apr 2026 10:22:09 +0300 Subject: [PATCH] feat: Search Bar Page Navigation --- .env.example | 4 + package-lock.json | 17 ++ package.json | 1 + src/components/admin-quick-search.tsx | 102 ++++++++++ src/components/ui/command.tsx | 134 +++++++++++++ src/config/admin-search-routes.ts | 276 ++++++++++++++++++++++++++ src/layouts/app-shell.tsx | 30 +-- src/services/api/client.ts | 8 +- vite.config.ts | 84 +++++--- 9 files changed, 597 insertions(+), 59 deletions(-) create mode 100644 src/components/admin-quick-search.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/config/admin-search-routes.ts diff --git a/.env.example b/.env.example index 2fe8f68..30414d7 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ # Backend API Configuration VITE_BACKEND_API_URL=http://localhost:3000/api/v1 +# Local dev: avoid CORS by proxying /api -> API (set VITE_USE_API_PROXY=true and restart dev server) +# VITE_USE_API_PROXY=true +# VITE_PROXY_TARGET=https://api.yaltopiaticket.com + # Environment VITE_ENV=development diff --git a/package-lock.json b/package-lock.json index 0d39949..175f7d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.561.0", "react": "^19.2.4", @@ -4816,6 +4817,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/package.json b/package.json index bd50229..f42d983 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.561.0", "react": "^19.2.4", diff --git a/src/components/admin-quick-search.tsx b/src/components/admin-quick-search.tsx new file mode 100644 index 0000000..d37f045 --- /dev/null +++ b/src/components/admin-quick-search.tsx @@ -0,0 +1,102 @@ +import { Fragment, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import { DialogTitle } from "@/components/ui/dialog"; +import { + ADMIN_SEARCH_ROUTES, + groupAdminSearchRoutes, +} from "@/config/admin-search-routes"; + +export function AdminQuickSearch() { + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((o) => !o); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + const grouped = groupAdminSearchRoutes(ADMIN_SEARCH_ROUTES); + const groups = [...grouped.keys()]; + + return ( + <> + + + + Go to page + + + No page matched. Try another word. + {groups.map((group, i) => ( + + {i > 0 ? : null} + + {grouped.get(group)?.map((item) => { + const Icon = item.icon; + return ( + { + navigate(item.path); + setOpen(false); + }} + > + +
+ + {item.title} + + + {item.description} + +
+
+ ); + })} +
+
+ ))} +
+
+ Navigate:{" "} + {" "} + {" "} + to move ·{" "} + to + open · Esc to close +
+
+ + ); +} diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..5d60b36 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,134 @@ +import * as React from "react" +import type { ComponentProps } from "react" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: ComponentProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandSeparator, +} diff --git a/src/config/admin-search-routes.ts b/src/config/admin-search-routes.ts new file mode 100644 index 0000000..82cff7b --- /dev/null +++ b/src/config/admin-search-routes.ts @@ -0,0 +1,276 @@ +import type { LucideIcon } from "lucide-react" +import { + LayoutDashboard, + Bell, + Receipt, + FileSearch, + ClipboardList, + CreditCard, + FileClock, + Users, + FileText, + Settings, + Wrench, + Megaphone, + Activity, + Shield, + BarChart3, + Heart, + AlertTriangle, + Key, + Gauge, + DollarSign, + HardDrive, +} from "lucide-react" + +export interface AdminSearchRoute { + id: string + path: string + title: string + /** Sub-heading style line shown under the title */ + description: string + group: string + icon: LucideIcon +} + +export const ADMIN_SEARCH_ROUTES: AdminSearchRoute[] = [ + { + id: "dashboard", + path: "/admin/dashboard", + title: "Dashboard", + description: + "High-level metrics, recent invoices, and platform status at a glance.", + group: "Overview", + icon: LayoutDashboard, + }, + { + id: "notifications", + path: "/notifications", + title: "Notifications", + description: + "System alerts and messages for your account; mark read or filter.", + group: "Overview", + icon: Bell, + }, + { + id: "invoices", + path: "/admin/invoices", + title: "Invoices", + description: "Browse, search, and manage issued invoices.", + group: "Commerce", + icon: Receipt, + }, + { + id: "proforma", + path: "/admin/proforma", + title: "Proforma", + description: "View and manage proforma invoices and drafts.", + group: "Commerce", + icon: FileSearch, + }, + { + id: "proforma-requests", + path: "/admin/proforma-requests", + title: "Proforma requests", + description: "Review and process incoming proforma requests.", + group: "Commerce", + icon: ClipboardList, + }, + { + id: "payments", + path: "/admin/payments", + title: "Payments", + description: "Recorded payments and transaction history.", + group: "Commerce", + icon: CreditCard, + }, + { + id: "payment-requests", + path: "/admin/payment-requests", + title: "Payment requests", + description: "Pending and processed payment requests.", + group: "Commerce", + icon: FileClock, + }, + { + id: "users", + path: "/admin/users", + title: "Users", + description: "Search, create, import, and manage user accounts.", + group: "People & activity", + icon: Users, + }, + { + id: "logs", + path: "/admin/logs", + title: "Activity log", + description: "User activity and system events across the platform.", + group: "People & activity", + icon: FileText, + }, + { + id: "settings", + path: "/admin/settings", + title: "Settings", + description: "System configuration, keys, and admin preferences.", + group: "Operations", + icon: Settings, + }, + { + id: "maintenance", + path: "/admin/maintenance", + title: "Maintenance", + description: "Enable maintenance mode and set user-facing messages.", + group: "Operations", + icon: Wrench, + }, + { + id: "announcements", + path: "/admin/announcements", + title: "Announcements", + description: "Create and broadcast announcements to users.", + group: "Operations", + icon: Megaphone, + }, + { + id: "audit", + path: "/admin/audit", + title: "Audit", + description: "Immutable audit trail of sensitive admin actions.", + group: "Operations", + icon: Activity, + }, + { + id: "security-hub", + path: "/admin/security", + title: "Security", + description: "Hub for sessions, API keys, failed logins, and limits.", + group: "Security", + icon: Shield, + }, + { + id: "failed-logins", + path: "/admin/security/failed-logins", + title: "Failed logins", + description: "View and manage failed login attempts.", + group: "Security", + icon: AlertTriangle, + }, + { + id: "suspicious", + path: "/admin/security/suspicious", + title: "Suspicious activity", + description: "Monitor suspicious IPs and emails.", + group: "Security", + icon: Shield, + }, + { + id: "api-keys", + path: "/admin/security/api-keys", + title: "API keys", + description: "Manage API keys and tokens.", + group: "Security", + icon: Key, + }, + { + id: "rate-limits", + path: "/admin/security/rate-limits", + title: "Rate limits", + description: "View rate limit violations and abuse patterns.", + group: "Security", + icon: Gauge, + }, + { + id: "sessions", + path: "/admin/security/sessions", + title: "Active sessions", + description: "Manage active user sessions across devices.", + group: "Security", + icon: Users, + }, + { + id: "analytics-hub", + path: "/admin/analytics", + title: "Analytics", + description: "Hub for charts, growth, revenue, and API usage.", + group: "Analytics", + icon: BarChart3, + }, + { + id: "analytics-overview", + path: "/admin/analytics/overview", + title: "Performance overview", + description: "Platform analytics overview and KPIs.", + group: "Analytics", + icon: BarChart3, + }, + { + id: "analytics-users", + path: "/admin/analytics/users", + title: "User dynamics", + description: "User growth and statistics over time.", + group: "Analytics", + icon: Users, + }, + { + id: "analytics-revenue", + path: "/admin/analytics/revenue", + title: "Revenue streams", + description: "Revenue trends and breakdowns.", + group: "Analytics", + icon: DollarSign, + }, + { + id: "analytics-storage", + path: "/admin/analytics/storage", + title: "Resource allocation", + description: "Storage usage and breakdown by user or resource.", + group: "Analytics", + icon: HardDrive, + }, + { + id: "analytics-api", + path: "/admin/analytics/api", + title: "API operations", + description: "API endpoint usage and traffic statistics.", + group: "Analytics", + icon: Activity, + }, + { + id: "health", + path: "/admin/health", + title: "System health", + description: "Service health checks, version, and diagnostics.", + group: "Operations", + icon: Heart, + }, +] + +const GROUP_ORDER = [ + "Overview", + "Commerce", + "People & activity", + "Operations", + "Security", + "Analytics", +] + +export function groupAdminSearchRoutes( + routes: AdminSearchRoute[], +): Map { + const map = new Map() + for (const r of routes) { + const list = map.get(r.group) ?? [] + list.push(r) + map.set(r.group, list) + } + const ordered = new Map() + for (const g of GROUP_ORDER) { + const list = map.get(g) + if (list?.length) ordered.set(g, list) + } + for (const [g, list] of map) { + if (!ordered.has(g)) ordered.set(g, list) + } + return ordered +} diff --git a/src/layouts/app-shell.tsx b/src/layouts/app-shell.tsx index 019c9b4..2eaa5ff 100644 --- a/src/layouts/app-shell.tsx +++ b/src/layouts/app-shell.tsx @@ -1,5 +1,5 @@ -import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"; import { useState } from "react"; +import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"; import { LayoutDashboard, Users, @@ -11,7 +11,6 @@ import { BarChart3, Activity, Heart, - Search, Bell, LogOut, CreditCard, @@ -21,7 +20,7 @@ import { ClipboardList, } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { AdminQuickSearch } from "@/components/admin-quick-search"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, @@ -33,7 +32,6 @@ import { } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { authService } from "@/services"; -import { toast } from "sonner"; interface User { email: string; @@ -71,7 +69,6 @@ const adminNavigationItems = [ export function AppShell() { const location = useLocation(); const navigate = useNavigate(); - const [searchQuery, setSearchQuery] = useState(""); // Initialize user from localStorage const [user] = useState(() => { @@ -98,19 +95,6 @@ export function AppShell() { navigate("/login", { replace: true }); }; - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - if (searchQuery.trim()) { - const currentPath = location.pathname; - navigate(`${currentPath}?search=${encodeURIComponent(searchQuery)}`); - toast.success(`Searching for: ${searchQuery}`); - } - }; - - const handleSearchChange = (e: React.ChangeEvent) => { - setSearchQuery(e.target.value); - }; - const handleNotificationClick = () => { navigate("/notifications"); }; @@ -205,15 +189,7 @@ export function AppShell() {
-
- - - +