QrRapido/wwwroot/js/qr-speed-generator.js
Ricardo Carneiro 70fbdaa3c2
Some checks failed
Deploy QR Rapido / test (push) Successful in 3m34s
Deploy QR Rapido / build-and-push (push) Failing after 8s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
feat: opacidade enquanto não selecionar o tipo.
2025-08-04 19:45:46 -03:00

3289 lines
125 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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));
}
// Logo size slider preview em tempo real
const logoSizeSlider = document.getElementById('logo-size-slider');
if (logoSizeSlider && typeof this.updateLogoReadabilityPreview === 'function') {
logoSizeSlider.addEventListener('input', this.updateLogoReadabilityPreview.bind(this));
}
// Logo colorize toggle preview em tempo real
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
if (logoColorizeToggle && typeof this.updateLogoReadabilityPreview === 'function') {
logoColorizeToggle.addEventListener('change', this.updateLogoReadabilityPreview.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) {
let timer;
qrContent.addEventListener('input', (e) => {
clearTimeout(timer);
timer = setTimeout(() => {
this.handleContentChange(e.target.value);
this.updateGenerateButton();
}, 300);
});
}
// 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'
};
}
console.log('🚀 Enviando requisição:', {
endpoint: requestData.endpoint,
isMultipart: requestData.isMultipart,
hasLogo: requestData.isMultipart
});
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);
console.log('✅ QR code recebido do backend:', {
success: result.success,
hasBase64: !!result.qrCodeBase64,
base64Length: result.qrCodeBase64?.length || 0,
generationTime: generationTime + 's'
});
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('<br>'));
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('<br>'));
return false;
}
return true;
} else if (qrType === 'sms') {
const errors = window.smsGenerator.validateSMSData();
if (errors.length > 0) {
this.showError(errors.join('<br>'));
return false;
}
return true;
} else if (qrType === 'email') {
const errors = window.emailGenerator.validateEmailData();
if (errors.length > 0) {
this.showError(errors.join('<br>'));
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);
// Check if logo is selected FIRST - this determines the endpoint
const logoUpload = document.getElementById('logo-upload');
const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0];
// Get content based on type
let content, actualType;
if (type === 'url') {
let dynamicData;
if (typeof this.getDynamicQRData === 'function') {
dynamicData = this.getDynamicQRData();
} else {
// Fallback se a função não existir
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
dynamicData = {
isDynamic: isDynamic,
originalUrl: document.getElementById('qr-content').value,
requiresPremium: !this.isPremium && isDynamic
};
}
if (dynamicData.requiresPremium) {
throw new Error('QR Dinâmico é exclusivo para usuários Premium. Faça upgrade para usar analytics.');
}
content = dynamicData.originalUrl;
actualType = 'url';
} else if (type === 'wifi') {
content = window.wifiGenerator.generateWiFiString();
actualType = 'text'; // WiFi is treated as text in the backend
} else if (type === 'sms') {
content = window.smsGenerator.generateSMSString();
actualType = 'text';
} else if (type === 'email') {
content = window.emailGenerator.generateEmailString();
actualType = 'text';
} else if (type === 'vcard') {
if (!window.vcardGenerator) {
throw new Error('VCard generator não está disponível');
}
content = window.vcardGenerator.getVCardContent();
actualType = 'vcard'; // Keep as vcard type for tracking
} else {
// Default case - get content from input
content = document.getElementById('qr-content').value;
actualType = type;
}
// Prepare final content
const encodedContent = this.prepareContentForQR(content, actualType);
// Get colors
const userPrimaryColor = document.getElementById('primary-color').value;
const userBackgroundColor = document.getElementById('bg-color').value;
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
// Common data for both endpoints
const commonData = {
type: actualType,
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
};
// Add dynamic QR data if it's a URL type
if (type === 'url') {
let dynamicData;
if (typeof this.getDynamicQRData === 'function') {
dynamicData = this.getDynamicQRData();
} else {
// Fallback se a função não existir
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
dynamicData = {
isDynamic: isDynamic,
originalUrl: document.getElementById('qr-content').value,
requiresPremium: !this.isPremium && isDynamic
};
}
commonData.isDynamic = dynamicData.isDynamic;
}
if (hasLogo) {
// Use FormData for requests with logo
const formData = new FormData();
// Add all common data to FormData
Object.keys(commonData).forEach(key => {
formData.append(key, commonData[key]);
});
// NOVOS PARÂMETROS DE LOGO APRIMORADO
const logoSettings = this.getLogoSettings();
formData.append('LogoSizePercent', logoSettings.logoSizePercent.toString());
formData.append('ApplyLogoColorization', logoSettings.applyColorization.toString());
// Add logo file
formData.append('logo', logoUpload.files[0]);
// CORREÇÃO: Log detalhado antes de enviar
const logoSizeSlider = document.getElementById('logo-size-slider');
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
console.log('🎨 Preparando FormData com logo:', {
logoFile: logoUpload.files[0].name,
logoSize: logoUpload.files[0].size + ' bytes',
LogoSizePercent: logoSettings.logoSizePercent,
ApplyLogoColorization: logoSettings.applyColorization,
checkboxChecked: logoColorizeToggle?.checked,
endpoint: '/api/QR/GenerateRapidWithLogo'
});
return {
data: formData,
isMultipart: true,
endpoint: '/api/QR/GenerateRapidWithLogo'
};
} else {
// Usar JSON para QR sem logo (método original)
console.log('📝 Preparando JSON sem logo - endpoint: /api/QR/GenerateRapid');
return {
data: commonData,
isMultipart: false,
endpoint: '/api/QR/GenerateRapid'
};
}
}
// Nova função para coletar configurações de logo
getLogoSettings() {
const logoSizeSlider = document.getElementById('logo-size-slider');
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
return {
logoSizePercent: parseInt(logoSizeSlider?.value || '20'),
applyColorization: logoColorizeToggle?.checked || false
};
}
// Função auxiliar para obter conteúdo baseado no tipo
getContentForType(type) {
if (type === 'url') {
let dynamicData;
if (typeof this.getDynamicQRData === 'function') {
dynamicData = this.getDynamicQRData();
} else {
// Fallback se a função não existir
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
dynamicData = {
isDynamic: isDynamic,
originalUrl: document.getElementById('qr-content').value,
requiresPremium: !this.isPremium && isDynamic
};
}
return dynamicData?.originalUrl || document.getElementById('qr-content').value;
} else if (type === 'wifi') {
return window.wifiGenerator?.generateWiFiString() || '';
} else if (type === 'vcard') {
return window.vcardGenerator?.getVCardContent() || '';
} else if (type === 'sms') {
return window.smsGenerator?.generateSMSString() || '';
} else if (type === 'email') {
return window.emailGenerator?.generateEmailString() || '';
} else {
return document.getElementById('qr-content').value || '';
}
}
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;
// CORREÇÃO SIMPLES: Remover cache buster que quebrava a imagem e adicionar debug
const imageUrl = `data:image/png;base64,${result.qrCodeBase64}`;
previewDiv.innerHTML = `
<img src="${imageUrl}"
class="img-fluid border rounded shadow-sm"
alt="QR Code gerado em ${generationTime}s"
style="image-rendering: crisp-edges;">
`;
// Exibir análise de legibilidade do logo se disponível
if (result.readabilityInfo && typeof this.displayReadabilityAnalysis === 'function') {
this.displayReadabilityAnalysis(result.readabilityInfo);
}
// CORREÇÃO: Log para debug - verificar se QR code tem logo
const logoUpload = document.getElementById('logo-upload');
const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0;
console.log('✅ QR Code exibido:', {
hasLogo: hasLogo,
logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum',
generationTime: generationTime + 's',
imageSize: result.qrCodeBase64.length + ' chars',
readabilityScore: result.readabilityInfo?.readabilityScore
});
// 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,
readabilityInfo: result.readabilityInfo
};
// 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 = `
<div class="text-center p-4">
<div class="spinner-border text-primary mb-3" role="status"></div>
<p class="text-muted">${this.languageStrings[this.currentLang].generating}</p>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
style="width: 100%"></div>
</div>
</div>
`;
}
}
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 = `
<span class="badge ${badgeClass}">
<i class="fas fa-bolt"></i> ${badgeText}
</span>
`;
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');
const logoPreviewImage = document.getElementById('logo-preview-image');
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;
}
// Create FileReader to show image preview
const reader = new FileReader();
reader.onload = (e) => {
if (logoPreviewImage) {
logoPreviewImage.src = e.target.result;
const logoVisualPreview = document.getElementById('logo-visual-preview');
if (logoVisualPreview) logoVisualPreview.style.display = 'block';
}
// Show file information
if (logoFilename) {
const fileSizeKB = Math.round(file.size / 1024);
logoFilename.textContent = `${file.name} (${fileSizeKB}KB)`;
}
logoPreview?.classList.remove('d-none');
// CORREÇÃO: Log detalhado do logo selecionado
console.log('📁 Logo selecionado:', {
name: file.name,
size: Math.round(file.size / 1024) + 'KB',
type: file.type,
timestamp: new Date().toLocaleTimeString()
});
// Atualizar preview de legibilidade
if (typeof this.updateLogoReadabilityPreview === 'function') {
this.updateLogoReadabilityPreview();
}
};
reader.readAsDataURL(file);
} else {
// Limpar preview quando arquivo removido
logoPreview?.classList.add('d-none');
const logoVisualPreview = document.getElementById('logo-visual-preview');
if (logoVisualPreview) logoVisualPreview.style.display = 'none';
if (logoPreviewImage) logoPreviewImage.src = '';
// Limpar preview de legibilidade
if (typeof this.clearReadabilityPreview === 'function') {
this.clearReadabilityPreview();
}
console.log('🗑️ Logo removido');
}
}
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 = `<i class="fas fa-stopwatch"></i> ${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 = `<?xml version="1.0" encoding="UTF-8"?>
<svg width="${img.width}" height="${img.height}" xmlns="http://www.w3.org/2000/svg">
<image width="${img.width}" height="${img.height}" href="data:image/png;base64,${base64Data}"/>
</svg>`;
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 = `
<i class="fas fa-crown text-warning"></i>
<strong>Sessão sem anúncios ativa!</strong>
Tempo restante: <span class="ad-free-countdown">${this.formatTime(timeRemaining)}</span>
<a href="/Premium/Upgrade" class="btn btn-sm btn-warning ms-2">Tornar Permanente</a>
`;
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 = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title">
<i class="fas fa-crown"></i> Upgrade para Premium
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>${message}</p>
<div class="row">
<div class="col-md-6">
<h6>Plano Atual (Free)</h6>
<ul class="list-unstyled">
<li>❌ Limite de 10 QR/dia</li>
<li>❌ Anúncios</li>
<li>✅ QR básicos</li>
</ul>
</div>
<div class="col-md-6">
<h6>Premium (R$ 19,90/mês)</h6>
<ul class="list-unstyled">
<li>✅ QR ilimitados</li>
<li>✅ Sem anúncios</li>
<li>✅ QR dinâmicos</li>
<li>✅ Analytics</li>
<li>✅ Suporte prioritário</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<a href="/Premium/Upgrade" class="btn btn-warning">
<i class="fas fa-crown"></i> Fazer Upgrade
</a>
</div>
</div>
</div>
`;
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 = `
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-${type === 'danger' ? 'exclamation-triangle' : 'check-circle'}"></i>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
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 = `<small><i class="fas fa-exclamation-triangle"></i> ${message}</small>`;
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 = `
<div class="d-flex">
<div class="toast-body fw-medium">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
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' || userStatus.value === 'premium')) {
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
};
}
displayReadabilityAnalysis(readabilityInfo) {
if (!readabilityInfo || !readabilityInfo.hasLogo) return;
// Criar ou encontrar container para análise de legibilidade
let analysisContainer = document.getElementById('readability-analysis');
if (!analysisContainer) {
// Criar container se não existir
analysisContainer = document.createElement('div');
analysisContainer.id = 'readability-analysis';
analysisContainer.className = 'mt-3 p-3 border rounded bg-light';
// Inserir após o preview do QR code
const previewDiv = document.getElementById('qr-preview');
if (previewDiv && previewDiv.parentNode) {
previewDiv.parentNode.insertBefore(analysisContainer, previewDiv.nextSibling);
}
}
// Determinar classe de cor baseada no score
const getScoreClass = (score) => {
if (score >= 85) return 'text-success';
if (score >= 70) return 'text-info';
if (score >= 55) return 'text-warning';
return 'text-danger';
};
// Determinar ícone baseado no nível de dificuldade
const getIconClass = (score) => {
if (score >= 85) return 'fas fa-check-circle';
if (score >= 70) return 'fas fa-thumbs-up';
if (score >= 55) return 'fas fa-exclamation-triangle';
return 'fas fa-exclamation-circle';
};
// Gerar lista de dicas
const tipsHtml = readabilityInfo.tips && readabilityInfo.tips.length > 0
? `<div class="collapse mt-2" id="readabilityTips">
<ul class="list-unstyled mb-0">
${readabilityInfo.tips.map(tip => `<li class="mb-1"><small>${tip}</small></li>`).join('')}
</ul>
</div>`
: '';
// Criar HTML da análise
const scoreClass = getScoreClass(readabilityInfo.readabilityScore);
const iconClass = getIconClass(readabilityInfo.readabilityScore);
analysisContainer.innerHTML = `
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<i class="${iconClass} ${scoreClass} me-2"></i>
<div>
<h6 class="mb-1 ${scoreClass}">Análise de Legibilidade</h6>
<p class="mb-0 small">${readabilityInfo.userMessage}</p>
</div>
</div>
<div class="text-end">
<div class="badge bg-secondary">${readabilityInfo.readabilityScore}/100</div>
${readabilityInfo.tips && readabilityInfo.tips.length > 0
? `<button class="btn btn-sm btn-outline-secondary ms-2" type="button"
data-bs-toggle="collapse" data-bs-target="#readabilityTips">
<i class="fas fa-lightbulb"></i> Dicas
</button>`
: ''}
</div>
</div>
${tipsHtml}
`;
// Animação de entrada suave
analysisContainer.style.opacity = '0';
analysisContainer.style.transform = 'translateY(-10px)';
setTimeout(() => {
analysisContainer.style.transition = 'all 0.3s ease';
analysisContainer.style.opacity = '1';
analysisContainer.style.transform = 'translateY(0)';
}, 100);
console.log('📊 Análise de legibilidade exibida:', {
score: readabilityInfo.readabilityScore,
level: readabilityInfo.difficultyLevel,
logoSize: readabilityInfo.logoSizePercent + '%',
tipsCount: readabilityInfo.tips?.length || 0
});
}
updateLogoReadabilityPreview() {
const logoUpload = document.getElementById('logo-upload');
const logoSizeSlider = document.getElementById('logo-size-slider');
// Só mostrar preview se há logo selecionado
if (!logoUpload || !logoUpload.files || logoUpload.files.length === 0) {
this.clearReadabilityPreview();
return;
}
const logoSize = parseInt(logoSizeSlider?.value || '20');
// Calcular score estimado baseado apenas no tamanho (simplificado para preview)
let estimatedScore = 100;
if (logoSize > 20) {
estimatedScore -= (logoSize - 20) * 2;
}
estimatedScore = Math.max(40, estimatedScore); // Mínimo de 40
// Simular análise básica para preview
const previewInfo = {
hasLogo: true,
logoSizePercent: logoSize,
readabilityScore: estimatedScore,
difficultyLevel: this.getDifficultyLevelFromScore(estimatedScore),
userMessage: this.generatePreviewMessage(estimatedScore, logoSize),
tips: this.generatePreviewTips(logoSize, estimatedScore)
};
this.displayReadabilityPreview(previewInfo);
}
getDifficultyLevelFromScore(score) {
if (score >= 85) return 'VeryEasy';
if (score >= 70) return 'Easy';
if (score >= 55) return 'Medium';
if (score >= 40) return 'Hard';
return 'VeryHard';
}
generatePreviewMessage(score, logoSize) {
if (score >= 85) {
return `✅ Estimativa: Excelente legibilidade com logo de ${logoSize}%`;
} else if (score >= 70) {
return `🟢 Estimativa: Boa legibilidade com logo de ${logoSize}%`;
} else if (score >= 55) {
return `⚠️ Estimativa: Legibilidade moderada com logo de ${logoSize}%`;
} else {
return `🟡 Estimativa: Legibilidade baixa com logo de ${logoSize}%`;
}
}
generatePreviewTips(logoSize, score) {
const tips = [];
if (logoSize > 22) {
tips.push(`📏 Considere reduzir para 18-20% (atual: ${logoSize}%)`);
}
if (score < 70) {
tips.push('💡 Use boa iluminação ao escanear');
}
tips.push('🎨 PNG transparente melhora a legibilidade');
tips.push('📱 Teste em vários apps de QR Code');
return tips;
}
displayReadabilityPreview(previewInfo) {
// Usar a mesma função de display, mas com ID diferente para preview
let previewContainer = document.getElementById('readability-preview');
if (!previewContainer) {
previewContainer = document.createElement('div');
previewContainer.id = 'readability-preview';
previewContainer.className = 'mt-2 p-2 border rounded bg-info bg-opacity-10 border-info border-opacity-25';
// Inserir após o slider de tamanho do logo
const logoSizeSlider = document.getElementById('logo-size-slider');
if (logoSizeSlider && logoSizeSlider.parentNode) {
logoSizeSlider.parentNode.insertBefore(previewContainer, logoSizeSlider.nextSibling);
}
}
const scoreClass = previewInfo.readabilityScore >= 85 ? 'text-success' :
previewInfo.readabilityScore >= 70 ? 'text-info' :
previewInfo.readabilityScore >= 55 ? 'text-warning' : 'text-danger';
const iconClass = previewInfo.readabilityScore >= 85 ? 'fas fa-check-circle' :
previewInfo.readabilityScore >= 70 ? 'fas fa-thumbs-up' :
previewInfo.readabilityScore >= 55 ? 'fas fa-exclamation-triangle' : 'fas fa-exclamation-circle';
previewContainer.innerHTML = `
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<i class="${iconClass} ${scoreClass} me-2"></i>
<small class="${scoreClass}">${previewInfo.userMessage}</small>
</div>
<div class="badge bg-secondary">${previewInfo.readabilityScore}/100</div>
</div>
`;
}
clearReadabilityPreview() {
const previewContainer = document.getElementById('readability-preview');
if (previewContainer) {
previewContainer.remove();
}
}
}
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
});
}
};