1
0
Fork 0

refactor(market): pass currency config through market UI

Market listings, offer dialogs, negotiation panels, and history now render prices from the full CurrencyConfig (symbol + singular/plural) instead of a nullable currency label string.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-06 01:23:57 -04:00
parent 52ecc4f3b6
commit 80ec3a0386
7 changed files with 42 additions and 47 deletions

View file

@ -6,7 +6,7 @@ import { StoreIcon } from "lucide-react";
import { MarketView } from "@/components/frontend/market/MarketView"; import { MarketView } from "@/components/frontend/market/MarketView";
import { hasPermission } from "@/utils/access-control/hasPermission"; import { hasPermission } from "@/utils/access-control/hasPermission";
import { ensureLockerStorage } from "@/lib/locker"; import { ensureLockerStorage } from "@/lib/locker";
import { getMainCurrencyName } from "@/lib/banking"; import { getCurrencyConfig } from "@/lib/banking";
export const metadata = { export const metadata = {
title: "Market — Polaris Task Force", title: "Market — Polaris Task Force",
@ -76,8 +76,8 @@ export default async function MarketPage() {
return false; return false;
}); });
const isManager = user ? (await hasPermission(payload, user, "logistics:manage")) : false; const isManager = user ? await hasPermission(payload, user, "logistics:manage") : false;
const currencyLabel = await getMainCurrencyName(payload); const currencyConfig = await getCurrencyConfig(payload);
return ( return (
<div className="p-5 flex flex-col gap-6"> <div className="p-5 flex flex-col gap-6">
@ -97,7 +97,7 @@ export default async function MarketPage() {
negotiations={negotiations} negotiations={negotiations}
locker={lockerFull} locker={lockerFull}
currentUserId={userId} currentUserId={userId}
currencyLabel={currencyLabel} currencyConfig={currencyConfig}
isManager={isManager} isManager={isManager}
/> />
</div> </div>

View file

@ -30,7 +30,7 @@ import { createMarketListing } from "@/app/(frontend)/logistics/market/actions";
interface CreateListingDialogProps { interface CreateListingDialogProps {
locker: LockerStorage | null; locker: LockerStorage | null;
currencyLabel: string | null; currencyLabel: string;
open?: boolean; open?: boolean;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
} }
@ -225,7 +225,7 @@ export function CreateListingDialog({
/> />
</div> </div>
<div className="flex flex-1 flex-col gap-1.5"> <div className="flex flex-1 flex-col gap-1.5">
<Label>Price per unit ({currencyLabel ?? "units"})</Label> <Label>Price per unit ({currencyLabel})</Label>
<Input <Input
type="number" type="number"
min={0.01} min={0.01}
@ -254,7 +254,7 @@ export function CreateListingDialog({
</label> </label>
{negotiable && ( {negotiable && (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Minimum price per unit ({currencyLabel ?? "units"})</Label> <Label>Minimum price per unit ({currencyLabel})</Label>
<Input <Input
type="number" type="number"
min={0.01} min={0.01}

View file

@ -18,7 +18,7 @@ import {
} from "@/components/ui/item"; } from "@/components/ui/item";
import { getAssetTypeLabel } from "@/lib/locker"; import { getAssetTypeLabel } from "@/lib/locker";
import { getRarityLabel, isNegotiable } from "@/lib/market"; import { getRarityLabel, isNegotiable } from "@/lib/market";
import { formatAmount } from "@/lib/banking/format"; import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
import { PackageIcon, ShoppingCartIcon, XIcon } from "lucide-react"; import { PackageIcon, ShoppingCartIcon, XIcon } from "lucide-react";
import { buyMarketListing, cancelMarketListing } from "@/app/(frontend)/logistics/market/actions"; import { buyMarketListing, cancelMarketListing } from "@/app/(frontend)/logistics/market/actions";
import { Countdown } from "@/components/frontend/market/Countdown"; import { Countdown } from "@/components/frontend/market/Countdown";
@ -28,7 +28,7 @@ import { useNow } from "@/hooks/useNow";
interface ListingCardProps { interface ListingCardProps {
listing: MarketListing; listing: MarketListing;
currentUserId: number; currentUserId: number;
currencyLabel: string | null; currencyConfig: CurrencyConfig;
canManage: boolean; canManage: boolean;
} }
@ -54,7 +54,7 @@ function sellerName(listing: MarketListing): string {
export function ListingCard({ export function ListingCard({
listing, listing,
currentUserId, currentUserId,
currencyLabel, currencyConfig,
canManage, canManage,
}: ListingCardProps) { }: ListingCardProps) {
const router = useRouter(); const router = useRouter();
@ -141,7 +141,7 @@ export function ListingCard({
</ItemDescription> </ItemDescription>
<div className="flex items-center justify-between gap-2 w-full"> <div className="flex items-center justify-between gap-2 w-full">
<span className="text-sm font-semibold font-mono"> <span className="text-sm font-semibold font-mono">
{formatAmount(listing.price ?? 0, { name: currencyLabel ?? undefined })} / unit {formatAmount(listing.price ?? 0, currencyConfig)} / unit
</span> </span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{stock > 1 && ( {stock > 1 && (
@ -149,7 +149,7 @@ export function ListingCard({
{selectedQty}/{stock} ·{" "} {selectedQty}/{stock} ·{" "}
</> </>
)} )}
total {formatAmount(selectedTotal, { name: currencyLabel ?? undefined })} total {formatAmount(selectedTotal, currencyConfig)}
</span> </span>
</div> </div>
{error && <p className="text-sm text-red-400">{error}</p>} {error && <p className="text-sm text-red-400">{error}</p>}
@ -188,7 +188,7 @@ export function ListingCard({
<MakeOfferDialog <MakeOfferDialog
listing={listing} listing={listing}
currentUserId={currentUserId} currentUserId={currentUserId}
currencyLabel={currencyLabel} currencyConfig={currencyConfig}
/> />
)} )}
</div> </div>

View file

@ -16,7 +16,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { formatAmount } from "@/lib/banking/format"; import { currencyLabel, formatAmount, type CurrencyConfig } from "@/lib/banking/format";
import { import {
acceptMarketOffer, acceptMarketOffer,
cancelMarketOffer, cancelMarketOffer,
@ -55,10 +55,10 @@ function outcomeOf(negotiation: MarketNegotiation | null): OfferOutcome | null {
interface MakeOfferDialogProps { interface MakeOfferDialogProps {
listing: MarketListing; listing: MarketListing;
currentUserId: number; currentUserId: number;
currencyLabel: string | null; currencyConfig: CurrencyConfig;
} }
export function MakeOfferDialog({ listing, currentUserId, currencyLabel }: MakeOfferDialogProps) { export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: MakeOfferDialogProps) {
const router = useRouter(); const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [amount, setAmount] = useState(""); const [amount, setAmount] = useState("");
@ -76,7 +76,8 @@ export function MakeOfferDialog({ listing, currentUserId, currencyLabel }: MakeO
const isVendor = listing.isAutoGenerated; const isVendor = listing.isAutoGenerated;
const stock = listing.quantity ?? 1; const stock = listing.quantity ?? 1;
const outcome = outcomeOf(negotiation); const outcome = outcomeOf(negotiation);
const fmt = (n: number) => formatAmount(n, { name: currencyLabel ?? undefined }); const displayCurrency = currencyLabel(currencyConfig);
const fmt = (n: number) => formatAmount(n, currencyConfig);
const clampedQty = Math.max(1, Math.min(stock, qty)); const clampedQty = Math.max(1, Math.min(stock, qty));
const sinceAccept = acceptedAt != null ? nowTick - acceptedAt : 0; const sinceAccept = acceptedAt != null ? nowTick - acceptedAt : 0;
@ -244,7 +245,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyLabel }: MakeO
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{isVendor && ( {isVendor && (
<NpcChat listing={listing} negotiation={negotiation} currencyLabel={currencyLabel} /> <NpcChat listing={listing} negotiation={negotiation} currencyLabel={displayCurrency} />
)} )}
{loadingThread && !outcome && ( {loadingThread && !outcome && (
@ -254,7 +255,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyLabel }: MakeO
{!outcome && !loadingThread && ( {!outcome && !loadingThread && (
<> <>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Your offer ({currencyLabel ?? "units"} per unit)</Label> <Label>Your offer ({displayCurrency} per unit)</Label>
<Input <Input
type="number" type="number"
min={0.01} min={0.01}
@ -310,7 +311,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyLabel }: MakeO
. .
</p> </p>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Your counter ({currencyLabel ?? "units"} per unit)</Label> <Label>Your counter ({displayCurrency} per unit)</Label>
<Input <Input
type="number" type="number"
min={0.01} min={0.01}

View file

@ -17,6 +17,7 @@ import { CreateListingDialog } from "@/components/frontend/market/CreateListingD
import { NegotiationPanel } from "@/components/frontend/market/NegotiationPanel"; import { NegotiationPanel } from "@/components/frontend/market/NegotiationPanel";
import { PlusIcon, SearchIcon } from "lucide-react"; import { PlusIcon, SearchIcon } from "lucide-react";
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext"; import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format";
const ASSET_TYPE_OPTIONS: { value: string; label: string }[] = [ const ASSET_TYPE_OPTIONS: { value: string; label: string }[] = [
{ value: "weapon", label: "Weapon" }, { value: "weapon", label: "Weapon" },
@ -61,7 +62,7 @@ interface MarketViewProps {
negotiations: MarketNegotiation[]; negotiations: MarketNegotiation[];
locker: LockerStorage | null; locker: LockerStorage | null;
currentUserId: number; currentUserId: number;
currencyLabel: string | null; currencyConfig: CurrencyConfig;
isManager: boolean; isManager: boolean;
} }
@ -71,13 +72,14 @@ export function MarketView({
negotiations, negotiations,
locker, locker,
currentUserId, currentUserId,
currencyLabel, currencyConfig,
isManager, isManager,
}: MarketViewProps) { }: MarketViewProps) {
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); const [createListingOpen, setCreateListingOpen] = useState(false);
const currencyName = currencyLabel(currencyConfig);
usePageCommands([ usePageCommands([
{ {
@ -153,7 +155,7 @@ export function MarketView({
</div> </div>
<CreateListingDialog <CreateListingDialog
locker={locker} locker={locker}
currencyLabel={currencyLabel} currencyLabel={currencyName}
open={createListingOpen} open={createListingOpen}
onOpenChange={setCreateListingOpen} onOpenChange={setCreateListingOpen}
/> />
@ -179,7 +181,7 @@ export function MarketView({
key={listing.id} key={listing.id}
listing={listing} listing={listing}
currentUserId={currentUserId} currentUserId={currentUserId}
currencyLabel={currencyLabel} currencyConfig={currencyConfig}
canManage={isManager} canManage={isManager}
/> />
))} ))}
@ -190,7 +192,7 @@ export function MarketView({
<NegotiationPanel <NegotiationPanel
negotiations={negotiations} negotiations={negotiations}
currentUserId={currentUserId} currentUserId={currentUserId}
currencyLabel={currencyLabel} currencyConfig={currencyConfig}
/> />
{myListings.length > 0 && ( {myListings.length > 0 && (
@ -207,7 +209,7 @@ export function MarketView({
key={listing.id} key={listing.id}
listing={listing} listing={listing}
currentUserId={currentUserId} currentUserId={currentUserId}
currencyLabel={currencyLabel} currencyConfig={currencyConfig}
canManage={isManager} canManage={isManager}
/> />
))} ))}

View file

@ -14,7 +14,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { formatAmount } from "@/lib/banking/format"; import { currencyLabel, formatAmount, type CurrencyConfig } from "@/lib/banking/format";
import { buildChatBubbles } from "@/lib/market/chatBubbles"; import { buildChatBubbles } from "@/lib/market/chatBubbles";
import { ChatBubbleList } from "@/components/frontend/market/ChatBubbleList"; import { ChatBubbleList } from "@/components/frontend/market/ChatBubbleList";
import { ChevronDownIcon, HistoryIcon } from "lucide-react"; import { ChevronDownIcon, HistoryIcon } from "lucide-react";
@ -71,15 +71,15 @@ function counterpartyLabel(negotiation: MarketNegotiation, role: "buyer" | "sell
function HistoryRow({ function HistoryRow({
negotiation, negotiation,
role, role,
currencyLabel, currencyConfig,
}: { }: {
negotiation: MarketNegotiation; negotiation: MarketNegotiation;
role: "buyer" | "seller"; role: "buyer" | "seller";
currencyLabel: string | null; currencyConfig: CurrencyConfig;
}) { }) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const listing = listingOf(negotiation); const listing = listingOf(negotiation);
const fmt = (n: number) => formatAmount(n, { name: currencyLabel ?? undefined }); const fmt = (n: number) => formatAmount(n, currencyConfig);
const buyerLabel = role === "buyer" ? "You" : counterpartyLabel(negotiation, "buyer"); const buyerLabel = role === "buyer" ? "You" : counterpartyLabel(negotiation, "buyer");
const sellerLabel = role === "seller" ? "You" : counterpartyLabel(negotiation, "seller"); const sellerLabel = role === "seller" ? "You" : counterpartyLabel(negotiation, "seller");
@ -112,7 +112,7 @@ function HistoryRow({
{listing && ( {listing && (
<CollapsibleContent className="border-t"> <CollapsibleContent className="border-t">
<ChatBubbleList <ChatBubbleList
bubbles={buildChatBubbles(negotiation, listing, currencyLabel)} bubbles={buildChatBubbles(negotiation, listing, currencyLabel(currencyConfig))}
buyerLabel={buyerLabel} buyerLabel={buyerLabel}
sellerLabel={sellerLabel} sellerLabel={sellerLabel}
maxHeight="max-h-64" maxHeight="max-h-64"
@ -126,13 +126,13 @@ function HistoryRow({
interface NegotiationHistoryDialogProps { interface NegotiationHistoryDialogProps {
negotiations: MarketNegotiation[]; negotiations: MarketNegotiation[];
currentUserId: number; currentUserId: number;
currencyLabel: string | null; currencyConfig: CurrencyConfig;
} }
export function NegotiationHistoryDialog({ export function NegotiationHistoryDialog({
negotiations, negotiations,
currentUserId, currentUserId,
currencyLabel, currencyConfig,
}: NegotiationHistoryDialogProps) { }: NegotiationHistoryDialogProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -170,7 +170,7 @@ export function NegotiationHistoryDialog({
key={negotiation.id} key={negotiation.id}
negotiation={negotiation} negotiation={negotiation}
role={role} role={role}
currencyLabel={currencyLabel} currencyConfig={currencyConfig}
/> />
); );
}) })

View file

@ -1,13 +1,5 @@
export type NpcDialogueKind = export type NpcDialogueKind =
| "firm" "firm" | "lowball" | "backward" | "stall" | "concede" | "good" | "overpay" | "closed" | "accept";
| "lowball"
| "backward"
| "stall"
| "concede"
| "good"
| "overpay"
| "closed"
| "accept";
type DialogueLine = (amount: string, asking: string) => string; type DialogueLine = (amount: string, asking: string) => string;
@ -18,8 +10,8 @@ const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
(amount) => `I'm holding at ${amount}. My prices are already fair.`, (amount) => `I'm holding at ${amount}. My prices are already fair.`,
], ],
lowball: [ lowball: [
(amount) => `That's an insult! I'll come down to ${amount} — not a unit less.`, (amount) => `That's an insult! I'll come down to ${amount} — not a fraction less.`,
(amount) => `You're joking, right? ${amount} and not a unit less.`, (amount) => `You're joking, right? ${amount} and not a fraction less.`,
(amount) => `I paid more than that for lunch! ${amount} — final offer.`, (amount) => `I paid more than that for lunch! ${amount} — final offer.`,
], ],
backward: [ backward: [
@ -28,8 +20,8 @@ const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
], ],
stall: [ stall: [
() => "Really? That's hardly different. Negotiate like you mean it.", () => "Really? That's hardly different. Negotiate like you mean it.",
() => "A whole dollar more? That's not negotiating, that's teasing. Come back with a real number.", () => "That tiny increase isn't negotiating. Come back with a real number.",
() => "You're just wearing me down with pennies now. Bring something meaningful.", () => "You're just inching up now. Bring something meaningful.",
], ],
concede: [ concede: [
(amount) => `Hmm, not sure about that... how about ${amount}?`, (amount) => `Hmm, not sure about that... how about ${amount}?`,