From ab052377b96bb8cb48e7fec7cd367ab759b99220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hn=C3=ADzdil=20SOFTIM=2ECZ?= Date: Tue, 7 Apr 2026 13:08:01 +0200 Subject: [PATCH] =?UTF-8?q?Landing=20page:=20add=20AccBot=20vs=20=C5=A0tos?= =?UTF-8?q?uj=20interactive=20comparison=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DCA simulator with date/amount/interval inputs fetching historical BTC/CZK prices from CryptoCompare API - Dual comparison cards showing fees, accumulated BTC/sats, and current CZK value - Interactive SVG chart with BTC price line (right Y-axis), accumulation curves (left Y-axis), and synchronized hover tooltips - Difference sparkline showing cumulative AccBot savings over time - End-of-chart bracket annotation highlighting the BTC difference - Full CS/EN i18n support with template-based translations via i18n.js t() method - Responsive layout for mobile Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/css/style.css | 371 +++++++++++++++++++++++++++++ docs/index.html | 102 ++++++++ docs/js/compare.js | 555 +++++++++++++++++++++++++++++++++++++++++++ docs/js/i18n.js | 9 +- docs/locales/cs.json | 23 ++ docs/locales/en.json | 23 ++ 6 files changed, 1082 insertions(+), 1 deletion(-) create mode 100644 docs/js/compare.js diff --git a/docs/css/style.css b/docs/css/style.css index 28f9e9f..be4dd8b 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1101,3 +1101,374 @@ section { padding: 96px 0; } margin-top: 40px; font-weight: 500; } + +/* ====================== + Compare Section + ====================== */ +.compare-form { + background: var(--surface); + border: 1px solid rgba(255,255,255,0.06); + border-radius: var(--radius-lg); + padding: 32px; + margin-bottom: 32px; +} + +.compare-inputs { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 20px; + margin-bottom: 24px; +} + +.compare-input-group label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-muted); + margin-bottom: 8px; +} + +.compare-input-group input { + width: 100%; + background: var(--surface-alt); + border: 1px solid rgba(255,255,255,0.1); + border-radius: var(--radius); + color: var(--text); + font-family: inherit; + font-size: 0.9375rem; + padding: 10px 14px; + outline: none; + transition: border-color 0.2s; + color-scheme: dark; +} + +.compare-input-group input:focus { + border-color: var(--accent); +} + +.compare-interval-btns { + display: flex; + gap: 0; + border: 1px solid rgba(255,255,255,0.1); + border-radius: var(--radius); + overflow: hidden; +} + +.compare-interval-btn { + flex: 1; + background: var(--surface-alt); + border: none; + color: var(--text-muted); + font-family: inherit; + font-size: 0.8125rem; + font-weight: 500; + padding: 10px 14px; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.compare-interval-btn:not(:last-child) { + border-right: 1px solid rgba(255,255,255,0.1); +} + +.compare-interval-btn:hover { + color: var(--text); +} + +.compare-interval-btn.active { + background: var(--surface-raised); + color: var(--text); +} + +.compare-calc-btn { + width: 100%; +} + +.compare-results { + margin-top: 32px; +} + +.compare-loading { + display: flex; + justify-content: center; + align-items: center; + padding: 48px; +} + +.compare-loading::after { + content: ''; + width: 32px; + height: 32px; + border: 3px solid rgba(255,255,255,0.1); + border-top-color: var(--accent); + border-radius: 50%; + animation: compare-spin 0.8s linear infinite; +} + +@keyframes compare-spin { + to { transform: rotate(360deg); } +} + +.compare-error { + text-align: center; + padding: 24px; + color: var(--error); + background: rgba(233, 69, 96, 0.1); + border: 1px solid rgba(233, 69, 96, 0.2); + border-radius: var(--radius); +} + +.compare-summary { + text-align: center; + color: var(--text-muted); + font-size: 0.875rem; + margin-bottom: 24px; +} + +.compare-cards { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 24px; +} + +.compare-card { + background: var(--surface); + border: 1px solid rgba(255,255,255,0.06); + border-radius: var(--radius-lg); + padding: 24px; +} + +.compare-card.accbot { + border-color: var(--accent); + box-shadow: 0 0 20px rgba(78, 204, 163, 0.1); +} + +.compare-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(255,255,255,0.06); +} + +.compare-card-name { + font-size: 1.125rem; + font-weight: 700; +} + +.compare-card-fee { + font-size: 0.8125rem; + font-weight: 600; + padding: 4px 10px; + border-radius: 4px; +} + +.compare-card.stosuj .compare-card-fee { + background: rgba(233, 69, 96, 0.1); + color: var(--error); +} + +.compare-card.accbot .compare-card-fee { + background: rgba(78, 204, 163, 0.1); + color: var(--accent); +} + +.compare-stat { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 8px 0; +} + +.compare-stat-label { + font-size: 0.8125rem; + color: var(--text-muted); +} + +.compare-stat-value { + font-size: 0.9375rem; + font-weight: 600; + text-align: right; +} + +.compare-stat-sub { + font-size: 0.75rem; + color: var(--text-muted); + text-align: right; + padding-bottom: 8px; +} + +.compare-savings { + background: rgba(78, 204, 163, 0.08); + border: 1px solid rgba(78, 204, 163, 0.2); + border-radius: var(--radius-lg); + padding: 20px 24px; + text-align: center; + font-size: 0.9375rem; + line-height: 1.7; + margin-bottom: 32px; +} + +.compare-savings strong { + color: var(--accent); +} + +.compare-chart-container { + background: var(--surface); + border: 1px solid rgba(255,255,255,0.06); + border-radius: var(--radius-lg); + padding: 24px; + position: relative; +} + +.compare-chart-container h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 16px; + text-align: center; +} + +.compare-svg { + width: 100%; + height: auto; + display: block; +} + +.compare-legend { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 12px; + flex-wrap: wrap; +} + +.compare-legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.8125rem; + color: var(--text-muted); +} + +.compare-legend-item::before { + content: ''; + width: 16px; + height: 3px; + border-radius: 1.5px; +} + +.compare-legend-item.stosuj::before { + background: #E94560; +} + +.compare-legend-item.accbot::before { + background: #4ECCA3; +} + +.compare-legend-item.price::before { + background: none; + border-top: 2px dashed #5B8DEF; + height: 0; +} + +/* Tooltip */ +.compare-tooltip { + position: absolute; + background: rgba(14, 14, 26, 0.95); + border: 1px solid rgba(255,255,255,0.12); + border-radius: var(--radius); + padding: 10px 14px; + font-size: 0.8125rem; + pointer-events: none; + z-index: 10; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + min-width: 200px; + white-space: nowrap; +} + +.ct-date { + font-weight: 600; + margin-bottom: 2px; +} + +.ct-price { + color: #5B8DEF; + font-size: 0.75rem; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.ct-row { + display: flex; + gap: 12px; + padding: 3px 0; + font-size: 0.8125rem; +} + +.ct-row span:first-child { + font-weight: 600; + min-width: 48px; +} + +.ct-row span:last-child { + color: var(--text-muted); + margin-left: auto; +} + +.ct-row.stosuj span:first-child { color: #E94560; } +.ct-row.accbot span:first-child { color: #4ECCA3; } + +.ct-diff { + display: flex; + gap: 12px; + padding: 6px 0 0; + margin-top: 4px; + border-top: 1px solid rgba(78, 204, 163, 0.25); + font-size: 0.8125rem; +} + +.ct-diff span:first-child { + font-weight: 600; + color: #4ECCA3; + min-width: 48px; +} + +.ct-diff span:nth-child(2) { + color: #4ECCA3; + font-weight: 600; +} + +.ct-diff span:last-child { + color: rgba(78, 204, 163, 0.7); + margin-left: auto; +} + +/* Diff sparkline */ +.compare-diff-chart { + margin-top: 4px; +} + +.compare-diff-svg { + height: auto; +} + +.compare-disclaimer { + text-align: center; + font-size: 0.75rem; + color: var(--text-muted); + opacity: 0.7; + margin-top: 24px; +} + +@media (max-width: 900px) { + .compare-inputs { + grid-template-columns: 1fr; + } + .compare-cards { + grid-template-columns: 1fr; + } +} diff --git a/docs/index.html b/docs/index.html index 9e63e93..384995e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -68,6 +68,7 @@
  • Features
  • Exchanges
  • Security
  • +
  • Compare
  • Download
  • @@ -799,6 +800,106 @@

    Accumulate

    + +
    +
    +
    +

    AccBot vs Štosuj

    +

    Štosuj charges 1.5% per purchase on Coinmate. AccBot lets you trade directly — just the standard 0.6% taker fee, no middleman markup.

    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + +
    + + + +
    +
    +
    + +
    + + +
    +
    +
    @@ -858,5 +959,6 @@

    Start Stacking Today

    + diff --git a/docs/js/compare.js b/docs/js/compare.js new file mode 100644 index 0000000..0bce18f --- /dev/null +++ b/docs/js/compare.js @@ -0,0 +1,555 @@ +(function () { + 'use strict'; + + var STOSUJ_FEE = 0.015; + var ACCBOT_FEE = 0.006; + + function init() { + var calcBtn = document.getElementById('compare-calc'); + if (!calcBtn) return; + + var startInput = document.getElementById('compare-start'); + var now = new Date(); + var ago = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); + startInput.value = isoDate(ago); + startInput.max = isoDate(now); + startInput.min = '2014-01-01'; + + var btns = document.querySelectorAll('.compare-interval-btn'); + for (var i = 0; i < btns.length; i++) { + btns[i].addEventListener('click', function () { + for (var j = 0; j < btns.length; j++) btns[j].classList.remove('active'); + this.classList.add('active'); + }); + } + + calcBtn.addEventListener('click', calculate); + document.getElementById('compare-amount').addEventListener('keydown', function (e) { + if (e.key === 'Enter') calculate(); + }); + document.getElementById('compare-start').addEventListener('keydown', function (e) { + if (e.key === 'Enter') calculate(); + }); + } + + function isoDate(d) { + var m = d.getMonth() + 1; + var day = d.getDate(); + return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day; + } + + function getInterval() { + var el = document.querySelector('.compare-interval-btn.active'); + return el ? el.getAttribute('data-interval') : 'weekly'; + } + + function genBuyDates(start, end, interval) { + var result = []; + var d = new Date(start); + while (d <= end) { + result.push(new Date(d)); + if (interval === 'daily') d.setDate(d.getDate() + 1); + else if (interval === 'weekly') d.setDate(d.getDate() + 7); + else d.setMonth(d.getMonth() + 1); + } + return result; + } + + function findPrice(prices, ts) { + var lo = 0, hi = prices.length - 1; + while (lo < hi) { + var mid = (lo + hi) >> 1; + if (prices[mid][0] < ts) lo = mid + 1; + else hi = mid; + } + if (lo > 0 && Math.abs(prices[lo - 1][0] - ts) < Math.abs(prices[lo][0] - ts)) lo--; + return prices[lo][1]; + } + + function findPriceIdx(prices, ts) { + var lo = 0, hi = prices.length - 1; + while (lo < hi) { + var mid = (lo + hi) >> 1; + if (prices[mid][0] < ts) lo = mid + 1; + else hi = mid; + } + if (lo > 0 && Math.abs(prices[lo - 1][0] - ts) < Math.abs(prices[lo][0] - ts)) lo--; + return lo; + } + + function fmtCZK(n) { + return new Intl.NumberFormat('cs-CZ', { + style: 'currency', currency: 'CZK', maximumFractionDigits: 0 + }).format(n); + } + + function fmtBTC(n) { + return n.toFixed(8) + ' BTC'; + } + + function fmtSats(n) { + return new Intl.NumberFormat('cs-CZ').format(Math.round(n * 1e8)) + ' sats'; + } + + function fmtAxisCZK(n) { + if (n >= 1e6) return (n / 1e6).toFixed(1).replace('.0', '') + 'M'; + if (n >= 1e3) return Math.round(n / 1e3) + 'k'; + return String(Math.round(n)); + } + + function fmtAxisSats(btc) { + var s = Math.round(btc * 1e8); + if (s >= 1e6) return (s / 1e6).toFixed(1).replace('.0', '') + 'M'; + if (s >= 1e3) return Math.round(s / 1e3) + 'k'; + return String(s); + } + + function tr(key, args) { + var str = window.AccBotI18n && window.AccBotI18n.t ? window.AccBotI18n.t(key) : key; + if (args) { + for (var i = 0; i < args.length; i++) { + str = str.replace('{' + i + '}', args[i]); + } + } + return str; + } + + function setText(id, val) { + var el = document.getElementById(id); + if (el) el.textContent = val; + } + + function fetchPrices(startTs, endTs) { + var allPrices = []; + + function fetchChunk(toTs) { + var daysNeeded = Math.ceil((toTs - startTs) / 86400) + 1; + var limit = Math.min(daysNeeded, 2000); + + return fetch( + 'https://min-api.cryptocompare.com/data/v2/histoday?fsym=BTC&tsym=CZK&limit=' + limit + '&toTs=' + toTs + ) + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function (data) { + if (data.Response !== 'Success') throw new Error(data.Message || 'API error'); + var points = data.Data.Data; + + for (var i = 0; i < points.length; i++) { + if (points[i].close > 0 && points[i].time >= startTs) { + allPrices.push([points[i].time * 1000, points[i].close]); + } + } + + var earliest = points[0] ? points[0].time : startTs; + if (limit === 2000 && earliest > startTs) { + return fetchChunk(earliest); + } + + allPrices.sort(function (a, b) { return a[0] - b[0]; }); + return allPrices; + }); + } + + return fetchChunk(endTs); + } + + function fetchCurrentPrice() { + return fetch('https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=CZK') + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function (data) { + if (!data.CZK) throw new Error('No CZK price'); + return data.CZK; + }); + } + + function calculate() { + var startVal = document.getElementById('compare-start').value; + var amount = parseFloat(document.getElementById('compare-amount').value); + var interval = getInterval(); + + if (!startVal || isNaN(amount) || amount <= 0) return; + + var startDate = new Date(startVal + 'T00:00:00'); + var now = new Date(); + if (startDate >= now) return; + + var resultsEl = document.getElementById('compare-results'); + var loadingEl = document.getElementById('compare-loading'); + var contentEl = document.getElementById('compare-content'); + var errorEl = document.getElementById('compare-error'); + + resultsEl.style.display = 'block'; + loadingEl.style.display = 'flex'; + contentEl.style.display = 'none'; + errorEl.style.display = 'none'; + + setTimeout(function () { + resultsEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, 100); + + var startTs = Math.floor(startDate.getTime() / 1000); + var endTs = Math.floor(now.getTime() / 1000); + + Promise.all([fetchPrices(startTs, endTs), fetchCurrentPrice()]) + .then(function (results) { + var prices = results[0]; + var curPrice = results[1]; + + if (!prices.length) throw new Error('No price data'); + + var dates = genBuyDates(startDate, now, interval); + if (!dates.length) throw new Error('No buy dates'); + + var sTotal = 0, aTotal = 0; + var sHist = [], aHist = []; + + for (var i = 0; i < dates.length; i++) { + var p = findPrice(prices, dates[i].getTime()); + sTotal += (amount * (1 - STOSUJ_FEE)) / p; + aTotal += (amount * (1 - ACCBOT_FEE)) / p; + sHist.push([dates[i].getTime(), sTotal]); + aHist.push([dates[i].getTime(), aTotal]); + } + + var invested = dates.length * amount; + + setText('stosuj-fees', fmtCZK(invested * STOSUJ_FEE)); + setText('stosuj-btc', fmtBTC(sTotal)); + setText('stosuj-sats', fmtSats(sTotal)); + setText('stosuj-value', fmtCZK(sTotal * curPrice)); + + setText('accbot-fees', fmtCZK(invested * ACCBOT_FEE)); + setText('accbot-btc', fmtBTC(aTotal)); + setText('accbot-sats', fmtSats(aTotal)); + setText('accbot-value', fmtCZK(aTotal * curPrice)); + + var extraBtc = aTotal - sTotal; + var extraVal = extraBtc * curPrice; + var feeSave = invested * (STOSUJ_FEE - ACCBOT_FEE); + + var sumEl = document.getElementById('compare-summary-text'); + var savEl = document.getElementById('compare-savings-text'); + + sumEl.textContent = tr('compare_summary_tpl', [dates.length, fmtCZK(invested), fmtCZK(curPrice)]); + savEl.innerHTML = tr('compare_savings_tpl', [fmtSats(extraBtc), fmtCZK(extraVal), fmtCZK(feeSave)]); + + drawChart(sHist, aHist, prices); + + loadingEl.style.display = 'none'; + contentEl.style.display = 'block'; + }) + .catch(function () { + loadingEl.style.display = 'none'; + errorEl.style.display = 'block'; + }); + } + + /* ===== Interactive Chart ===== */ + + function drawChart(sData, aData, rawPrices) { + var el = document.getElementById('compare-chart'); + if (!el || sData.length < 2) return; + + // Dimensions + var W = 700, H = 320; + var L = 68, R = 68, T = 16, B = 36; + var cW = W - L - R, cH = H - T - B; + + // Time range + var minT = sData[0][0], maxT = sData[sData.length - 1][0]; + var tRange = maxT - minT || 1; + + // BTC accumulated range (left Y) + var maxBtc = 0; + for (var i = 0; i < aData.length; i++) { + if (aData[i][1] > maxBtc) maxBtc = aData[i][1]; + } + maxBtc = maxBtc * 1.08 || 1; + + // BTC price range (right Y) — only prices in chart time range + var chartPrices = []; + var minP = Infinity, maxP = 0; + for (var i = 0; i < rawPrices.length; i++) { + if (rawPrices[i][0] >= minT && rawPrices[i][0] <= maxT) { + chartPrices.push(rawPrices[i]); + if (rawPrices[i][1] < minP) minP = rawPrices[i][1]; + if (rawPrices[i][1] > maxP) maxP = rawPrices[i][1]; + } + } + var pPad = (maxP - minP) * 0.12 || 1; + minP = Math.max(0, minP - pPad); + maxP = maxP + pPad; + var pRange = maxP - minP || 1; + + // Scale helpers + function sx(t) { return L + (t - minT) / tRange * cW; } + function syB(v) { return T + cH - (v / maxBtc) * cH; } + function syP(p) { return T + cH - ((p - minP) / pRange) * cH; } + + // --- Build SVG --- + var svg = ''; + + // Horizontal grid (5 lines) + for (var i = 1; i <= 4; i++) { + var gy = T + (cH / 5) * i; + svg += ''; + } + + // X-axis labels + var xTicks = Math.min(6, sData.length); + for (var i = 0; i <= xTicks; i++) { + var t = minT + (tRange / xTicks) * i; + var d = new Date(t); + var lbl = (d.getMonth() + 1) + '/' + String(d.getFullYear()).slice(2); + svg += '' + lbl + ''; + } + + // Left Y-axis labels (sats) + for (var i = 0; i <= 4; i++) { + var v = (maxBtc / 4) * i; + var yy = syB(v); + svg += '' + fmtAxisSats(v) + ''; + } + + // Right Y-axis labels (CZK price) + for (var i = 0; i <= 4; i++) { + var p = minP + (pRange / 4) * i; + var yy = syP(p); + svg += '' + fmtAxisCZK(p) + ' Kč'; + } + + // Axis lines + svg += ''; + svg += ''; + svg += ''; + + // BTC price line (dashed, blue) + if (chartPrices.length > 1) { + var pp = chartPrices.map(function (d) { return sx(d[0]).toFixed(1) + ',' + syP(d[1]).toFixed(1); }).join(' '); + svg += ''; + } + + // Fill area between AccBot and Štosuj (gradient) + svg += '' + + '' + + '' + + ''; + var topP = aData.map(function (d) { return sx(d[0]).toFixed(1) + ',' + syB(d[1]).toFixed(1); }).join(' '); + var botP = sData.slice().reverse().map(function (d) { return sx(d[0]).toFixed(1) + ',' + syB(d[1]).toFixed(1); }).join(' '); + svg += ''; + + // Štosuj line + var sP = sData.map(function (d) { return sx(d[0]).toFixed(1) + ',' + syB(d[1]).toFixed(1); }).join(' '); + svg += ''; + + // AccBot line + var aP = aData.map(function (d) { return sx(d[0]).toFixed(1) + ',' + syB(d[1]).toFixed(1); }).join(' '); + svg += ''; + + // End-of-chart bracket showing the difference + var li = sData.length - 1; + var yEndS = syB(sData[li][1]); + var yEndA = syB(aData[li][1]); + var xEnd = sx(sData[li][0]); + var gapPx = yEndS - yEndA; + var diffSatsEnd = aData[li][1] - sData[li][1]; + + if (gapPx > 6) { + var bx = Math.min(xEnd + 8, W - R - 2); + svg += ''; + svg += ''; + svg += ''; + var midY = (yEndA + yEndS) / 2; + svg += '+' + fmtAxisSats(diffSatsEnd) + ''; + } + + // End-point dots + svg += ''; + svg += ''; + + // Hover elements (hidden) + svg += ''; + svg += ''; + svg += ''; + svg += ''; + + // Invisible overlay + svg += ''; + + el.innerHTML = '' + svg + ''; + + // --- Difference sparkline below main chart --- + var diffEl = document.getElementById('compare-diff-chart'); + if (!diffEl) { + diffEl = document.createElement('div'); + diffEl.id = 'compare-diff-chart'; + diffEl.className = 'compare-diff-chart'; + el.parentElement.insertBefore(diffEl, el.nextSibling); + } + var dH = 80, dT = 6, dB = 22; + var dcH = dH - dT - dB; + var diffData = []; + var maxDiff = 0; + for (var i = 0; i < sData.length; i++) { + var dd = aData[i][1] - sData[i][1]; + diffData.push([sData[i][0], dd]); + if (dd > maxDiff) maxDiff = dd; + } + maxDiff = maxDiff * 1.15 || 1; + + function syD(v) { return dT + dcH - (v / maxDiff) * dcH; } + + var dSvg = ''; + // Fill under diff line + var dfPts = diffData.map(function (d) { return sx(d[0]).toFixed(1) + ',' + syD(d[1]).toFixed(1); }).join(' '); + var dfBase = sx(diffData[diffData.length - 1][0]).toFixed(1) + ',' + (dH - dB) + ' ' + sx(diffData[0][0]).toFixed(1) + ',' + (dH - dB); + dSvg += ''; + dSvg += ''; + dSvg += ''; + // Baseline + dSvg += ''; + // Y labels (0 and max) + dSvg += '0'; + dSvg += '+' + fmtAxisSats(maxDiff / 1.15) + ''; + // Label + var diffLabel = tr('compare_diff_chart_label'); + dSvg += '' + diffLabel + ''; + // End value + var lastDiff = diffData[diffData.length - 1]; + dSvg += ''; + dSvg += '+' + fmtAxisSats(lastDiff[1]) + ''; + + // Hover line for diff chart + dSvg += ''; + dSvg += ''; + dSvg += ''; + + diffEl.innerHTML = '' + dSvg + ''; + + // Tooltip element + var container = el.parentElement; + var tooltip = document.getElementById('compare-tooltip'); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.id = 'compare-tooltip'; + tooltip.className = 'compare-tooltip'; + container.appendChild(tooltip); + } + tooltip.style.display = 'none'; + + // --- Hover interaction --- + var overlay = document.getElementById('ch-overlay'); + var dOverlay = document.getElementById('ch-doverlay'); + var svgNode = el.querySelector('svg'); + var hLine = document.getElementById('ch-hline'); + var dotS = document.getElementById('ch-dot-s'); + var dotA = document.getElementById('ch-dot-a'); + var dotP = document.getElementById('ch-dot-p'); + var dhLine = document.getElementById('ch-dhline'); + var dotD = document.getElementById('ch-dot-d'); + + function snapIdx(mouseT) { + var idx = 0, best = Infinity; + for (var i = 0; i < sData.length; i++) { + var dd = Math.abs(sData[i][0] - mouseT); + if (dd < best) { best = dd; idx = i; } + } + return idx; + } + + function showHover(idx, e) { + var buyT = sData[idx][0]; + var xp = sx(buyT); + + var pIdx = findPriceIdx(chartPrices, buyT); + var priceAt = chartPrices[pIdx][1]; + + // Main chart hover elements + hLine.setAttribute('x1', xp.toFixed(1)); + hLine.setAttribute('x2', xp.toFixed(1)); + hLine.setAttribute('visibility', 'visible'); + dotS.setAttribute('cx', xp.toFixed(1)); + dotS.setAttribute('cy', syB(sData[idx][1]).toFixed(1)); + dotS.setAttribute('visibility', 'visible'); + dotA.setAttribute('cx', xp.toFixed(1)); + dotA.setAttribute('cy', syB(aData[idx][1]).toFixed(1)); + dotA.setAttribute('visibility', 'visible'); + dotP.setAttribute('cx', xp.toFixed(1)); + dotP.setAttribute('cy', syP(priceAt).toFixed(1)); + dotP.setAttribute('visibility', 'visible'); + + // Diff chart hover elements + dhLine.setAttribute('x1', xp.toFixed(1)); + dhLine.setAttribute('x2', xp.toFixed(1)); + dhLine.setAttribute('visibility', 'visible'); + dotD.setAttribute('cx', xp.toFixed(1)); + dotD.setAttribute('cy', syD(diffData[idx][1]).toFixed(1)); + dotD.setAttribute('visibility', 'visible'); + + // Tooltip + var dd = new Date(buyT); + var dateStr = dd.getDate() + '. ' + (dd.getMonth() + 1) + '. ' + dd.getFullYear(); + var sB = sData[idx][1], aB = aData[idx][1]; + var diff = aB - sB; + var diffLbl = tr('compare_diff_row'); + + tooltip.innerHTML = + '
    ' + dateStr + '
    ' + + '
    BTC: ' + fmtCZK(priceAt) + '
    ' + + '
    Štosuj' + fmtSats(sB) + '' + fmtCZK(sB * priceAt) + '
    ' + + '
    AccBot' + fmtSats(aB) + '' + fmtCZK(aB * priceAt) + '
    ' + + '
    ' + diffLbl + '+' + fmtSats(diff) + '+' + fmtCZK(diff * priceAt) + '
    '; + + tooltip.style.display = ''; + + var cRect = container.getBoundingClientRect(); + var tx = e.clientX - cRect.left + 16; + var ty = e.clientY - cRect.top - 10; + if (tx + tooltip.offsetWidth + 8 > cRect.width) { + tx = e.clientX - cRect.left - tooltip.offsetWidth - 16; + } + if (ty + tooltip.offsetHeight > cRect.height) { + ty = cRect.height - tooltip.offsetHeight - 8; + } + if (ty < 0) ty = 8; + tooltip.style.left = tx + 'px'; + tooltip.style.top = ty + 'px'; + } + + function hideHover() { + hLine.setAttribute('visibility', 'hidden'); + dotS.setAttribute('visibility', 'hidden'); + dotA.setAttribute('visibility', 'hidden'); + dotP.setAttribute('visibility', 'hidden'); + dhLine.setAttribute('visibility', 'hidden'); + dotD.setAttribute('visibility', 'hidden'); + tooltip.style.display = 'none'; + } + + function onMove(e, svgRef) { + var rect = svgRef.getBoundingClientRect(); + var mx = (e.clientX - rect.left) / rect.width * W; + var t = minT + ((mx - L) / cW) * tRange; + showHover(snapIdx(t), e); + } + + overlay.addEventListener('mousemove', function (e) { onMove(e, svgNode); }); + overlay.addEventListener('mouseleave', hideHover); + + var diffSvgNode = diffEl.querySelector('svg'); + dOverlay.addEventListener('mousemove', function (e) { onMove(e, diffSvgNode); }); + dOverlay.addEventListener('mouseleave', hideHover); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/docs/js/i18n.js b/docs/js/i18n.js index 6388847..ed14eab 100644 --- a/docs/js/i18n.js +++ b/docs/js/i18n.js @@ -15,7 +15,10 @@ return DEFAULT_LANG; } + var _tr = {}; + function apply(translations) { + _tr = translations; var els = document.querySelectorAll('[data-i18n]'); for (var i = 0; i < els.length; i++) { var key = els[i].getAttribute('data-i18n'); @@ -82,5 +85,9 @@ init(); } - window.AccBotI18n = { setLang: setLang, current: function () { return current; } }; + window.AccBotI18n = { + setLang: setLang, + current: function () { return current; }, + t: function (key) { return _tr[key] || key; } + }; })(); diff --git a/docs/locales/cs.json b/docs/locales/cs.json index 96764e9..2ae7a48 100644 --- a/docs/locales/cs.json +++ b/docs/locales/cs.json @@ -82,6 +82,29 @@ "download_ios": "iOS již brzy", "download_telegram": "Náš Telegram", + "nav_compare": "Srovnání", + "compare_title": "AccBot vs Štosuj", + "compare_subtitle": "Štosuj si účtuje 1,5 % za každý nákup na Coinmate. S AccBotem obchodujete přímo — platíte jen standardní taker poplatek burzy 0,6 %, žádná přirážka prostředníka.", + "compare_start_label": "Začátek DCA", + "compare_amount_label": "Částka za nákup (CZK)", + "compare_interval_label": "Interval nákupů", + "compare_daily": "Denně", + "compare_weekly": "Týdně", + "compare_monthly": "Měsíčně", + "compare_calculate": "Spočítat srovnání", + "compare_fee_label": "poplatek", + "compare_fees_paid": "Zaplaceno na poplatcích", + "compare_btc_accumulated": "Nastřádáno BTC", + "compare_current_value": "Aktuální hodnota", + "compare_chart_title": "Kumulativní akumulace BTC v čase", + "compare_error": "Nepodařilo se načíst cenová data. Zkuste to prosím později.", + "compare_legend_price": "Cena BTC/CZK", + "compare_summary_tpl": "{0} nákupů · {1} investováno · kurz BTC: {2}", + "compare_savings_tpl": "S AccBotem nastřádáte o {0} více ({1} v aktuální hodnotě) a ušetříte {2} na poplatcích.", + "compare_diff_chart_label": "Úspora AccBot (sats)", + "compare_diff_row": "Rozdíl", + "compare_disclaimer": "* Simulace na základě historických cen BTC/CZK z CryptoCompare. Minulá výkonnost nezaručuje budoucí výsledky.", + "footer_made": "Vytvořeno s láskou od Crynners", "footer_license": "Licence MIT", "footer_source": "Zdrojový kód", diff --git a/docs/locales/en.json b/docs/locales/en.json index 7be234f..17a8c44 100644 --- a/docs/locales/en.json +++ b/docs/locales/en.json @@ -82,6 +82,29 @@ "download_ios": "iOS Coming Soon", "download_telegram": "Join our Telegram", + "nav_compare": "Compare", + "compare_title": "AccBot vs Štosuj", + "compare_subtitle": "Štosuj charges 1.5% per purchase on Coinmate. AccBot lets you trade directly — just the standard 0.6% taker fee, no middleman markup.", + "compare_start_label": "DCA start date", + "compare_amount_label": "Amount per purchase (CZK)", + "compare_interval_label": "Purchase interval", + "compare_daily": "Daily", + "compare_weekly": "Weekly", + "compare_monthly": "Monthly", + "compare_calculate": "Calculate comparison", + "compare_fee_label": "fee", + "compare_fees_paid": "Fees paid", + "compare_btc_accumulated": "BTC accumulated", + "compare_current_value": "Current value", + "compare_chart_title": "Cumulative BTC accumulation", + "compare_error": "Failed to load price data. Please try again later.", + "compare_legend_price": "BTC/CZK price", + "compare_summary_tpl": "{0} purchases · {1} invested · BTC price: {2}", + "compare_savings_tpl": "With AccBot you accumulate {0} more ({1} in current value) and save {2} in fees.", + "compare_diff_chart_label": "AccBot savings (sats)", + "compare_diff_row": "Difference", + "compare_disclaimer": "* Simulation based on historical BTC/CZK prices from CryptoCompare. Past performance does not indicate future results.", + "footer_made": "Made with love by Crynners", "footer_license": "MIT License", "footer_source": "Source Code",