Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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 {
Expand Down
19 changes: 18 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,23 @@ <h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-6" data-i18n="
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2" data-i18n="color_warning"></p>
</div>

<!-- QR Size -->
<div class="card-bg rounded-xl p-6 border border-gray-200">
<p class="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center gap-2">
<span>📏</span>
<span data-i18n="qr_size"></span>
</p>

<div class="space-y-3">
<label class="text-xs text-gray-600 dark:text-gray-400 font-medium mb-2 block" data-i18n="qr_size_input"></label>
<div class="flex items-center gap-3">
<input type="number" id="qrSize" min="100" max="1000" value="300" class="flex-1 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">
<span class="text-sm text-gray-500 dark:text-gray-400">px</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400" data-i18n="qr_size_hint"></p>
</div>
</div>

<!-- Center Customization -->
<div class="card-bg rounded-xl p-6 border border-gray-200">
<p class="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-4" data-i18n="customize_center"></p>
Expand All @@ -175,7 +192,7 @@ <h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-6" data-i18n="
<span class="text-sm font-medium text-gray-700 dark:text-gray-300" data-i18n="add_text"></span>
</div>
</label>
<input type="text" id="centerText" placeholder="Nhập gì đó..." class="w-full px-4 py-3 border-2 input-bg rounded-xl transition-all mb-2" disabled>
<input type="text" id="centerText" data-i18n-placeholder="enter_text" class="w-full px-4 py-3 border-2 input-bg rounded-xl transition-all mb-2" disabled>
<div class="flex items-center gap-3">
<label class="text-sm text-gray-600 dark:text-gray-400 font-medium" data-i18n="text_color"></label>
<input type="color" id="centerTextColor" value="#000000" class="h-10 w-24 border-2 border-gray-200 dark:border-gray-600 rounded-lg cursor-pointer" disabled>
Expand Down
30 changes: 29 additions & 1 deletion js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -431,6 +432,7 @@ function setupEventListeners() {
hasText,
colorDark,
colorLight,
size,
});

try {
Expand All @@ -439,6 +441,7 @@ function setupEventListeners() {
colorLight,
hasLogo,
hasText,
size,
correctLevel: (hasLogo || hasText) ? QRCode.CorrectLevel.H : QRCode.CorrectLevel.M,
});

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
45 changes: 23 additions & 22 deletions js/qr-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const QRGenerator = {
hasLogo = false,
hasText = false,
correctLevel = QRCode.CorrectLevel.M,
size = 300,
} = options;

// Validate color contrast
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions js/translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
8 changes: 8 additions & 0 deletions js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading