feat: ajustes de focos dos campos.
Some checks failed
Deploy QR Rapido / test (push) Successful in 34s
Deploy QR Rapido / build-and-push (push) Failing after 10s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped

This commit is contained in:
Ricardo Carneiro 2025-07-31 21:43:27 -03:00
parent 2a623d1fd5
commit 9634176e18
3 changed files with 471 additions and 16 deletions

View File

@ -108,10 +108,11 @@
<i class="fas fa-edit"></i> @Localizer["Content"]
</label>
<textarea id="qr-content"
class="form-control form-control-lg"
class="form-control form-control-lg required-field"
rows="3"
placeholder="@Localizer["EnterQRCodeContent"]"
required></textarea>
<div class="invalid-feedback">Conteúdo deve ter pelo menos 3 caracteres</div>
<div class="form-text">
<span id="content-hints">@Localizer["ContentHints"]</span>
</div>
@ -134,7 +135,7 @@
<div class="row">
<div class="col-md-12 mb-3">
<label class="form-label fw-semibold">Nome Completo *</label>
<input type="text" id="vcard-name" class="form-control" placeholder="Ricardo Gonçalves" required>
<input type="text" id="vcard-name" class="form-control required-field" placeholder="Seu Nome Completo" required>
<div class="invalid-feedback">Nome é obrigatório</div>
</div>
</div>
@ -142,14 +143,14 @@
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Telefone Celular *</label>
<input type="tel" id="vcard-mobile" class="form-control" placeholder="11961534225" required>
<input type="tel" id="vcard-mobile" class="form-control required-field" placeholder="11999998888" required>
<small class="form-text text-muted">Apenas números (DDD + número)</small>
<div class="invalid-feedback">Telefone celular é obrigatório</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Email *</label>
<input type="email" id="vcard-email" class="form-control" placeholder="seu@email.com" required>
<input type="email" id="vcard-email" class="form-control required-field" placeholder="seu.email@exemplo.com" required>
<div class="invalid-feedback">Email válido é obrigatório</div>
</div>
</div>
@ -250,6 +251,7 @@
<div class="card-body">
<pre id="vcard-preview-text" class="mb-0 small text-muted">BEGIN:VCARD
VERSION:3.0
CHARSET=UTF-8
Preencha os campos acima para ver o preview...
END:VCARD</pre>
</div>

View File

