diff --git a/src/components/features/tool-launcher.tsx b/src/components/features/tool-launcher.tsx new file mode 100644 index 0000000..224330d --- /dev/null +++ b/src/components/features/tool-launcher.tsx @@ -0,0 +1,386 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import dynamic from "next/dynamic"; +import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react"; +import { useMemo, useState } from "react"; +import { ChevronUp, Grid2X2, List, Search, Star, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { groupRoutes, routeItems } from "@/lib/routes"; +import { cn } from "@/lib/utils"; +import { useUiStore } from "@/stores/ui-store"; + +const FavoriteToolsDnd = dynamic( + () => import("@/components/features/favorite-tools-dnd").then((mod) => mod.FavoriteToolsDnd), + { ssr: false }, +); + +const jupiterGroups = [ + "Email", + "Files", + "Domains", + "Databases", + "Security", + "Software", + "Metrics", + "Advanced", + "Developer Tools", + "Backups", + "Automation", + "Users", + "Resellers", + "Preferences", + "Server Administration", + "Notifications", + "Audit", + "Support", +]; + +export function ToolLauncher() { + const [query, setQuery] = useState(""); + const [category, setCategory] = useState("All"); + const [mode, setMode] = useState<"grid" | "list">("grid"); + const [railWidth, setRailWidth] = useState(292); + const favorites = useUiStore((state) => state.favorites); + const toggleFavorite = useUiStore((state) => state.toggleFavorite); + + const grouped = useMemo(() => groupRoutes(routeItems), []); + const categories = ["All", ...Object.keys(grouped)]; + const filtered = useMemo(() => { + const term = query.toLowerCase(); + return routeItems.filter((item) => { + const matchesCategory = category === "All" || item.category === category; + const matchesSearch = [ + item.label, + item.description, + item.category, + ...item.keywords, + ].some((value) => value.toLowerCase().includes(term)); + return matchesCategory && matchesSearch; + }); + }, [category, query]); + + function startRailResize(event: ReactPointerEvent) { + const startX = event.clientX; + const startWidth = railWidth; + event.currentTarget.setPointerCapture(event.pointerId); + + function onMove(moveEvent: globalThis.PointerEvent) { + const next = Math.min(430, Math.max(250, startWidth - (moveEvent.clientX - startX))); + setRailWidth(next); + } + + function onUp() { + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + } + + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp); + } + + return ( +
+
+
+
+ + setQuery(event.target.value)} + className="h-8 rounded-md pl-8" + placeholder="Search all CoaxysPanel tools" + /> +
+
+ + +
+
+ +
+ {categories.map((item) => ( + + ))} +
+ + {!query && category === "All" ? ( + + + Favorites + Drag to reorder your preferred tools. + + + + + + ) : null} + + {query || category !== "All" ? ( + + ) : ( + jupiterGroups + .map((group) => [group, grouped[group] ?? []] as const) + .filter(([, tools]) => tools.length) + .map(([group, tools]) => ( + + )) + )} +
+ +
+ + +
+ ); +} + +function ToolResults({ + tools, + mode, + favorites, + onToggleFavorite, +}: { + tools: typeof routeItems; + mode: "grid" | "list"; + favorites: string[]; + onToggleFavorite: (path: string) => void; +}) { + return ( +
+ {tools.map((tool) => ( + onToggleFavorite(tool.path)} + /> + ))} +
+ ); +} + +function ToolCategory({ + title, + tools, + favorites, + onToggleFavorite, +}: { + title: string; + tools: typeof routeItems; + favorites: string[]; + onToggleFavorite: (path: string) => void; +}) { + const HeaderIcon = tools[0]?.icon ?? Grid2X2; + return ( + + + + + {title} + + + + +
+ {tools.map((tool) => { + const Icon = tool.icon; + const favorite = favorites.includes(tool.path); + return ( +
+ + + + + + {tool.label} + + +
+ ); + })} +
+
+
+ ); +} + +function ToolCard({ + tool, + favorite, + compact, + onToggleFavorite, +}: { + tool: (typeof routeItems)[number]; + favorite: boolean; + compact?: boolean; + onToggleFavorite: () => void; +}) { + const Icon = tool.icon; + return ( + + +
+ + + + + + {tool.label} + + {tool.description} + + + + +
+ {!compact ? ( +
+ {tool.category} + + {tool.danger ? "confirmation" : "allowed"} + +
+ ) : null} +
+
+ ); +}