// QR Rapido Speed Generator class QRRapidoGenerator { constructor() { this.startTime = 0; this.currentQR = null; this.timerInterval = null; this.languageStrings = { 'pt-BR': { tagline: 'Gere QR codes em segundos!', generating: 'Gerando...', generated: 'Gerado em', seconds: 's', ultraFast: 'Geração ultra rápida!', fast: 'Geração rápida!', normal: 'Geração normal', error: 'Erro na geração. Tente novamente.', success: 'QR Code salvo no histórico!' }, 'es': { tagline: '¡Genera códigos QR en segundos!', generating: 'Generando...', generated: 'Generado en', seconds: 's', ultraFast: '¡Generación ultra rápida!', fast: '¡Generación rápida!', normal: 'Generación normal', error: 'Error en la generación. Inténtalo de nuevo.', success: '¡Código QR guardado en el historial!' }, 'en': { tagline: 'Generate QR codes in seconds!', generating: 'Generating...', generated: 'Generated in', seconds: 's', ultraFast: 'Ultra fast generation!', fast: 'Fast generation!', normal: 'Normal generation', error: 'Generation error. Please try again.', success: 'QR Code saved to history!' } }; 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() { // Form submission with timer const form = document.getElementById('qr-speed-form'); if (form) { form.addEventListener('submit', this.generateQRWithTimer.bind(this)); } // Quick style selection document.querySelectorAll('input[name="quick-style"]').forEach(radio => { radio.addEventListener('change', this.applyQuickStyle.bind(this)); }); // Logo upload feedback const logoUpload = document.getElementById('logo-upload'); if (logoUpload) { logoUpload.addEventListener('change', this.handleLogoSelection.bind(this)); } // Corner style validation for non-premium users const cornerStyle = document.getElementById('corner-style'); if (cornerStyle) { cornerStyle.addEventListener('change', this.handleCornerStyleChange.bind(this)); } // QR type change with hints and flow const qrType = document.getElementById('qr-type'); if (qrType) { 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); }); }); // Add listeners to all relevant fields to update button state const fieldsToWatch = [ 'qr-content', 'vcard-name', 'vcard-mobile', 'vcard-email', 'wifi-ssid', 'wifi-password', 'sms-number', 'sms-message', 'email-to', 'email-subject', 'email-body' ]; fieldsToWatch.forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('input', () => this.updateGenerateButton()); } }); document.querySelectorAll('input[name="wifi-security"]').forEach(radio => { radio.addEventListener('change', () => this.updateGenerateButton()); }); // Language selector document.querySelectorAll('[data-lang]').forEach(link => { link.addEventListener('click', this.changeLanguage.bind(this)); }); // Real-time preview for premium users if (this.isPremiumUser()) { this.setupRealTimePreview(); } // Download buttons this.setupDownloadButtons(); // Share functionality this.setupShareButtons(); // Save to history const saveBtn = document.getElementById('save-to-history'); if (saveBtn) { saveBtn.addEventListener('click', this.saveToHistory.bind(this)); } } setupDownloadButtons() { const pngBtn = document.getElementById('download-png'); const svgBtn = document.getElementById('download-svg'); const pdfBtn = document.getElementById('download-pdf'); if (pngBtn) pngBtn.addEventListener('click', () => this.downloadQR('png')); if (svgBtn) svgBtn.addEventListener('click', () => this.downloadQR('svg')); if (pdfBtn) pdfBtn.addEventListener('click', () => this.downloadQR('pdf')); } setupShareButtons() { // Check if Web Share API is supported and show/hide native share option if (navigator.share && this.isMobileDevice()) { const nativeShareOption = document.getElementById('native-share-option'); if (nativeShareOption) { nativeShareOption.classList.remove('d-none'); } } // Show save to gallery option on mobile if (this.isMobileDevice()) { const saveGalleryOption = document.getElementById('save-gallery-option'); if (saveGalleryOption) { saveGalleryOption.classList.remove('d-none'); } } // Add event listeners to share buttons const shareButtons = { 'native-share': () => this.shareNative(), 'share-whatsapp': () => this.shareWhatsApp(), 'share-telegram': () => this.shareTelegram(), 'share-email': () => this.shareEmail(), 'copy-qr-link': () => this.copyToClipboard(), 'save-to-gallery': () => this.saveToGallery() }; Object.entries(shareButtons).forEach(([id, handler]) => { const button = document.getElementById(id); if (button) { button.addEventListener('click', (e) => { e.preventDefault(); handler(); }); } }); } isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform)); } async shareNative() { if (!this.currentQR || !navigator.share) return; try { // Create a blob from the base64 image const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`); const blob = await base64Response.blob(); const file = new File([blob], 'qrcode.png', { type: 'image/png' }); const shareData = { title: 'QR Code - QR Rapido', text: 'QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!', url: window.location.origin, files: [file] }; // Check if files can be shared if (navigator.canShare && navigator.canShare(shareData)) { await navigator.share(shareData); } else { // Fallback without files await navigator.share({ title: shareData.title, text: shareData.text, url: shareData.url }); } this.trackShareEvent('native'); } catch (error) { console.error('Error sharing:', error); if (error.name !== 'AbortError') { this.showError('Erro ao compartilhar. Tente outro método.'); } } } shareWhatsApp() { if (!this.currentQR) return; const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil! ' + window.location.origin); const url = `https://wa.me/?text=${text}`; if (this.isMobileDevice()) { window.open(url, '_blank'); } else { window.open(`https://web.whatsapp.com/send?text=${text}`, '_blank'); } this.trackShareEvent('whatsapp'); } shareTelegram() { if (!this.currentQR) return; const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!'); const url = encodeURIComponent(window.location.origin); const telegramUrl = `https://t.me/share/url?url=${url}&text=${text}`; window.open(telegramUrl, '_blank'); this.trackShareEvent('telegram'); } shareEmail() { if (!this.currentQR) return; const subject = encodeURIComponent('QR Code - QR Rapido'); const body = encodeURIComponent(`Olá!\n\nCompartilho com você este QR Code gerado no QR Rapido, o gerador mais rápido do Brasil!\n\nAcesse: ${window.location.origin}\n\nAbraços!`); const mailtoUrl = `mailto:?subject=${subject}&body=${body}`; window.location.href = mailtoUrl; this.trackShareEvent('email'); } async copyToClipboard() { if (!this.currentQR) return; try { const shareText = `QR Code gerado com QR Rapido - ${window.location.origin}`; if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(shareText); } else { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = shareText; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); document.execCommand('copy'); textArea.remove(); } this.showSuccess('Link copiado para a área de transferência!'); this.trackShareEvent('copy'); } catch (error) { console.error('Error copying to clipboard:', error); this.showError('Erro ao copiar link. Tente novamente.'); } } async saveToGallery() { if (!this.currentQR) return; try { // Create a blob from the base64 image const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`); const blob = await base64Response.blob(); // Create download link const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `qrrapido-${new Date().toISOString().slice(0,10)}-${Date.now()}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); this.showSuccess('QR Code baixado! Verifique sua galeria/downloads.'); this.trackShareEvent('gallery'); } catch (error) { console.error('Error saving to gallery:', error); this.showError('Erro ao salvar na galeria. Tente novamente.'); } } trackShareEvent(method) { // Google Analytics if (typeof gtag !== 'undefined') { gtag('event', 'qr_shared', { 'share_method': method, 'user_type': this.isPremiumUser() ? 'premium' : 'free', 'language': this.currentLang }); } // Internal tracking console.log(`QR Code shared via ${method}`); } async generateQRWithTimer(e) { e.preventDefault(); // Validation if (!this.validateForm()) return; // Start timer this.startTime = performance.now(); this.showGenerationStarted(); const requestData = this.collectFormData(); try { // Build fetch options based on request type const fetchOptions = { method: 'POST', body: requestData.isMultipart ? requestData.data : JSON.stringify(requestData.data) }; // Add Content-Type header only for JSON requests (FormData sets its own) if (!requestData.isMultipart) { fetchOptions.headers = { 'Content-Type': 'application/json' }; } const response = await fetch(requestData.endpoint, fetchOptions); if (!response.ok) { const errorData = await response.json().catch(() => ({})); if (response.status === 429) { this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.'); return; } if (response.status === 400 && errorData.requiresPremium) { this.showUpgradeModal(errorData.error || 'Logo personalizado é exclusivo do plano Premium.'); return; } throw new Error(errorData.error || 'Erro na geração'); } const result = await response.json(); if (!result.success) { if (result.requiresPremium) { this.showUpgradeModal(result.error || 'Logo personalizado é exclusivo do plano Premium.'); return; } throw new Error(result.error || 'Erro desconhecido'); } const generationTime = ((performance.now() - this.startTime) / 1000).toFixed(1); this.displayQRResult(result, generationTime); this.updateSpeedStats(generationTime); this.trackGenerationEvent(requestData.data.type || requestData.data.get('type'), generationTime); } catch (error) { console.error('Erro ao gerar QR:', error); this.showError(this.languageStrings[this.currentLang].error); } finally { this.hideGenerationLoading(); } } validateForm() { const qrType = document.getElementById('qr-type').value; if (!qrType) { this.showError('Selecione o tipo de QR code'); return false; } // Special validation for VCard if (qrType === 'vcard') { try { if (window.vcardGenerator) { const errors = window.vcardGenerator.validateVCardData(); if (errors.length > 0) { this.showError(errors.join('
')); return false; } return true; } else { this.showError('VCard generator não está disponível'); return false; } } catch (error) { this.showError('Erro na validação do VCard: ' + error.message); return false; } } else if (qrType === 'wifi') { const errors = window.wifiGenerator.validateWiFiData(); if (errors.length > 0) { this.showError(errors.join('
')); return false; } return true; } else if (qrType === 'sms') { const errors = window.smsGenerator.validateSMSData(); if (errors.length > 0) { this.showError(errors.join('
')); return false; } return true; } else if (qrType === 'email') { const errors = window.emailGenerator.validateEmailData(); if (errors.length > 0) { this.showError(errors.join('
')); return false; } return true; } // Normal validation for other types const qrContent = document.getElementById('qr-content').value.trim(); if (!qrContent) { this.showError('Digite o conteúdo do QR code'); return false; } if (qrContent.length > 4000) { this.showError('Conteúdo muito longo. Máximo 4000 caracteres.'); return false; } return true; } collectFormData() { const type = document.getElementById('qr-type').value; const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic'; const styleSettings = this.getStyleSettings(quickStyle); if (type === 'wifi') { const wifiContent = window.wifiGenerator.generateWiFiString(); return { data: { type: 'text', // WiFi is treated as text in the backend content: wifiContent, quickStyle: quickStyle, primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'), backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'), size: parseInt(document.getElementById('qr-size').value), margin: parseInt(document.getElementById('qr-margin').value), cornerStyle: document.getElementById('corner-style')?.value || 'square', optimizeForSpeed: true, language: this.currentLang }, isMultipart: false, endpoint: '/api/QR/GenerateRapid' }; } else if (type === 'sms') { const smsContent = window.smsGenerator.generateSMSString(); return { data: { type: 'text', content: smsContent, quickStyle: quickStyle, primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'), backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'), size: parseInt(document.getElementById('qr-size').value), margin: parseInt(document.getElementById('qr-margin').value), cornerStyle: document.getElementById('corner-style')?.value || 'square', optimizeForSpeed: true, language: this.currentLang }, isMultipart: false, endpoint: '/api/QR/GenerateRapid' }; } else if (type === 'email') { const emailContent = window.emailGenerator.generateEmailString(); return { data: { type: 'text', content: emailContent, quickStyle: quickStyle, primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'), backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'), size: parseInt(document.getElementById('qr-size').value), margin: parseInt(document.getElementById('qr-margin').value), cornerStyle: document.getElementById('corner-style')?.value || 'square', optimizeForSpeed: true, language: this.currentLang }, isMultipart: false, endpoint: '/api/QR/GenerateRapid' }; } // Handle VCard type 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: encodedContent, quickStyle: quickStyle, primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'), backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'), size: parseInt(document.getElementById('qr-size').value), margin: parseInt(document.getElementById('qr-margin').value), cornerStyle: document.getElementById('corner-style')?.value || 'square', optimizeForSpeed: true, language: this.currentLang }, isMultipart: false, endpoint: '/api/QR/GenerateRapid' }; } else { throw new Error('VCard generator não está disponível'); } } // Check if logo is selected for premium users const logoUpload = document.getElementById('logo-upload'); const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0]; if (hasLogo) { // Use FormData for premium users with logo const formData = new FormData(); // Get user-selected colors with proper priority const userPrimaryColor = document.getElementById('primary-color').value; const userBackgroundColor = document.getElementById('bg-color').value; // Priority: User selection > Style defaults > Fallback // Always use user selection if it exists, regardless of what color it is const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000'); const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF'); // Debug logging for color selection console.log('🎨 Color Selection Debug (FormData):'); console.log(' Style:', quickStyle); console.log(' Style Default Primary:', styleSettings.primaryColor); console.log(' User Selected Primary:', userPrimaryColor); console.log(' Final Primary Color:', finalPrimaryColor); console.log(' Final Background Color:', finalBackgroundColor); // 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', encodedContent); formData.append('quickStyle', quickStyle); formData.append('primaryColor', finalPrimaryColor); formData.append('backgroundColor', finalBackgroundColor); formData.append('size', parseInt(document.getElementById('qr-size').value)); formData.append('margin', parseInt(document.getElementById('qr-margin').value)); formData.append('cornerStyle', document.getElementById('corner-style')?.value || 'square'); formData.append('optimizeForSpeed', 'true'); formData.append('language', this.currentLang); // Add logo file formData.append('logo', logoUpload.files[0]); console.log('Logo file added to form data:', logoUpload.files[0].name, logoUpload.files[0].size + ' bytes'); return { data: formData, isMultipart: true, endpoint: '/api/QR/GenerateRapidWithLogo' }; } else { // Use JSON for basic QR generation (original working method) // Get user-selected colors const userPrimaryColor = document.getElementById('primary-color').value; const userBackgroundColor = document.getElementById('bg-color').value; // Priority: User selection > Style defaults > Fallback // Always use user selection if it exists, regardless of what color it is const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000'); const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF'); // Debug logging for color selection console.log('🎨 Color Selection Debug (JSON):'); console.log(' Style:', quickStyle); console.log(' Style Default Primary:', styleSettings.primaryColor); console.log(' User Selected Primary:', userPrimaryColor); 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: encodedContent, quickStyle: quickStyle, primaryColor: finalPrimaryColor, backgroundColor: finalBackgroundColor, size: parseInt(document.getElementById('qr-size').value), margin: parseInt(document.getElementById('qr-margin').value), cornerStyle: document.getElementById('corner-style')?.value || 'square', optimizeForSpeed: true, language: this.currentLang }, isMultipart: false, endpoint: '/api/QR/GenerateRapid' }; } } getStyleSettings(style) { const styles = { classic: { primaryColor: '#000000', backgroundColor: '#FFFFFF' }, modern: { primaryColor: '#007BFF', backgroundColor: '#F8F9FA' }, colorful: { primaryColor: '#FF6B35', backgroundColor: '#FFF3E0' } }; return styles[style] || styles.classic; } displayQRResult(result, generationTime) { const previewDiv = document.getElementById('qr-preview'); if (!previewDiv) return; previewDiv.innerHTML = ` QR Code gerado em ${generationTime}s `; // Show generation statistics this.showGenerationStats(generationTime); // Show download buttons const downloadSection = document.getElementById('download-section'); if (downloadSection) { downloadSection.style.display = 'block'; } // Save current data this.currentQR = { base64: result.qrCodeBase64, id: result.qrId, generationTime: generationTime }; // Update counter for free users if (result.remainingQRs !== undefined) { this.updateRemainingCounter(result.remainingQRs); } } showGenerationStarted() { const button = document.getElementById('generate-btn'); const spinner = button?.querySelector('.spinner-border'); const timer = document.querySelector('.generation-timer'); if (button) button.disabled = true; if (spinner) spinner.classList.remove('d-none'); if (timer) timer.classList.remove('d-none'); // Update timer in real time this.timerInterval = setInterval(() => { const elapsed = ((performance.now() - this.startTime) / 1000).toFixed(1); const timerSpan = timer?.querySelector('span'); if (timerSpan) { timerSpan.textContent = `${elapsed}s`; } }, 100); // Preview loading const preview = document.getElementById('qr-preview'); if (preview) { preview.innerHTML = `

${this.languageStrings[this.currentLang].generating}

`; } } showGenerationStats(generationTime) { const statsDiv = document.querySelector('.generation-stats'); const speedBadge = document.querySelector('.speed-badge'); if (statsDiv) { statsDiv.classList.remove('d-none'); const timeSpan = statsDiv.querySelector('.generation-time'); if (timeSpan) { timeSpan.textContent = `${generationTime}s`; } } // Show speed badge if (speedBadge) { const strings = this.languageStrings[this.currentLang]; let badgeText = strings.normal; let badgeClass = 'bg-secondary'; if (generationTime < 1.0) { badgeText = strings.ultraFast; badgeClass = 'bg-success'; } else if (generationTime < 2.0) { badgeText = strings.fast; badgeClass = 'bg-primary'; } speedBadge.innerHTML = ` ${badgeText} `; speedBadge.classList.remove('d-none'); } } hideGenerationLoading() { const button = document.getElementById('generate-btn'); const spinner = button?.querySelector('.spinner-border'); if (button) button.disabled = false; if (spinner) spinner.classList.add('d-none'); if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; } } updateContentHints() { const type = document.getElementById('qr-type')?.value; const hintsElement = document.getElementById('content-hints'); const vcardInterface = document.getElementById('vcard-interface'); const contentTextarea = document.getElementById('qr-content'); if (!hintsElement || !type) return; // Show/hide VCard interface based on type if (type === 'vcard') { if (vcardInterface) vcardInterface.style.display = 'block'; if (contentTextarea) { contentTextarea.style.display = 'none'; contentTextarea.removeAttribute('required'); } hintsElement.textContent = 'Preencha os campos acima para criar seu cartão de visita digital'; return; // Skip normal hints for VCard } else { if (vcardInterface) vcardInterface.style.display = 'none'; if (contentTextarea) { contentTextarea.style.display = 'block'; contentTextarea.setAttribute('required', 'required'); } } const hints = { 'pt-BR': { 'url': 'Ex: https://www.exemplo.com.br', 'text': 'Digite qualquer texto que desejar', 'wifi': 'Nome da rede;Senha;Tipo de segurança (WPA/WEP)', 'vcard': 'Nome;Telefone;Email;Empresa', 'sms': 'Número;Mensagem', 'email': 'email@exemplo.com;Assunto;Mensagem' }, 'es': { 'url': 'Ej: https://www.ejemplo.com', 'text': 'Escribe cualquier texto que desees', 'wifi': 'Nombre de red;Contraseña;Tipo de seguridad (WPA/WEP)', 'vcard': 'Nombre;Teléfono;Email;Empresa', 'sms': 'Número;Mensaje', 'email': 'email@ejemplo.com;Asunto;Mensagem' } }; const langHints = hints[this.currentLang] || hints['pt-BR']; hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado'; } changeLanguage(e) { e.preventDefault(); this.currentLang = e.target.dataset.lang; this.updateLanguage(); this.updateContentHints(); // Save preference localStorage.setItem('qrrapido-lang', this.currentLang); // Track language change window.trackLanguageChange && window.trackLanguageChange('pt-BR', this.currentLang); } updateLanguage() { const strings = this.languageStrings[this.currentLang]; // Update tagline const tagline = document.getElementById('tagline'); if (tagline) { tagline.textContent = strings.tagline; } // Update language selector const langMap = { 'pt-BR': 'PT', 'es': 'ES', 'en': 'EN' }; const currentLang = document.getElementById('current-lang'); if (currentLang) { currentLang.textContent = langMap[this.currentLang]; } // Update hints if type already selected const qrType = document.getElementById('qr-type'); if (qrType?.value) { this.updateContentHints(); } } handleLogoSelection(e) { const file = e.target.files[0]; const logoPreview = document.getElementById('logo-preview'); const logoFilename = document.getElementById('logo-filename'); if (file) { // Validate file size (2MB max) if (file.size > 2 * 1024 * 1024) { this.showError('Logo muito grande. Máximo 2MB.'); e.target.value = ''; // Clear the input logoPreview?.classList.add('d-none'); return; } // Validate file type const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg']; if (!allowedTypes.includes(file.type)) { this.showError('Formato inválido. Use PNG ou JPG.'); e.target.value = ''; // Clear the input logoPreview?.classList.add('d-none'); return; } // Show success feedback if (logoFilename) { const fileSizeKB = Math.round(file.size / 1024); logoFilename.textContent = `${file.name} (${fileSizeKB}KB)`; } logoPreview?.classList.remove('d-none'); console.log('Logo selected:', file.name, file.size + ' bytes', file.type); } else { // Hide preview when no file selected logoPreview?.classList.add('d-none'); } } handleCornerStyleChange(e) { const selectedStyle = e.target.value; const premiumStyles = ['rounded', 'circle', 'leaf']; if (premiumStyles.includes(selectedStyle)) { // Check if user is premium (we can detect this by checking if the option is disabled) const option = e.target.options[e.target.selectedIndex]; if (option.disabled) { // Reset to square e.target.value = 'square'; this.showUpgradeModal('Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.'); return; } } console.log('Corner style selected:', selectedStyle); } applyQuickStyle(e) { const style = e.target.value; const settings = this.getStyleSettings(style); const primaryColor = document.getElementById('primary-color'); const bgColor = document.getElementById('bg-color'); if (primaryColor) primaryColor.value = settings.primaryColor; if (bgColor) bgColor.value = settings.backgroundColor; } updateStatsCounters() { // Simulate real-time counters setInterval(() => { const totalElement = document.getElementById('total-qrs'); if (totalElement) { const current = parseFloat(totalElement.textContent.replace('K', '')) || 10.5; const newValue = (current + Math.random() * 0.1).toFixed(1); totalElement.textContent = `${newValue}K`; } // Update average time based on real performance const avgElement = document.getElementById('avg-generation-time'); if (avgElement && window.qrRapidoStats) { const avg = window.qrRapidoStats.getAverageTime(); avgElement.textContent = `${avg}s`; } }, 30000); // Update every 30 seconds } trackGenerationEvent(type, time) { // Google Analytics if (typeof gtag !== 'undefined') { gtag('event', 'qr_generated', { 'qr_type': type, 'generation_time': parseFloat(time), 'user_type': this.isPremiumUser() ? 'premium' : 'free', 'language': this.currentLang }); } // Internal statistics if (!window.qrRapidoStats) { window.qrRapidoStats = { times: [], getAverageTime: function() { if (this.times.length === 0) return '1.2'; const avg = this.times.reduce((a, b) => a + b) / this.times.length; return avg.toFixed(1); } }; } window.qrRapidoStats.times.push(parseFloat(time)); } updateSpeedStats(generationTime) { // Update the live statistics display const timeFloat = parseFloat(generationTime); // Update average time display in the stats cards const avgElement = document.querySelector('.card-body h5:contains("1.2s")'); if (avgElement) { const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime; avgElement.innerHTML = ` ${avgTime}s`; } // Update the generation timer in the header const timerElement = document.querySelector('.generation-timer span'); if (timerElement) { timerElement.textContent = `${generationTime}s`; } // Update performance statistics if (!window.qrRapidoPerformance) { window.qrRapidoPerformance = { totalGenerations: 0, totalTime: 0, bestTime: Infinity, worstTime: 0 }; } const perf = window.qrRapidoPerformance; perf.totalGenerations++; perf.totalTime += timeFloat; perf.bestTime = Math.min(perf.bestTime, timeFloat); perf.worstTime = Math.max(perf.worstTime, timeFloat); // Log performance statistics for debugging console.log('📊 Performance Update:', { currentTime: `${generationTime}s`, averageTime: `${(perf.totalTime / perf.totalGenerations).toFixed(1)}s`, bestTime: `${perf.bestTime.toFixed(1)}s`, totalGenerations: perf.totalGenerations }); } isPremiumUser() { return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false; } async downloadQR(format) { if (!this.currentQR) return; try { const response = await fetch(`/api/QR/Download/${this.currentQR.id}?format=${format}`); if (!response.ok) throw new Error('Download failed'); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `qrrapido-${new Date().toISOString().slice(0,10)}.${format}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (error) { console.error('Download error:', error); this.showError('Erro ao fazer download. Tente novamente.'); } } async saveToHistory() { if (!this.currentQR) return; try { const response = await fetch('/api/QR/SaveToHistory', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ qrId: this.currentQR.id }) }); if (response.ok) { this.showSuccess(this.languageStrings[this.currentLang].success); } else { throw new Error('Failed to save'); } } catch (error) { console.error('Save error:', error); this.showError('Erro ao salvar no histórico.'); } } async checkAdFreeStatus() { try { const response = await fetch('/Account/AdFreeStatus'); const status = await response.json(); if (status.isAdFree) { this.hideAllAds(); this.showAdFreeMessage(status.timeRemaining); } } catch (error) { console.error('Error checking ad-free status:', error); } } hideAllAds() { document.querySelectorAll('.ad-container').forEach(ad => { ad.style.display = 'none'; }); } showAdFreeMessage(timeRemaining) { if (timeRemaining <= 0) return; const existing = document.querySelector('.ad-free-notice'); if (existing) return; // Already shown const message = document.createElement('div'); message.className = 'alert alert-success text-center mb-3 ad-free-notice'; message.innerHTML = ` Sessão sem anúncios ativa! Tempo restante: ${this.formatTime(timeRemaining)} Tornar Permanente `; const container = document.querySelector('.container'); const row = container?.querySelector('.row'); if (container && row) { container.insertBefore(message, row); } } formatTime(minutes) { if (minutes === 0) return '0m'; const days = Math.floor(minutes / 1440); const hours = Math.floor((minutes % 1440) / 60); const mins = minutes % 60; if (days > 0) return `${days}d ${hours}h ${mins}m`; if (hours > 0) return `${hours}h ${mins}m`; return `${mins}m`; } showUpgradeModal(message) { const modal = document.createElement('div'); modal.className = 'modal fade'; modal.innerHTML = ` `; document.body.appendChild(modal); const bsModal = new bootstrap.Modal(modal); bsModal.show(); // Remove modal when closed modal.addEventListener('hidden.bs.modal', () => { document.body.removeChild(modal); }); } updateRemainingCounter(remaining) { const counterElement = document.querySelector('.qr-counter'); if (counterElement) { counterElement.textContent = `${remaining} QR codes restantes hoje`; if (remaining <= 3) { counterElement.className = 'badge bg-warning qr-counter'; } if (remaining === 0) { counterElement.className = 'badge bg-danger qr-counter'; } } } showError(message) { this.showAlert(message, 'danger'); } showSuccess(message) { this.showAlert(message, 'success'); } showAlert(message, type) { const alert = document.createElement('div'); alert.className = `alert alert-${type} alert-dismissible fade show`; alert.innerHTML = `
${message}
`; const container = document.querySelector('.container'); const row = container?.querySelector('.row'); if (container && row) { container.insertBefore(alert, row); } // Auto-remove after delay setTimeout(() => { if (alert.parentNode) { alert.parentNode.removeChild(alert); } }, type === 'success' ? 3000 : 5000); } setupRealTimePreview() { const contentField = document.getElementById('qr-content'); const typeField = document.getElementById('qr-type'); if (contentField && typeField) { let previewTimeout; const updatePreview = () => { clearTimeout(previewTimeout); previewTimeout = setTimeout(() => { if (contentField.value.trim() && typeField.value) { // Could implement real-time preview for premium users console.log('Real-time preview update'); } }, 500); }; contentField.addEventListener('input', updatePreview); 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 contentGroup = document.getElementById('content-group'); const vcardInterface = document.getElementById('vcard-interface'); const wifiInterface = document.getElementById('wifi-interface'); const smsInterface = document.getElementById('sms-interface'); const emailInterface = document.getElementById('email-interface'); // Hide all interfaces by default if (vcardInterface) vcardInterface.style.display = 'none'; if (wifiInterface) wifiInterface.style.display = 'none'; if (smsInterface) smsInterface.style.display = 'none'; if (emailInterface) emailInterface.style.display = 'none'; if (contentGroup) contentGroup.style.display = 'block'; if (type === 'vcard') { // Para vCard, ocultar textarea e mostrar interface específica if (contentGroup) contentGroup.style.display = 'none'; if (vcardInterface) { vcardInterface.style.display = 'block'; this.enableVCardFields(); } } else if (type === 'wifi') { // Para WiFi, ocultar textarea e mostrar interface específica if (contentGroup) contentGroup.style.display = 'none'; if (wifiInterface) { wifiInterface.style.display = 'block'; } } else if (type === 'sms') { // Para SMS, ocultar textarea e mostrar interface específica if (contentGroup) contentGroup.style.display = 'none'; if (smsInterface) { smsInterface.style.display = 'block'; } } else if (type === 'email') { // Para Email, ocultar textarea e mostrar interface específica if (contentGroup) contentGroup.style.display = 'none'; if (emailInterface) { emailInterface.style.display = 'block'; } } else { // Para outros tipos, mostrar textarea if (contentGroup) contentGroup.style.display = 'block'; const qrContent = document.getElementById('qr-content'); if(qrContent) { qrContent.disabled = false; } } } 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 isValid = false; const type = this.selectedType; if (type === 'url' || type === 'text') { const content = document.getElementById('qr-content')?.value || ''; isValid = content.trim().length >= 3; } else if (type === 'vcard') { isValid = this.validateVCardFields(); } else if (type === 'wifi') { const data = window.wifiGenerator.collectWiFiData(); isValid = data.ssid.trim() !== ''; if (data.security !== 'nopass' && !data.password.trim()) { isValid = false; } } else if (type === 'sms') { const data = window.smsGenerator.collectSMSData(); isValid = data.number.trim() !== '' && data.message.trim() !== ''; } else if (type === 'email') { const data = window.emailGenerator.collectEmailData(); isValid = data.to.trim() !== '' && data.subject.trim() !== ''; } generateBtn.disabled = !isValid; if (isValid) { generateBtn.classList.remove('btn-secondary', 'disabled'); generateBtn.classList.add('btn-primary'); } else { generateBtn.classList.remove('btn-primary'); generateBtn.classList.add('btn-secondary', 'disabled'); } } // 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 = /[^-]/.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 document.addEventListener('DOMContentLoaded', () => { window.qrGenerator = new QRRapidoGenerator(); window.vcardGenerator = new VCardGenerator(); window.wifiGenerator = new WiFiQRGenerator(); window.smsGenerator = new SMSQRGenerator(); window.emailGenerator = new EmailQRGenerator(); // Initialize AdSense if necessary if (window.adsbygoogle && document.querySelector('.adsbygoogle')) { (adsbygoogle = window.adsbygoogle || []).push({}); } }); class SMSQRGenerator { constructor() { this.initializeSMSInterface(); } initializeSMSInterface() { // Atualizar preview em tempo real const fieldsToWatch = ['sms-number', 'sms-message']; fieldsToWatch.forEach(fieldId => { const element = document.getElementById(fieldId); if (element) { element.addEventListener('input', () => this.updatePreview()); element.addEventListener('change', () => this.updatePreview()); } }); // Inicializar preview this.updatePreview(); } updatePreview() { const smsString = this.generateSMSString(); const previewElement = document.getElementById('sms-preview-text'); if (previewElement) { previewElement.textContent = smsString; } } generateSMSString() { const data = this.collectSMSData(); if (!data.number || !data.message) { return 'SMSTO::'; } return `SMSTO:${data.number}:${data.message}`; } collectSMSData() { return { number: document.getElementById('sms-number')?.value || '', message: document.getElementById('sms-message')?.value || '' }; } validateSMSData() { const data = this.collectSMSData(); const errors = []; if (!data.number.trim()) { errors.push('Número do celular é obrigatório'); } if (!data.message.trim()) { errors.push('Mensagem é obrigatória'); } // Validação de telefone brasileiro básica const phoneRegex = /^\d{10,11}$/; if (data.number && !phoneRegex.test(data.number.replace(/\D/g, ''))) { errors.push('Número deve ter 10 ou 11 dígitos (DDD + número)'); } return errors; } } class EmailQRGenerator { constructor() { this.initializeEmailInterface(); } initializeEmailInterface() { // Atualizar preview em tempo real const fieldsToWatch = ['email-to', 'email-subject', 'email-body']; fieldsToWatch.forEach(fieldId => { const element = document.getElementById(fieldId); if (element) { element.addEventListener('input', () => this.updatePreview()); element.addEventListener('change', () => this.updatePreview()); } }); // Inicializar preview this.updatePreview(); } updatePreview() { const emailString = this.generateEmailString(); const previewElement = document.getElementById('email-preview-text'); if (previewElement) { previewElement.textContent = emailString; } } generateEmailString() { const data = this.collectEmailData(); if (!data.to) { return 'mailto:?subject=&body='; } let emailString = `mailto:${data.to}`; const params = []; if (data.subject) { params.push(`subject=${encodeURIComponent(data.subject)}`); } if (data.body) { params.push(`body=${encodeURIComponent(data.body)}`); } if (params.length > 0) { emailString += '?' + params.join('&'); } return emailString; } collectEmailData() { return { to: document.getElementById('email-to')?.value || '', subject: document.getElementById('email-subject')?.value || '', body: document.getElementById('email-body')?.value || '' }; } validateEmailData() { const data = this.collectEmailData(); const errors = []; if (!data.to.trim()) { errors.push('Email destinatário é obrigatório'); } if (!data.subject.trim()) { errors.push('Assunto é obrigatório'); } // Validação básica de email const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (data.to && !emailRegex.test(data.to)) { errors.push('Email destinatário inválido'); } return errors; } } class WiFiQRGenerator { constructor() { this.initializeWiFiInterface(); } initializeWiFiInterface() { // Radio buttons: usar name para group document.querySelectorAll('input[name="wifi-security"]').forEach(radio => { radio.addEventListener('change', () => { this.togglePasswordField(); this.updatePreview(); }); }); // Campos individuais: usar IDs únicos const fieldsToWatch = ['wifi-ssid', 'wifi-password', 'wifi-hidden']; fieldsToWatch.forEach(fieldId => { const element = document.getElementById(fieldId); if (element) { element.addEventListener('input', () => this.updatePreview()); element.addEventListener('change', () => this.updatePreview()); } }); const togglePassword = document.getElementById('toggle-password'); if (togglePassword) { togglePassword.addEventListener('click', () => { this.togglePasswordVisibility(); }); } // Inicializar estado this.togglePasswordField(); this.updatePreview(); } togglePasswordField() { const selectedRadio = document.querySelector('input[name="wifi-security"]:checked'); const securityType = selectedRadio ? selectedRadio.value : 'WPA'; const passwordGroup = document.getElementById('wifi-password-group'); const passwordInput = document.getElementById('wifi-password'); if (securityType === 'nopass') { passwordGroup.style.display = 'none'; passwordInput.removeAttribute('required'); passwordInput.value = ''; } else { passwordGroup.style.display = 'block'; passwordInput.setAttribute('required', 'required'); } } togglePasswordVisibility() { const passwordInput = document.getElementById('wifi-password'); const toggleIcon = document.getElementById('toggle-password'); if (passwordInput.type === 'password') { passwordInput.type = 'text'; toggleIcon.className = 'fas fa-eye-slash'; } else { passwordInput.type = 'password'; toggleIcon.className = 'fas fa-eye'; } } updatePreview() { const wifiString = this.generateWiFiString(); const previewText = document.getElementById('wifi-preview-text'); if(previewText) { previewText.textContent = wifiString; } } generateWiFiString() { const data = this.collectWiFiData(); // Validar dados mínimos if (!data.ssid) { return 'WIFI:T:WPA;S:;P:;H:false;;'; } // Escapar caracteres especiais const ssid = this.escapeWiFiString(data.ssid); const password = this.escapeWiFiString(data.password); // Construir string WiFi return `WIFI:T:${data.security};S:${ssid};P:${password};H:${data.hidden};;`; } collectWiFiData() { return { ssid: document.getElementById('wifi-ssid').value, security: document.querySelector('input[name="wifi-security"]:checked')?.value || 'WPA', password: document.getElementById('wifi-password').value, hidden: document.getElementById('wifi-hidden').checked.toString() }; } escapeWiFiString(str) { if (!str) return ''; // Escapar caracteres especiais do formato WiFi return str.replace(/[\\;,:"]/g, '\\escapeWiFiString(str) { if (!str) return; }'); // Escapar caracteres especiais do formato WiFi //return str.replace(/[\\;,:""]/g, '\\document.addEventListener('DOMContentLoaded', () => { // window.qrGenerator = new QRRapidoGenerator(); // window.vcardGenerator = new VCardGenerator(); // // Initialize AdSense if necessary // if (window.adsbygoogle && document.querySelector('.adsbygoogle')) { // (adsbygoogle = window.adsbygoogle || []).push({}); // } //}); return str.replace(/[\\;,:"']/g, '\\$&'); // VCard Generator Class'); } validateWiFiData() { const data = this.collectWiFiData(); const errors = []; // Validações obrigatórias if (!data.ssid.trim()) { errors.push('Nome da rede (SSID) é obrigatório'); } // Validação de senha conforme tipo de segurança if ((data.security === 'WPA' || data.security === 'WEP') && !data.password.trim()) { errors.push('Senha é obrigatória para redes protegidas'); } // Validação de comprimento if (data.ssid.length > 32) { errors.push('Nome da rede deve ter no máximo 32 caracteres'); } if (data.password.length > 63) { errors.push('Senha deve ter no máximo 63 caracteres'); } // Validação WEP específica if (data.security === 'WEP' && data.password) { const validWEPLengths = [5, 10, 13, 26]; // WEP 64/128 bit if (!validWEPLengths.includes(data.password.length)) { errors.push('Senha WEP deve ter 5, 10, 13 ou 26 caracteres'); } } return errors; } } // VCard Generator Class class VCardGenerator { constructor() { this.initializeVCardInterface(); } // Função para codificar caracteres especiais usando Quoted-Printable encodeQuotedPrintable(text) { if (!text) return ''; return text.replace(/[^\u0020-\u007E]/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 = /[^\u0020-\u007E]/.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 => { checkbox.addEventListener('change', (e) => { const groupId = e.target.id.replace('enable-', '') + '-group'; const group = document.getElementById(groupId); if (group) { group.style.display = e.target.checked ? 'block' : 'none'; // Clear values when hiding fields if (!e.target.checked) { const inputs = group.querySelectorAll('input'); inputs.forEach(input => input.value = ''); } } this.updatePreview(); }); }); // Update preview in real-time document.querySelectorAll('#vcard-interface input').forEach(input => { input.addEventListener('input', () => this.updatePreview()); input.addEventListener('blur', () => this.validateField(input)); }); } updatePreview() { const vcard = this.generateVCardContent(); const previewElement = document.getElementById('vcard-preview-text'); if (previewElement) { previewElement.textContent = vcard; } } generateVCardContent() { const data = this.collectVCardData(); 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(' ') || ''; // 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) { const encodedCompany = this.prepareTextForVCard(data.company); vcard += `ORG:${encodedCompany}\n`; } // Título 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 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`; } // Telefones if (data.phone) vcard += `TEL;WORK;VOICE:${data.phone}\n`; if (data.mobile) vcard += `TEL;CELL:${data.mobile}\n`; // Email if (data.email) vcard += `EMAIL;WORK;INTERNET:${data.email}\n`; // Website if (data.website) vcard += `URL:${data.website}\n`; vcard += 'END:VCARD'; return vcard; } collectVCardData() { return { name: document.getElementById('vcard-name')?.value?.trim() || '', mobile: document.getElementById('vcard-mobile')?.value?.trim() || '', email: document.getElementById('vcard-email')?.value?.trim() || '', company: document.getElementById('enable-company')?.checked ? (document.getElementById('vcard-company')?.value?.trim() || '') : '', title: document.getElementById('enable-title')?.checked ? (document.getElementById('vcard-title')?.value?.trim() || '') : '', website: document.getElementById('enable-website')?.checked ? (document.getElementById('vcard-website')?.value?.trim() || '') : '', address: document.getElementById('enable-address')?.checked ? (document.getElementById('vcard-address')?.value?.trim() || '') : '', city: document.getElementById('enable-address')?.checked ? (document.getElementById('vcard-city')?.value?.trim() || '') : '', state: document.getElementById('enable-address')?.checked ? (document.getElementById('vcard-state')?.value?.trim() || '') : '', zip: document.getElementById('enable-address')?.checked ? (document.getElementById('vcard-zip')?.value?.trim() || '') : '', phone: document.getElementById('enable-phone')?.checked ? (document.getElementById('vcard-phone')?.value?.trim() || '') : '' }; } validateVCardData() { const data = this.collectVCardData(); const errors = []; // Required field validations if (!data.name) errors.push('Nome é obrigatório'); if (!data.mobile) errors.push('Telefone celular é obrigatório'); if (!data.email) errors.push('Email é obrigatório'); // Format validations if (data.email && !this.isValidEmail(data.email)) { errors.push('Email inválido'); } if (data.website && !this.isValidURL(data.website)) { errors.push('Website inválido (deve começar com http:// ou https://)'); } if (data.mobile && !this.isValidPhone(data.mobile)) { errors.push('Telefone celular inválido (deve ter 10-11 dígitos)'); } if (data.phone && !this.isValidPhone(data.phone)) { errors.push('Telefone fixo inválido (deve ter 10-11 dígitos)'); } return errors; } validateField(input) { const value = input.value.trim(); let isValid = true; let message = ''; switch (input.id) { case 'vcard-email': if (value && !this.isValidEmail(value)) { isValid = false; message = 'Email inválido'; } break; case 'vcard-website': if (value && !this.isValidURL(value)) { isValid = false; message = 'Website inválido (deve começar com http:// ou https://)'; } break; case 'vcard-mobile': case 'vcard-phone': if (value && !this.isValidPhone(value)) { isValid = false; message = 'Telefone inválido (apenas números, 10-11 dígitos)'; } break; } // Update field validation state if (isValid) { input.classList.remove('is-invalid'); input.classList.add('is-valid'); } else { input.classList.remove('is-valid'); input.classList.add('is-invalid'); // Show error message const feedback = input.parentNode.querySelector('.invalid-feedback'); if (feedback) { feedback.textContent = message; } } return isValid; } isValidEmail(email) { return /^[^ @]+@[^ @]+\.[^ @]+$/.test(email); } isValidURL(url) { try { new URL(url); return url.startsWith('http://') || url.startsWith('https://'); } catch { return false; } } isValidPhone(phone) { // Brazilian phone validation (DDD + number) const digitsOnly = phone.replace(/\D/g, ''); return /^\d{10,11}$/.test(digitsOnly); } // Method to be called by main QR generator getVCardContent() { const errors = this.validateVCardData(); if (errors.length > 0) { throw new Error('Erro na validação: ' + errors.join(', ')); } return this.generateVCardContent(); } } // Global functions for ad control window.QRApp = { refreshAds: function() { if (window.adsbygoogle) { document.querySelectorAll('.adsbygoogle').forEach(ad => { (adsbygoogle = window.adsbygoogle || []).push({}); }); } }, hideAds: function() { document.querySelectorAll('.ad-container').forEach(ad => { ad.style.display = 'none'; }); } }; // Google Analytics 4 Event Tracking window.trackLanguageChange = function(from, to) { if (typeof gtag !== 'undefined') { gtag('event', 'language_change', { 'from_language': from, 'to_language': to }); } }; window.trackUpgradeClick = function(location) { if (typeof gtag !== 'undefined') { gtag('event', 'upgrade_click', { 'click_location': location }); } };