@ -1048,3 +1048,78 @@ html[data-theme="light"] {
color: #0056b3 !important;
}
}
/* ============================================ */
/* SISTEMA DE FLUXO ASCENDENTE PROGRESSIVO */
/* ============================================ */
/* Classe para destacar campo inicial */
.qr-field-highlight {
border: 2px solid #3B82F6 !important;
box-shadow: 0 0 5px rgba(59, 130, 246, 0.3) !important;
transition: all 0.3s ease !important;
}
/* Feedback visual para campos obrigatórios */
.required-field:invalid,
.form-control:invalid {
border-color: #dc3545 !important;
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25) !important;
}
.required-field:valid,
.form-control:valid {
border-color: #28a745 !important;
box-shadow: 0 0 0 0.25rem rgba(40, 167, 69, 0.25) !important;
}
/* Estados de campos desabilitados */
.disabled-section {
opacity: 0.6;
pointer-events: none;
transition: all 0.3s ease;
}
/* Transições suaves para seções */
.qr-section {
transition: all 0.3s ease;
}
/* Tooltip para campos bloqueados */
.field-blocked-hint {
color: #f59e0b;
font-size: 0.875rem;
margin-top: 0.25rem;
display: flex;
align-items: center;
gap: 0.25rem;
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
.field-blocked-hint.show {
opacity: 1;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
/* Tema escuro - ajustes para sistema de fluxo */
html[data-theme="dark"] .qr-field-highlight {
border-color: #60A5FA !important;
box-shadow: 0 0 5px rgba(96, 165, 250, 0.3) !important;
}
html[data-theme="dark"] .required-field:invalid,
html[data-theme="dark"] .form-control:invalid {
border-color: #ef4444 !important;
box-shadow: 0 0 0 0.25rem rgba(239, 68, 68, 0.25) !important;
}
html[data-theme="dark"] .required-field:valid,
html[data-theme="dark"] .form-control:valid {
border-color: #22c55e !important;
box-shadow: 0 0 0 0.25rem rgba(34, 197, 94, 0.25) !important;
}

View File

@ -41,10 +41,21 @@ class QRRapidoGenerator {
};
this.currentLang = localStorage.getItem('qrrapido-lang') || 'pt-BR';
this.selectedType = null;
this.selectedStyle = 'classic'; // Estilo padrão
this.contentValid = false;
this.initializeEvents();
this.checkAdFreeStatus();
this.updateLanguage();
this.updateStatsCounters();
this.initializeProgressiveFlow();
// Validar segurança dos dados após carregamento
setTimeout(() => {
this.validateDataSecurity();
this.testUTF8Encoding();
}, 1000);
}
initializeEvents() {
@ -71,12 +82,41 @@ class QRRapidoGenerator {
cornerStyle.addEventListener('change', this.handleCornerStyleChange.bind(this));
}
// QR type change with hints
// QR type change with hints and flow
const qrType = document.getElementById('qr-type');
if (qrType) {
qrType.addEventListener('change', this.updateContentHints.bind(this));
qrType.addEventListener('change', (e) => {
this.handleTypeSelection(e.target.value);
this.updateContentHints();
});
}
// Content validation
const qrContent = document.getElementById('qr-content');
if (qrContent) {
qrContent.addEventListener('input', (e) => {
this.handleContentChange(e.target.value);
});
}
// Style selection
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
radio.addEventListener('change', (e) => {
this.handleStyleSelection(e.target.value);
});
});
// VCard fields validation
const vcardFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
vcardFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.addEventListener('input', () => {
this.updateGenerateButton();
});
}
});
// Language selector
document.querySelectorAll('[data-lang]').forEach(link => {
link.addEventListener('click', this.changeLanguage.bind(this));
@ -418,10 +458,11 @@ class QRRapidoGenerator {
if (type === 'vcard') {
if (window.vcardGenerator) {
const vcardContent = window.vcardGenerator.getVCardContent();
const encodedContent = this.prepareContentForQR(vcardContent, 'vcard');
return {
data: {
type: 'vcard', // Keep as vcard type for tracking
content: vcardContent,
content: encodedContent,
quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
@ -464,9 +505,12 @@ class QRRapidoGenerator {
console.log(' Final Primary Color:', finalPrimaryColor);
console.log(' Final Background Color:', finalBackgroundColor);
// Add basic form fields
// Add basic form fields with UTF-8 encoding
const rawContent = document.getElementById('qr-content').value;
const encodedContent = this.prepareContentForQR(rawContent, type);
formData.append('type', document.getElementById('qr-type').value);
formData.append('content', document.getElementById('qr-content').value);
formData.append('content', encodedContent);
formData.append('quickStyle', quickStyle);
formData.append('primaryColor', finalPrimaryColor);
formData.append('backgroundColor', finalBackgroundColor);
@ -500,10 +544,13 @@ class QRRapidoGenerator {
console.log(' Final Primary Color:', finalPrimaryColor);
console.log(' Final Background Color:', finalBackgroundColor);
const rawContent = document.getElementById('qr-content').value;
const encodedContent = this.prepareContentForQR(rawContent, type);
return {
data: {
type: document.getElementById('qr-type').value,
content: document.getElementById('qr-content').value,
content: encodedContent,
quickStyle: quickStyle,
primaryColor: finalPrimaryColor,
backgroundColor: finalBackgroundColor,
@ -1105,6 +1152,289 @@ class QRRapidoGenerator {
typeField.addEventListener('change', updatePreview);
}
}
// ============================================
// SISTEMA DE FLUXO ASCENDENTE PROGRESSIVO
// ============================================
initializeProgressiveFlow() {
// Estado inicial: apenas tipo habilitado, botão gerar desabilitado
const qrContent = document.getElementById('qr-content');
const vcardInterface = document.getElementById('vcard-interface');
// Inicialmente desabilitar campo de conteúdo
if (qrContent) {
qrContent.disabled = true;
}
// Ocultar interface vCard
if (vcardInterface) {
vcardInterface.style.display = 'none';
}
this.updateGenerateButton();
}
handleTypeSelection(type) {
this.selectedType = type;
if (type) {
this.removeInitialHighlight();
// Sempre habilitar campos de conteúdo após selecionar tipo
this.enableContentFields(type);
} else {
this.disableAllFields();
}
this.updateGenerateButton();
}
handleStyleSelection(style) {
this.selectedStyle = style;
this.updateGenerateButton();
}
handleContentChange(content) {
const contentField = document.getElementById('qr-content');
this.contentValid = this.validateContent(content);
// Feedback visual para campo de conteúdo
if (contentField) {
this.validateField(contentField, this.contentValid, 'Conteúdo deve ter pelo menos 3 caracteres');
}
this.updateGenerateButton();
}
enableContentFields(type) {
const qrContent = document.getElementById('qr-content');
const vcardInterface = document.getElementById('vcard-interface');
if (type === 'vcard') {
// Para vCard, ocultar textarea e mostrar interface específica
if (qrContent) {
qrContent.style.display = 'none';
qrContent.removeAttribute('required');
}
if (vcardInterface) {
vcardInterface.style.display = 'block';
this.enableVCardFields();
}
} else {
// Para outros tipos, mostrar textarea
if (qrContent) {
qrContent.style.display = 'block';
qrContent.setAttribute('required', 'required');
qrContent.disabled = false;
}
if (vcardInterface) {
vcardInterface.style.display = 'none';
}
}
}
enableVCardFields() {
const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
requiredFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.disabled = false;
field.setAttribute('required', 'required');
}
});
}
disableAllFields() {
// Resetar estado
const qrContent = document.getElementById('qr-content');
const vcardInterface = document.getElementById('vcard-interface');
if (qrContent) {
qrContent.disabled = true;
qrContent.value = '';
}
if (vcardInterface) {
vcardInterface.style.display = 'none';
// Limpar campos vCard
const vcardFields = document.querySelectorAll('#vcard-interface input');
vcardFields.forEach(field => {
field.value = '';
field.disabled = true;
});
}
this.contentValid = false;
}
validateContent(content) {
if (!content) return false;
return content.trim().length >= 3;
}
validateVCardFields() {
const nameField = document.getElementById('vcard-name');
const mobileField = document.getElementById('vcard-mobile');
const emailField = document.getElementById('vcard-email');
const name = nameField?.value.trim() || '';
const mobile = mobileField?.value.trim() || '';
const email = emailField?.value.trim() || '';
// Validação individual dos campos com feedback visual
this.validateField(nameField, name !== '', 'Nome é obrigatório');
this.validateField(mobileField, mobile !== '' && mobile.length >= 10, 'Telefone deve ter pelo menos 10 dígitos');
this.validateField(emailField, email !== '' && email.includes('@'), 'Email válido é obrigatório');
const isValid = name !== '' && mobile !== '' && email !== '' && email.includes('@') && mobile.length >= 10;
this.contentValid = isValid;
return isValid;
}
validateField(field, isValid, errorMessage) {
if (!field) return;
const feedbackElement = field.parentNode.querySelector('.invalid-feedback');
if (isValid) {
field.classList.remove('is-invalid');
field.classList.add('is-valid');
if (feedbackElement) {
feedbackElement.style.display = 'none';
}
} else if (field.value.trim() !== '') {
// Só mostrar erro se o campo tem conteúdo
field.classList.remove('is-valid');
field.classList.add('is-invalid');
if (feedbackElement) {
feedbackElement.textContent = errorMessage;
feedbackElement.style.display = 'block';
}
} else {
// Campo vazio, remover validações visuais
field.classList.remove('is-valid', 'is-invalid');
if (feedbackElement) {
feedbackElement.style.display = 'none';
}
}
}
updateGenerateButton() {
const generateBtn = document.getElementById('generate-btn');
if (!generateBtn) return;
let shouldEnable = false;
// Verificar se tem tipo selecionado
if (this.selectedType) {
if (this.selectedType === 'vcard') {
// Para vCard, validar campos obrigatórios
shouldEnable = this.validateVCardFields();
} else {
// Para outros tipos, validar conteúdo
const content = document.getElementById('qr-content')?.value || '';
shouldEnable = this.validateContent(content);
}
}
generateBtn.disabled = !shouldEnable;
// Feedback visual
if (shouldEnable) {
generateBtn.classList.remove('btn-secondary');
generateBtn.classList.add('btn-primary');
} else {
generateBtn.classList.remove('btn-primary');
generateBtn.classList.add('btn-secondary');
}
}
// Remove destaque inicial quando tipo for selecionado
removeInitialHighlight() {
const typeField = document.getElementById('qr-type');
if (typeField && typeField.classList.contains('qr-field-highlight')) {
typeField.classList.remove('qr-field-highlight');
}
}
// ============================================
// FUNÇÕES DE CODIFICAÇÃO UTF-8
// ============================================
// Garantir codificação UTF-8 para todos os tipos de QR Code
prepareContentForQR(content, type) {
if (!content) return '';
// Garantir que o conteúdo seja tratado como UTF-8
try {
// Verificar se há caracteres especiais
const hasSpecialChars = /[^\x00-\x7F]/.test(content);
if (hasSpecialChars) {
console.log('Caracteres especiais detectados, aplicando codificação UTF-8');
// Forçar codificação UTF-8
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const encoded = encoder.encode(content);
return decoder.decode(encoded);
}
return content;
} catch (error) {
console.warn('Erro na codificação UTF-8:', error);
return content;
}
}
// Validação de segurança para dados pessoais
validateDataSecurity() {
const placeholders = document.querySelectorAll('[placeholder]');
placeholders.forEach(el => {
const placeholder = el.placeholder;
// Verificar dados pessoais em placeholders
if (placeholder.includes('@') && placeholder.includes('gmail')) {
console.warn('Dado pessoal detectado em placeholder:', placeholder);
el.placeholder = 'seu.email@exemplo.com';
}
// Verificar números de telefone reais
if (placeholder.match(/\d{11}/) && !placeholder.includes('99999')) {
console.warn('Possível telefone real em placeholder:', placeholder);
el.placeholder = el.placeholder.replace(/\d{11}/, '11999998888');
}
});
}
// Teste da codificação UTF-8
testUTF8Encoding() {
const testStrings = [
'Ação rápida çáéíóú',
'João Gonçalves',
'Coração brasileiro',
'Situação especial'
];
console.group('🧪 Teste de Codificação UTF-8');
testStrings.forEach(str => {
const encoded = this.prepareContentForQR(str, 'text');
console.log(`Original: "${str}"`);
console.log(`Codificado: "${encoded}"`);
console.log('---');
});
// Teste específico para vCard
if (window.vcardGenerator) {
const testName = 'João Gonçalves';
const encoded = window.vcardGenerator.encodeQuotedPrintable(testName);
console.log(`vCard Quoted-Printable test:`);
console.log(`Original: "${testName}"`);
console.log(`Encoded: "${encoded}"`);
}
console.groupEnd();
}
}
// Initialize when DOM loads
@ -1124,6 +1454,36 @@ class VCardGenerator {
this.initializeVCardInterface();
}
// Função para codificar caracteres especiais usando Quoted-Printable
encodeQuotedPrintable(text) {
if (!text) return '';
return text.replace(/[^\x20-\x7E]/g, (char) => {
// Para caracteres especiais comuns do português, usar codificação UTF-8
const utf8Bytes = new TextEncoder().encode(char);
return Array.from(utf8Bytes)
.map(byte => `=${byte.toString(16).toUpperCase().padStart(2, '0')}`)
.join('');
});
}
// Função para preparar texto com codificação adequada
prepareTextForVCard(text, needsEncoding = true) {
if (!text) return '';
const trimmedText = text.trim();
if (!needsEncoding) return trimmedText;
// Verifica se há caracteres especiais que precisam de codificação
const hasSpecialChars = /[^\x20-\x7E]/.test(trimmedText);
if (hasSpecialChars) {
return `CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:${this.encodeQuotedPrintable(trimmedText)}`;
}
return trimmedText;
}
initializeVCardInterface() {
// Show/hide optional fields based on checkboxes
document.querySelectorAll('#vcard-interface .form-check-input').forEach(checkbox => {
@ -1160,26 +1520,44 @@ class VCardGenerator {
generateVCardContent() {
const data = this.collectVCardData();
let vcard = 'BEGIN:VCARD\nVERSION:3.0\n';
let vcard = 'BEGIN:VCARD\nVERSION:3.0\nCHARSET=UTF-8\n';
// Nome (obrigatório)
if (data.name) {
const nameParts = data.name.trim().split(' ');
const firstName = nameParts[0] || '';
const lastName = nameParts.slice(1).join(' ') || '';
vcard += `N:${lastName};${firstName}\n`;
vcard += `FN:${data.name}\n`;
// Codificar nome se necessário
const encodedLastName = this.prepareTextForVCard(lastName);
const encodedFirstName = this.prepareTextForVCard(firstName);
const encodedFullName = this.prepareTextForVCard(data.name.trim());
vcard += `N:${encodedLastName};${encodedFirstName}\n`;
vcard += `FN:${encodedFullName}\n`;
}
// Empresa
if (data.company) vcard += `ORG:${data.company}\n`;
if (data.company) {
const encodedCompany = this.prepareTextForVCard(data.company);
vcard += `ORG:${encodedCompany}\n`;
}
// Título
if (data.title) vcard += `TITLE:${data.title}\n`;
if (data.title) {
const encodedTitle = this.prepareTextForVCard(data.title);
vcard += `TITLE:${encodedTitle}\n`;
}
// Endereço
if (data.address || data.city || data.state || data.zip) {
const addr = `;;${data.address || ''};${data.city || ''};${data.state || ''};${data.zip || ''};Brasil`;
const encodedAddress = this.prepareTextForVCard(data.address || '');
const encodedCity = this.prepareTextForVCard(data.city || '');
const encodedState = this.prepareTextForVCard(data.state || '');
const encodedZip = this.prepareTextForVCard(data.zip || '', false); // ZIP não precisa de codificação especial
const encodedCountry = this.prepareTextForVCard('Brasil');
const addr = `;;${encodedAddress};${encodedCity};${encodedState};${encodedZip};${encodedCountry}`;
vcard += `ADR:${addr}\n`;
}