201 lines
7 KiB
TypeScript
201 lines
7 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 { SearchIcon } from "lucide-react";
|
|
|
|
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;
|
|
currencyLabel: string | null;
|
|
isManager: boolean;
|
|
}
|
|
|
|
export function MarketView({
|
|
listings,
|
|
myListings,
|
|
negotiations,
|
|
locker,
|
|
currentUserId,
|
|
currencyLabel,
|
|
isManager,
|
|
}: MarketViewProps) {
|
|
const [search, setSearch] = useState("");
|
|
const [rarity, setRarity] = useState("all");
|
|
const [assetType, setAssetType] = useState("all");
|
|
|
|
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={currencyLabel} />
|
|
</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}
|
|
currencyLabel={currencyLabel}
|
|
canManage={isManager}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<NegotiationPanel
|
|
negotiations={negotiations}
|
|
currentUserId={currentUserId}
|
|
currencyLabel={currencyLabel}
|
|
/>
|
|
|
|
{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}
|
|
currencyLabel={currencyLabel}
|
|
canManage={isManager}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|