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"]
+
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`;
}