feat(banking): currency emblem image with CurrencyAmount display wrapper
- GameRules currencySymbol switches from text to a media upload - new CurrencyAmount/CurrencySymbol components; render sites across banking, base management, dashboard, market, campaigns and profiles are wired to show the emblem inline with amounts - migration 20260921_140000 (text column out, media fk in); registers the five pending migrations in the migrations index
This commit is contained in:
parent
9bbe44d38d
commit
8c10719124
22 changed files with 185 additions and 62 deletions
BIN
public/images/assets/icons/credits.png
Normal file
BIN
public/images/assets/icons/credits.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -37,9 +37,11 @@ export const GameRules: GlobalConfig = {
|
|||
},
|
||||
{
|
||||
name: "currencySymbol",
|
||||
type: "text",
|
||||
type: "upload",
|
||||
relationTo: "media",
|
||||
admin: {
|
||||
description: 'Optional symbol prefixed to amounts, e.g. "¢" or "$".',
|
||||
description:
|
||||
"Optional emblem shown inline before amounts. It renders scaled to the surrounding text height (unlike a text symbol).",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import {
|
|||
ItemTitle,
|
||||
} from "@/components/ui/item";
|
||||
import { LandmarkIcon, UsersIcon, WalletIcon } from "lucide-react";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import type { CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import { type AccountStatus, accountStatusBase, accountStatusClass } from "@/lib/banking/ui";
|
||||
|
||||
const accountTypeMeta = {
|
||||
|
|
@ -85,10 +86,10 @@ export function AccountCard({
|
|||
</ItemDescription>
|
||||
<div className="mt-1">
|
||||
<span className="text-lg font-semibold font-mono">
|
||||
{formatAmount(
|
||||
account.balance ?? 0,
|
||||
currencyConfig.resourceName ? currencyConfig : currency,
|
||||
)}
|
||||
<CurrencyAmount
|
||||
value={account.balance ?? 0}
|
||||
currency={currencyConfig.resourceName ? currencyConfig : currency}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import Link from "next/link";
|
|||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import type { BankAccount, LedgerEntry } from "@/payload-types";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import type { CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import { type AccountStatus, accountStatusBase, accountStatusClass } from "@/lib/banking/ui";
|
||||
import { accountTypeInfo } from "@/components/frontend/banking/AccountCard";
|
||||
import { BankTransactionDialog } from "@/components/frontend/banking/BankTransactionDialog";
|
||||
|
|
@ -72,10 +73,10 @@ export function AccountDetail({
|
|||
Balance
|
||||
</p>
|
||||
<p className="text-2xl font-semibold font-mono">
|
||||
{formatAmount(
|
||||
account.balance ?? 0,
|
||||
currencyConfig.resourceName ? currencyConfig : currency,
|
||||
)}
|
||||
<CurrencyAmount
|
||||
value={account.balance ?? 0}
|
||||
currency={currencyConfig.resourceName ? currencyConfig : currency}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { AccountCard } from "@/components/frontend/banking/AccountCard";
|
|||
import { CreateAccountDialog } from "@/components/frontend/banking/CreateAccountDialog";
|
||||
import { WalletHero } from "@/components/frontend/banking/WalletHero";
|
||||
import { ActivityFeed } from "@/components/frontend/banking/ActivityFeed";
|
||||
import { currencyLabel, formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import { usePageCommands } from "@/components/command-palette/CommandPaletteContext";
|
||||
|
||||
interface BankingOverviewProps {
|
||||
|
|
@ -126,7 +127,7 @@ export function BankingOverview({
|
|||
{isManager && (
|
||||
<p className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground/70">
|
||||
UNIT FUNDS
|
||||
{formatAmount(unitFundsTotal, currencyConfig)}
|
||||
<CurrencyAmount value={unitFundsTotal} currency={currencyConfig} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
23
src/components/frontend/banking/CurrencyAmount.tsx
Normal file
23
src/components/frontend/banking/CurrencyAmount.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { formatAmount } from "@/lib/banking/format";
|
||||
import type { CurrencyDisplay } from "@/lib/banking/format";
|
||||
import { CurrencySymbol } from "./CurrencySymbol";
|
||||
|
||||
/**
|
||||
* Renders the currency emblem inline with the formatted amount. Delegates
|
||||
* symbol rendering to <CurrencySymbol> and formatting to formatAmount.
|
||||
* Renders nothing when currency is null/undefined/number (no emblem to show).
|
||||
*/
|
||||
export function CurrencyAmount({
|
||||
value,
|
||||
currency,
|
||||
}: {
|
||||
readonly value: number;
|
||||
readonly currency?: CurrencyDisplay | null;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<CurrencySymbol currency={currency} />
|
||||
{formatAmount(value, currency)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
43
src/components/frontend/banking/CurrencySymbol.tsx
Normal file
43
src/components/frontend/banking/CurrencySymbol.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { CurrencyDisplay } from "@/lib/banking/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isCurrencyConfig } from "@/lib/banking/format";
|
||||
|
||||
/**
|
||||
* Renders the currency emblem (an uploaded media image) inline with the text it
|
||||
* sits beside. The image height is tied to the surrounding font size and capped
|
||||
* just below the line height, so it scales with the text and never floats larger
|
||||
* than the numbers next to it. Renders nothing when no emblem is configured.
|
||||
*/
|
||||
export function CurrencySymbol({
|
||||
currency,
|
||||
className,
|
||||
ariaHidden,
|
||||
}: {
|
||||
readonly currency?: CurrencyDisplay | null;
|
||||
readonly className?: string;
|
||||
readonly ariaHidden?: boolean;
|
||||
}) {
|
||||
if (!currency || typeof currency !== "object") return null;
|
||||
const config = isCurrencyConfig(currency) ? currency : null;
|
||||
const symbolUrl = config?.symbol;
|
||||
if (!symbolUrl) return null;
|
||||
|
||||
const alt =
|
||||
config?.name?.trim() ||
|
||||
config?.singular?.trim() ||
|
||||
config?.plural?.trim() ||
|
||||
"currency symbol";
|
||||
|
||||
return (
|
||||
<img
|
||||
src={symbolUrl}
|
||||
alt={alt}
|
||||
aria-hidden={ariaHidden}
|
||||
loading="lazy"
|
||||
className={cn(
|
||||
"inline-block h-[0.85em] align-text-bottom max-h-[1.15em] object-contain",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@ import Link from "next/link";
|
|||
import { ChevronRightIcon, WalletIcon } from "lucide-react";
|
||||
import type { BankAccount, Resource } from "@/payload-types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import type { CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import {
|
||||
type AccountStatus,
|
||||
accountStatusBase,
|
||||
|
|
@ -120,10 +121,10 @@ export function WalletHero({
|
|||
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-4xl md:text-5xl font-bold font-mono tracking-tight">
|
||||
{formatAmount(
|
||||
account.balance ?? 0,
|
||||
currencyConfig.resourceName ? currencyConfig : currency,
|
||||
)}
|
||||
<CurrencyAmount
|
||||
value={account.balance ?? 0}
|
||||
currency={currencyConfig.resourceName ? currencyConfig : currency}
|
||||
/>
|
||||
</p>
|
||||
<p className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground/60">
|
||||
LAST ACTIVITY {lastActivity}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { BadgeCheckIcon } from "lucide-react";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import type { CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { HireStaffDialog } from "@/components/frontend/baseManagement/HireStaffDialog";
|
||||
import type { LaborClassificationOption, StructureStaffingOption } from "@/lib/base";
|
||||
|
|
@ -51,7 +52,7 @@ export function EmploymentCard({
|
|||
{employment.positionType} · {employment.structureName}
|
||||
</p>
|
||||
<p className="font-mono text-sm tabular-nums">
|
||||
{formatAmount(employment.salary, currencyConfig)}/tick
|
||||
<CurrencyAmount value={employment.salary} currency={currencyConfig} />/tick
|
||||
</p>
|
||||
</div>
|
||||
) : canManage && hiringEnabled ? (
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { currencyLabel, formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import type { LaborClassificationOption, StructureStaffingOption } from "@/lib/base";
|
||||
import { HireStaffDialog } from "@/components/frontend/baseManagement/HireStaffDialog";
|
||||
|
||||
|
|
@ -239,7 +240,7 @@ export function StaffRoster({
|
|||
</CardDescription>
|
||||
{structure.maintenanceCost > 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Maintenance: {formatAmount(structure.maintenanceCost, currencyConfig)}/tick
|
||||
Maintenance: <CurrencyAmount value={structure.maintenanceCost} currency={currencyConfig} />/tick
|
||||
</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
|
@ -262,7 +263,7 @@ export function StaffRoster({
|
|||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="shrink-0 font-mono text-sm tabular-nums">
|
||||
{formatAmount(member.salary, currencyConfig)}/tick
|
||||
<CurrencyAmount value={member.salary} currency={currencyConfig} />/tick
|
||||
</span>
|
||||
{canManage && (
|
||||
<Button
|
||||
|
|
@ -333,12 +334,8 @@ export function StaffRoster({
|
|||
{crew.requiredHeadcount} required · max {crew.maxHeadcount}
|
||||
</p>
|
||||
<p className="font-mono text-sm tabular-nums text-muted-foreground">
|
||||
{formatAmount(crew.unitSalary, currencyConfig)}/tick each ·{" "}
|
||||
{formatAmount(
|
||||
crew.unitSalary * crew.headcount,
|
||||
currencyConfig,
|
||||
)}
|
||||
/tick total
|
||||
<CurrencyAmount value={crew.unitSalary} currency={currencyConfig} />/tick each ·{" "}
|
||||
<CurrencyAmount value={crew.unitSalary * crew.headcount} currency={currencyConfig} />/tick total
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import Link from "next/link";
|
|||
import type { MarketListing, MarketNegotiation } from "@/payload-types";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { ChevronRightIcon, HandshakeIcon } from "lucide-react";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import type { CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
|
||||
interface OpenNegotiationsProps {
|
||||
readonly negotiations: MarketNegotiation[];
|
||||
|
|
@ -36,7 +37,7 @@ export function OpenNegotiations({
|
|||
// NegotiationPanel on the market page).
|
||||
if (negotiations.length === 0) return null;
|
||||
|
||||
const fmt = (n: number) => formatAmount(n, currencyConfig);
|
||||
const fmt = (n: number) => <CurrencyAmount value={n} currency={currencyConfig} />;
|
||||
|
||||
return (
|
||||
<Item variant="outline">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import Link from "next/link";
|
|||
import type { BankAccount } from "@/payload-types";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { WalletIcon } from "lucide-react";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import type { CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import { MyWalletCard } from "@/components/frontend/banking/MyWalletCard";
|
||||
|
||||
interface WalletWidgetProps {
|
||||
|
|
@ -44,7 +45,7 @@ export function WalletWidget({ account, currencyConfig }: WalletWidgetProps) {
|
|||
</span>
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-xl font-semibold tabular-nums">
|
||||
{formatAmount(account.balance ?? 0, currencyConfig)}
|
||||
<CurrencyAmount value={account.balance ?? 0} currency={currencyConfig} />
|
||||
</span>
|
||||
</Link>
|
||||
</ItemContent>
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ const officialStatusLabels: Record<Campaign["status"], string> = {
|
|||
|
||||
const statusTextColors: Record<Campaign["status"], string> = {
|
||||
concept: "text-muted-foreground",
|
||||
inProgress: "text-blue-400",
|
||||
completed: "text-emerald-400",
|
||||
inProgress: "text-emerald-400",
|
||||
completed: "text-blue-400",
|
||||
};
|
||||
|
||||
export function CampaignCard({ campaign, size = "lg" }: CampaignCardProps) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import {
|
|||
} from "@/components/ui/item";
|
||||
import { getAssetTypeLabel } from "@/lib/locker";
|
||||
import { getRarityLabel, isNegotiable } from "@/lib/market";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import type { CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import { PackageIcon, ShoppingCartIcon, XIcon } from "lucide-react";
|
||||
import { buyMarketListing, cancelMarketListing, resetEconomyState } from "@/app/(frontend)/logistics/market/actions";
|
||||
import { Countdown } from "@/components/frontend/market/Countdown";
|
||||
|
|
@ -156,7 +157,7 @@ export function ListingCard({
|
|||
</ItemDescription>
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<span className="text-sm font-semibold font-mono">
|
||||
{formatAmount(listing.price ?? 0, currencyConfig)} / unit
|
||||
<CurrencyAmount value={listing.price ?? 0} currency={currencyConfig} /> / unit
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{stock > 1 && (
|
||||
|
|
@ -164,7 +165,7 @@ export function ListingCard({
|
|||
{selectedQty}/{stock} ·{" "}
|
||||
</>
|
||||
)}
|
||||
total {formatAmount(selectedTotal, currencyConfig)}
|
||||
total <CurrencyAmount value={selectedTotal} currency={currencyConfig} />
|
||||
</span>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { currencyLabel, formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import {
|
||||
acceptMarketOffer,
|
||||
cancelMarketOffer,
|
||||
|
|
@ -239,7 +240,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
<DialogTitle>Negotiate: {assetName}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isVendor ? "Haggle with the vendor." : "Send an offer to the seller."} Asking{" "}
|
||||
<span className="font-mono">{fmt(listing.price ?? 0)}</span> per unit.
|
||||
<span className="font-mono"><CurrencyAmount value={listing.price ?? 0} currency={currencyConfig} /></span> per unit.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
@ -282,7 +283,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{stock}× in stock · total{" "}
|
||||
<span className="font-mono">{fmt((Number(amount) || 0) * clampedQty)}</span>
|
||||
<span className="font-mono"><CurrencyAmount value={(Number(amount) || 0) * clampedQty} currency={currencyConfig} /></span>
|
||||
</p>
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
</>
|
||||
|
|
@ -291,7 +292,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
{outcome?.kind === "pending" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Offer of <span className="font-mono">{fmt(negotiation?.amount ?? 0)}</span> each for{" "}
|
||||
Offer of <span className="font-mono"><CurrencyAmount value={negotiation?.amount ?? 0} currency={currencyConfig} /></span> each for{" "}
|
||||
{negotiation?.quantity ?? stock}× sent. Waiting for the{" "}
|
||||
{isVendor ? "vendor" : "seller"} to respond.
|
||||
</p>
|
||||
|
|
@ -303,10 +304,10 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm">
|
||||
The {isVendor ? "vendor" : "seller"} counteroffers{" "}
|
||||
<span className="font-mono font-semibold">{fmt(negotiation?.amount ?? 0)}</span>{" "}
|
||||
<span className="font-mono font-semibold"><CurrencyAmount value={negotiation?.amount ?? 0} currency={currencyConfig} /></span>{" "}
|
||||
each for {negotiation?.quantity ?? stock}×, for a total of{" "}
|
||||
<span className="font-mono">
|
||||
{fmt((negotiation?.amount ?? 0) * (negotiation?.quantity ?? stock))}
|
||||
<CurrencyAmount value={(negotiation?.amount ?? 0) * (negotiation?.quantity ?? stock)} currency={currencyConfig} />
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
|
|
@ -330,10 +331,10 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
Deal closed
|
||||
</Badge>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Sold at <span className="font-mono">{fmt(outcome.price)}</span> each for{" "}
|
||||
Sold at <span className="font-mono"><CurrencyAmount value={outcome.price} currency={currencyConfig} /></span> each for{" "}
|
||||
{negotiation?.quantity ?? stock}×, for a total of{" "}
|
||||
<span className="font-mono">
|
||||
{fmt(outcome.price * (negotiation?.quantity ?? stock))}
|
||||
<CurrencyAmount value={outcome.price * (negotiation?.quantity ?? stock)} currency={currencyConfig} />
|
||||
</span>
|
||||
. The items are in your locker.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import {
|
|||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { currencyLabel, formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import { buildChatBubbles } from "@/lib/market/chatBubbles";
|
||||
import { ChatBubbleList } from "@/components/frontend/market/ChatBubbleList";
|
||||
import { ChevronDownIcon, HistoryIcon } from "lucide-react";
|
||||
|
|
@ -79,7 +80,7 @@ function HistoryRow({
|
|||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const listing = listingOf(negotiation);
|
||||
const fmt = (n: number) => formatAmount(n, currencyConfig);
|
||||
const fmt = (n: number) => <CurrencyAmount value={n} currency={currencyConfig} />;
|
||||
|
||||
const buyerLabel = role === "buyer" ? "You" : counterpartyLabel(negotiation, "buyer");
|
||||
const sellerLabel = role === "seller" ? "You" : counterpartyLabel(negotiation, "seller");
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
import {
|
||||
acceptMarketOffer,
|
||||
cancelMarketOffer,
|
||||
|
|
@ -131,7 +132,7 @@ function NegotiationRow({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="font-mono">{fmt(negotiation.amount ?? 0)}</span>
|
||||
<span className="font-mono"><CurrencyAmount value={negotiation.amount ?? 0} currency={currencyConfig} /></span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{negotiation.quantity ? `${negotiation.quantity}× · ` : ""}
|
||||
{theirProposal
|
||||
|
|
@ -143,7 +144,7 @@ function NegotiationRow({
|
|||
<>
|
||||
{" · total "}
|
||||
<span className="font-mono">
|
||||
{fmt((negotiation.amount ?? 0) * negotiation.quantity)}
|
||||
<CurrencyAmount value={(negotiation.amount ?? 0) * negotiation.quantity} currency={currencyConfig} />
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { formatAmount } from "@/lib/banking/format";
|
||||
import { CurrencyAmount } from "@/components/frontend/banking/CurrencyAmount";
|
||||
|
||||
interface NpcProfileProps {
|
||||
readonly npc: GameNpc;
|
||||
|
|
@ -183,7 +183,7 @@ export function NpcProfile({ npc, listings }: NpcProfileProps) {
|
|||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 text-sm font-semibold tabular-nums">
|
||||
{formatAmount(listing.price, listing.currency)}
|
||||
<CurrencyAmount value={listing.price} currency={listing.currency} />
|
||||
</span>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ export interface CurrencyConfig {
|
|||
readonly resourceName?: string | null;
|
||||
}
|
||||
|
||||
type CurrencyDisplay = CurrencyLike | CurrencyConfig | number | null;
|
||||
export type CurrencyDisplay = CurrencyLike | CurrencyConfig | number | null;
|
||||
|
||||
function isCurrencyConfig(currency: CurrencyLike | CurrencyConfig): currency is CurrencyConfig {
|
||||
export function isCurrencyConfig(currency: CurrencyLike | CurrencyConfig): currency is CurrencyConfig {
|
||||
return (
|
||||
"symbol" in currency ||
|
||||
"singular" in currency ||
|
||||
|
|
@ -47,12 +47,7 @@ export function currencyLabel(currency?: CurrencyDisplay, amount = 2): string {
|
|||
export function formatAmount(amount: number, currency?: CurrencyDisplay): string {
|
||||
const n = amount.toLocaleString();
|
||||
const label = currencyLabel(currency, amount);
|
||||
const symbol =
|
||||
currency && typeof currency === "object" && isCurrencyConfig(currency)
|
||||
? currency.symbol?.trim()
|
||||
: undefined;
|
||||
const formatted = symbol ? `${symbol}${n}` : n;
|
||||
return label ? `${formatted} ${label}` : formatted;
|
||||
return label ? `${n} ${label}` : n;
|
||||
}
|
||||
|
||||
export function formatDate(date: string | Date): string {
|
||||
|
|
|
|||
|
|
@ -51,10 +51,15 @@ export async function getMainCurrencyName(payload: PayloadType): Promise<string
|
|||
|
||||
export function currencyConfigFromGameRules(rules: GameRule): CurrencyConfig {
|
||||
const resource = typeof rules.mainCurrency === "object" ? rules.mainCurrency : null;
|
||||
// currencySymbol is a media id at depth 0 or a hydrated doc at depth >= 1, so guard the object case before reading .url.
|
||||
const symbolMedia =
|
||||
typeof rules.currencySymbol === "object" && rules.currencySymbol !== null
|
||||
? rules.currencySymbol
|
||||
: null;
|
||||
|
||||
return {
|
||||
name: rules.currencyName?.trim() || null,
|
||||
symbol: rules.currencySymbol?.trim() || null,
|
||||
symbol: symbolMedia?.url ?? null,
|
||||
singular: rules.currencySingular?.trim() || null,
|
||||
plural: rules.currencyPlural?.trim() || null,
|
||||
denomination: rules.currencyDenomination?.trim() || null,
|
||||
|
|
|
|||
17
src/migrations/20260921_140000_add_currency_symbol_image.ts
Normal file
17
src/migrations/20260921_140000_add_currency_symbol_image.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "game_rules" DROP COLUMN "currency_symbol";
|
||||
ALTER TABLE "game_rules" ADD COLUMN "currency_symbol_id" integer;
|
||||
ALTER TABLE "game_rules" ADD CONSTRAINT "game_rules_currency_symbol_id_media_id_fk" FOREIGN KEY ("currency_symbol_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
CREATE INDEX "game_rules_currency_symbol_idx" ON "game_rules" USING btree ("currency_symbol_id");`)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "game_rules" DROP CONSTRAINT "game_rules_currency_symbol_id_media_id_fk";
|
||||
DROP INDEX "game_rules_currency_symbol_idx";
|
||||
ALTER TABLE "game_rules" DROP COLUMN "currency_symbol_id";
|
||||
ALTER TABLE "game_rules" ADD COLUMN "currency_symbol" varchar;`)
|
||||
}
|
||||
|
|
@ -77,6 +77,11 @@ import * as migration_20260918_194842_add_operation_allocation from './20260918_
|
|||
import * as migration_20260918_222549_add_operation_readiness from './20260918_222549_add_operation_readiness';
|
||||
import * as migration_20260920_044743_add_zone_pressure from './20260920_044743_add_zone_pressure';
|
||||
import * as migration_20260921_113443_add_supply_boxes from './20260921_113443_add_supply_boxes';
|
||||
import * as migration_20260921_120000_add_world_map_flag from './20260921_120000_add_world_map_flag';
|
||||
import * as migration_20260921_123000_add_map_intel_fields from './20260921_123000_add_map_intel_fields';
|
||||
import * as migration_20260921_130500_weather_freeform from './20260921_130500_weather_freeform';
|
||||
import * as migration_20260921_134000_add_node_icons from './20260921_134000_add_node_icons';
|
||||
import * as migration_20260921_140000_add_currency_symbol_image from './20260921_140000_add_currency_symbol_image';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -472,6 +477,31 @@ export const migrations = [
|
|||
{
|
||||
up: migration_20260921_113443_add_supply_boxes.up,
|
||||
down: migration_20260921_113443_add_supply_boxes.down,
|
||||
name: '20260921_113443_add_supply_boxes'
|
||||
name: '20260921_113443_add_supply_boxes',
|
||||
},
|
||||
{
|
||||
up: migration_20260921_120000_add_world_map_flag.up,
|
||||
down: migration_20260921_120000_add_world_map_flag.down,
|
||||
name: '20260921_120000_add_world_map_flag',
|
||||
},
|
||||
{
|
||||
up: migration_20260921_123000_add_map_intel_fields.up,
|
||||
down: migration_20260921_123000_add_map_intel_fields.down,
|
||||
name: '20260921_123000_add_map_intel_fields',
|
||||
},
|
||||
{
|
||||
up: migration_20260921_130500_weather_freeform.up,
|
||||
down: migration_20260921_130500_weather_freeform.down,
|
||||
name: '20260921_130500_weather_freeform',
|
||||
},
|
||||
{
|
||||
up: migration_20260921_134000_add_node_icons.up,
|
||||
down: migration_20260921_134000_add_node_icons.down,
|
||||
name: '20260921_134000_add_node_icons',
|
||||
},
|
||||
{
|
||||
up: migration_20260921_140000_add_currency_symbol_image.up,
|
||||
down: migration_20260921_140000_add_currency_symbol_image.down,
|
||||
name: '20260921_140000_add_currency_symbol_image',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue