Files
satshkd-vercel/views/sats.hbs
Claude b3f7204291 Fix chart background image scaling for mobile devices
The background pattern image was not scaling appropriately on mobile screens,
causing only a portion of the banknote to be visible in the pattern tiles.

Changes:
- Added responsive scaling logic for background pattern image
- Mobile devices (<768px) now use proportional scaling based on viewport width
- Tablet devices (768-1200px) use 70% scaling
- Desktop devices maintain full-size image
- Uses temporary canvas to create scaled pattern for optimal display

This ensures the entire banknote is visible in pattern tiles across all device sizes.
2025-11-22 05:14:52 +00:00

426 lines
26 KiB
Handlebars
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<head>
<title>EURSAT</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Preconnect to external resources for faster loading -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://api-pub.bitfinex.com">
<link rel="preconnect" href="https://analytics.cypherpunk.cloud">
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="/static/skeleton.css">
<link rel="stylesheet" type="text/css" href="/static/normalize.css">
<link rel="stylesheet" type="text/css" href="/static/mobile.css">
<link rel="shortcut icon" type="image/png" href="/static/favicon.png" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans|Raleway&display=swap" rel="stylesheet">
<script defer src="/static/chart.min.js"></script>
<script defer src="/static/chartjs-adapter-date-fns.min.js"></script>
<script defer src="/static/chartjs-plugin-annotation.min.js"></script>
<script>
// Auto-detect domain for Plausible Analytics
document.addEventListener('DOMContentLoaded', function() {
const domain = window.location.hostname;
const script = document.createElement('script');
script.async = true;
script.defer = true;
script.setAttribute('data-domain', domain);
script.src = 'https://analytics.cypherpunk.cloud/js/script.js';
document.head.appendChild(script);
});
</script>
<!-- JavaScript Bundle with Popper -->
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8" crossorigin="anonymous"></script>
<style>
#historical {
position: relative;
width: 100%;
}
</style>
</head>
<body>
<div class="container" style="margin-top: 20px;">
<div class="row">
<center>
<h3> {{ Title }} <span id="current"></span> sats</h3>
</center>
<script>
var currentPrice = 0;
var ws_ticker_id, t;
document.addEventListener('DOMContentLoaded', function() {
startWebSocket();
}, false);
function startWebSocket() {
t = new WebSocket("wss://api-pub.bitfinex.com/ws/2");
t.onmessage = function(e) {
var msg = JSON.parse(e.data);
if (msg.event === 'subscribed' && msg.channel == 'ticker') {
ws_ticker_id = msg.chanId;
}
if (Array.isArray(msg) && msg[0] === ws_ticker_id && Array.isArray(msg[1])) {
updatePrice(msg[1]);
}
}
t.onopen = function(e) {
console.log('Web socket open.');
t.send(JSON.stringify({
"event": "subscribe",
"channel": "ticker",
"symbol": "tBTCEUR"
}));
setPingTimer();
}
t.onclose = function(e) {
console.log('Web socket closed. Attempting to reopen.');
setTimeout(function() {
startWebSocket();
}, 2000);
}
t.onerror = function(e) {
console.log(e);
}
}
function setPingTimer() {
setInterval(function() {
t.send("ping");
}, 10000);
}
function updatePrice(tickerArray) {
// Bitfinex ticker format: [ BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_PERC, LAST_PRICE, VOLUME, HIGH, LOW ]
var lastPriceEur = tickerArray[6];
currentPrice = Math.round(100000000 / lastPriceEur); // sats per EUR
document.title = currentPrice.toLocaleString() + " sats";
document.querySelector('#current').textContent = currentPrice.toLocaleString();
}
</script>
<div id='historical'>
<canvas id="chart"></canvas>
</div>
<div class="container">
<table class="u-full-width">
<thead>
<tr>
<th style="text-align:left;">{{ date }} </th>
<th style="text-align:right;">{{ price }}&nbsp;&nbsp;</th>
<th style="text-align:right;white-space: pre;"> {{ percentchange }} </th>
</tr>
</thead>
<tbody>
{{#each yeardata}}
<tr>
<td style="text-align:left">{{ this.year }}</td>
<td style="text-align:right">{{ this.sats }} sats </td>
<td style="text-align:right">{{ this.percent }} % </td>
</tr>
{{/each}}
</tbody>
</table>
<div class="mt-5">
<h4 class="mb-4">The Most Spectacularly Wrong Bitcoin Predictions by ECB Officials</h4>
<div class="mb-4">
<h6><strong>November 15, 2018</strong> - Benoît Cœuré, ECB Executive Board Member</h6>
<p><em>"In more ways than one, Bitcoin is the evil spawn of the financial crisis. Lightning may strike me for saying this in the Tower of Basel but Bitcoin was an extremely clever idea. Sadly, not every clever idea is a good idea. I believe that Agustín Carstens summed its manifold problems up well when he said that Bitcoin is 'a combination of a bubble, a Ponzi scheme and an environmental disaster.'"</em></p>
<p><small>Sources: <a href="https://www.ccn.com/bitcoin-is-the-evil-spawn-of-the-financial-crisis-european-central-bank-board-member/" target="_blank">CCN</a>, <a href="https://bitcoinist.com/the-ecb-is-right-bitcoin-was-spawned-by-the-financial-crisis-created-by-the-ecb/" target="_blank">Bitcoinist</a></small></p>
</div>
<div class="mb-4">
<h6><strong>May 9, 2019</strong> - Mario Draghi, ECB President</h6>
<p><em>"Cryptocurrencies or bitcoins, or anything like that, are not really currencies — they are assets. A euro is a euro — today, tomorrow, in a month — it's always a euro. And the ECB is behind the euro. Who is behind the cryptocurrencies? So they are very, very risky assets."</em></p>
<p><small>Sources: <a href="https://cointelegraph.com/news/ecb-president-mario-draghi-cryptos-are-not-currencies-they-are-very-risky-assets" target="_blank">Cointelegraph</a>, <a href="https://finance.yahoo.com/news/draghi-comments-ecb-press-conference-133645028.html" target="_blank">Yahoo Finance</a></small></p>
</div>
<div class="mb-4">
<h6><strong>May 29, 2019</strong> - Yves Mersch, ECB Executive Board Member</h6>
<p><em>"Bitcoin and other crypto-assets claim to need neither trust nor the backing of a sovereign. These self-proclaimed currencies, more accurately described as crypto-assets, have proved to be unfit for purpose, demonstrating that well-executed central bank policies are still the only sound basis for stability."</em></p>
<p><small>Source: <a href="https://www.bis.org/review/r190529a.htm" target="_blank">BIS</a></small></p>
</div>
<div class="mb-4">
<h6><strong>January 13, 2021</strong> - Christine Lagarde, ECB President</h6>
<p><em>"For those who had assumed that it might turn into a currency -- terribly sorry, but this is an asset and it's a highly speculative asset which has conducted some funny business and some interesting and totally reprehensible money-laundering activity."</em></p>
<p><small>Sources: <a href="https://www.bloomberg.com/news/articles/2021-01-13/lagarde-blasts-bitcoin-s-role-in-facilitating-money-laundering" target="_blank">Bloomberg</a>, <a href="https://www.theglobeandmail.com/business/international-business/european-business/article-ecbs-lagarde-calls-for-global-regulation-of-bitcoin-to-prevent-use-in/" target="_blank">The Globe and Mail</a>, <a href="https://www.coindesk.com/policy/2021/01/13/ecbs-christine-lagarde-says-speculative-bitcoin-needs-global-regulation" target="_blank">CoinDesk</a></small></p>
</div>
<div class="mb-4">
<h6><strong>April 9, 2021</strong> - Isabel Schnabel, ECB Executive Board Member</h6>
<p><em>"In our view it is wrong to describe bitcoin as a currency, because it does not fulfil the basic properties of money. It is a speculative asset without any recognisable fundamental value and is subject to massive price swings."</em></p>
<p><small>Source: <a href="https://www.ecb.europa.eu/press/inter/date/2021/html/ecb.in210409~c8c348a12c.en.html" target="_blank">ECB</a></small></p>
</div>
<div class="mb-4">
<h6><strong>May 19, 2021</strong> - Luis de Guindos, ECB Vice President</h6>
<p><em>"When you have difficulties to find out what are the real fundamentals of an investment, then what you're doing is not a real investment. This is an asset with very weak fundamentals, and that is going to be subject to a lot of volatility."</em></p>
<p><small>Sources: <a href="https://www.coindesk.com/markets/2021/05/19/crypto-assets-arent-a-real-investment-ecb-vice-president-says/" target="_blank">CoinDesk</a>, <a href="https://cointelegraph.com/news/ecb-veep-says-crypto-has-weak-fundamentals-is-not-a-real-asset-amid-30-dip" target="_blank">Cointelegraph</a></small></p>
</div>
<div class="mb-4">
<h6><strong>April 25, 2022</strong> - Fabio Panetta, ECB Executive Board Member</h6>
<p><em>"Crypto-assets are speculative assets that can cause major damage to society. They derive their value mainly from greed, they rely on the greed of others and the hope that the scheme continues unhindered. In fact, they are a gamble disguised as an investment asset."</em></p>
<p><small>Sources: <a href="https://www.bis.org/review/r220425j.htm" target="_blank">BIS</a>, <a href="https://www.paymentscardsandmobile.com/fabio-panetta-why-we-must-reign-in-the-wild-west-of-crypto/" target="_blank">Payments Cards & Mobile</a>, <a href="https://www.ledgerinsights.com/european-central-bank-ecb-panetta-likens-crypto-to-wild-west-ponzi-scheme/" target="_blank">Ledger Insights</a></small></p>
</div>
<div class="mb-4">
<h6><strong>May 22, 2022</strong> - Christine Lagarde, ECB President</h6>
<p><em>"My very humble assessment is that it is worth nothing. It is based on nothing, there is no underlying assets to act as an anchor of safety."</em></p>
<p><small>Sources: <a href="https://www.cnbc.com/amp/2022/05/23/ecb-chief-christine-lagarde-crypto-is-worth-nothing.html" target="_blank">CNBC</a>, <a href="https://forkast.news/cryptocurrency-ecb-christine-lagarde/" target="_blank">Forkast</a>, <a href="https://www.techspot.com/news/94679-european-central-bank-president-calls-crypto-worthless-based.html" target="_blank">TechSpot</a></small></p>
</div>
<div class="mb-4">
<h6><strong>December 7, 2022</strong> - Fabio Panetta, ECB Executive Board Member</h6>
<p><em>"Crypto assets should be banned if they are too energy intensive. Investors have been caught in the textbook definition of a bubble, lured by the promise of ever-rising prices. Many cryptocurrencies are just a new way of gambling."</em></p>
<p><small>Sources: <a href="https://www.coindesk.com/policy/2022/12/07/ban-energy-intensive-crypto-says-ecb-official" target="_blank">CoinDesk</a>, <a href="https://uk.finance.yahoo.com/news/ecb-ban-crypto-bitcoin-ethereum-123751593.html" target="_blank">Yahoo Finance UK</a></small></p>
</div>
<div class="mb-4">
<h6><strong>January 30, 2025</strong> - Christine Lagarde, ECB President</h6>
<p><em>"I am confident that bitcoin will not enter the reserves of any of the central banks of the General Council... reserves have to be liquid, that reserves have to be secure, that they have to be safe, that they should not be plagued by the suspicion of money laundering."</em></p>
<p><small>Sources: <a href="https://www.bloomberg.com/news/articles/2025-01-30/lagarde-is-confident-eu-central-banks-will-shun-bitcoin-reserves" target="_blank">Bloomberg</a>, <a href="https://cointelegraph.com/news/ecb-president-christine-lagarde-bitcoin-central-bank-reserves" target="_blank">Cointelegraph</a></small></p>
</div>
</div>
</div>
<script>
// Load data and initialize chart
fetch('/{{ data_file }}')
.then(response => response.json())
.then(data => {
// Convert date strings to Date objects
data = data.map(d => ({
...d,
date: new Date(d.date)
}));
// Calculate min and max values from data for proper axis scaling
const rateField = '{{ rate_field }}';
const values = data.map(d => d[rateField]).filter(v => v != null && v > 0);
const minValue = Math.min(...values);
const maxValue = Math.max(...values);
// Add more padding for better visualization, especially at bottom
const yAxisMin = Math.floor(minValue / 50); // Much more padding at bottom
const yAxisMax = Math.ceil(maxValue * 2.5); // Less padding at top for flatter look
// Calculate responsive height
const windowWidth = window.innerWidth;
let graphHeight;
if (windowWidth < 550) {
graphHeight = windowWidth / 1.9047;
} else if (windowWidth < 1200) {
graphHeight = windowWidth * 0.8 / 1.9047;
} else {
graphHeight = 500;
}
console.log("graph height: ", graphHeight);
// Event markers configuration
const qhLink = () => window.open('https://en.bitcoin.it/wiki/Controlled_supply', '_blank');
const qeLink = () => window.open('https://en.wikipedia.org/wiki/Quantitative_easing', '_blank');
const markers = [{
date: new Date('2010-11-03T00:00:00.000Z'),
label: 'QE2',
click: qeLink,
}, {
date: new Date('2012-09-13T00:00:00.000Z'),
label: 'QE3',
click: qeLink,
}, {
date: new Date('2012-11-28T00:00:00.000Z'),
label: 'QH1',
click: qhLink,
}, {
date: new Date('2016-07-09T00:00:00.000Z'),
label: 'QH2',
click: qhLink,
}, {
date: new Date('2019-10-11T00:00:00.000Z'),
label: 'QE4',
click: qeLink,
}, {
date: new Date('2020-05-11T00:00:00.000Z'),
label: 'QH3',
click: qhLink,
}];
// Create pattern fill
const canvas = document.getElementById('chart');
canvas.height = graphHeight;
const ctx = canvas.getContext('2d');
// Load the 100 EUR image for pattern fill
const patternImage = new Image();
patternImage.src = '/static/assets/100eur.jpg';
patternImage.onload = function() {
// Scale the pattern image based on viewport size for better mobile display
const scaleFactor = window.innerWidth < 768
? Math.max(0.3, window.innerWidth / 1500) // Mobile: scale down proportionally
: window.innerWidth < 1200
? 0.7 // Tablet: moderate scaling
: 1.0; // Desktop: full size
// Create a temporary canvas to scale the image
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = patternImage.width * scaleFactor;
tempCanvas.height = patternImage.height * scaleFactor;
// Draw scaled image to temporary canvas
tempCtx.drawImage(patternImage, 0, 0, tempCanvas.width, tempCanvas.height);
// Create pattern from scaled image
const pattern = ctx.createPattern(tempCanvas, 'repeat');
// Initialize Chart.js
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => d.date),
datasets: [{
label: '{{ subtitle }}',
data: data.map(d => d['{{ rate_field }}']),
borderColor: 'green',
backgroundColor: pattern,
fill: true,
tension: 0.1,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 4,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '{{ subtitle }}',
font: {
size: window.innerWidth < 768 ? 12 : 16
}
},
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
return context.parsed.y.toLocaleString() + ' sats';
}
}
},
annotation: {
annotations: markers.reduce((acc, marker, idx) => {
acc['line' + idx] = {
type: 'line',
xMin: marker.date,
xMax: marker.date,
borderColor: 'rgba(255, 99, 132, 0.5)',
borderWidth: 2,
borderDash: [5, 5],
label: {
display: true,
content: marker.label,
position: 'start',
backgroundColor: 'rgba(255, 99, 132, 0.8)',
color: 'white',
font: {
size: window.innerWidth < 768 ? 8 : 10,
weight: 'bold'
},
padding: window.innerWidth < 768 ? 2 : 4
},
click: function() {
marker.click();
}
};
return acc;
}, {})
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'year',
displayFormats: {
year: 'yyyy'
}
},
ticks: {
maxTicksLimit: window.innerWidth < 768 ? 8 : 16,
font: {
size: window.innerWidth < 768 ? 10 : 12
}
},
grid: {
display: true
}
},
y: {
type: 'logarithmic',
min: yAxisMin,
max: yAxisMax,
ticks: {
callback: function(value) {
return value.toLocaleString() + ' sats';
},
maxTicksLimit: window.innerWidth < 768 ? 8 : 12,
font: {
size: window.innerWidth < 768 ? 10 : 12
}
},
grid: {
display: true
}
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
onClick: function(event, elements, chart) {
// Handle marker clicks
const canvasPosition = Chart.helpers.getRelativePosition(event, chart);
const dataX = chart.scales.x.getValueForPixel(canvasPosition.x);
// Check if click is near any marker
markers.forEach(marker => {
const markerX = marker.date.getTime();
const tolerance = 365 * 24 * 60 * 60 * 1000; // 1 year tolerance
if (Math.abs(dataX - markerX) < tolerance) {
marker.click();
}
});
}
}
});
};
})
.catch(error => console.error('Error loading chart data:', error));
</script>
</body>