1
Fork 0
currency-converter/app.js
Leonardo Devai bb45a78a9c Review and modernize all 42 projects to the updated standard
Pinned 2026 toolchains (Go 1.26, Rust 1.98/edition 2024, Python 3.14 + uv, Node 24,
Zig 0.16, NixOS 26.05), postgres 18 / mongo 8, lockfiles built from, non-root
runtimes, .dockerignore, per-project LICENSE, READMEs with the git.devai.io clone
line, checkout@v7 CI. Security fixes in the legacy Rust APIs (any-password login,
self-assigned admin, hard-coded JWT secret), JWT alg/exp/sub enforcement across the
blog series, safe markdown links in the frontends, and many smaller bugs — every
project was built, run and exercised end to end.

Adds scripts/publish.sh + a CI publish job that splits every folder into its own
repo at git.devai.io/templates/<folder>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01128fhuZbgivaSJvtMf4s1G
2026-09-27 21:10:38 +02:00

94 lines
2.9 KiB
JavaScript

const amountInput = document.getElementById("amount");
const fromSelect = document.getElementById("from");
const toSelect = document.getElementById("to");
const rateEl = document.getElementById("rate");
const convertedEl = document.getElementById("converted");
const statusEl = document.getElementById("status");
const API = "https://api.frankfurter.dev/v1";
const money = (n, currency) =>
new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
// The rate for the chosen pair. Typing an amount only multiplies by it; the API
// is asked again only when a dropdown changes. null = no rate yet.
let rate = null;
let rateDate = "";
async function loadCurrencies() {
statusEl.textContent = "Loading…";
try {
const res = await fetch(`${API}/currencies`);
if (!res.ok) throw new Error(`Currencies returned ${res.status}`);
// { "AUD": "Australian Dollar", "BRL": "Brazilian Real", … }
const currencies = await res.json();
for (const [code, name] of Object.entries(currencies)) {
fromSelect.append(new Option(`${code} — ${name}`, code));
toSelect.append(new Option(`${code} — ${name}`, code));
}
fromSelect.value = "USD";
toSelect.value = "EUR";
statusEl.textContent = "";
loadRate();
} catch (err) {
statusEl.textContent = "Couldn't load the currency list. Try again in a moment.";
console.error(err);
}
}
async function loadRate() {
const from = fromSelect.value;
const to = toSelect.value;
rate = null;
if (from === to) {
rate = 1;
rateDate = "";
showResult();
return;
}
showResult();
statusEl.textContent = "Loading rate…";
try {
// QUERY PARAMETERS: everything after the "?" is name=value pairs joined by
// "&". They tell the same endpoint WHAT you want: here, from which currency
// to which. Change the values and you get a different answer.
const res = await fetch(`${API}/latest?from=${from}&to=${to}`);
if (!res.ok) throw new Error(`Rate returned ${res.status}`);
// { amount: 1, base: "USD", date: "2026-09-25", rates: { EUR: 0.87696 } }
const data = await res.json();
if (from !== fromSelect.value || to !== toSelect.value) return; // pair changed meanwhile
rate = data.rates[to];
rateDate = data.date;
statusEl.textContent = "";
showResult();
} catch (err) {
statusEl.textContent = "Couldn't get the rate. Try again in a moment.";
console.error(err);
}
}
function showResult() {
if (rate === null) {
rateEl.textContent = "";
convertedEl.textContent = "—";
return;
}
const amount = parseFloat(amountInput.value) || 0;
const to = toSelect.value;
rateEl.textContent = `1 ${fromSelect.value} = ${rate} ${to}` + (rateDate ? ` · ECB rate of ${rateDate}` : "");
convertedEl.textContent = money(amount * rate, to);
}
amountInput.addEventListener("input", showResult);
fromSelect.addEventListener("change", loadRate);
toSelect.addEventListener("change", loadRate);
loadCurrencies();