feat(ui): add dynamic amount currency prefix, normalize fiat symbols, and simplify data source badge

This commit is contained in:
2026-03-09 14:41:52 +01:00
parent 3fff234938
commit 9511ba94b1
3 changed files with 155 additions and 73 deletions

View File

@@ -18,6 +18,7 @@ import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { getDisplaySymbol } from "@/lib/currency-display";
import { useMarketRates } from "@/hooks/use-market-rates";
import {
formatAmount,
@@ -26,6 +27,7 @@ import {
formatTimestamp,
} from "@/lib/format";
import { buildRateMap, convertAmount } from "@/lib/rates";
import { cn } from "@/lib/utils";
import { validateAmount } from "@/lib/validation";
const DEFAULT_FROM = "USD";
@@ -210,6 +212,7 @@ export function ConverterCard({
: "--";
const amountError = inputValidation.ok ? null : inputValidation.error;
const amountPrefix = getDisplaySymbol(fromAsset);
return (
<Card className="relative overflow-hidden border-border/70 bg-card/90">
@@ -220,27 +223,49 @@ export function ConverterCard({
</CardTitle>
<Badge
variant="outline"
className="border-border/70 bg-background/50"
className="max-w-full border-border/70 bg-background/50 text-[11px] sm:text-xs"
>
<span className="text-muted-foreground">Fiat rates by</span>
<a
href="https://frankfurter.dev/"
target="_blank"
rel="noreferrer"
className="ml-1 text-foreground transition-colors hover:text-cyan-100"
>
{data.sources.fiat}
</a>
<span className="mx-1 text-muted-foreground"></span>
<span className="text-muted-foreground">Price data by</span>
<a
href="https://www.coingecko.com/"
target="_blank"
rel="noreferrer"
className="ml-1 text-foreground transition-colors hover:text-cyan-100"
>
{data.sources.crypto}
</a>
<span className="inline-flex items-center gap-1 whitespace-nowrap sm:hidden">
<span className="text-muted-foreground">Data sources:</span>
<a
href="https://frankfurter.dev/"
target="_blank"
rel="noreferrer"
className="text-foreground transition-colors hover:text-cyan-100"
>
{data.sources.fiat}
</a>
<span className="text-muted-foreground"></span>
<a
href="https://www.coingecko.com/"
target="_blank"
rel="noreferrer"
className="text-foreground transition-colors hover:text-cyan-100"
>
{data.sources.crypto}
</a>
</span>
<span className="hidden items-center gap-1 whitespace-nowrap sm:inline-flex">
<span className="text-muted-foreground">Data sources:</span>
<a
href="https://frankfurter.dev/"
target="_blank"
rel="noreferrer"
className="text-foreground transition-colors hover:text-cyan-100"
>
{data.sources.fiat}
</a>
<span className="text-muted-foreground"></span>
<a
href="https://www.coingecko.com/"
target="_blank"
rel="noreferrer"
className="text-foreground transition-colors hover:text-cyan-100"
>
{data.sources.crypto}
</a>
</span>
</Badge>
</div>
<CardDescription className="pr-1 text-base/7 sm:text-sm">
@@ -265,17 +290,27 @@ export function ConverterCard({
>
Amount
</label>
<Input
id="amount"
type="text"
inputMode="decimal"
value={amountInput}
onChange={(event) => setAmountInput(event.target.value)}
placeholder="Enter amount"
className="h-14 rounded-xl bg-background/70 px-4 text-lg"
aria-invalid={Boolean(amountError)}
aria-describedby={amountError ? "amount-error" : undefined}
/>
<div className="relative">
{amountPrefix ? (
<span className="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-sm text-muted-foreground/80">
{amountPrefix}
</span>
) : null}
<Input
id="amount"
type="text"
inputMode="decimal"
value={amountInput}
onChange={(event) => setAmountInput(event.target.value)}
placeholder="Enter amount"
className={cn(
"h-14 rounded-xl bg-background/70 px-4 text-lg",
amountPrefix ? "pl-10" : "",
)}
aria-invalid={Boolean(amountError)}
aria-describedby={amountError ? "amount-error" : undefined}
/>
</div>
{amountError ? (
<p id="amount-error" className="text-sm text-red-300">
{amountError}

View File

@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
import { Check, ChevronDown } from "lucide-react";
import { POPULAR_CODES } from "@/lib/assets";
import { getDisplaySymbol } from "@/lib/currency-display";
import { RateAsset } from "@/lib/rates";
import { cn } from "@/lib/utils";
import { CurrencyIcon } from "@/components/converter/currency-icon";
@@ -34,6 +35,8 @@ function AssetLabel({ asset }: { asset?: RateAsset }) {
);
}
const displaySymbol = getDisplaySymbol(asset);
return (
<span className="flex min-w-0 items-center gap-2.5 text-left">
<CurrencyIcon code={asset.code} type={asset.type} size="md" className="shrink-0" />
@@ -43,7 +46,7 @@ function AssetLabel({ asset }: { asset?: RateAsset }) {
</span>
<span className="truncate text-xs text-muted-foreground">
{asset.type === "fiat" ? "Fiat" : "Crypto"}
{asset.symbol ? ` | ${asset.symbol}` : ""}
{displaySymbol ? ` | ${displaySymbol}` : ""}
</span>
</span>
</span>
@@ -111,7 +114,50 @@ export function CurrencySelect({
<CommandEmpty>No assets found.</CommandEmpty>
{popularAssets.length > 0 ? (
<CommandGroup heading="Popular">
{popularAssets.map((asset) => (
{popularAssets.map((asset) => {
const displaySymbol = getDisplaySymbol(asset);
return (
<CommandItem
key={asset.code}
value={`${asset.code} ${asset.name} ${asset.type}`}
onSelect={() => {
onChange(asset.code);
setOpen(false);
}}
>
<div className="flex min-w-0 flex-1 items-center gap-2.5">
<CurrencyIcon
code={asset.code}
type={asset.type}
size="md"
className="shrink-0"
/>
<span className="flex min-w-0 flex-col">
<span className="truncate font-medium">
{asset.code} - {asset.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{asset.type === "fiat" ? "Fiat" : "Crypto"}
{displaySymbol ? ` | ${displaySymbol}` : ""}
</span>
</span>
</div>
<Check
className={cn(
"ml-2 h-4 w-4",
value === asset.code ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
);
})}
</CommandGroup>
) : null}
{allAssets.length > 0 ? <CommandSeparator /> : null}
<CommandGroup heading="All assets">
{allAssets.map((asset) => {
const displaySymbol = getDisplaySymbol(asset);
return (
<CommandItem
key={asset.code}
value={`${asset.code} ${asset.name} ${asset.type}`}
@@ -133,7 +179,7 @@ export function CurrencySelect({
</span>
<span className="truncate text-xs text-muted-foreground">
{asset.type === "fiat" ? "Fiat" : "Crypto"}
{asset.symbol ? ` | ${asset.symbol}` : ""}
{displaySymbol ? ` | ${displaySymbol}` : ""}
</span>
</span>
</div>
@@ -144,45 +190,8 @@ export function CurrencySelect({
)}
/>
</CommandItem>
))}
</CommandGroup>
) : null}
{allAssets.length > 0 ? <CommandSeparator /> : null}
<CommandGroup heading="All assets">
{allAssets.map((asset) => (
<CommandItem
key={asset.code}
value={`${asset.code} ${asset.name} ${asset.type}`}
onSelect={() => {
onChange(asset.code);
setOpen(false);
}}
>
<div className="flex min-w-0 flex-1 items-center gap-2.5">
<CurrencyIcon
code={asset.code}
type={asset.type}
size="md"
className="shrink-0"
/>
<span className="flex min-w-0 flex-col">
<span className="truncate font-medium">
{asset.code} - {asset.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{asset.type === "fiat" ? "Fiat" : "Crypto"}
{asset.symbol ? ` | ${asset.symbol}` : ""}
</span>
</span>
</div>
<Check
className={cn(
"ml-2 h-4 w-4",
value === asset.code ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
);
})}
</CommandGroup>
</CommandList>
</Command>