// 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('
'));
return false;
}
return true;
} else {
this.showError('VCard generator não está disponível');
return false;
}
} catch (error) {
this.showError('Erro na validação do VCard: ' + error.message);
return false;
}
} else if (qrType === 'wifi') {
const errors = window.wifiGenerator.validateWiFiData();
if (errors.length > 0) {
this.showError(errors.join('
'));
return false;
}
return true;
} else if (qrType === 'sms') {
const errors = window.smsGenerator.validateSMSData();
if (errors.length > 0) {
this.showError(errors.join('
'));
return false;
}
return true;
} else if (qrType === 'email') {
const errors = window.emailGenerator.validateEmailData();
if (errors.length > 0) {
this.showError(errors.join('
'));
return false;
}
return true;
}
// Normal validation for other types
const qrContent = document.getElementById('qr-content').value.trim();
if (!qrContent) {
this.showError('Digite o conteúdo do QR code');
return false;
}
if (qrContent.length > 4000) {
this.showError('Conteúdo muito longo. Máximo 4000 caracteres.');
return false;
}
return true;
}
collectFormData() {
const type = document.getElementById('qr-type').value;
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
const styleSettings = this.getStyleSettings(quickStyle);
// 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 = `
`;
// 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 = `
${this.languageStrings[this.currentLang].generating}
`;
}
}
showGenerationStats(generationTime) {
const statsDiv = document.querySelector('.generation-stats');
const speedBadge = document.querySelector('.speed-badge');
if (statsDiv) {
statsDiv.classList.remove('d-none');
const timeSpan = statsDiv.querySelector('.generation-time');
if (timeSpan) {
timeSpan.textContent = `${generationTime}s`;
}
}
// Show speed badge
if (speedBadge) {
const strings = this.languageStrings[this.currentLang];
let badgeText = strings.normal;
let badgeClass = 'bg-secondary';
if (generationTime < 1.0) {
badgeText = strings.ultraFast;
badgeClass = 'bg-success';
} else if (generationTime < 2.0) {
badgeText = strings.fast;
badgeClass = 'bg-primary';
}
speedBadge.innerHTML = `
${badgeText}
`;
speedBadge.classList.remove('d-none');
}
}
hideGenerationLoading() {
const button = document.getElementById('generate-btn');
const spinner = button?.querySelector('.spinner-border');
if (button) button.disabled = false;
if (spinner) spinner.classList.add('d-none');
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
updateContentHints() {
const type = document.getElementById('qr-type')?.value;
const hintsElement = document.getElementById('content-hints');
const vcardInterface = document.getElementById('vcard-interface');
const contentTextarea = document.getElementById('qr-content');
if (!hintsElement || !type) return;
// Show/hide VCard interface based on type
if (type === 'vcard') {
if (vcardInterface) vcardInterface.style.display = 'block';
if (contentTextarea) {
contentTextarea.style.display = 'none';
contentTextarea.removeAttribute('required');
}
hintsElement.textContent = 'Preencha os campos acima para criar seu cartão de visita digital';
return; // Skip normal hints for VCard
} else {
if (vcardInterface) vcardInterface.style.display = 'none';
if (contentTextarea) {
contentTextarea.style.display = 'block';
contentTextarea.setAttribute('required', 'required');
}
}
const hints = {
'pt-BR': {
'url': 'Ex: https://www.exemplo.com.br',
'text': 'Digite qualquer texto que desejar',
'wifi': 'Nome da rede;Senha;Tipo de segurança (WPA/WEP)',
'vcard': 'Nome;Telefone;Email;Empresa',
'sms': 'Número;Mensagem',
'email': 'email@exemplo.com;Assunto;Mensagem'
},
'es': {
'url': 'Ej: https://www.ejemplo.com',
'text': 'Escribe cualquier texto que desees',
'wifi': 'Nombre de red;Contraseña;Tipo de seguridad (WPA/WEP)',
'vcard': 'Nombre;Teléfono;Email;Empresa',
'sms': 'Número;Mensaje',
'email': 'email@ejemplo.com;Asunto;Mensagem'
}
};
const langHints = hints[this.currentLang] || hints['pt-BR'];
hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado';
}
changeLanguage(e) {
e.preventDefault();
this.currentLang = e.target.dataset.lang;
this.updateLanguage();
this.updateContentHints();
// Save preference
localStorage.setItem('qrrapido-lang', this.currentLang);
// Track language change
window.trackLanguageChange && window.trackLanguageChange('pt-BR', this.currentLang);
}
updateLanguage() {
const strings = this.languageStrings[this.currentLang];
// Update tagline
const tagline = document.getElementById('tagline');
if (tagline) {
tagline.textContent = strings.tagline;
}
// Update language selector
const langMap = { 'pt-BR': 'PT', 'es': 'ES', 'en': 'EN' };
const currentLang = document.getElementById('current-lang');
if (currentLang) {
currentLang.textContent = langMap[this.currentLang];
}
// Update hints if type already selected
const qrType = document.getElementById('qr-type');
if (qrType?.value) {
this.updateContentHints();
}
}
handleLogoSelection(e) {
const file = e.target.files[0];
const logoPreview = document.getElementById('logo-preview');
const logoFilename = document.getElementById('logo-filename');
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 = ` ${avgTime}s`;
}
});
// Update the generation timer in the header
const timerElement = document.querySelector('.generation-timer span');
if (timerElement) {
timerElement.textContent = `${generationTime}s`;
}
// Update performance statistics
if (!window.qrRapidoPerformance) {
window.qrRapidoPerformance = {
totalGenerations: 0,
totalTime: 0,
bestTime: Infinity,
worstTime: 0
};
}
const perf = window.qrRapidoPerformance;
perf.totalGenerations++;
perf.totalTime += timeFloat;
perf.bestTime = Math.min(perf.bestTime, timeFloat);
perf.worstTime = Math.max(perf.worstTime, timeFloat);
// Log performance statistics for debugging
console.log('📊 Performance Update:', {
currentTime: `${generationTime}s`,
averageTime: `${(perf.totalTime / perf.totalGenerations).toFixed(1)}s`,
bestTime: `${perf.bestTime.toFixed(1)}s`,
totalGenerations: perf.totalGenerations
});
}
isPremiumUser() {
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
}
async downloadQR(format) {
if (!this.currentQR || !this.currentQR.qrCodeBase64) {
this.showError('Nenhum QR Code gerado para download.');
return;
}
try {
const timestamp = new Date().toISOString().slice(0,10);
const base64Data = this.currentQR.qrCodeBase64;
if (format === 'png') {
// Download PNG directly from base64
const link = document.createElement('a');
link.href = `data:image/png;base64,${base64Data}`;
link.download = `qrrapido-${timestamp}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else if (format === 'svg') {
// Convert PNG to SVG
const svgData = await this.convertPngToSvg(base64Data);
const link = document.createElement('a');
link.href = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgData)}`;
link.download = `qrrapido-${timestamp}.svg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else if (format === 'pdf') {
// Convert PNG to PDF
const pdfBlob = await this.convertPngToPdf(base64Data);
const url = window.URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
link.download = `qrrapido-${timestamp}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
} catch (error) {
console.error('Download error:', error);
this.showError(`Erro ao fazer download ${format.toUpperCase()}. Tente novamente.`);
}
}
async convertPngToSvg(base64Data) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// Create SVG with embedded base64 image
const svgData = `
`;
resolve(svgData);
};
img.src = `data:image/png;base64,${base64Data}`;
});
}
async convertPngToPdf(base64Data) {
return new Promise((resolve, reject) => {
try {
// Try to use jsPDF if available, otherwise use a simpler approach
if (typeof window.jsPDF !== 'undefined') {
const pdf = new window.jsPDF();
const img = new Image();
img.onload = () => {
// Calculate dimensions to fit on page
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const imgRatio = img.width / img.height;
let width = Math.min(pdfWidth - 20, 150);
let height = width / imgRatio;
if (height > pdfHeight - 20) {
height = pdfHeight - 20;
width = height * imgRatio;
}
// Center on page
const x = (pdfWidth - width) / 2;
const y = (pdfHeight - height) / 2;
pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, width, height);
const pdfBlob = pdf.output('blob');
resolve(pdfBlob);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = `data:image/png;base64,${base64Data}`;
} else {
// Fallback: create a very simple PDF
this.createBasicPdf(base64Data).then(resolve).catch(reject);
}
} catch (error) {
reject(error);
}
});
}
async createBasicPdf(base64Data) {
// Load jsPDF dynamically if not available
if (typeof window.jsPDF === 'undefined') {
await this.loadJsPDF();
}
return new Promise((resolve) => {
const pdf = new window.jsPDF();
const img = new Image();
img.onload = () => {
// Add QR code to PDF
const pdfWidth = pdf.internal.pageSize.getWidth();
const size = Math.min(pdfWidth - 40, 100);
const x = (pdfWidth - size) / 2;
const y = 50;
pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, size, size);
pdf.text('QR Code - QR Rapido', pdfWidth / 2, 30, { align: 'center' });
const pdfBlob = pdf.output('blob');
resolve(pdfBlob);
};
img.src = `data:image/png;base64,${base64Data}`;
});
}
async loadJsPDF() {
return new Promise((resolve, reject) => {
if (typeof window.jsPDF !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
script.onload = () => {
window.jsPDF = window.jspdf.jsPDF;
resolve();
};
script.onerror = () => reject(new Error('Failed to load jsPDF'));
document.head.appendChild(script);
});
}
async saveToHistory() {
if (!this.currentQR) return;
try {
const response = await fetch('/api/QR/SaveToHistory', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
qrId: this.currentQR.id
})
});
if (response.ok) {
this.showSuccess(this.languageStrings[this.currentLang].success);
} else {
throw new Error('Failed to save');
}
} catch (error) {
console.error('Save error:', error);
this.showError('Erro ao salvar no histórico.');
}
}
async checkAdFreeStatus() {
try {
const response = await fetch('/Account/AdFreeStatus');
const status = await response.json();
if (status.isAdFree) {
this.hideAllAds();
this.showAdFreeMessage(status.timeRemaining);
}
} catch (error) {
console.error('Error checking ad-free status:', error);
}
}
hideAllAds() {
document.querySelectorAll('.ad-container').forEach(ad => {
ad.style.display = 'none';
});
}
showAdFreeMessage(timeRemaining) {
if (timeRemaining <= 0) return;
const existing = document.querySelector('.ad-free-notice');
if (existing) return; // Already shown
const message = document.createElement('div');
message.className = 'alert alert-success text-center mb-3 ad-free-notice';
message.innerHTML = `
Sessão sem anúncios ativa!
Tempo restante: ${this.formatTime(timeRemaining)}
Tornar Permanente
`;
const container = document.querySelector('.container');
const row = container?.querySelector('.row');
if (container && row) {
container.insertBefore(message, row);
}
}
formatTime(minutes) {
if (minutes === 0) return '0m';
const days = Math.floor(minutes / 1440);
const hours = Math.floor((minutes % 1440) / 60);
const mins = minutes % 60;
if (days > 0) return `${days}d ${hours}h ${mins}m`;
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
showUpgradeModal(message) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
${message}
Plano Atual (Free)
- ❌ Limite de 10 QR/dia
- ❌ Anúncios
- ✅ QR básicos
Premium (R$ 19,90/mês)
- ✅ QR ilimitados
- ✅ Sem anúncios
- ✅ QR dinâmicos
- ✅ Analytics
- ✅ Suporte prioritário
`;
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 = `
`;
toastContainer.appendChild(toast);
// Show toast
const bsToast = new bootstrap.Toast(toast, {
delay: type === 'success' ? 3000 : 5000,
autohide: true
});
bsToast.show();
// Remove from DOM after hidden
toast.addEventListener('hidden.bs.toast', () => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
});
}
setupRealTimePreview() {
const contentField = document.getElementById('qr-content');
const typeField = document.getElementById('qr-type');
if (contentField && typeField) {
let previewTimeout;
const updatePreview = () => {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(() => {
if (contentField.value.trim() && typeField.value) {
// Could implement real-time preview for premium users
console.log('Real-time preview update');
}
}, 500);
};
contentField.addEventListener('input', updatePreview);
typeField.addEventListener('change', updatePreview);
}
}
// ============================================
// SISTEMA DE FLUXO ASCENDENTE PROGRESSIVO
// ============================================
initializeProgressiveFlow() {
// Estado inicial: apenas tipo habilitado, botão gerar desabilitado
const qrContent = document.getElementById('qr-content');
const vcardInterface = document.getElementById('vcard-interface');
// FIXED: Don't disable content field initially - let enableContentFields handle it
// The field should be available for typing once a type is selected
// Ocultar interface vCard
if (vcardInterface) {
vcardInterface.style.display = 'none';
}
this.updateGenerateButton();
// Setup URL validation event listeners
this.setupURLValidationListeners();
// Setup hint auto-hide timer
this.setupHintAutoHide();
}
setupHintAutoHide() {
const hint = document.getElementById('type-selection-hint');
if (hint) {
// Auto-hide after 30 seconds
setTimeout(() => {
if (hint && !hint.classList.contains('fade-out')) {
hint.classList.add('fade-out');
// Remove from DOM after animation
setTimeout(() => {
if (hint.parentNode) {
hint.style.display = 'none';
}
}, 1000); // 1s for fade-out animation
}
}, 30000); // 30 seconds
}
}
setupURLValidationListeners() {
const contentField = document.getElementById('qr-content');
if (!contentField) return;
// Auto-fix URL on blur (when user leaves the field)
contentField.addEventListener('blur', () => {
const type = document.getElementById('qr-type')?.value;
if (type === 'url' && contentField.value.trim()) {
const originalValue = contentField.value.trim();
const fixedValue = this.autoFixURL(originalValue);
if (originalValue !== fixedValue) {
contentField.value = fixedValue;
console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue);
// Revalidar após correção
this.updateGenerateButton();
}
}
});
// Real-time validation with debounce
contentField.addEventListener('input', () => {
const type = document.getElementById('qr-type')?.value;
if (type === 'url') {
// Debounce para não validar a cada caractere
clearTimeout(contentField.validationTimeout);
contentField.validationTimeout = setTimeout(() => {
this.updateGenerateButton();
}, 500);
}
});
}
handleTypeSelection(type) {
this.selectedType = type;
if (type) {
this.removeInitialHighlight();
// Sempre habilitar campos de conteúdo após selecionar tipo
this.enableContentFields(type);
// Show guidance toast for the selected type
this.showTypeGuidanceToast(type);
} else {
this.disableAllFields();
}
this.updateGenerateButton();
}
handleStyleSelection(style) {
this.selectedStyle = style;
this.updateGenerateButton();
}
handleContentChange(content) {
const contentField = document.getElementById('qr-content');
this.contentValid = this.validateContent(content);
// Feedback visual para campo de conteúdo
if (contentField) {
this.validateField(contentField, this.contentValid, 'Conteúdo deve ter pelo menos 3 caracteres');
}
this.updateGenerateButton();
}
enableContentFields(type) {
const contentGroup = document.getElementById('content-group');
const vcardInterface = document.getElementById('vcard-interface');
const wifiInterface = document.getElementById('wifi-interface');
const smsInterface = document.getElementById('sms-interface');
const emailInterface = document.getElementById('email-interface');
const dynamicQRSection = document.getElementById('dynamic-qr-section');
const urlPreview = document.getElementById('url-preview');
// Hide all interfaces by default
if (vcardInterface) vcardInterface.style.display = 'none';
if (wifiInterface) wifiInterface.style.display = 'none';
if (smsInterface) smsInterface.style.display = 'none';
if (emailInterface) emailInterface.style.display = 'none';
if (dynamicQRSection) dynamicQRSection.style.display = 'none';
if (urlPreview) urlPreview.style.display = 'none';
if (contentGroup) contentGroup.style.display = 'block';
if (type === 'vcard') {
// Para vCard, ocultar textarea e mostrar interface específica
if (contentGroup) contentGroup.style.display = 'none';
if (vcardInterface) {
vcardInterface.style.display = 'block';
this.enableVCardFields();
}
} else if (type === 'wifi') {
// Para WiFi, ocultar textarea e mostrar interface específica
if (contentGroup) contentGroup.style.display = 'none';
if (wifiInterface) {
wifiInterface.style.display = 'block';
}
} else if (type === 'sms') {
// Para SMS, ocultar textarea e mostrar interface específica
if (contentGroup) contentGroup.style.display = 'none';
if (smsInterface) {
smsInterface.style.display = 'block';
}
} else if (type === 'email') {
// Para Email, ocultar textarea e mostrar interface específica
if (contentGroup) contentGroup.style.display = 'none';
if (emailInterface) {
emailInterface.style.display = 'block';
}
} else if (type === 'url') {
if (dynamicQRSection) dynamicQRSection.style.display = 'block';
if (urlPreview) urlPreview.style.display = 'block';
// CRITICAL FIX: Enable content field for URL type
const qrContent = document.getElementById('qr-content');
if(qrContent) {
qrContent.disabled = false;
}
} else {
// Para outros tipos, mostrar textarea
if (contentGroup) contentGroup.style.display = 'block';
const qrContent = document.getElementById('qr-content');
if(qrContent) {
qrContent.disabled = false;
}
}
}
enableVCardFields() {
const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
requiredFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.disabled = false;
field.setAttribute('required', 'required');
}
});
}
disableAllFields() {
// Resetar estado
const qrContent = document.getElementById('qr-content');
const vcardInterface = document.getElementById('vcard-interface');
if (qrContent) {
// FIXED: Don't disable content field, just clear it
qrContent.value = '';
}
if (vcardInterface) {
vcardInterface.style.display = 'none';
// Limpar campos vCard
const vcardFields = document.querySelectorAll('#vcard-interface input');
vcardFields.forEach(field => {
field.value = '';
field.disabled = true;
});
}
this.contentValid = false;
}
validateContent(content) {
if (!content) return false;
return content.trim().length >= 3;
}
validateVCardFields() {
const nameField = document.getElementById('vcard-name');
const mobileField = document.getElementById('vcard-mobile');
const emailField = document.getElementById('vcard-email');
const name = nameField?.value.trim() || '';
const mobile = mobileField?.value.trim() || '';
const email = emailField?.value.trim() || '';
// Validação individual dos campos com feedback visual
this.validateField(nameField, name !== '', 'Nome é obrigatório');
this.validateField(mobileField, mobile !== '' && mobile.length >= 10, 'Telefone deve ter pelo menos 10 dígitos');
this.validateField(emailField, email !== '' && email.includes('@'), 'Email válido é obrigatório');
const isValid = name !== '' && mobile !== '' && email !== '' && email.includes('@') && mobile.length >= 10;
this.contentValid = isValid;
return isValid;
}
validateField(field, isValid, errorMessage) {
if (!field) return;
const feedbackElement = field.parentNode.querySelector('.invalid-feedback');
if (isValid) {
field.classList.remove('is-invalid');
field.classList.add('is-valid');
if (feedbackElement) {
feedbackElement.style.display = 'none';
}
} else if (field.value.trim() !== '') {
// Só mostrar erro se o campo tem conteúdo
field.classList.remove('is-valid');
field.classList.add('is-invalid');
if (feedbackElement) {
feedbackElement.textContent = errorMessage;
feedbackElement.style.display = 'block';
}
} else {
// Campo vazio, remover validações visuais
field.classList.remove('is-valid', 'is-invalid');
if (feedbackElement) {
feedbackElement.style.display = 'none';
}
}
}
// ============================================
// URL VALIDATION FUNCTIONS
// ============================================
isValidURL(url) {
if (!url || typeof url !== 'string') return false;
const trimmedUrl = url.trim();
// Verificar se começa com http:// ou https://
const hasProtocol = trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://');
// Verificar se tem pelo menos um ponto
const hasDot = trimmedUrl.includes('.');
// Verificar se não é só o protocolo
const hasContent = trimmedUrl.length > 8; // mais que "https://"
// Regex mais robusta
const urlRegex = /^https?:\/\/.+\..+/i;
return hasProtocol && hasDot && hasContent && urlRegex.test(trimmedUrl);
}
autoFixURL(url) {
if (!url || typeof url !== 'string') return '';
let fixedUrl = url.trim();
// Se não tem protocolo, adicionar https://
if (!fixedUrl.startsWith('http://') && !fixedUrl.startsWith('https://')) {
fixedUrl = 'https://' + fixedUrl;
}
return fixedUrl;
}
showValidationError(message) {
let errorDiv = document.getElementById('url-validation-error');
if (!errorDiv) {
// Criar div de erro se não existir
errorDiv = document.createElement('div');
errorDiv.id = 'url-validation-error';
errorDiv.className = 'alert alert-danger mt-2';
errorDiv.style.display = 'none';
// Inserir após o campo de conteúdo
const contentGroup = document.getElementById('content-group');
if (contentGroup) {
contentGroup.appendChild(errorDiv);
}
}
errorDiv.innerHTML = ` ${message}`;
errorDiv.style.display = 'block';
}
clearValidationError() {
const errorDiv = document.getElementById('url-validation-error');
if (errorDiv) {
errorDiv.style.display = 'none';
}
}
updateGenerateButton() {
const generateBtn = document.getElementById('generate-btn');
if (!generateBtn) return;
let isValid = false;
const type = this.selectedType;
if (type === 'url') {
const contentField = document.getElementById('qr-content');
const content = contentField?.value || '';
const trimmedContent = content.trim();
if (!trimmedContent) {
this.showValidationError('URL é obrigatória');
if (contentField) {
contentField.classList.remove('is-valid');
contentField.classList.add('is-invalid');
}
isValid = false;
} else if (!this.isValidURL(trimmedContent)) {
this.showValidationError('URL deve começar com http:// ou https:// e conter pelo menos um ponto (ex: https://google.com)');
if (contentField) {
contentField.classList.remove('is-valid');
contentField.classList.add('is-invalid');
}
isValid = false;
} else {
this.clearValidationError();
if (contentField) {
contentField.classList.remove('is-invalid');
contentField.classList.add('is-valid');
}
isValid = true;
}
} else if (type === 'text') {
const contentField = document.getElementById('qr-content');
const content = contentField?.value || '';
isValid = content.trim().length >= 3;
this.clearValidationError(); // Clear any URL validation errors
if (contentField) {
contentField.classList.remove('is-valid', 'is-invalid');
}
} else if (type === 'vcard') {
isValid = this.validateVCardFields();
} else if (type === 'wifi') {
const data = window.wifiGenerator.collectWiFiData();
isValid = data.ssid.trim() !== '';
if (data.security !== 'nopass' && !data.password.trim()) {
isValid = false;
}
} else if (type === 'sms') {
const data = window.smsGenerator.collectSMSData();
isValid = data.number.trim() !== '' && data.message.trim() !== '';
} else if (type === 'email') {
const data = window.emailGenerator.collectEmailData();
isValid = data.to.trim() !== '' && data.subject.trim() !== '';
}
generateBtn.disabled = !isValid;
if (isValid) {
generateBtn.classList.remove('btn-secondary', 'disabled');
generateBtn.classList.add('btn-primary');
} else {
generateBtn.classList.remove('btn-primary');
generateBtn.classList.add('btn-secondary', 'disabled');
}
}
// Remove destaque inicial quando tipo for selecionado
removeInitialHighlight() {
const typeField = document.getElementById('qr-type');
const hint = document.getElementById('type-selection-hint');
if (typeField && typeField.classList.contains('qr-type-highlight')) {
typeField.classList.remove('qr-type-highlight');
}
if (hint && !hint.classList.contains('fade-out')) {
hint.classList.add('fade-out');
// Hide after animation
setTimeout(() => {
if (hint.parentNode) {
hint.style.display = 'none';
}
}, 1000);
}
}
showTypeGuidanceToast(type) {
// Get localized message based on QR type
const messages = this.getTypeGuidanceMessages();
const message = messages[type];
if (message) {
// Create info toast with 30 second duration
const toast = this.createGuidanceToast(message);
this.showGuidanceToast(toast, 30000); // 30 seconds
}
}
getTypeGuidanceMessages() {
// These should match the resource file keys
// In a real implementation, these would come from server-side localization
// For now, we'll use the JavaScript language strings or fallback to Portuguese
return {
'url': document.querySelector('[data-type-guide-url]')?.textContent || '🌐 Para gerar QR de URL, digite o endereço completo (ex: https://google.com)',
'vcard': document.querySelector('[data-type-guide-vcard]')?.textContent || '👤 Para cartão de visita, preencha nome, telefone e email nos campos abaixo',
'wifi': document.querySelector('[data-type-guide-wifi]')?.textContent || '📶 Para WiFi, informe nome da rede, senha e tipo de segurança',
'sms': document.querySelector('[data-type-guide-sms]')?.textContent || '💬 Para SMS, digite o número do destinatário e a mensagem',
'email': document.querySelector('[data-type-guide-email]')?.textContent || '📧 Para email, preencha destinatário, assunto e mensagem (opcional)',
'text': document.querySelector('[data-type-guide-text]')?.textContent || '📝 Para texto livre, digite qualquer conteúdo que desejar'
};
}
createGuidanceToast(message) {
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-bg-info border-0';
toast.setAttribute('role', 'alert');
toast.style.minWidth = '400px';
toast.innerHTML = `
`;
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
? `
${readabilityInfo.tips.map(tip => `- ${tip}
`).join('')}
`
: '';
// Criar HTML da análise
const scoreClass = getScoreClass(readabilityInfo.readabilityScore);
const iconClass = getIconClass(readabilityInfo.readabilityScore);
analysisContainer.innerHTML = `
Análise de Legibilidade
${readabilityInfo.userMessage}
${readabilityInfo.readabilityScore}/100
${readabilityInfo.tips && readabilityInfo.tips.length > 0
? `
`
: ''}
${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 = `
${previewInfo.userMessage}
${previewInfo.readabilityScore}/100
`;
}
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
});
}
};