bigbullui logobigbullui
Componentdata

Exchange Rate

Exchange rate converter with two-way live calculation.

Preview

Live Preview
2,9

1 TRY = 0.0290 USD

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add exchange-rate

Or install the full npm package:

npm install bigbullui

Usage

import { ExchangeRate } from "@/components/ui/exchange-rate";

<ExchangeRate />

Source code

exchange-rate.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface ExchangeRateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
  rates?: Record<string, number>;
  defaultFrom?: string;
  defaultTo?: string;
}

const RATE_TABLE: Record<string, number> = { TRY: 1, USD: 0.029, EUR: 0.027, GBP: 0.023 };

function convert(amount: number, from: string, to: string, rates: Record<string, number>): number {
  const f = rates[from] ?? 1;
  const t = rates[to] ?? 1;
  if (f === 0) return 0;
  return (amount / f) * t;
}

/** Döviz çevirici: iki alan canlı çapraz hesap. */
export function ExchangeRate({ rates = RATE_TABLE, defaultFrom = "TRY", defaultTo = "USD", className, ...props }: ExchangeRateProps) {
  const codes = Object.keys(rates);
  const [from, setFrom] = React.useState(defaultFrom);
  const [to, setTo] = React.useState(defaultTo);
  const [amount, setAmount] = React.useState(100);

  const result = convert(amount, from, to, rates);

  const select = (side: "from" | "to", code: string) => {
    if (side === "from") {
      if (code === to) setTo(from);
      setFrom(code);
    } else {
      if (code === from) setFrom(to);
      setTo(code);
    }
  };

  const Select = ({ side, value }: { side: "from" | "to"; value: string }) => (
    <select
      value={value}
      onChange={(e) => select(side, e.target.value)}
      aria-label={side === "from" ? "Kaynak para birimi" : "Hedef para birimi"}
      className="rounded-md border border-input bg-background px-2 py-1.5 font-mono text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors motion-reduce:transition-none"
    >
      {codes.map((c) => <option key={c} value={c}>{c}</option>)}
    </select>
  );

  return (
    <div className={cn("w-full max-w-sm rounded-lg border border-border bg-card p-4", className)} {...props}>
      <style>{`@keyframes exSwap { from { transform: rotate(0); } to { transform: rotate(180deg); } }`}</style>
      <div className="flex items-center gap-2">
        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(Math.max(0, Number(e.target.value)))}
          aria-label="Miktar"
          className="min-w-0 flex-1 rounded-md border border-input bg-background px-3 py-2 font-mono text-sm tabular-nums focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors motion-reduce:transition-none"
        />
        <Select side="from" value={from} />
      </div>
      <div className="my-2 flex justify-center">
        <button
          onClick={() => { const f = from; setFrom(to); setTo(f); }}
          className="rounded-full border border-border p-1.5 text-muted-foreground transition-transform duration-300 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none"
          style={{ transitionProperty: "transform" }}
          onTransitionEnd={undefined}
          title="Değiştir"
        >
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="hover:animate-[exSwap_0.3s_ease-out] motion-reduce:animate-none" aria-hidden="true"><path d="M7 16V4m0 0L3 8m4-4l4 4" /><path d="M17 8v12m0 0l4-4m-4 4l-4-4" /></svg>
        </button>
      </div>
      <div className="flex items-center gap-2">
        <output className="min-w-0 flex-1 rounded-md border border-dashed border-accent/50 bg-accent/5 px-3 py-2 font-mono text-sm font-bold tabular-nums text-accent">
          {result.toLocaleString("tr-TR", { maximumFractionDigits: 2 })}
        </output>
        <Select side="to" value={to} />
      </div>
      <p className="mt-2 text-center font-mono text-[9px] uppercase tracking-wider text-muted-foreground">
        1 {from} = {convert(1, from, to, rates).toFixed(4)} {to}
      </p>
    </div>
  );
}

Props

PropTypeDescription
ratesRecord<string, number>
defaultFromstring
defaultTostring