From 8bcdb33fa353f003bc21a93871328b042a709f1e Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Wed, 25 Mar 2026 16:50:55 +0100 Subject: [PATCH] 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 --- app.js | 65 +++++++++++++++++++++++++++++++++++++++-------- app.min.js | 1 + i18n.js | 17 ++++++++++++- i18n.min.js | 1 + index.html | 23 +++++++---------- style.css | 72 +++++++++++++++++++++++++++++++++++++---------------- 6 files changed, 133 insertions(+), 46 deletions(-) create mode 100644 app.min.js create mode 100644 i18n.min.js diff --git a/app.js b/app.js index 75f1dee..4f57f62 100644 --- a/app.js +++ b/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,17 +814,40 @@ function showPaidStatus(data) { 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' - }); + + // 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'); } - paymentStatus.innerHTML = ''; - // Hide proof section when 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); + dateStr = ' — ' + d.toLocaleDateString(I18n.getLang() === 'de' ? 'de-CH' : 'en-US', { + year: 'numeric', month: 'long', day: 'numeric' + }); + } + 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(); diff --git a/app.min.js b/app.min.js new file mode 100644 index 0000000..59be42a --- /dev/null +++ b/app.min.js @@ -0,0 +1 @@ +(function(){"use strict";const St="https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=eur,usd,chf",It=/^[48][1-9A-HJ-NP-Za-km-z]{94}$/,_t=/^4[1-9A-HJ-NP-Za-km-z]{105}$/;let y=null,lt=0,I=null,z=!1,_=null;const i=e=>document.querySelector(e),u=i("#addr"),C=i("#amount"),L=i("#currency"),q=i("#desc"),T=i("#timerCustom"),D=i("#deadlineBadges");let f=0;const tt=i("#generate"),B=i("#result"),v=i("#qr"),et=i("#uri"),dt=i("#openWallet"),Tt=i("#copyAddr"),w=i("#countdown"),h=i("#fiatHint"),nt=i("#toast"),H=i("#shareLink"),Ft=i("#copyShareLink"),Et=i("#newRequest"),Rt=i("#homeLink"),bt=i("#proofToggle"),M=i("#proofPanel"),F=i("#txHash"),U=i("#txKey"),E=i("#verifyProof"),p=i("#proofResult"),R=i("#paymentStatus"),ut=i("#paymentSummary"),Pt=i("#downloadPdf");let ft=!1,kt=!1,at=null;xt(),Mt()||Bt(),Ut(),I18n.onChange(function(){var e=v.querySelector(".qr-hint");e&&(e.textContent=I18n.t("qr_hint"));var t=v.querySelector(".paid-stamp");if(t&&(t.textContent=I18n.t("status_paid")),at&&it(at),B.classList.contains("visible")){var n=rt(),a=q.value.trim();mt(n,a,f),ht(n,a)}}),u.addEventListener("input",ot),C.addEventListener("input",O),L.addEventListener("change",O),tt.addEventListener("click",pt),Tt.addEventListener("click",()=>gt(u.value.trim())),Ft.addEventListener("click",()=>gt(H.value)),v.addEventListener("click",At),Et.addEventListener("click",vt),Rt.addEventListener("click",function(e){e.preventDefault(),vt()}),D.querySelectorAll(".badge").forEach(function(e){e.addEventListener("click",function(){const t=parseInt(e.getAttribute("data-days"));e.classList.contains("active")?(e.classList.remove("active"),f=0,T.value=""):(D.querySelectorAll(".badge").forEach(function(n){n.classList.remove("active")}),e.classList.add("active"),f=t,T.value="")})}),T.addEventListener("input",function(){D.querySelectorAll(".badge").forEach(function(e){e.classList.remove("active")}),f=parseInt(T.value)||0}),Pt.addEventListener("click",Wt),bt.addEventListener("click",Gt),F.addEventListener("input",Ct),U.addEventListener("input",Ct),E.addEventListener("click",Yt);function vt(){u.value="",C.value="",L.value="EUR",q.value="",f=0,T.value="",D.querySelectorAll(".badge").forEach(function(e){e.classList.remove("active")}),h.textContent="",h.classList.remove("error"),u.classList.remove("valid","invalid"),tt.disabled=!0,B.classList.remove("visible"),I&&clearInterval(I),v.innerHTML="",et.textContent="",H.value="",_=null,M.classList.remove("open"),F.value="",U.value="",E.disabled=!0,p.innerHTML="",p.className="proof-result",R.innerHTML="",R.className="payment-status",ut.innerHTML="",document.title="xmrpay.link \u2014 Monero Invoice Generator",history.replaceState(null,"",location.pathname),window.scrollTo({top:0,behavior:"smooth"}),u.focus()}function N(e){return It.test(e)||_t.test(e)}function ot(){const e=u.value.trim();u.classList.remove("valid","invalid"),e.length!==0&&(N(e)?u.classList.add("valid"):e.length>=10&&u.classList.add("invalid"),Xt())}function Xt(){const e=u.value.trim();tt.disabled=!N(e)}function O(){const e=parseFloat(C.value),t=L.value;if(!e||e<=0){h.textContent="",h.classList.remove("error");return}if(t!=="XMR"&&!y){h.textContent=z?I18n.t("rates_offline"):"",h.classList.toggle("error",z);return}if(h.classList.remove("error"),t==="XMR")if(y){const n=(e*y.eur).toFixed(2);h.textContent="\u2248 "+n+" EUR"}else h.textContent="";else{const n=y[t.toLowerCase()];if(n&&n>0){const a=(e/n).toFixed(8);h.textContent="\u2248 "+a+" XMR"}}}function rt(){const e=parseFloat(C.value),t=L.value;if(!e||e<=0)return null;if(t==="XMR")return e;if(y){const n=y[t.toLowerCase()];if(n&&n>0)return e/n}return null}function qt(e,t,n){let a="monero:"+e;const o=[];return t&&o.push("tx_amount="+t.toFixed(12)),n&&o.push("tx_description="+encodeURIComponent(n)),o.length&&(a+="?"+o.join("&")),a}function Dt(e,t,n,a){const o=new URLSearchParams;return o.set("a",e),t&&o.set("x",t.toFixed(12)),n&&o.set("d",n),a&&o.set("t",a),o.toString()}async function Ht(e){try{const t=await fetch("/api/shorten.php",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({hash:e})});if(!t.ok)throw new Error("HTTP "+t.status);const n=await t.json();return _||(_=n.code),location.origin+"/s/"+n.code}catch(t){return console.warn("Short URL failed:",t),null}}function pt(){const e=u.value.trim();if(!N(e))return;const t=rt(),n=q.value.trim(),a=f,o=qt(e,t,n);B.classList.add("visible"),et.textContent=o,dt.onclick=function(){window.location.href=o},mt(t,n,a),ht(t,n);const c=Dt(e,t,n,a);H.value=location.origin+"/#"+c,Ht(c).then(function(l){l&&(H.value=l)}),v.innerHTML="",new QRCode(v,{text:o,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M});const d=document.createElement("div");d.className="qr-hint",d.textContent=I18n.t("qr_hint"),v.appendChild(d),Nt(),zt(e),B.scrollIntoView({behavior:"smooth",block:"start"})}function Mt(){const e=location.hash.substring(1);if(!e)return!1;const t=new URLSearchParams(e),n=t.get("a");if(!n||!N(n))return!1;u.value=n,ot();const a=t.get("x");a&&(C.value=parseFloat(a),L.value="XMR");const o=t.get("d");o&&(q.value=o);const c=t.get("t");if(c&&parseInt(c)>0){f=parseInt(c);const l=D.querySelector('.badge[data-days="'+f+'"]');l?l.classList.add("active"):T.value=f}const d=t.get("c");return d&&(_=d,setTimeout(function(){Jt(d)},200)),setTimeout(pt,100),!0}function mt(e,t,n){var a="";if(e){a+='
'+e.toFixed(8)+" XMR
";var o=parseFloat(C.value),c=L.value;c!=="XMR"&&o&&(a+='
\u2248 '+o.toFixed(2)+" "+c+"
")}t&&(a+='
'+t.replace(/"),ut.innerHTML=a}function ht(e,t){var n=[];e&&n.push(e.toFixed(4)+" XMR"),t&&n.push(t),n.length&&(document.title=n.join(" \u2014 ")+" | xmrpay.link")}function Nt(){if(I&&clearInterval(I),w.textContent="",w.className="countdown",!f||f<=0)return;const e=Date.now()+f*864e5;w.classList.add("active");function t(){const n=e-Date.now();if(n<=0){clearInterval(I),w.textContent=I18n.t("countdown_expired"),w.className="countdown expired";return}const a=Math.floor(n/864e5),o=Math.floor(n%864e5/36e5),c=Math.floor(n%36e5/6e4);a>0?w.textContent=I18n.t("countdown_remaining_days").replace("{d}",a).replace("{h}",o):w.textContent=I18n.t("countdown_remaining_hours").replace("{h}",yt(o)).replace("{m}",yt(c))}t(),I=setInterval(t,6e4)}function yt(e){return e<10?"0"+e:""+e}function At(){const e=v.querySelector("canvas");if(!e)return;const t=document.createElement("a");t.download="xmrpay-qr.png",t.href=e.toDataURL("image/png"),t.click()}function gt(e){navigator.clipboard.writeText(e).then(()=>{jt(I18n.t("toast_copied"))})}function jt(e){nt.textContent=e,nt.classList.add("show"),setTimeout(()=>nt.classList.remove("show"),2e3)}function zt(e){try{localStorage.setItem("xmrpay_addr",e)}catch{}}function Bt(){try{const e=localStorage.getItem("xmrpay_addr");e&&(u.value=e,ot())}catch{}}async function xt(){if(!(y&&Date.now()-lt<6e4))try{const e=await fetch(St);if(!e.ok)throw new Error("HTTP "+e.status);y=(await e.json()).monero,lt=Date.now(),z=!1,O()}catch(e){console.warn("Kurse konnten nicht geladen werden:",e),z=!0,O(),setTimeout(xt,1e4)}}function Ut(){"serviceWorker"in navigator&&navigator.serviceWorker.register("sw.js").catch(function(){})}function Ot(){return new Promise(function(e,t){if(window.jspdf){e();return}var n=document.createElement("script");n.src="lib/jspdf.min.js",n.onload=function(){kt=!0,e()},n.onerror=function(){t(new Error("Failed to load jsPDF"))},document.head.appendChild(n)})}async function Wt(){await Ot();var e=window.jspdf.jsPDF,t=new e({orientation:"portrait",unit:"mm",format:"a4"}),n=u.value.trim(),a=rt(),o=q.value.trim(),c=parseFloat(C.value),d=L.value,l=t.internal.pageSize.getWidth(),s=20,g=l-s*2,r=s;t.setFillColor(242,104,33),t.rect(0,0,l,8,"F"),r=22,t.setFont("helvetica","bold"),t.setFontSize(22),t.setTextColor(242,104,33),t.text(I18n.t("pdf_title"),s,r),t.setFont("helvetica","normal"),t.setFontSize(10),t.setTextColor(120,120,120);var st=new Date().toLocaleDateString(I18n.getLang()==="de"?"de-CH":"en-US",{year:"numeric",month:"long",day:"numeric"});t.text(I18n.t("pdf_date")+": "+st,l-s,r,{align:"right"}),r+=6,t.setDrawColor(220,220,220),t.setLineWidth(.3),t.line(s,r,l-s,r);var G=v.querySelector("canvas"),x=50,b=l-s-x,A=r+6;if(G){var K=G.toDataURL("image/png");t.addImage(K,"PNG",b,A,x,x),t.setFontSize(7),t.setTextColor(150,150,150),t.text(I18n.t("pdf_scan_qr"),b+x/2,A+x+4,{align:"center"})}var P=s,Y=b-s-10;r+=14;function k(j,Lt){t.setFont("helvetica","normal"),t.setFontSize(9),t.setTextColor(150,150,150),t.text(j,P,r),r+=5,t.setFont("helvetica","bold"),t.setFontSize(11),t.setTextColor(40,40,40);var wt=t.splitTextToSize(Lt,Y);t.text(wt,P,r),r+=wt.length*5+4}if(a){var m=a.toFixed(8)+" XMR";d!=="XMR"&&c&&(m+=" (\u2248 "+c.toFixed(2)+" "+d+")"),k(I18n.t("pdf_amount"),m)}if(o&&k(I18n.t("pdf_desc"),o),f>0){var S=new Date(Date.now()+f*864e5),J=S.toLocaleDateString(I18n.getLang()==="de"?"de-CH":"en-US",{year:"numeric",month:"long",day:"numeric"});k(I18n.t("pdf_deadline"),J+" ("+I18n.t("pdf_deadline_days").replace("{d}",f)+")")}r=Math.max(r,A+x+12),t.setFont("helvetica","normal"),t.setFontSize(9),t.setTextColor(150,150,150),t.text(I18n.t("pdf_address"),s,r),r+=5,t.setFillColor(245,245,245),t.roundedRect(s,r-3.5,g,10,2,2,"F"),t.setFont("courier","normal"),t.setFontSize(8),t.setTextColor(60,60,60),t.text(n,s+3,r+2.5),r+=14;var V=et.textContent;if(V){t.setFillColor(245,245,245),t.roundedRect(s,r-3.5,g,10,2,2,"F"),t.setFont("courier","normal"),t.setFontSize(6.5),t.setTextColor(100,100,100);var $=t.splitTextToSize(V,g-6);t.text($,s+3,r+2),r+=$.length*3+10}if(R.classList.contains("paid")){r+=4,t.setFillColor(76,175,80),t.roundedRect(s,r-4,g,16,2,2,"F"),t.setFont("helvetica","bold"),t.setFontSize(12),t.setTextColor(255,255,255),t.text(I18n.t("status_paid").toUpperCase(),s+g/2,r+2,{align:"center"});var Q=R.querySelector(".paid-detail");Q&&(t.setFont("helvetica","normal"),t.setFontSize(8),t.text(Q.textContent,s+g/2,r+8,{align:"center"})),r+=22}t.setDrawColor(220,220,220),t.setLineWidth(.3);var X=t.internal.pageSize.getHeight()-15;t.line(s,X,l-s,X),t.setFont("helvetica","normal"),t.setFontSize(7),t.setTextColor(180,180,180),t.text(I18n.t("pdf_footer"),l/2,X+5,{align:"center"});var Z=H.value;Z&&t.text(Z,l/2,X+9,{align:"center"});var ct="xmrpay-"+(o?o.replace(/[^a-zA-Z0-9]/g,"-").substring(0,30):"invoice")+".pdf";t.save(ct)}function Gt(){if(M.classList.contains("open")){M.classList.remove("open");return}if(!ft&&!window.XmrCrypto){Kt().then(function(){ft=!0,M.classList.add("open"),F.focus()});return}M.classList.add("open"),F.focus()}function Kt(){return new Promise(function(e,t){if(window.XmrCrypto){e();return}const n=document.createElement("script");n.src="lib/xmr-crypto.bundle.js",n.onload=e,n.onerror=function(){t(new Error("Failed to load crypto module"))},document.head.appendChild(n)})}function W(e){return/^[0-9a-fA-F]{64}$/.test(e)}function Ct(){const e=F.value.trim(),t=U.value.trim();E.disabled=!(W(e)&&W(t))}async function Yt(){const e=F.value.trim(),t=U.value.trim(),n=u.value.trim();if(!(!W(e)||!W(t)||!N(n))){E.disabled=!0,p.className="proof-result active",p.textContent=I18n.t("proof_verifying");try{var a=await fetch("/api/node.php",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:"gettransactions",params:{txs_hashes:[e],decode_as_json:!0}})}),o=await a.json(),c=o.txs||[];if(c.length===0){p.className="proof-result active error",p.textContent=I18n.t("proof_tx_not_found"),E.disabled=!1;return}for(var d=c[0],l=JSON.parse(d.as_json),s=XmrCrypto.getKeysFromAddress(n),g=s.publicViewKey,r=s.publicSpendKey,st=XmrCrypto.bytesToScalar(XmrCrypto.hexToBytes(t)),G=XmrCrypto.Point.fromHex(g),x=G.multiply(st).multiply(8n),b=x.toBytes(),A=XmrCrypto.Point.fromHex(r),K=l.vout||[],P=l.rct_signatures&&l.rct_signatures.ecdhInfo||[],Y=0n,k=!1,m=0;mSource', + 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 · Source', + 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 }; })(); diff --git a/i18n.min.js b/i18n.min.js new file mode 100644 index 0000000..ee58699 --- /dev/null +++ b/i18n.min.js @@ -0,0 +1 @@ +var I18n=(function(){"use strict";var d={de:{name:"Deutsch",flag:"DE"},en:{name:"English",flag:"EN"}},i={de:{subtitle:"Monero-Zahlungsanforderung in Sekunden",label_addr:"XMR-Adresse",placeholder_addr:"4...",label_amount:"Betrag",label_desc:"Beschreibung (optional)",placeholder_desc:"z.B. Rechnung #42, Freelance-Arbeit...",label_timer:"Zahlungsfrist (optional)",days:"Tage",placeholder_timer_custom:"Tage",btn_generate:"Zahlungsanforderung erstellen",btn_open_wallet:"In Wallet \xF6ffnen",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 \u2014 Keine Registrierung, kein KYC",qr_hint:"Klick auf QR zum Speichern",footer:'Open Source · Kein Backend · Kein KYC · Source',aria_currency:"W\xE4hrung",label_uri_details:"Monero-URI anzeigen",label_share_link:"Teilbarer Link",btn_new_request:"Neue Zahlungsanforderung",toast_copied:"Kopiert!",countdown_expired:"Zahlungsfrist abgelaufen",countdown_remaining_days:"Zahlungsfrist: {d} Tage, {h} Std.",countdown_remaining_hours:"Zahlungsfrist: {h}:{m} Std.",rates_offline:"Kurse nicht verf\xFCgbar \u2014 nur XMR-Betrag m\xF6glich",btn_prove_payment:"Zahlung nachweisen",label_tx_hash:"Transaction ID (TX Hash)",placeholder_tx_hash:"64 Hex-Zeichen...",label_tx_key:"Transaction Key (TX Key)",placeholder_tx_key:"64 Hex-Zeichen...",btn_verify_proof:"Zahlung verifizieren",proof_verifying:"Verifiziere...",proof_verified:"Zahlung best\xE4tigt: {amount} XMR",proof_no_match:"Kein passender Output gefunden \u2014 TX Key oder Adresse stimmt nicht",proof_tx_not_found:"Transaktion nicht gefunden",proof_error:"Fehler bei der Verifizierung",status_paid:"Bezahlt"},en:{subtitle:"Monero payment request in seconds",label_addr:"XMR Address",placeholder_addr:"4...",label_amount:"Amount",label_desc:"Description (optional)",placeholder_desc:"e.g. Invoice #42, freelance work...",label_timer:"Payment deadline (optional)",days:"days",placeholder_timer_custom:"Days",btn_generate:"Create payment request",btn_open_wallet:"Open in wallet",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 \u2014 No registration, no KYC",qr_hint:"Click QR to save",footer:'Open Source · No Backend · No KYC · Source',aria_currency:"Currency",label_uri_details:"Show Monero URI",label_share_link:"Shareable link",btn_new_request:"New payment request",toast_copied:"Copied!",countdown_expired:"Payment deadline expired",countdown_remaining_days:"Deadline: {d} days, {h} hrs",countdown_remaining_hours:"Deadline: {h}:{m} hrs",rates_offline:"Rates unavailable \u2014 XMR amount only",btn_prove_payment:"Prove payment",label_tx_hash:"Transaction ID (TX Hash)",placeholder_tx_hash:"64 hex characters...",label_tx_key:"Transaction Key (TX Key)",placeholder_tx_key:"64 hex characters...",btn_verify_proof:"Verify payment",proof_verifying:"Verifying...",proof_verified:"Payment confirmed: {amount} XMR",proof_no_match:"No matching output found \u2014 TX key or address mismatch",proof_tx_not_found:"Transaction not found",proof_error:"Verification error",status_paid:"Paid"}},o="de";function u(){var e=null;try{e=localStorage.getItem("xmrpay_lang")}catch{}if(e&&i[e])return e;for(var n=navigator.languages||[navigator.language||"de"],t=0;t - + + @@ -28,7 +29,7 @@
- @@ -59,15 +60,13 @@
+
-
- -
-
+ - - -
-
@@ -126,8 +121,8 @@
- - - + + + diff --git a/style.css b/style.css index 31ab17f..9e060ce 100644 --- a/style.css +++ b/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; }