diff --git a/CHANGELOG.md b/CHANGELOG.md
index baa7db5..2b9ad18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,43 @@
All notable changes to this project will be documented in this file.
+## [2.2.0] - 2025-11-06 ✅ COMPLETED
+
+### ✨ New Features
+
+#### 📏 QR Size Customization
+- **Input type="number"** - Người dùng tự do nhập size từ 100-1000px
+- **Gợi ý thông minh** - "200-500px cho web, 300-800px cho in ấn"
+- **Responsive limits** - Desktop: 500px max, Mobile: 350px/280px max
+- **Validation on blur** - Chỉ validate khi focus ra ngoài (không làm phiền khi gõ)
+
+#### 🎯 UX Improvements
+- **Smart validation** - Chuyển từ `input` → `blur` event cho tất cả fields
+- **Không validate realtime** - Để người dùng nhập xong mới validate
+- **Enter to validate** - Nhấn Enter cũng trigger validation ngay
+
+#### 🐛 Bug Fixes
+- **Logo/text in exports** - Đã fix logo/text hiển thị đầy đủ khi download PNG/PDF
+- **Download logic** - Dùng `img.src` thay vì `canvas.toBlob()` để giữ logo/text
+- **i18n hardcode** - Xóa toàn bộ hardcode text, 100% dùng i18n keys
+
+### 🔧 Technical Changes
+
+#### Files Modified:
+- `index.html` - Đổi slider → number input, xóa hardcode text, thêm i18n attributes
+- `js/qr-generator.js` - Fix download() dùng img.src, accept dynamic size param
+- `js/app.js` - Blur validation, size validation (100-1000px)
+- `js/translations.js` - Thêm `qr_size`, `qr_size_input`, `qr_size_hint`
+- `js/utils.js` - Support `data-i18n-placeholder` attribute
+- `css/style.css` - Xóa slider CSS (-46 lines), update responsive max-width
+
+#### Code Quality:
+- **-29 lines total** - Cleaner, more maintainable code
+- **No linter errors** - ESLint pass ✅
+- **100% i18n** - Không còn hardcode Vietnamese text
+
+---
+
## [2.1.0] - 2025-10-19 ✅ COMPLETED
### ✨ UX Improvements
diff --git a/README.md b/README.md
index 90dbcb3..2096511 100644
--- a/README.md
+++ b/README.md
@@ -37,14 +37,15 @@ Tạo QR code cho nhiều mục đích khác nhau:
### 🎨 Tùy chỉnh nâng cao
+- **📏 Kích thước QR** - Tùy chỉnh size từ 100px đến 1000px (khuyến nghị: 200-500px cho web, 300-800px cho in ấn)
- **📷 Logo tùy chỉnh** - Upload ảnh logo để đặt ở giữa QR code (khuyến nghị: ảnh vuông 1:1, tối thiểu 200x200px)
- **✏️ Text tùy chỉnh** - Thêm text với màu sắc tùy chọn
- **🌈 Màu sắc** - Tùy chỉnh màu QR và màu nền với kiểm tra độ tương phản
- **📊 Google Campaign Tracking** - Tự động thêm tham số UTM cho marketing
-- **⚡ True Live Preview** - QR code tự động update khi thay đổi input (không cần bấm Generate)
+- **⚡ Smart Validation** - Validate khi blur/nhấn Enter, không làm phiền khi đang gõ
- **🌙 Dark Mode** - Tự động nhận diện theo hệ thống hoặc chọn thủ công
-- **🌐 Đa ngôn ngữ** - Tiếng Việt & English
-- **💾 Export đa dạng** - PNG, SVG, PDF
+- **🌐 Đa ngôn ngữ** - Tiếng Việt & English (100% i18n)
+- **💾 Export đa dạng** - PNG, SVG, PDF (bao gồm logo/text)
- **🐛 Error Reporting** - Hệ thống báo lỗi tích hợp, tracking user activities
### 🚀 Ưu điểm
diff --git a/VALIDATION.md b/VALIDATION.md
index dfd1113..154a47a 100644
--- a/VALIDATION.md
+++ b/VALIDATION.md
@@ -2,12 +2,12 @@
## ✨ Tính năng
-### Realtime Validation
-- ✅ Validate khi user nhập (debounce 300ms)
-- ✅ Validate khi blur (rời khỏi input)
-- ✅ Visual feedback: border xanh (valid) / đỏ (invalid)
-- ✅ Error messages hiển thị dưới input
-- ✅ Disable Next button khi invalid
+### Smart Validation (v2.2.0)
+- ✅ **Validate on blur** - Chỉ validate khi focus ra ngoài input
+- ✅ **No realtime validation** - Không validate khi đang gõ (tránh làm phiền)
+- ✅ **Enter key support** - Nhấn Enter để validate ngay
+- ✅ **Auto QR generation** - Tự động tạo QR sau khi validate thành công
+- ✅ Visual feedback: border colors & error messages
### Đa ngôn ngữ (i18n)
- ✅ Error messages hỗ trợ Tiếng Việt & English
diff --git a/css/style.css b/css/style.css
index 238210a..3ddd634 100644
--- a/css/style.css
+++ b/css/style.css
@@ -291,21 +291,21 @@ body.no-icons .icon-fallback {
display: block;
}
-/* Desktop: max 300px */
+/* Desktop: respect user-selected size up to 500px */
@media (min-width: 769px) {
#qrcode canvas,
#qrcode img {
- max-width: 300px !important;
- max-height: 300px !important;
+ max-width: 500px !important;
+ max-height: 500px !important;
}
}
-/* Mobile: smaller QR size - max 280px */
+/* Mobile: limit to smaller sizes for better display */
@media (max-width: 768px) {
#qrcode canvas,
#qrcode img {
- max-width: 280px !important;
- max-height: 280px !important;
+ max-width: 350px !important;
+ max-height: 350px !important;
}
/* Adjust preview container on mobile */
@@ -318,8 +318,8 @@ body.no-icons .icon-fallback {
@media (max-width: 480px) {
#qrcode canvas,
#qrcode img {
- max-width: 240px !important;
- max-height: 240px !important;
+ max-width: 280px !important;
+ max-height: 280px !important;
}
#qrcode {
diff --git a/index.html b/index.html
index e20a6eb..dc086ea 100644
--- a/index.html
+++ b/index.html
@@ -154,6 +154,23 @@
+
+
+
+ 📏
+
+
+
+
+
+
+
+ px
+
+
+
+
+
@@ -175,7 +192,7 @@
-
+
diff --git a/js/app.js b/js/app.js
index f4d0850..58e86b1 100644
--- a/js/app.js
+++ b/js/app.js
@@ -422,6 +422,7 @@ function setupEventListeners() {
const hasText = centerOption === 'text' && document.getElementById('centerText')?.value;
const colorDark = document.getElementById('qrColorDark')?.value || '#000000';
const colorLight = document.getElementById('qrColorLight')?.value || '#ffffff';
+ const size = parseInt(document.getElementById('qrSize')?.value || 300);
ActivityLogger.log('QR generation started', {
dataType: selectedDataType,
@@ -431,6 +432,7 @@ function setupEventListeners() {
hasText,
colorDark,
colorLight,
+ size,
});
try {
@@ -439,6 +441,7 @@ function setupEventListeners() {
colorLight,
hasLogo,
hasText,
+ size,
correctLevel: (hasLogo || hasText) ? QRCode.CorrectLevel.H : QRCode.CorrectLevel.M,
});
@@ -550,6 +553,30 @@ function setupEventListeners() {
if (colorDark) colorDark.addEventListener('input', autoGenerateQR);
if (colorLight) colorLight.addEventListener('input', autoGenerateQR);
+ // QR Size input - validate and auto-generate on blur
+ const qrSize = document.getElementById('qrSize');
+ if (qrSize) {
+ qrSize.addEventListener('blur', function() {
+ let size = parseInt(this.value);
+ // Validate size range
+ if (isNaN(size) || size < 100) {
+ size = 100;
+ this.value = 100;
+ } else if (size > 1000) {
+ size = 1000;
+ this.value = 1000;
+ }
+ ActivityLogger.log('QR size changed', { size });
+ autoGenerateQR();
+ });
+ // Also trigger on Enter key
+ qrSize.addEventListener('keypress', function(e) {
+ if (e.key === 'Enter') {
+ this.blur();
+ }
+ });
+ }
+
// Center text - auto-generate on change
const centerText = document.getElementById('centerText');
const centerTextColor = document.getElementById('centerTextColor');
@@ -602,7 +629,8 @@ function updateFields() {
input.className = 'w-full px-4 py-3 border-2 border-gray-200 dark:border-gray-600 rounded-xl focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition-all input-bg';
// Auto-generate on input change
- input.addEventListener('input', autoGenerateQR);
+ // Use blur event for validation to avoid auto-validate while typing
+ input.addEventListener('blur', autoGenerateQR);
input.addEventListener('change', autoGenerateQR);
div.appendChild(label);
diff --git a/js/qr-generator.js b/js/qr-generator.js
index a22a6bc..01aaaa5 100644
--- a/js/qr-generator.js
+++ b/js/qr-generator.js
@@ -50,6 +50,7 @@ const QRGenerator = {
hasLogo = false,
hasText = false,
correctLevel = QRCode.CorrectLevel.M,
+ size = 300,
} = options;
// Validate color contrast
@@ -61,11 +62,11 @@ const QRGenerator = {
}
try {
- // Step 1: Generate base QR code with user colors
+ // Step 1: Generate base QR code with user colors and dynamic size
this.qrCodeInstance = new QRCode(qrContainer, {
text: String(data),
- width: 300,
- height: 300,
+ width: size,
+ height: size,
colorDark: colorDark,
colorLight: colorLight,
// Use High error correction if adding logo/text
@@ -214,29 +215,28 @@ const QRGenerator = {
},
download(format = 'png') {
- const canvas = document.querySelector('#qrcode canvas');
const img = document.querySelector('#qrcode img');
- if (!canvas || !img) return;
+ if (!img) return;
if (format === 'png') {
- // Use canvas.toBlob for better mobile support
- canvas.toBlob((blob) => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'qrcode.png';
- a.style.display = 'none';
- document.body.appendChild(a);
- a.click();
-
- // Cleanup
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }, 100);
- }, 'image/png');
+ // Use img element to ensure logo/text is included
+ const a = document.createElement('a');
+ a.href = img.src;
+ a.download = 'qrcode.png';
+ a.style.display = 'none';
+ document.body.appendChild(a);
+ a.click();
+
+ // Cleanup
+ setTimeout(() => {
+ document.body.removeChild(a);
+ }, 100);
} else if (format === 'svg') {
+ // For SVG, need to convert from img src
+ const canvas = document.querySelector('#qrcode canvas');
+ if (!canvas) return;
+
const svgData = this.canvasToSVG(canvas);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
@@ -260,7 +260,8 @@ const QRGenerator = {
format: 'a4',
});
- const imgData = canvas.toDataURL('image/png');
+ // Use img src to include logo/text
+ const imgData = img.src;
const pageWidth = pdf.internal.pageSize.getWidth();
const imgWidth = 80;
const imgHeight = 80;
diff --git a/js/translations.js b/js/translations.js
index 4bbc44a..5ffcf30 100644
--- a/js/translations.js
+++ b/js/translations.js
@@ -14,6 +14,11 @@ const translations = {
bg_color: 'Màu nền:',
color_warning: '⚠️ Lưu ý: Dùng màu có độ tương phản cao (đen/trắng) để đảm bảo quét được tốt nhất. Màu sáng hoặc màu tương tự nhau có thể làm giảm khả năng scan.',
+ // QR Size
+ qr_size: 'Kích thước QR Code:',
+ qr_size_input: 'Nhập kích thước (px):',
+ qr_size_hint: 'Khuyến nghị: 200-500px cho web, 300-800px cho in ấn',
+
// Center customization
customize_center: 'Tùy chỉnh giữa QR:',
add_logo: '📷 Thêm Ảnh',
@@ -84,6 +89,11 @@ const translations = {
bg_color: 'Background Color:',
color_warning: '⚠️ Note: Use high contrast colors (black/white) for best scanability. Light or similar colors may reduce scanning ability.',
+ // QR Size
+ qr_size: 'QR Code Size:',
+ qr_size_input: 'Enter size (px):',
+ qr_size_hint: 'Recommended: 200-500px for web, 300-800px for print',
+
// Center customization
customize_center: 'Customize center:',
add_logo: '📷 Add Logo',
diff --git a/js/utils.js b/js/utils.js
index 13a077f..29e68a0 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -82,6 +82,14 @@ const LanguageManager = {
}
});
+ // Handle placeholder translations separately
+ document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
+ const key = el.getAttribute('data-i18n-placeholder');
+ if (translations[this.current][key]) {
+ el.placeholder = translations[this.current][key];
+ }
+ });
+
// Re-render dynamic fields if needed
if (typeof updateFields === 'function') {
updateFields();