1
0
Fork 0
polaris-task-force/src/components/frontend/market/MarketView.tsx

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>
);
}