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.
This commit is contained in:
parent
4906fc8008
commit
cb2029ea97
11 changed files with 196 additions and 38 deletions
|
|
@ -52,7 +52,7 @@ export default async function FlappyPage() {
|
||||||
<h1 className="text-lg font-semibold">Flight Simulator</h1>
|
<h1 className="text-lg font-semibold">Flight Simulator</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
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.
|
passed in a single run.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts";
|
||||||
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
|
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
|
||||||
import { LandingPage } from "@/components/frontend/LandingPage";
|
import { LandingPage } from "@/components/frontend/LandingPage";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
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)/).
|
// Skip static prerendering of this layout (and all pages under (frontend)/).
|
||||||
// The layout calls `getPayload()` at render time to resolve the current user,
|
// 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 }) {
|
||||||
<body className="h-full font-sans m-0 text-white">
|
<body className="h-full font-sans m-0 text-white">
|
||||||
{user ? (
|
{user ? (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar
|
<KeyboardShortcutsProvider
|
||||||
user={{
|
userRoles={user.roles ?? []}
|
||||||
displayName: user.payloadDisplayName ?? "Guest",
|
|
||||||
username: user.username ?? "Not logged in",
|
|
||||||
}}
|
|
||||||
xpLevel={xpLevel}
|
|
||||||
adminUrl={payloadConfig.routes.admin}
|
|
||||||
hasIntelligence={isIntelligence}
|
hasIntelligence={isIntelligence}
|
||||||
hasLogistics={isLogistics}
|
hasLogistics={isLogistics}
|
||||||
showCallsign={user.preferences?.display?.showCallsign ?? false}
|
>
|
||||||
className="border-r"
|
<CommandPaletteProvider>
|
||||||
/>
|
<AppSidebar
|
||||||
<SidebarInset>
|
user={{
|
||||||
<SiteHeader />
|
displayName: user.payloadDisplayName ?? "Guest",
|
||||||
{children}
|
username: user.username ?? "Not logged in",
|
||||||
</SidebarInset>
|
}}
|
||||||
|
xpLevel={xpLevel}
|
||||||
|
adminUrl={payloadConfig.routes.admin}
|
||||||
|
hasIntelligence={isIntelligence}
|
||||||
|
hasLogistics={isLogistics}
|
||||||
|
showCallsign={user.preferences?.display?.showCallsign ?? false}
|
||||||
|
className="border-r"
|
||||||
|
/>
|
||||||
|
<SidebarInset>
|
||||||
|
<SiteHeader />
|
||||||
|
{children}
|
||||||
|
</SidebarInset>
|
||||||
|
<CommandPalette
|
||||||
|
userRoles={user.roles ?? []}
|
||||||
|
hasIntelligence={isIntelligence}
|
||||||
|
hasLogistics={isLogistics}
|
||||||
|
username={user.username}
|
||||||
|
/>
|
||||||
|
</CommandPaletteProvider>
|
||||||
|
</KeyboardShortcutsProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
) : (
|
) : (
|
||||||
<LandingPage />
|
<LandingPage />
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { NotificationsBell } from "@/components/frontend/notifications/NotificationsBell";
|
import { NotificationsBell } from "@/components/frontend/notifications/NotificationsBell";
|
||||||
|
import { Command } from "lucide-react";
|
||||||
|
|
||||||
export const SiteHeader = () => {
|
export const SiteHeader = () => {
|
||||||
return (
|
return (
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||||
|
<div className="flex items-center gap-1.5 rounded-md border border-border/50 bg-muted/50 px-2 py-1 text-xs text-muted-foreground select-none">
|
||||||
|
<Command className="h-3 w-3" />
|
||||||
|
<span>K</span>
|
||||||
|
</div>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
<NotificationsBell />
|
<NotificationsBell />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import type { BankAccount, BankTransaction, Resource } from "@/payload-types";
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { AccountCard } from "@/components/frontend/banking/AccountCard";
|
import { AccountCard } from "@/components/frontend/banking/AccountCard";
|
||||||
import { CreateAccountDialog } from "@/components/frontend/banking/CreateAccountDialog";
|
import { CreateAccountDialog } from "@/components/frontend/banking/CreateAccountDialog";
|
||||||
import { WalletHero } from "@/components/frontend/banking/WalletHero";
|
import { WalletHero } from "@/components/frontend/banking/WalletHero";
|
||||||
import { ActivityFeed } from "@/components/frontend/banking/ActivityFeed";
|
import { ActivityFeed } from "@/components/frontend/banking/ActivityFeed";
|
||||||
import { currencyLabel, formatAmount } from "@/lib/banking/format";
|
import { currencyLabel, formatAmount } from "@/lib/banking/format";
|
||||||
|
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
|
||||||
|
|
||||||
interface BankingOverviewProps {
|
interface BankingOverviewProps {
|
||||||
accounts: BankAccount[];
|
accounts: BankAccount[];
|
||||||
|
|
@ -55,6 +56,19 @@ export function BankingOverview({
|
||||||
isManager,
|
isManager,
|
||||||
factions,
|
factions,
|
||||||
}: BankingOverviewProps) {
|
}: 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 treasury = accounts.filter((a) => a.accountType === "treasury");
|
||||||
const factionAccounts = accounts.filter((a) => a.accountType === "faction");
|
const factionAccounts = accounts.filter((a) => a.accountType === "faction");
|
||||||
const personal = accounts.filter((a) => a.accountType === "personal");
|
const personal = accounts.filter((a) => a.accountType === "personal");
|
||||||
|
|
@ -129,7 +143,12 @@ export function BankingOverview({
|
||||||
{visibleAccountsCount}
|
{visibleAccountsCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CreateAccountDialog isManager={isManager} factions={factions} />
|
<CreateAccountDialog
|
||||||
|
isManager={isManager}
|
||||||
|
factions={factions}
|
||||||
|
open={createAccountOpen}
|
||||||
|
onOpenChange={setCreateAccountOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{visibleAccountsCount === 0 ? (
|
{visibleAccountsCount === 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { CreateTicketDialog } from "@/components/frontend/helpdesk/CreateTicketDialog";
|
import { CreateTicketDialog } from "@/components/frontend/helpdesk/CreateTicketDialog";
|
||||||
import { TicketCard } from "@/components/frontend/helpdesk/TicketCard";
|
import { TicketCard } from "@/components/frontend/helpdesk/TicketCard";
|
||||||
import { CLOSING_STATUSES, STATUS_STYLES, type TicketStatus } from "@/lib/tickets/ticketMeta";
|
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 { cn } from "@/lib/utils";
|
||||||
|
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
|
||||||
|
|
||||||
type TicketFilter = "all" | "open" | "resolved" | "closed" | "unassigned";
|
type TicketFilter = "all" | "open" | "resolved" | "closed" | "unassigned";
|
||||||
|
|
||||||
|
|
@ -56,6 +57,18 @@ function isUnassignedTicket(ticket: Ticket): boolean {
|
||||||
export function TicketsView({ tickets, currentUserId: _currentUserId, isStaff }: TicketsViewProps) {
|
export function TicketsView({ tickets, currentUserId: _currentUserId, isStaff }: TicketsViewProps) {
|
||||||
const [activeFilter, setActiveFilter] = useState<TicketFilter>("all");
|
const [activeFilter, setActiveFilter] = useState<TicketFilter>("all");
|
||||||
const [search, setSearch] = useState("");
|
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<Record<TicketFilter, number>>(
|
const counts = useMemo<Record<TicketFilter, number>>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -132,7 +145,7 @@ export function TicketsView({ tickets, currentUserId: _currentUserId, isStaff }:
|
||||||
: "Keep an eye on your open requests."}
|
: "Keep an eye on your open requests."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CreateTicketDialog />
|
<CreateTicketDialog open={createOpen} onOpenChange={setCreateOpen} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||||
import { setMissionAttendance } from "@/app/(frontend)/intelligence/missions/actions";
|
import { setMissionAttendance } from "@/app/(frontend)/intelligence/missions/actions";
|
||||||
|
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
|
||||||
|
|
||||||
type AttendanceResponse = "yes" | "no" | "tentative";
|
type AttendanceResponse = "yes" | "no" | "tentative";
|
||||||
|
|
||||||
|
|
@ -80,6 +81,39 @@ export function MissionAttendance({
|
||||||
router.refresh();
|
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 (
|
return (
|
||||||
<Item variant="outline">
|
<Item variant="outline">
|
||||||
<ItemHeader>
|
<ItemHeader>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Crosshair, Layers, Package, Palette, SearchIcon } from "lucide-react";
|
import { Crosshair, Layers, Package, Palette, SearchIcon } from "lucide-react";
|
||||||
import type { Asset, Loadout } from "@/payload-types";
|
import type { Asset, Loadout } from "@/payload-types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -53,17 +53,6 @@ interface LockerSearchProps {
|
||||||
export function LockerSearch({ items, loadouts, onSelect }: LockerSearchProps) {
|
export function LockerSearch({ items, loadouts, onSelect }: LockerSearchProps) {
|
||||||
const [open, setOpen] = useState(false);
|
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 entries = useMemo(() => buildSearchEntries(items, loadouts), [items, loadouts]);
|
||||||
|
|
||||||
const groups = useMemo(() => {
|
const groups = useMemo(() => {
|
||||||
|
|
@ -82,10 +71,7 @@ export function LockerSearch({ items, loadouts, onSelect }: LockerSearchProps) {
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
||||||
<SearchIcon className="size-3.5" />
|
<SearchIcon className="size-3.5" />
|
||||||
Search
|
Search Locker
|
||||||
<kbd className="ml-1 inline-flex h-4 items-center gap-0.5 rounded border border-border bg-muted px-1 text-[10px] font-medium text-muted-foreground">
|
|
||||||
⌘K
|
|
||||||
</kbd>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<CommandDialog open={open} onOpenChange={setOpen} title="Search Locker">
|
<CommandDialog open={open} onOpenChange={setOpen} title="Search Locker">
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import {
|
||||||
rotateLockerItem,
|
rotateLockerItem,
|
||||||
splitLockerStack,
|
splitLockerStack,
|
||||||
} from "@/app/(frontend)/locker/actions";
|
} from "@/app/(frontend)/locker/actions";
|
||||||
|
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
|
||||||
|
import { LayoutGridIcon } from "lucide-react";
|
||||||
|
|
||||||
interface LockerViewProps {
|
interface LockerViewProps {
|
||||||
locker: LockerStorage;
|
locker: LockerStorage;
|
||||||
|
|
@ -141,6 +143,17 @@ export function LockerView({
|
||||||
if (result.success) refresh();
|
if (result.success) refresh();
|
||||||
}, [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(
|
const handleAttachItem = useCallback(
|
||||||
async (weaponEntryId: string, attachmentEntryId: string) => {
|
async (weaponEntryId: string, attachmentEntryId: string) => {
|
||||||
const result = await attachItemFromGrid(weaponEntryId, attachmentEntryId);
|
const result = await attachItemFromGrid(weaponEntryId, attachmentEntryId);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,14 @@ import { useMemo, useState } from "react";
|
||||||
import type { Shipment } from "@/payload-types";
|
import type { Shipment } from "@/payload-types";
|
||||||
import { ShipmentCard } from "@/components/frontend/logistics/ShipmentCard";
|
import { ShipmentCard } from "@/components/frontend/logistics/ShipmentCard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
ListFilterIcon,
|
||||||
|
TruckIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
const statusFilters = [
|
const statusFilters = [
|
||||||
{ label: "All", value: null },
|
{ label: "All", value: null },
|
||||||
|
|
@ -22,6 +30,49 @@ interface ShipmentsListProps {
|
||||||
export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
||||||
const [filter, setFilter] = useState<string | null>(null);
|
const [filter, setFilter] = useState<string | null>(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(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!filter) return shipments;
|
if (!filter) return shipments;
|
||||||
if (filter === "active") {
|
if (filter === "active") {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import { RARITY_LABELS } from "@/lib/market";
|
||||||
import { ListingCard } from "@/components/frontend/market/ListingCard";
|
import { ListingCard } from "@/components/frontend/market/ListingCard";
|
||||||
import { CreateListingDialog } from "@/components/frontend/market/CreateListingDialog";
|
import { CreateListingDialog } from "@/components/frontend/market/CreateListingDialog";
|
||||||
import { NegotiationPanel } from "@/components/frontend/market/NegotiationPanel";
|
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 }[] = [
|
const ASSET_TYPE_OPTIONS: { value: string; label: string }[] = [
|
||||||
{ value: "weapon", label: "Weapon" },
|
{ value: "weapon", label: "Weapon" },
|
||||||
|
|
@ -76,6 +77,18 @@ export function MarketView({
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [rarity, setRarity] = useState("all");
|
const [rarity, setRarity] = useState("all");
|
||||||
const [assetType, setAssetType] = 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 filtered = useMemo(() => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
|
|
@ -138,7 +151,12 @@ export function MarketView({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CreateListingDialog locker={locker} currencyLabel={currencyLabel} />
|
<CreateListingDialog
|
||||||
|
locker={locker}
|
||||||
|
currencyLabel={currencyLabel}
|
||||||
|
open={createListingOpen}
|
||||||
|
onOpenChange={setCreateListingOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
|
|
|
||||||
|
|
@ -1288,6 +1288,7 @@ export interface User {
|
||||||
};
|
};
|
||||||
display?: {
|
display?: {
|
||||||
showCallsign?: boolean | null;
|
showCallsign?: boolean | null;
|
||||||
|
leadOrActual?: ('lead' | 'actual') | null;
|
||||||
};
|
};
|
||||||
discord?: {
|
discord?: {
|
||||||
enabled?: boolean | null;
|
enabled?: boolean | null;
|
||||||
|
|
@ -2758,6 +2759,7 @@ export interface UsersSelect<T extends boolean = true> {
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
showCallsign?: T;
|
showCallsign?: T;
|
||||||
|
leadOrActual?: T;
|
||||||
};
|
};
|
||||||
discord?:
|
discord?:
|
||||||
| T
|
| T
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue