diff --git a/frontend/app.js b/frontend/app.js index c6fcaa9..851b263 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1,7 +1,7 @@ // /home/def/monitor/frontend/app.js function el(tag, cls) { - const n = document.createElement(tag); + const n = document.createElement(tag || "div"); if (cls) n.className = cls; return n; } @@ -376,12 +376,120 @@ function makeOraclePanel(pricesData) { `; wrap.appendChild(meta); + const quoteSection = el("div", "oracle-quote-wrap"); + const quoteTitle = el("div", "oracle-quote-title"); + quoteTitle.textContent = "Live Quote Calculator"; + + const quoteForm = el("div", "oracle-quote-form"); + + const amountWrap = el("div", "oracle-quote-field"); + const amountLabel = el("label", "oracle-quote-label"); + amountLabel.setAttribute("for", "oracleQuoteAmount"); + amountLabel.textContent = "Amount (CAD)"; + const amountInput = document.createElement("input"); + amountInput.id = "oracleQuoteAmount"; + amountInput.className = "oracle-quote-input"; + amountInput.type = "number"; + amountInput.min = "0.01"; + amountInput.step = "0.01"; + amountInput.value = "100.00"; + amountWrap.appendChild(amountLabel); + amountWrap.appendChild(amountInput); + + const buttonWrap = el("div", "oracle-quote-actions"); + const quoteBtn = document.createElement("button"); + quoteBtn.className = "oracle-quote-button"; + quoteBtn.type = "button"; + quoteBtn.textContent = "Get Quote"; + buttonWrap.appendChild(quoteBtn); + + quoteForm.appendChild(amountWrap); + quoteForm.appendChild(buttonWrap); + + const quoteResults = el("div", "oracle-quote-results"); + quoteResults.id = "oracleQuoteResults"; + quoteResults.innerHTML = `
Enter an amount and load a live quote.
`; + + quoteSection.appendChild(quoteTitle); + quoteSection.appendChild(quoteForm); + quoteSection.appendChild(quoteResults); + wrap.appendChild(quoteSection); + const list = el("div", "oracle-assets"); for (const asset of assets) { list.appendChild(makeOracleAssetRow(asset)); } wrap.appendChild(list); + requestAnimationFrame(() => { + const btn = document.querySelector(".oracle-quote-button"); + const input = document.getElementById("oracleQuoteAmount"); + const results = document.getElementById("oracleQuoteResults"); + + async function loadQuote() { + const raw = input?.value; + const amount = Number(raw); + + if (!Number.isFinite(amount) || amount <= 0) { + results.innerHTML = `
Enter a valid CAD amount above 0.
`; + return; + } + + results.innerHTML = `
Loading quote…
`; + + try { + const res = await fetch(`/api/oracle/quote?fiat=CAD&amount=${encodeURIComponent(amount)}`, { cache: "no-store" }); + const data = await res.json(); + + if (!res.ok || !data || !Array.isArray(data.quotes)) { + results.innerHTML = `
Quote request failed.
`; + return; + } + + const rows = data.quotes.map((q) => { + const availability = q.available ? "available" : (q.reason || "unavailable"); + const recommended = q.recommended ? `recommended` : ""; + const stale = q.available ? `live` : `${availability}`; + + return ` +
+
+
${q.symbol} • ${q.chain}
+
1 ${q.symbol} = ${q.price_cad != null ? fmtNumber(q.price_cad) : "—"} CAD
+
+
+
${recommended}${stale}
+
${q.display_amount || "—"}
+
+
+ `; + }).join(""); + + results.innerHTML = ` +
+
Quoted: ${fmtDateTime(data.quoted_at)}
+
Expires: ${fmtDateTime(data.expires_at)}
+
+
${rows}
+ `; + } catch (_err) { + results.innerHTML = `
Quote request failed.
`; + } + } + + if (btn && !btn.dataset.bound) { + btn.dataset.bound = "1"; + btn.addEventListener("click", loadQuote); + } + + if (input && !input.dataset.bound) { + input.dataset.bound = "1"; + input.addEventListener("keydown", (ev) => { + if (ev.key === "Enter") loadQuote(); + }); + } + }); + return wrap; } @@ -432,7 +540,7 @@ async function refresh() { linesData = await linesRes.json(); oraclePricesData = await oracleRes.json(); - } catch (e) { + } catch (_e) { setStatus(false, false, "Fetch failed"); root.innerHTML = ""; const m = el("div", "subline"); @@ -449,7 +557,7 @@ async function refresh() { root.innerHTML = ""; - if (oraclePricesData && oraclePricesData.assets) { + if (oraclePricesData && oraclePricesData.assets && Object.keys(oraclePricesData.assets).length > 0) { root.appendChild(makeOraclePanel(oraclePricesData)); const sep0 = el("div", "sep"); diff --git a/frontend/styles.css b/frontend/styles.css index 68ad9f6..524dec8 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -380,3 +380,167 @@ canvas.spark{ grid-template-columns:1fr; } } + + +/* Quote calculator */ +.oracle-quote-wrap{ + display:flex; + flex-direction:column; + gap:10px; + border:1px solid rgba(255,255,255,.06); + border-radius:14px; + padding:12px; + background: rgba(0,0,0,.12); +} +[data-theme="light"] .oracle-quote-wrap{ + background: rgba(255,255,255,.55); + border:1px solid rgba(0,0,0,.06); +} + +.oracle-quote-title{ + font-size:13px; + letter-spacing:.08em; + text-transform:uppercase; + color:var(--muted); + font-weight:800; +} + +.oracle-quote-form{ + display:grid; + grid-template-columns:minmax(0,1fr) auto; + gap:10px; + align-items:end; +} + +.oracle-quote-field{ + display:flex; + flex-direction:column; + gap:6px; +} + +.oracle-quote-label{ + font-size:12px; + color:var(--muted); +} + +.oracle-quote-input{ + width:100%; + background: rgba(255,255,255,.04); + color: var(--text); + border:1px solid rgba(255,255,255,.10); + border-radius:10px; + padding:10px 12px; + outline:none; +} +[data-theme="light"] .oracle-quote-input{ + background: rgba(0,0,0,.03); + border:1px solid rgba(0,0,0,.10); +} + +.oracle-quote-actions{ + display:flex; + align-items:end; +} + +.oracle-quote-button{ + border:1px solid rgba(255,255,255,.10); + background: var(--toggle-on); + color:#fff; + border-radius:10px; + padding:10px 14px; + font-weight:800; + cursor:pointer; +} +.oracle-quote-button:hover{ + filter:brightness(1.06); +} + +.oracle-quote-results{ + display:flex; + flex-direction:column; + gap:8px; +} + +.oracle-quote-empty{ + font-size:12px; + color:var(--muted); +} + +.oracle-quote-meta{ + display:flex; + justify-content:space-between; + gap:10px; + font-size:12px; + color:var(--muted); +} + +.oracle-quote-list{ + display:flex; + flex-direction:column; + gap:8px; +} + +.oracle-quote-row{ + display:flex; + justify-content:space-between; + align-items:center; + gap:10px; + border:1px solid rgba(255,255,255,.06); + border-radius:12px; + padding:10px 12px; + background: rgba(255,255,255,.02); +} +[data-theme="light"] .oracle-quote-row{ + background: rgba(0,0,0,.02); + border:1px solid rgba(0,0,0,.06); +} + +.oracle-quote-row-left{ + min-width:0; +} + +.oracle-quote-asset{ + font-size:14px; + font-weight:800; +} + +.oracle-quote-sub{ + font-size:12px; + color:var(--muted); + margin-top:3px; +} + +.oracle-quote-row-right{ + display:flex; + flex-direction:column; + align-items:flex-end; + gap:6px; +} + +.oracle-quote-badges{ + display:flex; + gap:6px; + flex-wrap:wrap; + justify-content:flex-end; +} + +.oracle-quote-amount{ + font-size:15px; + font-weight:900; +} + +@media (max-width: 720px){ + .oracle-quote-form{ + grid-template-columns:1fr; + } + + .oracle-quote-row, + .oracle-quote-meta{ + flex-direction:column; + align-items:flex-start; + } + + .oracle-quote-row-right{ + align-items:flex-start; + } +}