From cb2029ea972942120ea3f0d5ae9466a3c17baa4b Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 18 Aug 2026 02:05:36 -0400 Subject: [PATCH] feat(command-palette): register page-specific commands across UI Wire CommandPalette/Provider into the frontend layout and add Cmd+K hint in the SiteHeader. Register context-aware commands for banking, helpdesk, mission RSVP, locker, shipments, and market pages. Also removes the locker search's duplicate Cmd+K listener (now handled globally), renames its button to 'Search Locker', and cleans up a flappy page parenthetical. --- src/app/(frontend)/flappy/page.tsx | 2 +- src/app/(frontend)/layout.tsx | 45 +++++++++++----- src/components/frontend/SiteHeader.tsx | 5 ++ .../frontend/banking/BankingOverview.tsx | 25 +++++++-- .../frontend/helpdesk/TicketsView.tsx | 17 ++++++- .../intelligence/MissionAttendance.tsx | 34 +++++++++++++ .../frontend/locker/LockerSearch.tsx | 18 +------ src/components/frontend/locker/LockerView.tsx | 13 +++++ .../frontend/logistics/ShipmentsList.tsx | 51 +++++++++++++++++++ src/components/frontend/market/MarketView.tsx | 22 +++++++- src/payload-types.ts | 2 + 11 files changed, 196 insertions(+), 38 deletions(-) diff --git a/src/app/(frontend)/flappy/page.tsx b/src/app/(frontend)/flappy/page.tsx index f181efc..bf484f2 100644 --- a/src/app/(frontend)/flappy/page.tsx +++ b/src/app/(frontend)/flappy/page.tsx @@ -52,7 +52,7 @@ export default async function FlappyPage() {

Flight Simulator

- An unofficial side activity. (Definitely not Flappy Bird.) Earn 1 XP per 15 obstacles + An unofficial side activity. Earn 1 XP per 15 obstacles passed in a single run.

diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 3edb259..5bf69a7 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -14,6 +14,9 @@ import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts"; import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime"; import { LandingPage } from "@/components/frontend/LandingPage"; import { Toaster } from "@/components/ui/sonner"; +import { CommandPalette } from "@/components/command-palette/CommandPalette"; +import { CommandPaletteProvider } from "@/components/command-palette/CommandPaletteContext"; +import { KeyboardShortcutsProvider } from "@/components/command-palette/KeyboardShortcutsProvider"; // Skip static prerendering of this layout (and all pages under (frontend)/). // The layout calls `getPayload()` at render time to resolve the current user, @@ -93,22 +96,36 @@ export default async function RootLayout(props: { children: React.ReactNode }) { {user ? ( - - - - {children} - + > + + + + + {children} + + + + ) : ( diff --git a/src/components/frontend/SiteHeader.tsx b/src/components/frontend/SiteHeader.tsx index 5a9555f..469552a 100644 --- a/src/components/frontend/SiteHeader.tsx +++ b/src/components/frontend/SiteHeader.tsx @@ -1,12 +1,17 @@ import { SidebarTrigger } from "@/components/ui/sidebar"; import { Separator } from "@/components/ui/separator"; import { NotificationsBell } from "@/components/frontend/notifications/NotificationsBell"; +import { Command } from "lucide-react"; export const SiteHeader = () => { return (
+
+ + K +
diff --git a/src/components/frontend/banking/BankingOverview.tsx b/src/components/frontend/banking/BankingOverview.tsx index 65cb9a2..fc415b3 100644 --- a/src/components/frontend/banking/BankingOverview.tsx +++ b/src/components/frontend/banking/BankingOverview.tsx @@ -1,14 +1,15 @@ "use client"; -import type { ReactNode } from "react"; +import { useState, type ReactNode } from "react"; import type { BankAccount, BankTransaction, Resource } from "@/payload-types"; -import { LandmarkIcon } from "lucide-react"; +import { LandmarkIcon, PlusIcon } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AccountCard } from "@/components/frontend/banking/AccountCard"; import { CreateAccountDialog } from "@/components/frontend/banking/CreateAccountDialog"; import { WalletHero } from "@/components/frontend/banking/WalletHero"; import { ActivityFeed } from "@/components/frontend/banking/ActivityFeed"; import { currencyLabel, formatAmount } from "@/lib/banking/format"; +import { usePageCommands } from "@/components/command-palette/CommandPaletteContext"; interface BankingOverviewProps { accounts: BankAccount[]; @@ -55,6 +56,19 @@ export function BankingOverview({ isManager, factions, }: BankingOverviewProps) { + const [createAccountOpen, setCreateAccountOpen] = useState(false); + + usePageCommands([ + { + id: "banking:create-account", + group: "Banking", + label: "Create Bank Account", + icon: PlusIcon, + keywords: ["new", "account", "bank"], + onSelect: () => setCreateAccountOpen(true), + }, + ]); + const treasury = accounts.filter((a) => a.accountType === "treasury"); const factionAccounts = accounts.filter((a) => a.accountType === "faction"); const personal = accounts.filter((a) => a.accountType === "personal"); @@ -129,7 +143,12 @@ export function BankingOverview({ {visibleAccountsCount} - + {visibleAccountsCount === 0 ? ( diff --git a/src/components/frontend/helpdesk/TicketsView.tsx b/src/components/frontend/helpdesk/TicketsView.tsx index 19aae39..9572937 100644 --- a/src/components/frontend/helpdesk/TicketsView.tsx +++ b/src/components/frontend/helpdesk/TicketsView.tsx @@ -10,8 +10,9 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { CreateTicketDialog } from "@/components/frontend/helpdesk/CreateTicketDialog"; import { TicketCard } from "@/components/frontend/helpdesk/TicketCard"; import { CLOSING_STATUSES, STATUS_STYLES, type TicketStatus } from "@/lib/tickets/ticketMeta"; -import { InboxIcon, SearchIcon } from "lucide-react"; +import { InboxIcon, PlusIcon, SearchIcon } from "lucide-react"; import { cn } from "@/lib/utils"; +import { usePageCommands } from "@/components/command-palette/CommandPaletteContext"; type TicketFilter = "all" | "open" | "resolved" | "closed" | "unassigned"; @@ -56,6 +57,18 @@ function isUnassignedTicket(ticket: Ticket): boolean { export function TicketsView({ tickets, currentUserId: _currentUserId, isStaff }: TicketsViewProps) { const [activeFilter, setActiveFilter] = useState("all"); const [search, setSearch] = useState(""); + const [createOpen, setCreateOpen] = useState(false); + + usePageCommands([ + { + id: "helpdesk:create-ticket", + group: "Helpdesk", + label: "Create Support Ticket", + icon: PlusIcon, + keywords: ["help", "support", "new", "ticket"], + onSelect: () => setCreateOpen(true), + }, + ]); const counts = useMemo>( () => ({ @@ -132,7 +145,7 @@ export function TicketsView({ tickets, currentUserId: _currentUserId, isStaff }: : "Keep an eye on your open requests."}

- +
diff --git a/src/components/frontend/intelligence/MissionAttendance.tsx b/src/components/frontend/intelligence/MissionAttendance.tsx index 99fd7b7..cadca2b 100644 --- a/src/components/frontend/intelligence/MissionAttendance.tsx +++ b/src/components/frontend/intelligence/MissionAttendance.tsx @@ -7,6 +7,7 @@ import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; import { setMissionAttendance } from "@/app/(frontend)/intelligence/missions/actions"; +import { usePageCommands } from "@/components/command-palette/CommandPaletteContext"; type AttendanceResponse = "yes" | "no" | "tentative"; @@ -80,6 +81,39 @@ export function MissionAttendance({ router.refresh(); } + const canRsvp = currentUserId !== null && !attendanceClosed; + + usePageCommands( + canRsvp + ? [ + { + id: `mission:${missionId}:rsvp-yes`, + group: "Mission RSVP", + label: "RSVP — Yes", + icon: CheckIcon, + keywords: ["attend", "going", "yes", "rsvp"], + onSelect: () => void handleSet("yes"), + }, + { + id: `mission:${missionId}:rsvp-tentative`, + group: "Mission RSVP", + label: "RSVP — Tentative", + icon: MinusIcon, + keywords: ["maybe", "tentative", "unsure", "rsvp"], + onSelect: () => void handleSet("tentative"), + }, + { + id: `mission:${missionId}:rsvp-no`, + group: "Mission RSVP", + label: "RSVP — No", + icon: XIcon, + keywords: ["decline", "not going", "no", "rsvp"], + onSelect: () => void handleSet("no"), + }, + ] + : [], + ); + return ( diff --git a/src/components/frontend/locker/LockerSearch.tsx b/src/components/frontend/locker/LockerSearch.tsx index 0bc8783..ed44b81 100644 --- a/src/components/frontend/locker/LockerSearch.tsx +++ b/src/components/frontend/locker/LockerSearch.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Crosshair, Layers, Package, Palette, SearchIcon } from "lucide-react"; import type { Asset, Loadout } from "@/payload-types"; import { Button } from "@/components/ui/button"; @@ -53,17 +53,6 @@ interface LockerSearchProps { export function LockerSearch({ items, loadouts, onSelect }: LockerSearchProps) { const [open, setOpen] = useState(false); - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { - e.preventDefault(); - setOpen((v) => !v); - } - }; - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, []); - const entries = useMemo(() => buildSearchEntries(items, loadouts), [items, loadouts]); const groups = useMemo(() => { @@ -82,10 +71,7 @@ export function LockerSearch({ items, loadouts, onSelect }: LockerSearchProps) { <> diff --git a/src/components/frontend/locker/LockerView.tsx b/src/components/frontend/locker/LockerView.tsx index fdc495e..f479ed7 100644 --- a/src/components/frontend/locker/LockerView.tsx +++ b/src/components/frontend/locker/LockerView.tsx @@ -24,6 +24,8 @@ import { rotateLockerItem, splitLockerStack, } from "@/app/(frontend)/locker/actions"; +import { usePageCommands } from "@/components/command-palette/CommandPaletteContext"; +import { LayoutGridIcon } from "lucide-react"; interface LockerViewProps { locker: LockerStorage; @@ -141,6 +143,17 @@ export function LockerView({ if (result.success) refresh(); }, [refresh]); + usePageCommands([ + { + id: "locker:auto-arrange", + group: "Locker", + label: "Auto-Arrange Locker", + icon: LayoutGridIcon, + keywords: ["arrange", "organize", "sort", "grid"], + onSelect: () => void handleAutoArrange(), + }, + ]); + const handleAttachItem = useCallback( async (weaponEntryId: string, attachmentEntryId: string) => { const result = await attachItemFromGrid(weaponEntryId, attachmentEntryId); diff --git a/src/components/frontend/logistics/ShipmentsList.tsx b/src/components/frontend/logistics/ShipmentsList.tsx index 60901a0..f9348fe 100644 --- a/src/components/frontend/logistics/ShipmentsList.tsx +++ b/src/components/frontend/logistics/ShipmentsList.tsx @@ -4,6 +4,14 @@ import { useMemo, useState } from "react"; import type { Shipment } from "@/payload-types"; import { ShipmentCard } from "@/components/frontend/logistics/ShipmentCard"; import { Button } from "@/components/ui/button"; +import { usePageCommands } from "@/components/command-palette/CommandPaletteContext"; +import { + CheckCircleIcon, + ClockIcon, + ListFilterIcon, + TruckIcon, + XCircleIcon, +} from "lucide-react"; const statusFilters = [ { label: "All", value: null }, @@ -22,6 +30,49 @@ interface ShipmentsListProps { export function ShipmentsList({ shipments }: ShipmentsListProps) { const [filter, setFilter] = useState(null); + usePageCommands([ + { + id: "shipments:filter-all", + group: "Shipments", + label: "Filter — All Shipments", + icon: ListFilterIcon, + keywords: ["filter", "all", "shipments"], + onSelect: () => setFilter(null), + }, + { + id: "shipments:filter-active", + group: "Shipments", + label: "Filter — Active Shipments", + icon: ClockIcon, + keywords: ["filter", "active", "in transit", "pending"], + onSelect: () => setFilter("active"), + }, + { + id: "shipments:filter-completed", + group: "Shipments", + label: "Filter — Completed Shipments", + icon: CheckCircleIcon, + keywords: ["filter", "completed", "arrived", "done"], + onSelect: () => setFilter("completed"), + }, + { + id: "shipments:filter-cancelled", + group: "Shipments", + label: "Filter — Cancelled Shipments", + icon: XCircleIcon, + keywords: ["filter", "cancelled", "failed"], + onSelect: () => setFilter("cancelled"), + }, + { + id: "shipments:filter-stranded", + group: "Shipments", + label: "Filter — Stranded Shipments", + icon: TruckIcon, + keywords: ["filter", "stranded", "stuck"], + onSelect: () => setFilter("stranded"), + }, + ]); + const filtered = useMemo(() => { if (!filter) return shipments; if (filter === "active") { diff --git a/src/components/frontend/market/MarketView.tsx b/src/components/frontend/market/MarketView.tsx index 2aceb5e..c5af077 100644 --- a/src/components/frontend/market/MarketView.tsx +++ b/src/components/frontend/market/MarketView.tsx @@ -15,7 +15,8 @@ import { RARITY_LABELS } from "@/lib/market"; import { ListingCard } from "@/components/frontend/market/ListingCard"; import { CreateListingDialog } from "@/components/frontend/market/CreateListingDialog"; import { NegotiationPanel } from "@/components/frontend/market/NegotiationPanel"; -import { SearchIcon } from "lucide-react"; +import { PlusIcon, SearchIcon } from "lucide-react"; +import { usePageCommands } from "@/components/command-palette/CommandPaletteContext"; const ASSET_TYPE_OPTIONS: { value: string; label: string }[] = [ { value: "weapon", label: "Weapon" }, @@ -76,6 +77,18 @@ export function MarketView({ const [search, setSearch] = useState(""); const [rarity, setRarity] = useState("all"); const [assetType, setAssetType] = useState("all"); + const [createListingOpen, setCreateListingOpen] = useState(false); + + usePageCommands([ + { + id: "market:create-listing", + group: "Market", + label: "Create Listing", + icon: PlusIcon, + keywords: ["sell", "list", "new", "market"], + onSelect: () => setCreateListingOpen(true), + }, + ]); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); @@ -138,7 +151,12 @@ export function MarketView({
- +
diff --git a/src/payload-types.ts b/src/payload-types.ts index d5674d3..deb3c59 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1288,6 +1288,7 @@ export interface User { }; display?: { showCallsign?: boolean | null; + leadOrActual?: ('lead' | 'actual') | null; }; discord?: { enabled?: boolean | null; @@ -2758,6 +2759,7 @@ export interface UsersSelect { | T | { showCallsign?: T; + leadOrActual?: T; }; discord?: | T