feat: PDF invoice, payment summary, UI polish
- PDF invoice generation with jsPDF (lazy-loaded, includes paid status) - Payment summary card: amount, fiat equivalent, description prominently displayed - URI box hidden behind collapsible "Show Monero URI" details - Smart countdown: "29 Tage, 23 Std." instead of ticking seconds - Dynamic page title: "0.017 XMR — Rechnung #42 | xmrpay.link" - Result card fade-in animation - Responsive QR code on mobile - Rename .btn-monitor to .btn-proof - "In Wallet öffnen" unified as <button>
This commit is contained in:
238
app.js
238
app.js
@@ -47,7 +47,10 @@
|
|||||||
const verifyProofBtn = $('#verifyProof');
|
const verifyProofBtn = $('#verifyProof');
|
||||||
const proofResult = $('#proofResult');
|
const proofResult = $('#proofResult');
|
||||||
const paymentStatus = $('#paymentStatus');
|
const paymentStatus = $('#paymentStatus');
|
||||||
|
const paymentSummary = $('#paymentSummary');
|
||||||
|
const downloadPdfBtn = $('#downloadPdf');
|
||||||
let cryptoLoaded = false;
|
let cryptoLoaded = false;
|
||||||
|
let pdfLoaded = false;
|
||||||
|
|
||||||
// --- Init ---
|
// --- Init ---
|
||||||
fetchRates();
|
fetchRates();
|
||||||
@@ -86,6 +89,9 @@
|
|||||||
selectedDays = parseInt(timerCustom.value) || 0;
|
selectedDays = parseInt(timerCustom.value) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PDF
|
||||||
|
downloadPdfBtn.addEventListener('click', generatePdf);
|
||||||
|
|
||||||
// TX Proof events
|
// TX Proof events
|
||||||
proofToggle.addEventListener('click', toggleProofPanel);
|
proofToggle.addEventListener('click', toggleProofPanel);
|
||||||
txHashInput.addEventListener('input', validateProofInputs);
|
txHashInput.addEventListener('input', validateProofInputs);
|
||||||
@@ -121,6 +127,8 @@
|
|||||||
proofResult.className = 'proof-result';
|
proofResult.className = 'proof-result';
|
||||||
paymentStatus.innerHTML = '';
|
paymentStatus.innerHTML = '';
|
||||||
paymentStatus.className = 'payment-status';
|
paymentStatus.className = 'payment-status';
|
||||||
|
paymentSummary.innerHTML = '';
|
||||||
|
document.title = 'xmrpay.link \u2014 Monero Invoice Generator';
|
||||||
history.replaceState(null, '', location.pathname);
|
history.replaceState(null, '', location.pathname);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
addrInput.focus();
|
addrInput.focus();
|
||||||
@@ -243,7 +251,11 @@
|
|||||||
// Show result
|
// Show result
|
||||||
resultSection.classList.add('visible');
|
resultSection.classList.add('visible');
|
||||||
uriBox.textContent = uri;
|
uriBox.textContent = uri;
|
||||||
openWalletBtn.href = uri;
|
openWalletBtn.onclick = function () { window.location.href = uri; };
|
||||||
|
|
||||||
|
// Payment summary + page title
|
||||||
|
buildSummary(xmrAmount, desc, timer);
|
||||||
|
updatePageTitle(xmrAmount, desc);
|
||||||
|
|
||||||
// Share link — show long URL immediately, then replace with short
|
// Share link — show long URL immediately, then replace with short
|
||||||
const hash = buildHash(addr, xmrAmount, desc, timer);
|
const hash = buildHash(addr, xmrAmount, desc, timer);
|
||||||
@@ -321,6 +333,31 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSummary(xmrAmount, desc, days) {
|
||||||
|
var html = '';
|
||||||
|
if (xmrAmount) {
|
||||||
|
html += '<div class="summary-amount">' + xmrAmount.toFixed(8) + ' XMR</div>';
|
||||||
|
var amount = parseFloat(amountInput.value);
|
||||||
|
var currency = currencySelect.value;
|
||||||
|
if (currency !== 'XMR' && amount) {
|
||||||
|
html += '<div class="summary-fiat">\u2248 ' + amount.toFixed(2) + ' ' + currency + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (desc) {
|
||||||
|
html += '<div class="summary-desc">' + desc.replace(/</g, '<') + '</div>';
|
||||||
|
}
|
||||||
|
paymentSummary.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePageTitle(xmrAmount, desc) {
|
||||||
|
var parts = [];
|
||||||
|
if (xmrAmount) parts.push(xmrAmount.toFixed(4) + ' XMR');
|
||||||
|
if (desc) parts.push(desc);
|
||||||
|
if (parts.length) {
|
||||||
|
document.title = parts.join(' — ') + ' | xmrpay.link';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
if (countdownInterval) clearInterval(countdownInterval);
|
if (countdownInterval) clearInterval(countdownInterval);
|
||||||
countdownEl.textContent = '';
|
countdownEl.textContent = '';
|
||||||
@@ -342,18 +379,17 @@
|
|||||||
const d = Math.floor(remaining / 86400000);
|
const d = Math.floor(remaining / 86400000);
|
||||||
const h = Math.floor((remaining % 86400000) / 3600000);
|
const h = Math.floor((remaining % 86400000) / 3600000);
|
||||||
const m = Math.floor((remaining % 3600000) / 60000);
|
const m = Math.floor((remaining % 3600000) / 60000);
|
||||||
const s = Math.floor((remaining % 60000) / 1000);
|
|
||||||
if (d > 0) {
|
if (d > 0) {
|
||||||
countdownEl.textContent = I18n.t('countdown_remaining_days')
|
countdownEl.textContent = I18n.t('countdown_remaining_days')
|
||||||
.replace('{d}', d).replace('{h}', pad(h)).replace('{m}', pad(m)).replace('{s}', pad(s));
|
.replace('{d}', d).replace('{h}', h);
|
||||||
} else {
|
} else {
|
||||||
countdownEl.textContent = I18n.t('countdown_remaining_hours')
|
countdownEl.textContent = I18n.t('countdown_remaining_hours')
|
||||||
.replace('{h}', pad(h)).replace('{m}', pad(m)).replace('{s}', pad(s));
|
.replace('{h}', pad(h)).replace('{m}', pad(m));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tick();
|
tick();
|
||||||
countdownInterval = setInterval(tick, 1000);
|
countdownInterval = setInterval(tick, 60000); // Update every minute, not every second
|
||||||
}
|
}
|
||||||
|
|
||||||
function pad(n) {
|
function pad(n) {
|
||||||
@@ -421,6 +457,186 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- PDF Invoice ---
|
||||||
|
|
||||||
|
function loadJsPdf() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (window.jspdf) { resolve(); return; }
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'lib/jspdf.min.js';
|
||||||
|
script.onload = function () { pdfLoaded = true; resolve(); };
|
||||||
|
script.onerror = function () { reject(new Error('Failed to load jsPDF')); };
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatePdf() {
|
||||||
|
await loadJsPdf();
|
||||||
|
var jsPDF = window.jspdf.jsPDF;
|
||||||
|
var doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
||||||
|
|
||||||
|
var addr = addrInput.value.trim();
|
||||||
|
var xmrAmount = getXmrAmount();
|
||||||
|
var desc = descInput.value.trim();
|
||||||
|
var amount = parseFloat(amountInput.value);
|
||||||
|
var currency = currencySelect.value;
|
||||||
|
var pageW = doc.internal.pageSize.getWidth();
|
||||||
|
var margin = 20;
|
||||||
|
var contentW = pageW - margin * 2;
|
||||||
|
var y = margin;
|
||||||
|
|
||||||
|
// --- Header: Orange accent bar ---
|
||||||
|
doc.setFillColor(242, 104, 33);
|
||||||
|
doc.rect(0, 0, pageW, 8, 'F');
|
||||||
|
|
||||||
|
// --- Title ---
|
||||||
|
y = 22;
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(22);
|
||||||
|
doc.setTextColor(242, 104, 33);
|
||||||
|
doc.text(I18n.t('pdf_title'), margin, y);
|
||||||
|
|
||||||
|
// --- Date (top right) ---
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setTextColor(120, 120, 120);
|
||||||
|
var dateStr = new Date().toLocaleDateString(I18n.getLang() === 'de' ? 'de-CH' : 'en-US', {
|
||||||
|
year: 'numeric', month: 'long', day: 'numeric'
|
||||||
|
});
|
||||||
|
doc.text(I18n.t('pdf_date') + ': ' + dateStr, pageW - margin, y, { align: 'right' });
|
||||||
|
|
||||||
|
// --- Divider ---
|
||||||
|
y += 6;
|
||||||
|
doc.setDrawColor(220, 220, 220);
|
||||||
|
doc.setLineWidth(0.3);
|
||||||
|
doc.line(margin, y, pageW - margin, y);
|
||||||
|
|
||||||
|
// --- QR Code (right side) ---
|
||||||
|
var qrCanvas = qrContainer.querySelector('canvas');
|
||||||
|
var qrSize = 50;
|
||||||
|
var qrX = pageW - margin - qrSize;
|
||||||
|
var qrY = y + 6;
|
||||||
|
if (qrCanvas) {
|
||||||
|
var qrData = qrCanvas.toDataURL('image/png');
|
||||||
|
doc.addImage(qrData, 'PNG', qrX, qrY, qrSize, qrSize);
|
||||||
|
// QR hint
|
||||||
|
doc.setFontSize(7);
|
||||||
|
doc.setTextColor(150, 150, 150);
|
||||||
|
doc.text(I18n.t('pdf_scan_qr'), qrX + qrSize / 2, qrY + qrSize + 4, { align: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Invoice details (left side, next to QR) ---
|
||||||
|
var detailX = margin;
|
||||||
|
var detailW = qrX - margin - 10;
|
||||||
|
y += 14;
|
||||||
|
|
||||||
|
function addField(label, value) {
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(150, 150, 150);
|
||||||
|
doc.text(label, detailX, y);
|
||||||
|
y += 5;
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.setTextColor(40, 40, 40);
|
||||||
|
var lines = doc.splitTextToSize(value, detailW);
|
||||||
|
doc.text(lines, detailX, y);
|
||||||
|
y += lines.length * 5 + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
if (xmrAmount) {
|
||||||
|
var amountStr = xmrAmount.toFixed(8) + ' XMR';
|
||||||
|
if (currency !== 'XMR' && amount) {
|
||||||
|
amountStr += ' (\u2248 ' + amount.toFixed(2) + ' ' + currency + ')';
|
||||||
|
}
|
||||||
|
addField(I18n.t('pdf_amount'), amountStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (desc) {
|
||||||
|
addField(I18n.t('pdf_desc'), desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deadline
|
||||||
|
if (selectedDays > 0) {
|
||||||
|
var deadlineDate = new Date(Date.now() + selectedDays * 86400000);
|
||||||
|
var deadlineStr = deadlineDate.toLocaleDateString(I18n.getLang() === 'de' ? 'de-CH' : 'en-US', {
|
||||||
|
year: 'numeric', month: 'long', day: 'numeric'
|
||||||
|
});
|
||||||
|
addField(I18n.t('pdf_deadline'), deadlineStr + ' (' + I18n.t('pdf_deadline_days').replace('{d}', selectedDays) + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address (below QR if needed, full width)
|
||||||
|
y = Math.max(y, qrY + qrSize + 12);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(150, 150, 150);
|
||||||
|
doc.text(I18n.t('pdf_address'), margin, y);
|
||||||
|
y += 5;
|
||||||
|
|
||||||
|
// Address in monospace box
|
||||||
|
doc.setFillColor(245, 245, 245);
|
||||||
|
doc.roundedRect(margin, y - 3.5, contentW, 10, 2, 2, 'F');
|
||||||
|
doc.setFont('courier', 'normal');
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setTextColor(60, 60, 60);
|
||||||
|
doc.text(addr, margin + 3, y + 2.5);
|
||||||
|
y += 14;
|
||||||
|
|
||||||
|
// monero: URI
|
||||||
|
var uri = uriBox.textContent;
|
||||||
|
if (uri) {
|
||||||
|
doc.setFillColor(245, 245, 245);
|
||||||
|
doc.roundedRect(margin, y - 3.5, contentW, 10, 2, 2, 'F');
|
||||||
|
doc.setFont('courier', 'normal');
|
||||||
|
doc.setFontSize(6.5);
|
||||||
|
doc.setTextColor(100, 100, 100);
|
||||||
|
var uriLines = doc.splitTextToSize(uri, contentW - 6);
|
||||||
|
doc.text(uriLines, margin + 3, y + 2);
|
||||||
|
y += uriLines.length * 3 + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Payment Status ---
|
||||||
|
if (paymentStatus.classList.contains('paid')) {
|
||||||
|
y += 4;
|
||||||
|
doc.setFillColor(76, 175, 80);
|
||||||
|
doc.roundedRect(margin, y - 4, contentW, 16, 2, 2, 'F');
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.setTextColor(255, 255, 255);
|
||||||
|
doc.text(I18n.t('status_paid').toUpperCase(), margin + contentW / 2, y + 2, { align: 'center' });
|
||||||
|
// Extract details from the paid-detail div
|
||||||
|
var paidDetail = paymentStatus.querySelector('.paid-detail');
|
||||||
|
if (paidDetail) {
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.text(paidDetail.textContent, margin + contentW / 2, y + 8, { align: 'center' });
|
||||||
|
}
|
||||||
|
y += 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Footer ---
|
||||||
|
doc.setDrawColor(220, 220, 220);
|
||||||
|
doc.setLineWidth(0.3);
|
||||||
|
var footerY = doc.internal.pageSize.getHeight() - 15;
|
||||||
|
doc.line(margin, footerY, pageW - margin, footerY);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(7);
|
||||||
|
doc.setTextColor(180, 180, 180);
|
||||||
|
doc.text(I18n.t('pdf_footer'), pageW / 2, footerY + 5, { align: 'center' });
|
||||||
|
|
||||||
|
// Share link
|
||||||
|
var shareLink = shareLinkInput.value;
|
||||||
|
if (shareLink) {
|
||||||
|
doc.text(shareLink, pageW / 2, footerY + 9, { align: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
var filename = 'xmrpay-' + (desc ? desc.replace(/[^a-zA-Z0-9]/g, '-').substring(0, 30) : 'invoice') + '.pdf';
|
||||||
|
doc.save(filename);
|
||||||
|
}
|
||||||
|
|
||||||
// --- TX Proof Functions ---
|
// --- TX Proof Functions ---
|
||||||
|
|
||||||
function toggleProofPanel() {
|
function toggleProofPanel() {
|
||||||
@@ -576,9 +792,19 @@
|
|||||||
|
|
||||||
function showPaidStatus(data) {
|
function showPaidStatus(data) {
|
||||||
paymentStatus.className = 'payment-status paid';
|
paymentStatus.className = 'payment-status paid';
|
||||||
|
var dateStr = '';
|
||||||
|
if (data.verified_at) {
|
||||||
|
var d = new Date(data.verified_at * 1000);
|
||||||
|
dateStr = ' — ' + d.toLocaleDateString(I18n.getLang() === 'de' ? 'de-CH' : 'en-US', {
|
||||||
|
year: 'numeric', month: 'long', day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
paymentStatus.innerHTML = '<div class="paid-badge">' + I18n.t('status_paid') +
|
paymentStatus.innerHTML = '<div class="paid-badge">' + I18n.t('status_paid') +
|
||||||
'</div><div class="paid-detail">' + data.amount.toFixed(6) + ' XMR — TX ' +
|
'</div><div class="paid-detail">' + data.amount.toFixed(6) + ' XMR — TX ' +
|
||||||
data.tx_hash.substring(0, 8) + '...</div>';
|
data.tx_hash.substring(0, 8) + '...' + dateStr + '</div>';
|
||||||
|
// Hide proof section when paid
|
||||||
|
var proofSection = document.getElementById('proofSection');
|
||||||
|
if (proofSection) proofSection.style.display = 'none';
|
||||||
setPaidFavicon();
|
setPaidFavicon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
i18n.js
30
i18n.js
@@ -20,14 +20,25 @@ var I18n = (function () {
|
|||||||
btn_generate: 'Zahlungsanforderung erstellen',
|
btn_generate: 'Zahlungsanforderung erstellen',
|
||||||
btn_open_wallet: 'In Wallet öffnen',
|
btn_open_wallet: 'In Wallet öffnen',
|
||||||
btn_copy_addr: 'Adresse kopieren',
|
btn_copy_addr: 'Adresse kopieren',
|
||||||
|
btn_download_pdf: 'PDF Rechnung',
|
||||||
|
pdf_title: 'Zahlungsanforderung',
|
||||||
|
pdf_address: 'XMR-Adresse',
|
||||||
|
pdf_amount: 'Betrag',
|
||||||
|
pdf_desc: 'Beschreibung',
|
||||||
|
pdf_deadline: 'Zahlungsfrist',
|
||||||
|
pdf_deadline_days: '{d} Tage',
|
||||||
|
pdf_date: 'Datum',
|
||||||
|
pdf_scan_qr: 'QR-Code scannen zum Bezahlen',
|
||||||
|
pdf_footer: 'Erstellt mit xmrpay.link — Keine Registrierung, kein KYC',
|
||||||
qr_hint: 'Klick auf QR zum Speichern',
|
qr_hint: 'Klick auf QR zum Speichern',
|
||||||
footer: 'Open Source · Kein Backend · Kein KYC · <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank">Source</a>',
|
footer: 'Open Source · Kein Backend · Kein KYC · <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank">Source</a>',
|
||||||
|
label_uri_details: 'Monero-URI anzeigen',
|
||||||
label_share_link: 'Teilbarer Link',
|
label_share_link: 'Teilbarer Link',
|
||||||
btn_new_request: 'Neue Zahlungsanforderung',
|
btn_new_request: 'Neue Zahlungsanforderung',
|
||||||
toast_copied: 'Kopiert!',
|
toast_copied: 'Kopiert!',
|
||||||
countdown_expired: 'Zahlungsfrist abgelaufen',
|
countdown_expired: 'Zahlungsfrist abgelaufen',
|
||||||
countdown_remaining_days: 'Zahlungsfrist: {d} Tage, {h}:{m}:{s}',
|
countdown_remaining_days: 'Zahlungsfrist: {d} Tage, {h} Std.',
|
||||||
countdown_remaining_hours: 'Zahlungsfrist: {h}:{m}:{s}',
|
countdown_remaining_hours: 'Zahlungsfrist: {h}:{m} Std.',
|
||||||
rates_offline: 'Kurse nicht verfügbar — nur XMR-Betrag möglich',
|
rates_offline: 'Kurse nicht verfügbar — nur XMR-Betrag möglich',
|
||||||
btn_prove_payment: 'Zahlung nachweisen',
|
btn_prove_payment: 'Zahlung nachweisen',
|
||||||
label_tx_hash: 'Transaction ID (TX Hash)',
|
label_tx_hash: 'Transaction ID (TX Hash)',
|
||||||
@@ -55,14 +66,25 @@ var I18n = (function () {
|
|||||||
btn_generate: 'Create payment request',
|
btn_generate: 'Create payment request',
|
||||||
btn_open_wallet: 'Open in wallet',
|
btn_open_wallet: 'Open in wallet',
|
||||||
btn_copy_addr: 'Copy address',
|
btn_copy_addr: 'Copy address',
|
||||||
|
btn_download_pdf: 'PDF Invoice',
|
||||||
|
pdf_title: 'Payment Request',
|
||||||
|
pdf_address: 'XMR Address',
|
||||||
|
pdf_amount: 'Amount',
|
||||||
|
pdf_desc: 'Description',
|
||||||
|
pdf_deadline: 'Payment deadline',
|
||||||
|
pdf_deadline_days: '{d} days',
|
||||||
|
pdf_date: 'Date',
|
||||||
|
pdf_scan_qr: 'Scan QR code to pay',
|
||||||
|
pdf_footer: 'Created with xmrpay.link — No registration, no KYC',
|
||||||
qr_hint: 'Click QR to save',
|
qr_hint: 'Click QR to save',
|
||||||
footer: 'Open Source · No Backend · No KYC · <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank">Source</a>',
|
footer: 'Open Source · No Backend · No KYC · <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank">Source</a>',
|
||||||
|
label_uri_details: 'Show Monero URI',
|
||||||
label_share_link: 'Shareable link',
|
label_share_link: 'Shareable link',
|
||||||
btn_new_request: 'New payment request',
|
btn_new_request: 'New payment request',
|
||||||
toast_copied: 'Copied!',
|
toast_copied: 'Copied!',
|
||||||
countdown_expired: 'Payment deadline expired',
|
countdown_expired: 'Payment deadline expired',
|
||||||
countdown_remaining_days: 'Deadline: {d} days, {h}:{m}:{s}',
|
countdown_remaining_days: 'Deadline: {d} days, {h} hrs',
|
||||||
countdown_remaining_hours: 'Deadline: {h}:{m}:{s}',
|
countdown_remaining_hours: 'Deadline: {h}:{m} hrs',
|
||||||
rates_offline: 'Rates unavailable — XMR amount only',
|
rates_offline: 'Rates unavailable — XMR amount only',
|
||||||
btn_prove_payment: 'Prove payment',
|
btn_prove_payment: 'Prove payment',
|
||||||
label_tx_hash: 'Transaction ID (TX Hash)',
|
label_tx_hash: 'Transaction ID (TX Hash)',
|
||||||
|
|||||||
11
index.html
11
index.html
@@ -58,8 +58,12 @@
|
|||||||
|
|
||||||
<div id="result" class="card">
|
<div id="result" class="card">
|
||||||
<div class="qr-container" id="qr"></div>
|
<div class="qr-container" id="qr"></div>
|
||||||
|
<div class="payment-summary" id="paymentSummary"></div>
|
||||||
<div class="countdown" id="countdown"></div>
|
<div class="countdown" id="countdown"></div>
|
||||||
<div class="uri-box" id="uri"></div>
|
<details class="uri-details">
|
||||||
|
<summary data-i18n="label_uri_details"></summary>
|
||||||
|
<div class="uri-box" id="uri"></div>
|
||||||
|
</details>
|
||||||
<div class="share-link-box" id="shareLinkBox">
|
<div class="share-link-box" id="shareLinkBox">
|
||||||
<label data-i18n="label_share_link"></label>
|
<label data-i18n="label_share_link"></label>
|
||||||
<div class="share-link-row">
|
<div class="share-link-row">
|
||||||
@@ -70,12 +74,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="btn btn-secondary" id="openWallet" href="#" data-i18n="btn_open_wallet"></a>
|
<button class="btn btn-secondary" id="openWallet" data-i18n="btn_open_wallet"></button>
|
||||||
<button class="btn btn-secondary" id="copyAddr" data-i18n="btn_copy_addr"></button>
|
<button class="btn btn-secondary" id="copyAddr" data-i18n="btn_copy_addr"></button>
|
||||||
|
<button class="btn btn-secondary" id="downloadPdf" data-i18n="btn_download_pdf"></button>
|
||||||
</div>
|
</div>
|
||||||
<!-- TX Proof Verification -->
|
<!-- TX Proof Verification -->
|
||||||
<div class="proof-section" id="proofSection">
|
<div class="proof-section" id="proofSection">
|
||||||
<button class="btn btn-monitor" id="proofToggle">
|
<button class="btn btn-proof" id="proofToggle">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||||
|
|||||||
373
lib/jspdf.min.js
vendored
Normal file
373
lib/jspdf.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
70
style.css
70
style.css
@@ -325,6 +325,12 @@ textarea {
|
|||||||
|
|
||||||
#result.visible {
|
#result.visible {
|
||||||
display: block;
|
display: block;
|
||||||
|
animation: fadeSlideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeSlideIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-container {
|
.qr-container {
|
||||||
@@ -355,6 +361,48 @@ textarea {
|
|||||||
margin-top: 0.3rem;
|
margin-top: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-summary {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem 0 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-amount {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-fiat {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uri-details {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uri-details summary {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 0.3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uri-details summary:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.uri-box {
|
.uri-box {
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -364,7 +412,7 @@ textarea {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 0.8rem;
|
margin-top: 0.4rem;
|
||||||
max-height: 80px;
|
max-height: 80px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -401,10 +449,16 @@ textarea {
|
|||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions .btn-secondary {
|
.actions .btn-secondary {
|
||||||
flex: 1;
|
flex: 1 1 30%;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- TX Proof Section --- */
|
/* --- TX Proof Section --- */
|
||||||
@@ -415,7 +469,7 @@ textarea {
|
|||||||
padding-top: 0.8rem;
|
padding-top: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-monitor {
|
.btn-proof {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -433,7 +487,7 @@ textarea {
|
|||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-monitor:hover {
|
.btn-proof:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@@ -572,4 +626,12 @@ footer a:hover {
|
|||||||
header h1 {
|
header h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
.qr-container canvas,
|
||||||
|
.qr-container img {
|
||||||
|
max-width: 80vw;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.summary-amount {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user