From 9634176e1853c3b241e8d6fbdda8cc5dfe9a68eb Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Thu, 31 Jul 2025 21:43:27 -0300 Subject: [PATCH] feat: ajustes de focos dos campos. --- Views/Home/Index.cshtml | 10 +- wwwroot/css/qrrapido-theme.css | 75 ++++++ wwwroot/js/qr-speed-generator.js | 402 ++++++++++++++++++++++++++++++- 3 files changed, 471 insertions(+), 16 deletions(-) diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 36ee0d3..0406913 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -108,10 +108,11 @@ @Localizer["Content"] +
Conteúdo deve ter pelo menos 3 caracteres
@Localizer["ContentHints"]
@@ -134,7 +135,7 @@
- +
Nome é obrigatório
@@ -142,14 +143,14 @@
- + Apenas números (DDD + número)
Telefone celular é obrigatório
- +
Email válido é obrigatório
@@ -250,6 +251,7 @@
BEGIN:VCARD
 VERSION:3.0
+CHARSET=UTF-8
 Preencha os campos acima para ver o preview...
 END:VCARD
diff --git a/wwwroot/css/qrrapido-theme.css b/wwwroot/css/qrrapido-theme.css index f8af186..91cdff6 100644 --- a/wwwroot/css/qrrapido-theme.css +++ b/wwwroot/css/qrrapido-theme.css @@ -1047,4 +1047,79 @@ html[data-theme="light"] { footer a:hover { 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; } \ No newline at end of file diff --git a/wwwroot/js/qr-speed-generator.js b/wwwroot/js/qr-speed-generator.js index ed32cef..71fc512 100644 --- a/wwwroot/js/qr-speed-generator.js +++ b/wwwroot/js/qr-speed-generator.js @@ -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 @@ -1123,6 +1453,36 @@ class VCardGenerator { constructor() { 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 @@ -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`; }