import { Plus, Vault, X } from "lucide-react"; import { useState } from "react"; import { createPortal } from "react-dom"; import { ChannelPicker, UserPicker } from "./EntityPicker"; import { ActionButton } from "./ActionButton"; import { currencyExponent, formatCurrency, toSmallestUnits } from "../lib/format"; import type { AccountRow, ChannelRow, CollectibleCurrency } from "../types"; type OwnerKind = "vault" | "user" | "channel"; // MintCollectibleUsernameModal collects everything a new collectible needs, // grouped into clearly labeled steps (identity, owner, price, optional // marketplace record) instead of one flat wall of fields -- then hands off to // ActionButton for the usual reason/dry-run/confirm flow. export function MintCollectibleUsernameModal({ onClose, onMinted }: { onClose: () => void; onMinted: () => void }) { const [ownerKind, setOwnerKind] = useState("vault"); const [owner, setOwner] = useState(null); const [ownerChannel, setOwnerChannel] = useState(null); const [mintUsername, setMintUsername] = useState(""); const [currency, setCurrency] = useState("XTR"); const [amount, setAmount] = useState(""); const [addCryptoLeg, setAddCryptoLeg] = useState(false); const [cryptoCurrency, setCryptoCurrency] = useState("TON"); const [cryptoAmount, setCryptoAmount] = useState(""); const [showRecord, setShowRecord] = useState(false); const [url, setUrl] = useState(""); const [purchaseDate, setPurchaseDate] = useState(""); const [purchaseTime, setPurchaseTime] = useState(""); // int64 request fields are sent as decimal strings (the backend tags them // `,string`); purchase_date is Unix seconds. Both amounts are typed in whole // currency units and converted here: the API and fragment.collectibleInfo // carry smallest units, so 900 TON has to leave the panel as // 900000000000 nanotons or clients render 0.0000009. const minorAmount = toSmallestUnits(amount, currency); const minorCryptoAmount = addCryptoLeg ? toSmallestUnits(cryptoAmount, cryptoCurrency) : "0"; const amountInvalid = minorAmount === null; const cryptoAmountInvalid = addCryptoLeg && minorCryptoAmount === null; const canSubmit = mintUsername.trim() !== "" && amount.trim() !== "" && !amountInvalid && !cryptoAmountInvalid && (ownerKind === "vault" || (ownerKind === "user" ? owner !== null : ownerChannel !== null)); function mintPayload(): Record { const payload: Record = { username: mintUsername.trim().replace(/^@/, ""), currency, amount: minorAmount ?? "0" }; if (ownerKind === "user" && owner) payload.owner_user_id = String(owner.ID); if (ownerKind === "channel" && ownerChannel) payload.owner_channel_id = String(ownerChannel.ID); // The backend accepts either no crypto leg at all, or TON with a positive // nanoton amount -- never a currency without an amount. if (addCryptoLeg) { payload.crypto_currency = cryptoCurrency; payload.crypto_amount = minorCryptoAmount ?? "0"; } if (url.trim()) payload.url = url.trim(); if (purchaseDate) { // fragment.collectibleInfo.purchase_date is a unix timestamp, and the date // has always been read as UTC here. The time follows the same clock rather // than the operator's local one, so adding it cannot silently shift what a // date-only entry used to mean; the field label says UTC. const parsed = Date.parse(`${purchaseDate}T${purchaseTime || "00:00"}:00Z`); if (Number.isFinite(parsed)) payload.purchase_date = Math.floor(parsed / 1000); } return payload; } return createPortal(
{"NFT usernames"}

{"Mint a collectible username"}

{"1. Username"}
{"2. Owner"}
{ownerKind === "user" && } {ownerKind === "channel" && } {ownerKind === "vault" &&

{"Mints the asset unassigned; issue it to someone later from the asset page."}

}
{"3. Price"}

{"A record of what it was sold for -- minting doesn't charge anyone."}

{amount.trim() !== "" && !amountInvalid && (

{`Clients will show: ${formatCurrency(minorAmount ?? "0", currency)}.`}

)} {amountInvalid &&

{`Not a valid ${currency} amount: digits only, at most ${String(currencyExponent(currency))} decimal places.`}

} {addCryptoLeg && (
)} {cryptoAmountInvalid &&

{`Not a valid ${cryptoCurrency} amount: digits only, at most ${String(currencyExponent(cryptoCurrency))} decimal places.`}

}
{showRecord && (
)}
} tone="neutral" path="/api/actions/mint-collectible-username" payload={mintPayload} onDone={onMinted} />
, document.body ); }