1
0
Fork 0
polaris-task-force/src/components/frontend/market/MarketView.tsx
Z8MB1E 13f2132e25 feat(market): add economy reference prices and per-listing reset tool
Add getReferencePrices (modifier-applied NPC vendor price tip in CreateListingDialog) and resetEconomyState server action with a permission-gated 'Reset economy' button on listing cards for managers.

Ultraworked with [Sisyphus](https://github.com/code-yeongju/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-06 17:22:34 -04:00

225 lines
7.8 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import type { Asset, LockerStorage, MarketListing, MarketNegotiation } from "@/payload-types";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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 { PlusIcon, SearchIcon } from "lucide-react";
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format";
const ASSET_TYPE_OPTIONS: { value: string; label: string }[] = [
{ value: "weapon", label: "Weapon" },
{ value: "magazine", label: "Magazine" },
{ value: "optic", label: "Optic" },
{ value: "muzzle", label: "Muzzle" },
{ value: "bipod", label: "Bipod" },
{ value: "uniform", label: "Uniform" },
{ value: "vest", label: "Vest" },
{ value: "backpack", label: "Backpack" },
{ value: "helmet", label: "Helmet" },
{ value: "facewear", label: "Facewear" },
{ value: "nvg", label: "NVG" },
{ value: "radio", label: "Radio" },
{ value: "medical", label: "Medical" },
{ value: "tool", label: "Tool" },
{ value: "explosive", label: "Explosive" },
{ value: "skin", label: "Skin" },
{ value: "miscellaneous", label: "Miscellaneous" },
];
const RARITY_OPTIONS = Object.entries(RARITY_LABELS);
function assetName(listing: MarketListing): string {
const asset = listing.asset as Asset;
return typeof asset === "object" && asset ? asset.name.toLowerCase() : "";
}
function assetTypeOf(listing: MarketListing): string | null {
const asset = listing.asset as Asset;
return typeof asset === "object" && asset ? asset.assetType : null;
}
function rarityOf(listing: MarketListing): string | null {
const asset = listing.asset as Asset;
return typeof asset === "object" && asset ? (asset.marketTrading?.Rarity ?? null) : null;
}
interface MarketViewProps {
listings: MarketListing[];
myListings: MarketListing[];
negotiations: MarketNegotiation[];
locker: LockerStorage | null;
currentUserId: number;
currencyConfig: CurrencyConfig;
isManager: boolean;
canResetEconomy: boolean;
}
export function MarketView({
listings,
myListings,
negotiations,
locker,
currentUserId,
currencyConfig,
isManager,
canResetEconomy,
}: MarketViewProps) {
const [search, setSearch] = useState("");
const [rarity, setRarity] = useState("all");
const [assetType, setAssetType] = useState("all");
const [createListingOpen, setCreateListingOpen] = useState(false);
const currencyName = currencyLabel(currencyConfig);
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();
const myIds = new Set(myListings.map((l) => l.id));
return listings.filter((listing) => {
if (myIds.has(listing.id)) return false;
if (rarity !== "all" && rarityOf(listing) !== rarity) return false;
if (assetType !== "all" && assetTypeOf(listing) !== assetType) return false;
if (q && !assetName(listing).includes(q)) return false;
return true;
});
}, [listings, myListings, search, rarity, assetType]);
return (
<div className="flex flex-col gap-8">
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1.5">
<Label>Search</Label>
<div className="relative">
<SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-8"
placeholder="Search items..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex flex-col gap-1.5">
<Label>Rarity</Label>
<Select value={rarity} onValueChange={setRarity}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All rarities</SelectItem>
{RARITY_OPTIONS.map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<Label>Type</Label>
<Select value={assetType} onValueChange={setAssetType}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All types</SelectItem>
{ASSET_TYPE_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<CreateListingDialog
locker={locker}
currencyLabel={currencyName}
open={createListingOpen}
onOpenChange={setCreateListingOpen}
/>
</div>
<section className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
For sale
</h2>
<span className="text-xs text-muted-foreground">
{filtered.length} listing{filtered.length !== 1 ? "s" : ""}
</span>
</div>
{filtered.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
No listings match. Try clearing the filters.
</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3">
{filtered.map((listing) => (
<ListingCard
key={listing.id}
listing={listing}
currentUserId={currentUserId}
currencyConfig={currencyConfig}
canManage={isManager}
canResetEconomy={canResetEconomy}
/>
))}
</div>
)}
</section>
<NegotiationPanel
negotiations={negotiations}
currentUserId={currentUserId}
currencyConfig={currencyConfig}
/>
{myListings.length > 0 && (
<section className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
My listings
</h2>
<span className="text-xs text-muted-foreground">{myListings.length} active</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3">
{myListings.map((listing) => (
<ListingCard
key={listing.id}
listing={listing}
currentUserId={currentUserId}
currencyConfig={currencyConfig}
canManage={isManager}
canResetEconomy={canResetEconomy}
/>
))}
</div>
</section>
)}
</div>
);
}