feat: UI polish, a11y, performance optimizations
- Payment summary card with prominent amount display - "Bezahlt" stamp over dimmed QR code with TX details below - Hide wallet/address buttons when paid, show only PDF - URI box removed (was technical noise) - Smart countdown: "29 Tage, 23 Std." instead of ticking seconds - Dynamic page title for shared invoices - Font fallbacks with size-adjust to prevent layout shifts - Async Google Fonts loading, proper preconnect hints - Deferred script loading (defer attribute) - Minified JS (app.min.js, i18n.min.js) - WCAG contrast fixes for badges and disabled button - Footer link always underlined for a11y - Translated aria-labels via data-i18n-aria - i18n onChange callback for dynamic content updates - Result card fade-in animation, responsive QR on mobile
This commit is contained in:
53
app.js
53
app.js
@@ -51,12 +51,34 @@
|
||||
const downloadPdfBtn = $('#downloadPdf');
|
||||
let cryptoLoaded = false;
|
||||
let pdfLoaded = false;
|
||||
let lastPaidData = null;
|
||||
|
||||
// --- Init ---
|
||||
fetchRates();
|
||||
loadFromHash() || loadSaved();
|
||||
registerSW();
|
||||
|
||||
// Re-render dynamic texts on language change
|
||||
I18n.onChange(function () {
|
||||
// QR hint
|
||||
var hint = qrContainer.querySelector('.qr-hint');
|
||||
if (hint) hint.textContent = I18n.t('qr_hint');
|
||||
// Paid stamp
|
||||
var stamp = qrContainer.querySelector('.paid-stamp');
|
||||
if (stamp) stamp.textContent = I18n.t('status_paid');
|
||||
// Paid detail
|
||||
if (lastPaidData) {
|
||||
showPaidStatus(lastPaidData);
|
||||
}
|
||||
// Summary
|
||||
if (resultSection.classList.contains('visible')) {
|
||||
var xmrAmount = getXmrAmount();
|
||||
var desc = descInput.value.trim();
|
||||
buildSummary(xmrAmount, desc, selectedDays);
|
||||
updatePageTitle(xmrAmount, desc);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Events ---
|
||||
addrInput.addEventListener('input', validateAddress);
|
||||
amountInput.addEventListener('input', updateFiatHint);
|
||||
@@ -792,6 +814,22 @@
|
||||
|
||||
function showPaidStatus(data) {
|
||||
paymentStatus.className = 'payment-status paid';
|
||||
|
||||
// Stamp over QR + dim QR
|
||||
qrContainer.classList.add('paid');
|
||||
var existingStamp = qrContainer.querySelector('.paid-stamp');
|
||||
if (!existingStamp) {
|
||||
var stamp = document.createElement('div');
|
||||
stamp.className = 'paid-stamp';
|
||||
stamp.textContent = I18n.t('status_paid');
|
||||
qrContainer.appendChild(stamp);
|
||||
} else {
|
||||
existingStamp.textContent = I18n.t('status_paid');
|
||||
}
|
||||
|
||||
// Replace QR hint with payment detail
|
||||
var hint = qrContainer.querySelector('.qr-hint');
|
||||
if (hint) {
|
||||
var dateStr = '';
|
||||
if (data.verified_at) {
|
||||
var d = new Date(data.verified_at * 1000);
|
||||
@@ -799,10 +837,17 @@
|
||||
year: 'numeric', month: 'long', day: 'numeric'
|
||||
});
|
||||
}
|
||||
paymentStatus.innerHTML = '<div class="paid-badge">' + I18n.t('status_paid') +
|
||||
'</div><div class="paid-detail">' + data.amount.toFixed(6) + ' XMR — TX ' +
|
||||
data.tx_hash.substring(0, 8) + '...' + dateStr + '</div>';
|
||||
// Hide proof section when paid
|
||||
hint.textContent = data.amount.toFixed(6) + ' XMR — TX ' +
|
||||
data.tx_hash.substring(0, 8) + '...' + dateStr;
|
||||
hint.className = 'qr-hint paid-info';
|
||||
}
|
||||
|
||||
paymentStatus.innerHTML = '';
|
||||
lastPaidData = data;
|
||||
|
||||
// Hide unnecessary buttons when paid
|
||||
openWalletBtn.style.display = 'none';
|
||||
document.getElementById('copyAddr').style.display = 'none';
|
||||
var proofSection = document.getElementById('proofSection');
|
||||
if (proofSection) proofSection.style.display = 'none';
|
||||
setPaidFavicon();
|
||||
|
||||
1
app.min.js
vendored
Normal file
1
app.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
17
i18n.js
17
i18n.js
@@ -32,6 +32,7 @@ var I18n = (function () {
|
||||
pdf_footer: 'Erstellt mit xmrpay.link — Keine Registrierung, kein KYC',
|
||||
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>',
|
||||
aria_currency: 'Währung',
|
||||
label_uri_details: 'Monero-URI anzeigen',
|
||||
label_share_link: 'Teilbarer Link',
|
||||
btn_new_request: 'Neue Zahlungsanforderung',
|
||||
@@ -78,6 +79,7 @@ var I18n = (function () {
|
||||
pdf_footer: 'Created with xmrpay.link — No registration, no KYC',
|
||||
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>',
|
||||
aria_currency: 'Currency',
|
||||
label_uri_details: 'Show Monero URI',
|
||||
label_share_link: 'Shareable link',
|
||||
btn_new_request: 'New payment request',
|
||||
@@ -126,6 +128,9 @@ var I18n = (function () {
|
||||
document.querySelectorAll('[data-i18n-html]').forEach(function (el) {
|
||||
el.innerHTML = t[el.getAttribute('data-i18n-html')] || '';
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-aria]').forEach(function (el) {
|
||||
el.setAttribute('aria-label', t[el.getAttribute('data-i18n-aria')] || '');
|
||||
});
|
||||
}
|
||||
|
||||
function apply(lang) {
|
||||
@@ -144,6 +149,16 @@ var I18n = (function () {
|
||||
document.querySelectorAll('.lang-option').forEach(function (btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
|
||||
});
|
||||
|
||||
// Notify listeners
|
||||
for (var i = 0; i < onChangeCallbacks.length; i++) {
|
||||
onChangeCallbacks[i](lang);
|
||||
}
|
||||
}
|
||||
|
||||
var onChangeCallbacks = [];
|
||||
function onChange(fn) {
|
||||
onChangeCallbacks.push(fn);
|
||||
}
|
||||
|
||||
function buildDropdown() {
|
||||
@@ -205,5 +220,5 @@ var I18n = (function () {
|
||||
apply(currentLang);
|
||||
});
|
||||
|
||||
return { t: t, apply: apply, getLang: getLang };
|
||||
return { t: t, apply: apply, getLang: getLang, onChange: onChange };
|
||||
})();
|
||||
|
||||
1
i18n.min.js
vendored
Normal file
1
i18n.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
23
index.html
23
index.html
@@ -7,8 +7,9 @@
|
||||
<meta name="description" content="Create Monero payment requests in seconds. No account, no backend, no KYC.">
|
||||
<link rel="icon" id="favicon" href="favicon.svg" type="image/svg+xml">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -28,7 +29,7 @@
|
||||
<label for="amount" data-i18n="label_amount"></label>
|
||||
<div class="amount-row">
|
||||
<input type="number" id="amount" placeholder="0.00" min="0" step="any">
|
||||
<select id="currency">
|
||||
<select id="currency" data-i18n-aria="aria_currency">
|
||||
<option value="XMR">XMR</option>
|
||||
<option value="EUR" selected>EUR</option>
|
||||
<option value="USD">USD</option>
|
||||
@@ -59,15 +60,13 @@
|
||||
<div id="result" class="card">
|
||||
<div class="qr-container" id="qr"></div>
|
||||
<div class="payment-summary" id="paymentSummary"></div>
|
||||
<div class="payment-status" id="paymentStatus"></div>
|
||||
<div class="countdown" id="countdown"></div>
|
||||
<details class="uri-details">
|
||||
<summary data-i18n="label_uri_details"></summary>
|
||||
<div class="uri-box" id="uri"></div>
|
||||
</details>
|
||||
<div class="uri-box" id="uri" style="display:none"></div>
|
||||
<div class="share-link-box" id="shareLinkBox">
|
||||
<label data-i18n="label_share_link"></label>
|
||||
<div class="share-link-row">
|
||||
<input type="text" id="shareLink" readonly>
|
||||
<input type="text" id="shareLink" readonly data-i18n-aria="label_share_link">
|
||||
<button class="btn btn-secondary btn-icon" id="copyShareLink" title="Copy">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
@@ -100,10 +99,6 @@
|
||||
<div class="proof-result" id="proofResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment status (shown when proof is stored) -->
|
||||
<div class="payment-status" id="paymentStatus"></div>
|
||||
|
||||
<button class="btn btn-primary btn-new" id="newRequest" data-i18n="btn_new_request"></button>
|
||||
</div>
|
||||
</main>
|
||||
@@ -126,8 +121,8 @@
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="lib/qrcode.min.js"></script>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="lib/qrcode.min.js" defer></script>
|
||||
<script src="i18n.min.js" defer></script>
|
||||
<script src="app.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
72
style.css
72
style.css
@@ -1,3 +1,21 @@
|
||||
@font-face {
|
||||
font-family: 'Inter fallback';
|
||||
src: local('Arial');
|
||||
size-adjust: 107%;
|
||||
ascent-override: 90%;
|
||||
descent-override: 25%;
|
||||
line-gap-override: 0%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono fallback';
|
||||
src: local('Courier New');
|
||||
size-adjust: 112%;
|
||||
ascent-override: 78%;
|
||||
descent-override: 22%;
|
||||
line-gap-override: 0%;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0d0d0d;
|
||||
--bg-card: #1a1a1a;
|
||||
@@ -10,8 +28,8 @@
|
||||
--success: #4caf50;
|
||||
--error: #f44336;
|
||||
--radius: 8px;
|
||||
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
--font: 'Inter', 'Inter fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--mono: 'JetBrains Mono', 'JetBrains Mono fallback', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -133,6 +151,7 @@ main {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
@@ -247,7 +266,7 @@ input.valid {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-muted);
|
||||
color: #aaa;
|
||||
font-family: var(--font);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
@@ -301,7 +320,7 @@ textarea {
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.4;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -532,24 +551,39 @@ textarea {
|
||||
|
||||
.payment-status.paid {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.paid-badge {
|
||||
display: inline-block;
|
||||
background: var(--success);
|
||||
color: #fff;
|
||||
padding: 0.4rem 1.2rem;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
.paid-stamp {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-12deg);
|
||||
border: 4px solid var(--success);
|
||||
border-radius: 12px;
|
||||
padding: 0.4rem 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 900;
|
||||
color: var(--success);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.paid-detail {
|
||||
.qr-container.paid canvas,
|
||||
.qr-container.paid img {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.paid-info {
|
||||
color: var(--success);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
@@ -594,10 +628,6 @@ footer {
|
||||
|
||||
footer a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user