// 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.initializeUserCounter(); this.initializeProgressiveFlow(); this.initializeRateLimiting(); // 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(); // Check rate limit for anonymous users if (!this.checkRateLimit()) return; // 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 === 'url') { const dynamicData = window.dynamicQRManager.getDynamicQRData(); if (dynamicData.requiresPremium) { throw new Error('QR Dinâmico é exclusivo para usuários Premium. Faça upgrade para usar analytics.'); } return { data: { type: 'url', content: dynamicData.originalUrl, isDynamic: dynamicData.isDynamic, 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' }; } 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, qrCodeBase64: result.qrCodeBase64, // Both properties for compatibility id: result.qrId, generationTime: generationTime }; // Increment rate limit counter after successful generation this.incrementRateLimit(); // Update counter for logged users if (result.remainingQRs !== undefined) { if (result.remainingQRs === -1 || result.remainingQRs === 2147483647 || result.remainingQRs >= 1000000) { // Logged user - show unlimited this.showUnlimitedCounter(); } else { 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 } async initializeUserCounter() { try { const response = await fetch('/api/QR/GetUserStats'); if (response.ok) { const stats = await response.json(); console.log('User stats loaded:', stats); console.log('remainingCount:', stats.remainingCount, 'type:', typeof stats.remainingCount); console.log('isUnlimited:', stats.isUnlimited); // For logged users, always show unlimited (they all have unlimited QR codes) console.log('Calling showUnlimitedCounter directly for logged user'); this.showUnlimitedCounter(); } else { console.log('GetUserStats response not ok:', response.status); } } catch (error) { // If not authenticated or error, keep the default "Carregando..." text console.debug('User not authenticated or error loading stats:', error); } } 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 // Find h5 elements in card bodies and look for one containing time info const cardElements = document.querySelectorAll('.card-body h5'); cardElements.forEach(element => { // Look for elements containing time format (e.g., "1.2s", "0.8s", etc.) if (element.textContent.includes('s') && element.textContent.match(/\d+\.\d+s/)) { const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime; element.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 || !this.currentQR.qrCodeBase64) { this.showError('Nenhum QR Code gerado para download.'); return; } try { const timestamp = new Date().toISOString().slice(0,10); const base64Data = this.currentQR.qrCodeBase64; if (format === 'png') { // Download PNG directly from base64 const link = document.createElement('a'); link.href = `data:image/png;base64,${base64Data}`; link.download = `qrrapido-${timestamp}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else if (format === 'svg') { // Convert PNG to SVG const svgData = await this.convertPngToSvg(base64Data); const link = document.createElement('a'); link.href = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgData)}`; link.download = `qrrapido-${timestamp}.svg`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else if (format === 'pdf') { // Convert PNG to PDF const pdfBlob = await this.convertPngToPdf(base64Data); const url = window.URL.createObjectURL(pdfBlob); const link = document.createElement('a'); link.href = url; link.download = `qrrapido-${timestamp}.pdf`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } } catch (error) { console.error('Download error:', error); this.showError(`Erro ao fazer download ${format.toUpperCase()}. Tente novamente.`); } } async convertPngToSvg(base64Data) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); // Create SVG with embedded base64 image const svgData = ` `; resolve(svgData); }; img.src = `data:image/png;base64,${base64Data}`; }); } async convertPngToPdf(base64Data) { return new Promise((resolve, reject) => { try { // Try to use jsPDF if available, otherwise use a simpler approach if (typeof window.jsPDF !== 'undefined') { const pdf = new window.jsPDF(); const img = new Image(); img.onload = () => { // Calculate dimensions to fit on page const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const imgRatio = img.width / img.height; let width = Math.min(pdfWidth - 20, 150); let height = width / imgRatio; if (height > pdfHeight - 20) { height = pdfHeight - 20; width = height * imgRatio; } // Center on page const x = (pdfWidth - width) / 2; const y = (pdfHeight - height) / 2; pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, width, height); const pdfBlob = pdf.output('blob'); resolve(pdfBlob); }; img.onerror = () => reject(new Error('Failed to load image')); img.src = `data:image/png;base64,${base64Data}`; } else { // Fallback: create a very simple PDF this.createBasicPdf(base64Data).then(resolve).catch(reject); } } catch (error) { reject(error); } }); } async createBasicPdf(base64Data) { // Load jsPDF dynamically if not available if (typeof window.jsPDF === 'undefined') { await this.loadJsPDF(); } return new Promise((resolve) => { const pdf = new window.jsPDF(); const img = new Image(); img.onload = () => { // Add QR code to PDF const pdfWidth = pdf.internal.pageSize.getWidth(); const size = Math.min(pdfWidth - 40, 100); const x = (pdfWidth - size) / 2; const y = 50; pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, size, size); pdf.text('QR Code - QR Rapido', pdfWidth / 2, 30, { align: 'center' }); const pdfBlob = pdf.output('blob'); resolve(pdfBlob); }; img.src = `data:image/png;base64,${base64Data}`; }); } async loadJsPDF() { return new Promise((resolve, reject) => { if (typeof window.jsPDF !== 'undefined') { resolve(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; script.onload = () => { window.jsPDF = window.jspdf.jsPDF; resolve(); }; script.onerror = () => reject(new Error('Failed to load jsPDF')); document.head.appendChild(script); }); } 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 && remaining !== null && remaining !== undefined) { // If it's unlimited (any special value indicating unlimited) if (remaining === -1 || remaining >= 2147483647 || remaining > 1000000) { this.showUnlimitedCounter(); return; } const remainingText = this.getLocalizedString('QRCodesRemainingToday'); counterElement.textContent = `${remaining} ${remainingText}`; if (remaining <= 3) { counterElement.className = 'badge bg-warning qr-counter'; } if (remaining === 0) { counterElement.className = 'badge bg-danger qr-counter'; } } } showUnlimitedCounter() { console.log('showUnlimitedCounter called'); const counterElement = document.querySelector('.qr-counter'); if (counterElement) { counterElement.textContent = 'QR Codes ilimitados'; counterElement.className = 'badge bg-success qr-counter'; console.log('Set counter to: QR Codes ilimitados'); } else { console.log('Counter element not found'); } } showError(message) { this.showAlert(message, 'danger'); } showSuccess(message) { this.showAlert(message, 'success'); } showAlert(message, type) { // Create toast container if doesn't exist let toastContainer = document.getElementById('toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'toast-container'; toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'; toastContainer.style.zIndex = '1060'; document.body.appendChild(toastContainer); } // Create toast element const toast = document.createElement('div'); toast.className = `toast align-items-center text-bg-${type} border-0`; toast.setAttribute('role', 'alert'); toast.innerHTML = `
${message}
`; toastContainer.appendChild(toast); // Show toast const bsToast = new bootstrap.Toast(toast, { delay: type === 'success' ? 3000 : 5000, autohide: true }); bsToast.show(); // Remove from DOM after hidden toast.addEventListener('hidden.bs.toast', () => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }); } 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'); // FIXED: Don't disable content field initially - let enableContentFields handle it // The field should be available for typing once a type is selected // Ocultar interface vCard if (vcardInterface) { vcardInterface.style.display = 'none'; } this.updateGenerateButton(); // Setup URL validation event listeners this.setupURLValidationListeners(); // Setup hint auto-hide timer this.setupHintAutoHide(); } setupHintAutoHide() { const hint = document.getElementById('type-selection-hint'); if (hint) { // Auto-hide after 30 seconds setTimeout(() => { if (hint && !hint.classList.contains('fade-out')) { hint.classList.add('fade-out'); // Remove from DOM after animation setTimeout(() => { if (hint.parentNode) { hint.style.display = 'none'; } }, 1000); // 1s for fade-out animation } }, 30000); // 30 seconds } } setupURLValidationListeners() { const contentField = document.getElementById('qr-content'); if (!contentField) return; // Auto-fix URL on blur (when user leaves the field) contentField.addEventListener('blur', () => { const type = document.getElementById('qr-type')?.value; if (type === 'url' && contentField.value.trim()) { const originalValue = contentField.value.trim(); const fixedValue = this.autoFixURL(originalValue); if (originalValue !== fixedValue) { contentField.value = fixedValue; console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue); // Revalidar após correção this.updateGenerateButton(); } } }); // Real-time validation with debounce contentField.addEventListener('input', () => { const type = document.getElementById('qr-type')?.value; if (type === 'url') { // Debounce para não validar a cada caractere clearTimeout(contentField.validationTimeout); contentField.validationTimeout = setTimeout(() => { this.updateGenerateButton(); }, 500); } }); } handleTypeSelection(type) { this.selectedType = type; if (type) { this.removeInitialHighlight(); // Sempre habilitar campos de conteúdo após selecionar tipo this.enableContentFields(type); // Show guidance toast for the selected type this.showTypeGuidanceToast(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'); const dynamicQRSection = document.getElementById('dynamic-qr-section'); const urlPreview = document.getElementById('url-preview'); // 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 (dynamicQRSection) dynamicQRSection.style.display = 'none'; if (urlPreview) urlPreview.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 if (type === 'url') { if (dynamicQRSection) dynamicQRSection.style.display = 'block'; if (urlPreview) urlPreview.style.display = 'block'; // CRITICAL FIX: Enable content field for URL type const qrContent = document.getElementById('qr-content'); if(qrContent) { qrContent.disabled = false; } } 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) { // FIXED: Don't disable content field, just clear it 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'; } } } // ============================================ // URL VALIDATION FUNCTIONS // ============================================ isValidURL(url) { if (!url || typeof url !== 'string') return false; const trimmedUrl = url.trim(); // Verificar se começa com http:// ou https:// const hasProtocol = trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://'); // Verificar se tem pelo menos um ponto const hasDot = trimmedUrl.includes('.'); // Verificar se não é só o protocolo const hasContent = trimmedUrl.length > 8; // mais que "https://" // Regex mais robusta const urlRegex = /^https?:\/\/.+\..+/i; return hasProtocol && hasDot && hasContent && urlRegex.test(trimmedUrl); } autoFixURL(url) { if (!url || typeof url !== 'string') return ''; let fixedUrl = url.trim(); // Se não tem protocolo, adicionar https:// if (!fixedUrl.startsWith('http://') && !fixedUrl.startsWith('https://')) { fixedUrl = 'https://' + fixedUrl; } return fixedUrl; } showValidationError(message) { let errorDiv = document.getElementById('url-validation-error'); if (!errorDiv) { // Criar div de erro se não existir errorDiv = document.createElement('div'); errorDiv.id = 'url-validation-error'; errorDiv.className = 'alert alert-danger mt-2'; errorDiv.style.display = 'none'; // Inserir após o campo de conteúdo const contentGroup = document.getElementById('content-group'); if (contentGroup) { contentGroup.appendChild(errorDiv); } } errorDiv.innerHTML = ` ${message}`; errorDiv.style.display = 'block'; } clearValidationError() { const errorDiv = document.getElementById('url-validation-error'); if (errorDiv) { errorDiv.style.display = 'none'; } } updateGenerateButton() { const generateBtn = document.getElementById('generate-btn'); if (!generateBtn) return; let isValid = false; const type = this.selectedType; if (type === 'url') { const contentField = document.getElementById('qr-content'); const content = contentField?.value || ''; const trimmedContent = content.trim(); if (!trimmedContent) { this.showValidationError('URL é obrigatória'); if (contentField) { contentField.classList.remove('is-valid'); contentField.classList.add('is-invalid'); } isValid = false; } else if (!this.isValidURL(trimmedContent)) { this.showValidationError('URL deve começar com http:// ou https:// e conter pelo menos um ponto (ex: https://google.com)'); if (contentField) { contentField.classList.remove('is-valid'); contentField.classList.add('is-invalid'); } isValid = false; } else { this.clearValidationError(); if (contentField) { contentField.classList.remove('is-invalid'); contentField.classList.add('is-valid'); } isValid = true; } } else if (type === 'text') { const contentField = document.getElementById('qr-content'); const content = contentField?.value || ''; isValid = content.trim().length >= 3; this.clearValidationError(); // Clear any URL validation errors if (contentField) { contentField.classList.remove('is-valid', 'is-invalid'); } } 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'); const hint = document.getElementById('type-selection-hint'); if (typeField && typeField.classList.contains('qr-type-highlight')) { typeField.classList.remove('qr-type-highlight'); } if (hint && !hint.classList.contains('fade-out')) { hint.classList.add('fade-out'); // Hide after animation setTimeout(() => { if (hint.parentNode) { hint.style.display = 'none'; } }, 1000); } } showTypeGuidanceToast(type) { // Get localized message based on QR type const messages = this.getTypeGuidanceMessages(); const message = messages[type]; if (message) { // Create info toast with 30 second duration const toast = this.createGuidanceToast(message); this.showGuidanceToast(toast, 30000); // 30 seconds } } getTypeGuidanceMessages() { // These should match the resource file keys // In a real implementation, these would come from server-side localization // For now, we'll use the JavaScript language strings or fallback to Portuguese return { 'url': document.querySelector('[data-type-guide-url]')?.textContent || '🌐 Para gerar QR de URL, digite o endereço completo (ex: https://google.com)', 'vcard': document.querySelector('[data-type-guide-vcard]')?.textContent || '👤 Para cartão de visita, preencha nome, telefone e email nos campos abaixo', 'wifi': document.querySelector('[data-type-guide-wifi]')?.textContent || '📶 Para WiFi, informe nome da rede, senha e tipo de segurança', 'sms': document.querySelector('[data-type-guide-sms]')?.textContent || '💬 Para SMS, digite o número do destinatário e a mensagem', 'email': document.querySelector('[data-type-guide-email]')?.textContent || '📧 Para email, preencha destinatário, assunto e mensagem (opcional)', 'text': document.querySelector('[data-type-guide-text]')?.textContent || '📝 Para texto livre, digite qualquer conteúdo que desejar' }; } createGuidanceToast(message) { const toast = document.createElement('div'); toast.className = 'toast align-items-center text-bg-info border-0'; toast.setAttribute('role', 'alert'); toast.style.minWidth = '400px'; toast.innerHTML = `
${message}
`; return toast; } showGuidanceToast(toast, duration = 30000) { // Create toast container if doesn't exist (positioned below header stats) let toastContainer = document.getElementById('guidance-toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'guidance-toast-container'; toastContainer.className = 'toast-container position-relative w-100 d-flex justify-content-center mb-3'; toastContainer.style.zIndex = '1060'; // Try to find existing toast container first (if manually positioned) const existingContainer = document.getElementById('guidance-toast-container'); if (existingContainer) { // Use existing container that was manually positioned toastContainer = existingContainer; } else { // Find the main content area and insert after hero section const mainElement = document.querySelector('main[role="main"]'); if (mainElement) { // Insert at the beginning of main content mainElement.insertBefore(toastContainer, mainElement.firstChild); } else { // Fallback: find container and insert at top const container = document.querySelector('.container'); if (container) { container.insertBefore(toastContainer, container.firstChild); } } } } toastContainer.appendChild(toast); // Show toast with custom duration const bsToast = new bootstrap.Toast(toast, { delay: duration, autohide: true }); bsToast.show(); // Remove from DOM after hidden toast.addEventListener('hidden.bs.toast', () => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }); } // ============================================ // 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(); } // Rate Limiting Methods initializeRateLimiting() { // Wait a bit for DOM to be fully ready setTimeout(() => { this.updateRateDisplayCounter(); }, 100); } checkRateLimit() { // Check if user is logged in (unlimited access) const userStatus = document.getElementById('user-premium-status'); console.log('🔍 Rate limit check - User status:', userStatus ? userStatus.value : 'not found'); if (userStatus && userStatus.value === 'logged-in') { console.log('✅ User is logged in - unlimited access'); return true; // Unlimited for logged users } // For anonymous users, check daily limit const today = new Date().toDateString(); const cookieName = 'qr_daily_count'; const rateLimitData = this.getCookie(cookieName); console.log('📅 Today:', today); console.log('🍪 Cookie data:', rateLimitData); let currentData = { date: today, count: 0 }; if (rateLimitData) { try { currentData = JSON.parse(rateLimitData); console.log('📊 Parsed data:', currentData); // Reset count if it's a new day if (currentData.date !== today) { console.log('🔄 New day detected, resetting count'); currentData = { date: today, count: 0 }; } } catch (e) { console.log('❌ Error parsing cookie:', e); currentData = { date: today, count: 0 }; } } else { console.log('🆕 No cookie found, starting fresh'); } console.log('📈 Current count:', currentData.count); // Check if limit exceeded (don't increment here) if (currentData.count >= 3) { console.log('🚫 Rate limit exceeded'); this.showRateLimitError(); return false; } console.log('✅ Rate limit check passed'); return true; } incrementRateLimit() { // Only increment after successful QR generation const userStatus = document.getElementById('user-premium-status'); if (userStatus && userStatus.value === 'logged-in') { return; // No limits for logged users } const today = new Date().toDateString(); const cookieName = 'qr_daily_count'; const rateLimitData = this.getCookie(cookieName); let currentData = { date: today, count: 0 }; if (rateLimitData) { try { currentData = JSON.parse(rateLimitData); if (currentData.date !== today) { currentData = { date: today, count: 0 }; } } catch (e) { currentData = { date: today, count: 0 }; } } // Increment count and save currentData.count++; this.setCookie(cookieName, JSON.stringify(currentData), 1); // Update display counter this.updateRateDisplayCounter(); } showRateLimitError() { const message = this.getLocalizedString('RateLimitExceeded') || 'Daily limit reached! Login for unlimited access.'; this.showError(message); } updateRateDisplayCounter() { const counterElement = document.querySelector('.qr-counter'); if (!counterElement) return; // Check user status const userStatus = document.getElementById('user-premium-status'); if (userStatus && userStatus.value === 'premium') { // Premium users have unlimited QRs const unlimitedText = this.getLocalizedString('UnlimitedToday'); counterElement.textContent = unlimitedText; counterElement.className = 'badge bg-success qr-counter'; return; } else if (userStatus && userStatus.value === 'logged-in') { // Free logged users - we need to get their actual remaining count this.updateLoggedUserCounter(); return; } // For anonymous users, show remaining count const today = new Date().toDateString(); const cookieName = 'qr_daily_count'; const rateLimitData = this.getCookie(cookieName); let remaining = 3; if (rateLimitData) { try { const currentData = JSON.parse(rateLimitData); if (currentData.date === today) { remaining = Math.max(0, 3 - currentData.count); } } catch (e) { remaining = 3; } } const remainingText = this.getLocalizedString('QRCodesRemainingToday'); counterElement.textContent = `${remaining} ${remainingText}`; } async updateLoggedUserCounter() { const counterElement = document.querySelector('.qr-counter'); if (!counterElement) return; try { // Fetch user's remaining QR count from the server const response = await fetch('/api/QR/GetUserStats', { method: 'GET', headers: { 'Content-Type': 'application/json', } }); if (response.ok) { const data = await response.json(); if (data.isPremium) { // Premium user - show unlimited const unlimitedText = this.getLocalizedString('UnlimitedToday'); counterElement.textContent = unlimitedText; counterElement.className = 'badge bg-success qr-counter'; } else { // Free user - show remaining count const remaining = data.remainingCount || 0; const remainingText = this.getLocalizedString('QRCodesRemainingToday'); if (remaining !== -1) { counterElement.textContent = `${remaining} ${remainingText}`; counterElement.className = 'badge bg-primary qr-counter'; if (remaining <= 3) { counterElement.className = 'badge bg-warning qr-counter'; } if (remaining === 0) { counterElement.className = 'badge bg-danger qr-counter'; } } else { const unlimitedText = this.getLocalizedString('UnlimitedToday'); counterElement.textContent = unlimitedText; counterElement.className = 'badge bg-success qr-counter'; } } } else { // Fallback to showing 50 remaining if API fails const remainingText = this.getLocalizedString('QRCodesRemainingToday'); counterElement.textContent = `50 ${remainingText}`; counterElement.className = 'badge bg-primary qr-counter'; } } catch (error) { console.warn('Failed to fetch user stats:', error); // Fallback to showing 50 remaining if API fails const remainingText = this.getLocalizedString('QRCodesRemainingToday'); counterElement.textContent = `50 ${remainingText}`; counterElement.className = 'badge bg-primary qr-counter'; } } getLocalizedString(key) { // Try to get from server-side localization first const element = document.querySelector(`[data-localized="${key}"]`); if (element) { const text = element.textContent.trim() || element.value; if (text) return text; } // Fallback to client-side strings if (this.languageStrings[this.currentLang] && this.languageStrings[this.currentLang][key]) { return this.languageStrings[this.currentLang][key]; } // Default fallbacks based on key const defaults = { 'UnlimitedToday': 'Ilimitado hoje', 'QRCodesRemainingToday': 'QR codes restantes hoje', 'RateLimitExceeded': 'Limite diário atingido! Faça login para acesso ilimitado.' }; return defaults[key] || key; } setCookie(name, value, days) { const expires = new Date(); expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Strict`; } getCookie(name) { const nameEQ = name + "="; const ca = document.cookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) === ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); } return null; } // Debug/reset function - call from console if needed resetRateLimit() { // Multiple ways to clear the cookie document.cookie = 'qr_daily_count=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = 'qr_daily_count=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=' + window.location.hostname + ';'; document.cookie = 'qr_daily_count=; max-age=0; path=/;'; // Also clear from storage if exists if (typeof Storage !== "undefined") { localStorage.removeItem('qr_daily_count'); sessionStorage.removeItem('qr_daily_count'); } this.updateRateDisplayCounter(); console.log('🧹 Rate limit completely reset!'); console.log('🍪 All cookies:', document.cookie); } } // 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(); window.dynamicQRManager = new DynamicQRManager(); // Initialize AdSense if necessary if (window.adsbygoogle && document.querySelector('.adsbygoogle')) { (adsbygoogle = window.adsbygoogle || []).push({}); } }); class DynamicQRManager { constructor() { this.initializeDynamicQR(); } initializeDynamicQR() { // Verificar se usuário é premium this.isPremium = this.checkUserPremium(); // Configurar interface baseado no status this.setupPremiumInterface(); // Event listeners this.setupEventListeners(); // Preview inicial this.updatePreview(); } checkUserPremium() { // IMPLEMENTAR: verificar se usuário atual é premium // Pode ser via elemento hidden, variável global, ou chamada AJAX // Exemplo 1: Via elemento hidden const premiumStatus = document.getElementById('user-premium-status'); if (premiumStatus) { return premiumStatus.value === 'true'; } // Exemplo 2: Via variável global if (window.userInfo && window.userInfo.isPremium !== undefined) { return window.userInfo.isPremium; } // Fallback: assumir não-premium return false; } setupPremiumInterface() { const toggleContainer = document.getElementById('dynamic-toggle-container'); const upgradePrompt = document.querySelector('.premium-upgrade-prompt'); if (this.isPremium) { // Usuário premium: mostrar toggle funcional if(toggleContainer) toggleContainer.style.display = 'block'; if (upgradePrompt) upgradePrompt.style.display = 'none'; } else { // Usuário não-premium: mostrar prompt de upgrade if(toggleContainer) toggleContainer.style.display = 'none'; if (upgradePrompt) upgradePrompt.style.display = 'block'; } } setupEventListeners() { // Toggle do QR Dinâmico const dynamicToggle = document.getElementById('qr-dynamic-toggle'); if (dynamicToggle) { dynamicToggle.addEventListener('change', () => { this.updatePreview(); }); } // Campo URL const urlField = document.getElementById('qr-content'); if (urlField) { urlField.addEventListener('input', () => { this.updatePreview(); }); } } updatePreview() { const urlField = document.getElementById('qr-content'); const dynamicToggle = document.getElementById('qr-dynamic-toggle'); const finalUrlDisplay = document.getElementById('final-url-display'); const typeDisplay = document.getElementById('qr-type-display'); if (!urlField || !finalUrlDisplay || !typeDisplay) return; const originalUrl = urlField.value; const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium; if (isDynamic && originalUrl) { // QR Dinâmico: mostra URL do QRRapido finalUrlDisplay.textContent = `https://qrrapido.site/r/{ID_UNICO}`; typeDisplay.textContent = "QR Code Dinâmico com Analytics "; typeDisplay.className = "text-success fw-bold"; } else { // QR Estático: mostra URL original finalUrlDisplay.textContent = originalUrl || 'Digite uma URL...'; typeDisplay.textContent = "QR Code Estático"; typeDisplay.className = "text-muted"; } } // Método para integração com collectFormData() getDynamicQRData() { const dynamicToggle = document.getElementById('qr-dynamic-toggle'); const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium; return { isDynamic: isDynamic, originalUrl: document.getElementById('qr-content').value, requiresPremium: !this.isPremium && isDynamic }; } } 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 }); } };