From 373ca0cf9f6fe34352716fa98932f06fd12d029b Mon Sep 17 00:00:00 2001 From: Michal Vrba Date: Wed, 7 Jan 2026 14:48:45 +0100 Subject: [PATCH] Add "Copy QR Code" button with clipboard support Implemented a copy-to-clipboard button for QR codes, including Blazor logic for feedback and disabled states. Injected IJSRuntime for JS interop and added supporting JS functions for image copying and fallback download. Reduced QR code image size for better compatibility. Added responsive and accessible CSS for the new button. Cleaned up timers and feedback state on component disposal. --- QRPay/Pages/Home.razor | 77 ++++++++++++++++++- QRPay/Pages/Home.razor.css | 102 ++++++++++++++++++++++++- QRPay/wwwroot/index.html | 148 +++++++++++++++++++++++++++++++++++++ 3 files changed, 320 insertions(+), 7 deletions(-) diff --git a/QRPay/Pages/Home.razor b/QRPay/Pages/Home.razor index 55a60a2..81cee84 100644 --- a/QRPay/Pages/Home.razor +++ b/QRPay/Pages/Home.razor @@ -2,6 +2,7 @@ @using QRCoder @using SixLabors.ImageSharp.Formats @using System.ComponentModel.DataAnnotations +@inject IJSRuntime JSRuntime QRPay - QR Payment Generator
@@ -72,7 +73,24 @@
- Payment QR Code + Payment QR Code + + +
@@ -105,6 +123,9 @@ private string QRCodeStr = string.Empty; private bool _showQRCode = false; private bool _isGenerating = false; + private bool _isCopying = false; + private bool _copySuccess = false; + private System.Threading.Timer? _copySuccessTimer; public class PaymentModel { @@ -154,10 +175,11 @@ var payBySquare = new PayBySquareTextGenerator.PayBySquareOverkill(payment); var stringData = payBySquare.GeneratePayBySquareOverkillString(); - // Generate QR code data + // Generate QR code data - smaller size for better clipboard handling using var qrCodeData = QRCodeGenerator.GenerateQrCode(stringData, QRCodeGenerator.ECCLevel.Q); using var qrCode = new PngByteQRCode(qrCodeData); - byte[] qrCodeImage = qrCode.GetGraphic(20); + // Reduced from 20 to 10 for smaller clipboard image + byte[] qrCodeImage = qrCode.GetGraphic(10); QRCodeStr = $"data:image/png;base64,{Convert.ToBase64String(qrCodeImage)}"; } @@ -172,11 +194,60 @@ } } + public async Task CopyQRCodeToClipboard() + { + try + { + _isCopying = true; + _copySuccess = false; + StateHasChanged(); + + // Use JavaScript to copy the QR code image to clipboard with smaller size + var success = await JSRuntime.InvokeAsync("copyImageToClipboardSmall", "qrcode-image"); + + _isCopying = false; + + if (success) + { + _copySuccess = true; + StateHasChanged(); + + // Reset the success state after 3 seconds + _copySuccessTimer?.Dispose(); + _copySuccessTimer = new System.Threading.Timer(_ => + { + InvokeAsync(() => + { + _copySuccess = false; + StateHasChanged(); + }); + }, null, TimeSpan.FromSeconds(3), Timeout.InfiniteTimeSpan); + } + else + { + StateHasChanged(); + // Could show an error message here if needed + } + } + catch (Exception) + { + _isCopying = false; + StateHasChanged(); + } + } + public void GoBack() { _showQRCode = false; QRCodeStr = string.Empty; paymentModel = new PaymentModel(); // Reset the form + _copySuccess = false; + _copySuccessTimer?.Dispose(); StateHasChanged(); } + + public void Dispose() + { + _copySuccessTimer?.Dispose(); + } } \ No newline at end of file diff --git a/QRPay/Pages/Home.razor.css b/QRPay/Pages/Home.razor.css index 9854742..3681990 100644 --- a/QRPay/Pages/Home.razor.css +++ b/QRPay/Pages/Home.razor.css @@ -174,6 +174,63 @@ background: rgba(74, 124, 89, 0.2); } +/* Copy Button Styles */ +.btn-copy { + background: rgba(16, 185, 129, 0.1); + color: #065f46; + border: 2px solid #10b981; + padding: 12px 20px; + border-radius: 10px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + margin-top: 16px; + min-height: 48px; + width: auto; + max-width: 200px; + margin-left: auto; + margin-right: auto; +} + +.btn-copy:hover:not(:disabled) { + background: rgba(16, 185, 129, 0.15); + border-color: #059669; + color: #047857; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2); +} + +.btn-copy:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(16, 185, 129, 0.15); +} + +.btn-copy:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.btn-copy.copying { + background: rgba(16, 185, 129, 0.05); + border-color: #6ee7b7; + color: #059669; +} + +.copy-spinner { + width: 16px; + height: 16px; + border: 2px solid rgba(6, 95, 70, 0.3); + border-radius: 50%; + border-top-color: #065f46; + animation: spin 1s ease-in-out infinite; +} + .spinner { width: 20px; height: 20px; @@ -230,6 +287,8 @@ max-width: 100%; height: auto; aspect-ratio: 1; + display: block; + margin: 0 auto 16px auto; } /* Payment Details */ @@ -323,6 +382,12 @@ .form-control { font-size: 16px; /* Prevents zoom on iOS */ } + + .btn-copy { + font-size: 0.9rem; + padding: 10px 16px; + max-width: 180px; + } } /* Portrait tablets and large phones */ @@ -376,6 +441,13 @@ height: 240px; } + .btn-copy { + font-size: 0.85rem; + padding: 10px 14px; + max-width: 160px; + margin-top: 12px; + } + .payment-details { padding: 20px; margin-bottom: 24px; @@ -471,6 +543,13 @@ height: 200px; } + .btn-copy { + font-size: 0.8rem; + padding: 8px 12px; + max-width: 140px; + margin-top: 10px; + } + .payment-details { padding: 16px; margin-bottom: 20px; @@ -518,6 +597,12 @@ height: 180px; } + .btn-copy { + font-size: 0.75rem; + padding: 8px 10px; + max-width: 120px; + } + .form-control { padding: 12px 14px; } @@ -557,6 +642,12 @@ height: 160px; } + .btn-copy { + font-size: 0.8rem; + padding: 6px 10px; + max-width: 110px; + } + .payment-details { margin-bottom: 20px; } @@ -565,7 +656,8 @@ /* Touch improvements for mobile */ @media (max-width: 768px) { .btn-generate:hover, - .btn-back:hover { + .btn-back:hover, + .btn-copy:hover { transform: none; /* Disable hover transforms on mobile */ } @@ -575,8 +667,9 @@ /* Larger touch targets */ .btn-generate, - .btn-back { - min-height: 48px; /* WCAG recommended minimum touch target */ + .btn-back, + .btn-copy { + min-height: 44px; /* WCAG recommended minimum touch target */ } } @@ -591,7 +684,8 @@ animation: none; } - .spinner { + .spinner, + .copy-spinner { animation: none; } diff --git a/QRPay/wwwroot/index.html b/QRPay/wwwroot/index.html index bff5d8b..1ce9077 100644 --- a/QRPay/wwwroot/index.html +++ b/QRPay/wwwroot/index.html @@ -54,6 +54,154 @@ Reload 🗙
+ + + +