feat: Search Bar Page Navigation
This commit is contained in:
parent
5adda68494
commit
ae7a366e2f
|
|
@ -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
|
||||
|
||||
|
|
|
|||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
102
src/components/admin-quick-search.tsx
Normal file
102
src/components/admin-quick-search.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="relative h-9 w-64 justify-start text-muted-foreground sm:pr-12"
|
||||
onClick={() => setOpen(true)}
|
||||
title="Search and jump to a page (Ctrl+K or ⌘K)"
|
||||
aria-label="Open quick search to go to any page"
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0" />
|
||||
<span className="truncate">Quick search pages…</span>
|
||||
<kbd className="pointer-events-none absolute right-1.5 top-1/2 hidden h-6 -translate-y-1/2 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
||||
<span className="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
</Button>
|
||||
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTitle className="sr-only">Go to page</DialogTitle>
|
||||
<CommandInput placeholder="Type a page name or what you want to do…" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No page matched. Try another word.</CommandEmpty>
|
||||
{groups.map((group, i) => (
|
||||
<Fragment key={group}>
|
||||
{i > 0 ? <CommandSeparator className="my-1" /> : null}
|
||||
<CommandGroup heading={group}>
|
||||
{grouped.get(group)?.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={`${item.title} ${item.description} ${item.path} ${item.group}`}
|
||||
onSelect={() => {
|
||||
navigate(item.path);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Icon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="font-medium leading-tight">
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="line-clamp-2 text-xs leading-snug text-muted-foreground">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</Fragment>
|
||||
))}
|
||||
</CommandList>
|
||||
<div className="border-t px-3 py-2 text-[11px] text-muted-foreground">
|
||||
<span className="font-medium text-foreground">Navigate:</span>{" "}
|
||||
<kbd className="rounded border bg-muted px-1 font-mono">↑</kbd>{" "}
|
||||
<kbd className="rounded border bg-muted px-1 font-mono">↓</kbd>{" "}
|
||||
to move ·{" "}
|
||||
<kbd className="rounded border bg-muted px-1 font-mono">↵</kbd> to
|
||||
open · Esc to close
|
||||
</div>
|
||||
</CommandDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
134
src/components/ui/command.tsx
Normal file
134
src/components/ui/command.tsx
Normal file
|
|
@ -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<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
const CommandDialog = ({ children, ...props }: ComponentProps<typeof Dialog>) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg max-w-xl [&>button]:hidden">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-2.5 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[min(420px,60vh)] overflow-y-auto overflow-x-hidden p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm text-muted-foreground"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-start gap-3 rounded-sm px-2 py-2 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandSeparator,
|
||||
}
|
||||
276
src/config/admin-search-routes.ts
Normal file
276
src/config/admin-search-routes.ts
Normal file
|
|
@ -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<string, AdminSearchRoute[]> {
|
||||
const map = new Map<string, AdminSearchRoute[]>()
|
||||
for (const r of routes) {
|
||||
const list = map.get(r.group) ?? []
|
||||
list.push(r)
|
||||
map.set(r.group, list)
|
||||
}
|
||||
const ordered = new Map<string, AdminSearchRoute[]>()
|
||||
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
|
||||
}
|
||||
|
|
@ -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<User | null>(() => {
|
||||
|
|
@ -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<HTMLInputElement>) => {
|
||||
setSearchQuery(e.target.value);
|
||||
};
|
||||
|
||||
const handleNotificationClick = () => {
|
||||
navigate("/notifications");
|
||||
};
|
||||
|
|
@ -205,15 +189,7 @@ export function AppShell() {
|
|||
<header className="h-16 border-b bg-background flex items-center justify-between px-6">
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-4">
|
||||
<form onSubmit={handleSearch} className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Quick Search..."
|
||||
className="pl-10 w-64"
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</form>
|
||||
<AdminQuickSearch />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ import axios, {
|
|||
type InternalAxiosRequestConfig,
|
||||
} from "axios";
|
||||
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_BACKEND_API_URL || "http://localhost:3001";
|
||||
const useDevApiProxy =
|
||||
import.meta.env.DEV && import.meta.env.VITE_USE_API_PROXY === "true";
|
||||
|
||||
const API_BASE_URL = useDevApiProxy
|
||||
? "/api"
|
||||
: import.meta.env.VITE_BACKEND_API_URL || "http://localhost:3001";
|
||||
|
||||
interface RetryableAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||
_retry?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,37 +1,61 @@
|
|||
import { defineConfig } from "vite"
|
||||
import { defineConfig, loadEnv } from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import path from "node:path"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
|
||||
'ui-vendor': ['@radix-ui/react-avatar', '@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu', '@radix-ui/react-select'],
|
||||
'chart-vendor': ['recharts'],
|
||||
'query-vendor': ['@tanstack/react-query'],
|
||||
},
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "")
|
||||
const useApiProxy = env.VITE_USE_API_PROXY === "true"
|
||||
const proxyTarget =
|
||||
env.VITE_PROXY_TARGET ||
|
||||
env.VITE_BACKEND_API_URL ||
|
||||
"http://localhost:3001"
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
strictPort: false,
|
||||
host: true,
|
||||
},
|
||||
preview: {
|
||||
port: 4173,
|
||||
strictPort: false,
|
||||
host: true,
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
"react-vendor": ["react", "react-dom", "react-router-dom"],
|
||||
"ui-vendor": [
|
||||
"@radix-ui/react-avatar",
|
||||
"@radix-ui/react-dialog",
|
||||
"@radix-ui/react-dropdown-menu",
|
||||
"@radix-ui/react-select",
|
||||
],
|
||||
"chart-vendor": ["recharts"],
|
||||
"query-vendor": ["@tanstack/react-query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
strictPort: false,
|
||||
host: true,
|
||||
...(useApiProxy && {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: proxyTarget.replace(/\/$/, ""),
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
rewrite: (p) => p.replace(/^\/api/, "") || "/",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
preview: {
|
||||
port: 4173,
|
||||
strictPort: false,
|
||||
host: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user