Files
satshkd-vercel/views/sats.hbs
Claude 6e99c2c733 Add comprehensive mobile responsiveness to website
- Created mobile.css with responsive styles for all screen sizes
- Updated table headers to use responsive font sizes (removed 2rem inline styles)
- Fixed text-align from non-standard -webkit-left to standard left/right values
- Made chart configuration mobile-aware with smaller fonts and fewer ticks on mobile
- Optimized chart annotations and labels for mobile screens
- Added responsive breakpoints for tablets (768px) and phones (480px)
- Improved touch targets for mobile devices
- Added landscape orientation optimizations
- Enhanced dark mode toggle positioning for small screens

The website now properly fits mobile screens and provides an optimal viewing experience across all device sizes.
2025-11-21 20:13:31 +00:00

339 lines
17 KiB
Handlebars

<head>
<title>EURSAT</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- 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 src="/static/chart.min.js"></script>
<script src="/static/chartjs-adapter-date-fns.min.js"></script>
<script 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 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>
<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() {
const pattern = ctx.createPattern(patternImage, '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>