// QR Rapido Speed Generator class QRRapidoGenerator { constructor() { this.startTime = 0; this.currentQR = null; this.timerInterval = null; this.selectedType = null; this.selectedStyle = null; this.contentValid = false; 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.initializeEvents(); this.checkAdFreeStatus(); this.updateLanguage(); this.updateStatsCounters(); this.initializeProgressiveFlow(); } initializeEvents() { // Form submission with timer const form = document.getElementById('qr-speed-form'); if (form) { form.addEventListener('submit', this.generateQRWithTimer.bind(this)); } // Quick style selection with flow control document.querySelectorAll('input[name="quick-style"]').forEach(radio => { radio.addEventListener('change', (e) => { this.handleStyleSelection(e.target.value); this.applyQuickStyle(e); }); }); // Content field monitoring const contentField = document.getElementById('qr-content'); if (contentField) { contentField.addEventListener('input', (e) => { this.handleContentChange(e.target.value); }); } // 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 control const qrType = document.getElementById('qr-type'); if (qrType) { qrType.addEventListener('change', (e) => { this.handleTypeSelection(e.target.value); this.updateContentHints(); }); } // 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; } // Check if flow is properly completed if (!this.selectedType || !this.selectedStyle || !this.contentValid) { this.showError('Complete todas as etapas antes de gerar o 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; } } // 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); // Handle VCard type if (type === 'vcard') { if (window.vcardGenerator) { const vcardContent = window.vcardGenerator.getVCardContent(); return { data: { type: 'vcard', // Keep as vcard type for tracking content: vcardContent, 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 formData.append('type', document.getElementById('qr-type').value); formData.append('content', document.getElementById('qr-content').value); 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); return { data: { type: document.getElementById('qr-type').value, content: document.getElementById('qr-content').value, 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'; // Add VCard input monitoring for progressive flow this.setupVCardMonitoring(); } 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;Mensaje' } }; const langHints = hints[this.currentLang] || hints['pt-BR']; hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado'; } setupVCardMonitoring() { // Monitor VCard required fields for progressive flow const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email']; requiredFields.forEach(fieldId => { const field = document.getElementById(fieldId); if (field) { field.addEventListener('input', () => { // Use a small delay to allow for validation setTimeout(() => { const isValid = this.validateContent(''); // VCard validation is internal this.contentValid = isValid; if (isValid && this.selectedType && this.selectedStyle) { this.showGenerateButton(); } else { this.hideGenerateButton(); } }, 100); }); } }); } 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); } initializeProgressiveFlow() { // Reset all states to initial disabled state this.selectedType = null; this.selectedStyle = null; this.contentValid = false; // Ensure proper initial state this.disableStyleSelection(); this.disableContentFields(); this.disableAdvancedOptions(); this.hideGenerateButton(); } handleTypeSelection(type) { this.selectedType = type; if (type) { // Remove highlight when type is selected this.removeInitialHighlight(); // Enable style selection this.enableStyleSelection(); // Reset subsequent selections this.selectedStyle = null; this.contentValid = false; this.disableContentFields(); this.hideGenerateButton(); } else { // Disable everything if no type selected this.disableStyleSelection(); this.disableContentFields(); this.disableAdvancedOptions(); this.hideGenerateButton(); } } handleStyleSelection(style) { this.selectedStyle = style; if (style && this.selectedType) { // Enable content fields this.enableContentFields(); // Open advanced panel if 'advanced' style is selected (future enhancement) if (style === 'advanced') { this.openAdvancedCustomization(); } // Reset content validation this.contentValid = false; this.hideGenerateButton(); } } handleContentChange(content) { const isValid = this.validateContent(content); this.contentValid = isValid; if (isValid && this.selectedType && this.selectedStyle) { this.showGenerateButton(); } else { this.hideGenerateButton(); } } validateContent(content) { if (!content) return false; const trimmedContent = content.trim(); // VCard has its own validation if (this.selectedType === 'vcard') { try { if (window.vcardGenerator) { const errors = window.vcardGenerator.validateVCardData(); return errors.length === 0; } } catch (error) { return false; } return false; } // General validation: at least 3 characters return trimmedContent.length >= 3; } enableStyleSelection() { const styleContainer = document.getElementById('style-selector'); const styleInputs = document.querySelectorAll('input[name="quick-style"]'); const styleLabel = styleContainer?.parentElement.querySelector('.form-label'); if (styleContainer) { styleContainer.classList.remove('qr-flow-disabled'); styleContainer.classList.add('qr-flow-enabled', 'qr-flow-ascending'); } if (styleLabel) { styleLabel.classList.remove('qr-flow-disabled'); styleLabel.classList.add('qr-flow-enabled'); } styleInputs.forEach(input => { input.disabled = false; }); } disableStyleSelection() { const styleContainer = document.getElementById('style-selector'); const styleInputs = document.querySelectorAll('input[name="quick-style"]'); const styleLabel = styleContainer?.parentElement.querySelector('.form-label'); if (styleContainer) { styleContainer.classList.remove('qr-flow-enabled', 'qr-flow-ascending'); styleContainer.classList.add('qr-flow-disabled'); } if (styleLabel) { styleLabel.classList.remove('qr-flow-enabled'); styleLabel.classList.add('qr-flow-disabled'); } styleInputs.forEach(input => { input.disabled = true; input.checked = false; }); // Reset to default selection const defaultStyle = document.getElementById('style-classic'); if (defaultStyle) { defaultStyle.checked = true; } } enableContentFields() { const contentTextarea = document.getElementById('qr-content'); const contentLabel = contentTextarea?.parentElement.querySelector('.form-label'); const vcardInterface = document.getElementById('vcard-interface'); if (contentTextarea) { contentTextarea.classList.remove('qr-flow-disabled'); contentTextarea.classList.add('qr-flow-enabled', 'qr-flow-ascending'); contentTextarea.disabled = false; } if (contentLabel) { contentLabel.classList.remove('qr-flow-disabled'); contentLabel.classList.add('qr-flow-enabled'); } if (vcardInterface && this.selectedType === 'vcard') { vcardInterface.classList.remove('qr-flow-disabled'); vcardInterface.classList.add('qr-flow-enabled', 'qr-flow-ascending'); // Enable VCard form fields const vcardInputs = vcardInterface.querySelectorAll('input, textarea, select'); vcardInputs.forEach(input => { input.disabled = false; }); } // Enable advanced customization this.enableAdvancedOptions(); } disableContentFields() { const contentTextarea = document.getElementById('qr-content'); const contentLabel = contentTextarea?.parentElement.querySelector('.form-label'); const vcardInterface = document.getElementById('vcard-interface'); if (contentTextarea) { contentTextarea.classList.remove('qr-flow-enabled', 'qr-flow-ascending'); contentTextarea.classList.add('qr-flow-disabled'); contentTextarea.disabled = true; contentTextarea.value = ''; } if (contentLabel) { contentLabel.classList.remove('qr-flow-enabled'); contentLabel.classList.add('qr-flow-disabled'); } if (vcardInterface) { vcardInterface.classList.remove('qr-flow-enabled', 'qr-flow-ascending'); vcardInterface.classList.add('qr-flow-disabled'); // Disable VCard form fields const vcardInputs = vcardInterface.querySelectorAll('input, textarea, select'); vcardInputs.forEach(input => { input.disabled = true; input.value = ''; input.checked = false; }); } this.disableAdvancedOptions(); } enableAdvancedOptions() { const advancedAccordion = document.getElementById('customization-accordion'); if (advancedAccordion) { advancedAccordion.classList.remove('qr-flow-disabled'); advancedAccordion.classList.add('qr-flow-enabled', 'qr-flow-ascending'); // Enable form controls inside accordion const advancedInputs = advancedAccordion.querySelectorAll('input, select'); advancedInputs.forEach(input => { input.disabled = false; }); } } disableAdvancedOptions() { const advancedAccordion = document.getElementById('customization-accordion'); if (advancedAccordion) { advancedAccordion.classList.remove('qr-flow-enabled', 'qr-flow-ascending'); advancedAccordion.classList.add('qr-flow-disabled'); // Disable form controls inside accordion const advancedInputs = advancedAccordion.querySelectorAll('input, select'); advancedInputs.forEach(input => { input.disabled = true; }); } } showGenerateButton() { const buttonContainer = document.getElementById('generate-button-container'); const generateBtn = document.getElementById('generate-btn'); if (buttonContainer) { buttonContainer.style.display = 'block'; buttonContainer.classList.add('show'); } if (generateBtn) { generateBtn.disabled = false; } } hideGenerateButton() { const buttonContainer = document.getElementById('generate-button-container'); const generateBtn = document.getElementById('generate-btn'); if (buttonContainer) { buttonContainer.classList.remove('show'); setTimeout(() => { if (!buttonContainer.classList.contains('show')) { buttonContainer.style.display = 'none'; } }, 400); } if (generateBtn) { generateBtn.disabled = true; } } openAdvancedCustomization() { const advancedPanel = document.getElementById('customization-panel'); const accordionButton = document.querySelector('[data-bs-target="#customization-panel"]'); if (advancedPanel && accordionButton) { // Use Bootstrap's collapse to open the panel const collapse = new bootstrap.Collapse(advancedPanel, { show: true }); } } 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); } } // 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'); } } } // Initialize when DOM loads 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({}); } }); // VCard Generator Class class VCardGenerator { constructor() { this.initializeVCardInterface(); } 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\n'; // Nome (obrigatório) if (data.name) { const nameParts = data.name.trim().split(' '); const firstName = nameParts[0] || ''; const lastName = nameParts.slice(1).join(' ') || ''; vcard += `N:${lastName};${firstName}\n`; vcard += `FN:${data.name}\n`; } // Empresa if (data.company) vcard += `ORG:${data.company}\n`; // Título if (data.title) vcard += `TITLE:${data.title}\n`; // Endereço if (data.address || data.city || data.state || data.zip) { const addr = `;;${data.address || ''};${data.city || ''};${data.state || ''};${data.zip || ''};Brasil`; 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 /^[^\s@]+@[^\s@]+\.[^\s@]+$/.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'; }); } };