// QR Rapido Speed Generator
class QRRapidoGenerator {
constructor() {
this.startTime = 0;
this.currentQR = null;
this.timerInterval = null;
// Initialize premium status from hidden input
this.isPremium = this.checkUserPremium();
console.log('[INIT] QRRapidoGenerator - isPremium:', this.isPremium);
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.previousType = null;
this.selectedStyle = 'classic'; // Estilo padrão
this.contentValid = false;
// UX Improvements - Intelligent delay system
this.contentDelayTimer = null;
this.hasShownContentToast = false;
this.buttonReadyState = false;
this.urlPrefix = 'https://';
this.initializeEvents();
this.checkAdFreeStatus();
this.updateLanguage();
this.updateStatsCounters();
this.initializeUserCounter();
// Initialize progressive flow
this.initializeProgressiveFlow();
// Check for type in URL (SEO landing pages)
const urlParams = new URLSearchParams(window.location.search);
const typeFromUrl = urlParams.get('type') || window.location.pathname.split('/').pop();
// Map SEO paths to internal types
const typeMap = {
'pix': 'pix',
'wifi': 'wifi',
'vcard': 'vcard',
'whatsapp': 'whatsapp', // Maps to url usually, or custom
'email': 'email',
'sms': 'sms',
'texto': 'text',
'text': 'text',
'url': 'url'
};
if (typeFromUrl && typeMap[typeFromUrl]) {
const select = document.getElementById('qr-type');
if (select) {
select.value = typeMap[typeFromUrl];
// CRITICAL: Dispatch change event to trigger UI updates
select.dispatchEvent(new Event('change'));
}
}
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);
// Clear URL validation timeout if exists
if (qrContent.validationTimeout) {
clearTimeout(qrContent.validationTimeout);
}
timer = setTimeout(() => {
this.handleContentChange(e.target.value);
this.updateGenerateButton();
// Handle URL-specific validation
const type = document.getElementById('qr-type')?.value;
if (type === 'url') {
qrContent.validationTimeout = setTimeout(() => {
this.updateGenerateButton();
}, 200); // Additional URL validation delay
}
}, 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',
'pix-key', 'pix-name', 'pix-city', 'pix-amount'
];
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));
}
// Accordion State Persistence
this.initializeAccordionState();
this.setupUrlFieldHandlers();
}
initializeAccordionState() {
const accordionBtn = document.getElementById('btn-customization-toggle');
const accordionPanel = document.getElementById('customization-panel');
if (accordionBtn && accordionPanel) {
// Check saved state
const isOpen = localStorage.getItem('qr_customization_open') === 'true';
if (isOpen) {
accordionBtn.classList.remove('collapsed');
accordionBtn.setAttribute('aria-expanded', 'true');
accordionPanel.classList.add('show');
}
// Save state on toggle
accordionPanel.addEventListener('shown.bs.collapse', () => {
localStorage.setItem('qr_customization_open', 'true');
});
accordionPanel.addEventListener('hidden.bs.collapse', () => {
localStorage.setItem('qr_customization_open', 'false');
});
}
}
setupUrlFieldHandlers() {
const contentField = document.getElementById('qr-content');
if (!contentField) return;
contentField.addEventListener('focus', () => {
if (this.selectedType === 'url') {
if (!contentField.value.trim()) {
contentField.value = this.urlPrefix;
} else {
contentField.value = this.ensureUrlPrefix(contentField.value);
}
if (this.isProtocolOnly(contentField.value.trim())) {
this.clearValidationError();
contentField.classList.remove('is-valid', 'is-invalid');
this.contentValid = false;
this.updateGenerateButton();
this.setCaretPosition(contentField, this.getProtocolLength(contentField.value));
}
}
});
contentField.addEventListener('paste', (event) => {
if (this.selectedType !== 'url') return;
event.preventDefault();
const text = (event.clipboardData || window.clipboardData)?.getData('text') || '';
const normalized = this.normalizeUrlInput(text);
contentField.value = normalized;
this.setCaretPosition(contentField, normalized.length);
this.handleContentChange(normalized);
this.updateGenerateButton();
});
contentField.addEventListener('input', () => {
if (this.selectedType !== 'url') return;
const currentValue = contentField.value;
const sanitized = this.ensureUrlPrefix(currentValue);
if (sanitized !== currentValue) {
const caret = typeof contentField.selectionStart === 'number' ? contentField.selectionStart : sanitized.length;
const delta = sanitized.length - currentValue.length;
contentField.value = sanitized;
const protocolLength = this.getProtocolLength(sanitized);
const newPos = Math.max(protocolLength, caret + delta);
this.setCaretPosition(contentField, newPos);
}
if (this.isProtocolOnly(contentField.value.trim())) {
this.clearValidationError();
contentField.classList.remove('is-valid', 'is-invalid');
}
});
}
prefillContentField(type, previousType = null) {
const contentField = document.getElementById('qr-content');
if (!contentField) return;
if (type === 'url') {
const returningToUrl = previousType === 'url';
const hasValue = !!contentField.value.trim();
if (!returningToUrl || !hasValue || this.isProtocolOnly(contentField.value.trim())) {
contentField.value = this.urlPrefix;
} else {
contentField.value = this.ensureUrlPrefix(contentField.value);
}
contentField.classList.remove('is-valid', 'is-invalid');
this.clearValidationError();
this.contentValid = false;
} else {
contentField.value = '';
contentField.classList.remove('is-valid', 'is-invalid');
this.clearValidationError();
this.contentValid = false;
}
this.updateGenerateButton();
}
isProtocolOnly(value) {
if (!value) return true;
const normalized = value.toString().trim().toLowerCase();
return normalized === 'https://' || normalized === 'http://';
}
normalizeUrlInput(raw) {
if (!raw) {
return this.urlPrefix;
}
let value = raw.toString().trim();
if (!value) {
return this.urlPrefix;
}
const protocolMatch = value.match(/^(https?:\/\/)/i);
let protocol = this.urlPrefix;
if (protocolMatch) {
protocol = protocolMatch[0].toLowerCase();
value = value.slice(protocolMatch[0].length);
}
value = value.replace(/^(https?:\/\/)/i, '');
return protocol + value;
}
ensureUrlPrefix(value) {
if (!value) {
return this.urlPrefix;
}
let working = value.toString().trimStart();
const protocolMatch = working.match(/^(https?:\/\/)/i);
let protocol = this.urlPrefix;
if (protocolMatch) {
protocol = protocolMatch[0].toLowerCase();
working = working.slice(protocolMatch[0].length);
}
working = working.replace(/^(https?:\/\/)/i, '');
return protocol + working;
}
getProtocolLength(value) {
if (!value) return this.urlPrefix.length;
if (value.startsWith('http://')) return 'http://'.length;
if (value.startsWith('https://')) return 'https://'.length;
return this.urlPrefix.length;
}
setCaretPosition(input, position) {
if (!input || typeof input.setSelectionRange !== 'function') return;
const pos = Math.max(0, Math.min(position, input.value.length));
requestAnimationFrame(() => {
input.setSelectionRange(pos, pos);
});
}
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(window.QRRapidoTranslations?.shareError || 'Error sharing. Try another method.');
}
}
}
shareWhatsApp() {
if (!this.currentQR) return;
const text = encodeURIComponent((window.QRRapidoTranslations?.fastestGeneratorBrazil || 'QR Code generated with QR Rapido!') + ' ' + 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(window.QRRapidoTranslations?.fastestGeneratorBrazil || 'QR Code generated with QR Rapido!');
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(window.QRRapidoTranslations?.linkCopied || 'Link copied to clipboard!');
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
// 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'
};
}
// Sending request to backend
const response = await fetch(requestData.endpoint, fetchOptions);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
if (response.status === 429) {
this.showUpgradeModal(window.QRRapidoTranslations?.rateLimitReached || 'Limite diário atingido! Faça login.');
return;
}
// NEW: Handle Payment Required (No Credits)
if (response.status === 402) {
this.showCreditsModal(errorData.error || 'Saldo insuficiente.');
return;
}
if (response.status === 400 && errorData.requiresPremium) {
this.showUpgradeModal(errorData.error || window.QRRapidoTranslations?.premiumLogoRequired || 'Recurso Premium.');
return;
}
throw new Error(errorData.error || 'Erro na geração');
}
const result = await response.json();
if (!result.success) {
if (result.requiresPremium) {
this.showUpgradeModal(result.error || 'Logo personalizado é exclusivo do plano Premium.');
return;
}
throw new Error(result.error || 'Erro desconhecido');
}
const generationTime = ((performance.now() - this.startTime) / 1000).toFixed(1);
this.displayQRResult(result, generationTime);
this.updateSpeedStats(generationTime);
this.trackGenerationEvent(requestData.data.type || requestData.data.get('type'), generationTime);
this.smoothScrollToQRArea();
} 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(window.QRRapidoTranslations?.featureNotAvailable || 'Feature not available');
return false;
}
} catch (error) {
this.showError((window.QRRapidoTranslations?.vCardValidationError || 'VCard validation error: ') + 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;
} else if (qrType === 'pix') {
const errors = window.pixGenerator.validatePixData();
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(window.QRRapidoTranslations?.enterQRContent || 'Enter QR code content');
return false;
}
if (qrContent.length > 4000) {
this.showError(window.QRRapidoTranslations?.contentTooLong || 'Content too long. Maximum 4000 characters.');
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 === 'pix') {
content = window.pixGenerator.generatePixPayload();
actualType = 'text'; // Pix is treated as 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 trackingEnabled = this.getTrackingEnabled();
console.log('[DEBUG] getTrackingEnabled() returned:', trackingEnabled, 'type:', typeof trackingEnabled);
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,
enableTracking: trackingEnabled // Analytics feature for premium users
};
console.log('[DEBUG] commonData.enableTracking:', commonData.enableTracking);
// 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 with PascalCase for ASP.NET Core model binding
Object.keys(commonData).forEach(key => {
// Convert camelCase to PascalCase for FormData (ASP.NET Core compatibility)
const pascalKey = key.charAt(0).toUpperCase() + key.slice(1);
const value = commonData[key];
console.log('[DEBUG] FormData.append:', pascalKey, '=', value, 'type:', typeof value);
formData.append(pascalKey, value);
});
// 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');
return {
data: formData,
isMultipart: true,
endpoint: '/api/QR/GenerateRapidWithLogo'
};
} else {
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
};
}
// Function to check if tracking is enabled (Premium feature)
getTrackingEnabled() {
const trackingCheckbox = document.getElementById('enable-tracking');
// DEBUG: Log each condition separately
console.log('[DEBUG] getTrackingEnabled() - Checkbox exists:', !!trackingCheckbox);
console.log('[DEBUG] getTrackingEnabled() - Checkbox checked:', trackingCheckbox?.checked);
console.log('[DEBUG] getTrackingEnabled() - this.isPremium:', this.isPremium);
// Only return true if checkbox exists, is checked, and user is premium
// IMPORTANT: Always return boolean (not undefined) for FormData compatibility
const result = !!(trackingCheckbox && trackingCheckbox.checked && this.isPremium);
console.log('[DEBUG] getTrackingEnabled() - Final result:', result);
return result;
}
// Check if user has premium status
checkUserPremium() {
const premiumStatus = document.getElementById('user-premium-status');
if (premiumStatus) {
return premiumStatus.value === 'premium';
}
return 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 if (type === 'pix') {
return window.pixGenerator?.generatePixPayload() || '';
} 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);
}
// Scroll suave para o preview após a geração
this.scrollToPreview();
// 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;
// Show generation statistics
this.showGenerationStats(generationTime);
// Show download buttons
const downloadSection = document.getElementById('download-section');
if (downloadSection) {
downloadSection.style.display = 'block';
// Remove existing upsell if any
const existingUpsell = document.getElementById('post-gen-upsell');
if (existingUpsell) existingUpsell.remove();
// Inject Buy Credits Upsell Button
const userStatus = document.getElementById('user-premium-status')?.value;
if (userStatus === 'logged-in' || userStatus === 'premium') {
const upsellDiv = document.createElement('div');
upsellDiv.id = 'post-gen-upsell';
upsellDiv.className = 'mt-3 pt-3 border-top';
upsellDiv.innerHTML = `
Adicionar Mais Créditos
Garanta o próximo QR Code!
`;
downloadSection.appendChild(upsellDiv);
}
}
// 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' || type === 'wifi' || type === 'sms' || type === 'email' || type === 'pix') {
if (type === 'vcard' && vcardInterface) vcardInterface.style.display = 'block';
// For these specific types, we always hide the main content textarea
// because they have their own specialized interfaces
if (contentTextarea) {
contentTextarea.style.display = 'none';
contentTextarea.removeAttribute('required');
}
// Update hints text if needed
if (type === 'vcard') hintsElement.textContent = 'Preencha os campos acima para criar seu cartão de visita digital';
// For other types, hints are less relevant as they have dedicated forms,
// but we keep the logic below for consistency
} 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(window.QRRapidoTranslations?.logoTooLarge || 'Logo too large. Maximum 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(window.QRRapidoTranslations?.invalidLogoFormat || 'Invalid format. Use PNG or 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');
// 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();
}
}
}
handleCornerStyleChange(e) {
const selectedStyle = e.target.value;
const premiumStyles = ['rounded', 'circle'];
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(window.QRRapidoTranslations?.premiumCornerStyleRequired || 'Custom corner styles are exclusive to Premium plan.');
return;
}
}
// If a QR code already exists, regenerate with new corner style
if (this.currentQR) {
console.log('[CORNER STYLE] Corner style changed to:', selectedStyle, '- regenerating QR code');
this.generateQRWithTimer(e);
}
}
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() {
const userStatus = document.getElementById('user-premium-status')?.value;
if (!userStatus || userStatus === 'anonymous') return;
try {
const response = await fetch('/api/QR/GetUserStats');
if (response.ok) {
const stats = await response.json();
this.updateCreditDisplay(stats);
}
} catch (error) {
console.debug('Error loading user stats:', error);
}
}
updateCreditDisplay(stats) {
const counterElements = document.querySelectorAll('.qr-counter');
if (counterElements.length === 0) return;
let text = '';
let className = 'badge qr-counter ';
if (stats.freeUsed < stats.freeLimit) {
const remaining = stats.freeLimit - stats.freeUsed;
text = `${remaining} Grátis Restantes`;
className += 'bg-success';
} else if (stats.credits > 0) {
text = `${stats.credits} Créditos`;
className += 'bg-primary';
} else {
text = '0 Créditos';
className += 'bg-danger';
}
counterElements.forEach(el => {
el.textContent = text;
// Preserve other classes if needed, but for now enforcing badge style
// Ensure we don't wipe out structural classes if they exist, but here we replace for badge style
el.className = className;
});
// Atualizar também o input hidden para lógica interna se necessário
this.isPremium = stats.credits > 0 || stats.freeUsed < stats.freeLimit;
}
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);
}
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(window.QRRapidoTranslations?.errorSavingHistory || 'Error saving to history.');
}
}
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);
});
}
showCreditsModal(message) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
${message}
Adquira um novo pacote de créditos para continuar gerando QR Codes de alta qualidade.
Créditos não expiram!
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
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() {
const counterElement = document.querySelector('.qr-counter');
if (counterElement) {
counterElement.textContent = 'QR Codes ilimitados';
counterElement.className = 'badge bg-success qr-counter';
} 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 start-0 p-3';
toastContainer.style.zIndex = '1060';
toastContainer.style.marginTop = '80px'; // Avoid covering logo/header
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;
//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();
// }
// }
//});
// Note: Real-time validation is now handled in initializeEvents() to avoid duplicate listeners
}
handleTypeSelection(type) {
const previousType = this.selectedType;
this.selectedType = type;
// Reset UX improvements flags for new type selection
this.hasShownContentToast = false;
if (this.contentDelayTimer) {
clearTimeout(this.contentDelayTimer);
}
this.removeButtonReadyState();
if (type) {
this.removeInitialHighlight();
// Sempre habilitar campos de conteúdo após selecionar tipo
this.enableContentFields(type);
this.prefillContentField(type, previousType);
// Show guidance toast for the selected type
this.showTypeGuidanceToast(type);
} else {
this.disableAllFields();
}
this.updateGenerateButton();
this.previousType = type;
}
handleStyleSelection(style) {
this.selectedStyle = style;
this.updateGenerateButton();
}
handleContentChange(content) {
const contentField = document.getElementById('qr-content');
const trimmedContent = typeof content === 'string' ? content.trim() : '';
if (this.selectedType === 'url' && this.isProtocolOnly(trimmedContent)) {
this.contentValid = false;
} else {
this.contentValid = this.validateContent(content);
}
// Feedback visual para campo de conteúdo
if (contentField) {
if (this.selectedType === 'url') {
contentField.classList.remove('is-valid');
if (!trimmedContent || this.isProtocolOnly(trimmedContent)) {
contentField.classList.remove('is-invalid');
}
} else {
this.validateField(contentField, this.contentValid, window.QRRapidoTranslations?.validationContentMinLength || 'Content must have at least 3 characters');
}
}
this.updateGenerateButton();
}
// UX Improvements - Intelligent delay system
handleContentInputWithDelay(content) {
// UX delay desabilitado para evitar correções automáticas tardias
if (this.contentDelayTimer) {
clearTimeout(this.contentDelayTimer);
}
}
triggerContentReadyUX() {
// Função mantida por compatibilidade, sem efeitos colaterais
return;
}
showContentAddedToast() {
const toastTitle = window.QRRapidoTranslations?.contentAddedToastTitle || '✅ Conteúdo adicionado!';
const toastMessage = window.QRRapidoTranslations?.contentAddedToastMessage ||
'Use as avançadas abaixo para personalizar o QR. \n Clique em "Gerar QR Code" quando estiver pronto.';
const fullMessage = `${toastTitle}
${toastMessage}`;
// Create educational toast similar to existing system but with longer duration
const toast = this.createEducationalToast(fullMessage);
this.showGuidanceToast(toast, 10000); // 10 seconds
}
createEducationalToast(message) {
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-bg-success border-0';
toast.setAttribute('role', 'alert');
toast.style.minWidth = '400px';
toast.id = 'content-added-toast';
toast.innerHTML = `
`;
return toast;
}
updateGenerateButtonToReady() {
const generateBtn = document.getElementById('generate-btn');
if (generateBtn && this.contentValid) {
this.buttonReadyState = true;
// Add visual ready state
generateBtn.classList.add('btn-pulse', 'btn-ready');
// Update button text with ready indicator
const readyText = window.QRRapidoTranslations?.generateButtonReady || '✨ Pronto para gerar!';
const originalHtml = generateBtn.innerHTML;
// Store original for restoration
if (!generateBtn.dataset.originalHtml) {
generateBtn.dataset.originalHtml = originalHtml;
}
generateBtn.innerHTML = ` ${readyText}`;
// Remove ready state after 5 seconds
setTimeout(() => {
this.removeButtonReadyState();
}, 5000);
}
}
removeButtonReadyState() {
const generateBtn = document.getElementById('generate-btn');
if (generateBtn && generateBtn.dataset.originalHtml) {
generateBtn.classList.remove('btn-pulse', 'btn-ready');
generateBtn.innerHTML = generateBtn.dataset.originalHtml;
this.buttonReadyState = false;
}
}
smoothScrollToQRArea() {
// Wait a bit for the toast to appear, then scroll
setTimeout(() => {
let targetElement;
// Find the QR preview area or advanced options
const qrPreview = document.getElementById('qr-preview');
const advancedSection = document.querySelector('.advanced-options');
const generateBtn = document.getElementById('generate-btn');
const toast = document.getElementById('content-added-toast');
// Priority: QR result > Advanced options > Generate button
targetElement = qrPreview || advancedSection || toast || generateBtn;
if (targetElement) {
// Calculate offset - less aggressive on mobile
const isMobile = window.innerWidth <= 768;
const offset = isMobile ? 20 : 100;
const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementPosition - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}, 1000); // 1 second delay to let toast show first
}
enableContentFields(type) {
console.log('Enabling fields for type:', 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 pixInterface = document.getElementById('pix-interface');
const dynamicQRSection = document.getElementById('dynamic-qr-section');
const urlPreview = document.getElementById('url-preview');
// Helper to safely hide
const safeHide = (el) => { if (el) { el.style.display = 'none'; el.classList.add('disabled-state'); } };
const safeShow = (el) => { if (el) { el.style.display = 'block'; el.classList.remove('disabled-state'); } };
// 1. Hide EVERYTHING specific first
safeHide(vcardInterface);
safeHide(wifiInterface);
safeHide(smsInterface);
safeHide(emailInterface);
safeHide(pixInterface);
safeHide(dynamicQRSection);
safeHide(urlPreview);
safeHide(contentGroup); // Hide default group initially
// 2. Enable specific interface based on type
if (type === 'vcard') {
safeShow(vcardInterface);
this.enableVCardFields();
}
else if (type === 'wifi') {
safeShow(wifiInterface);
}
else if (type === 'sms') {
safeShow(smsInterface);
}
else if (type === 'email') {
safeShow(emailInterface);
}
else if (type === 'pix') {
safeShow(pixInterface);
}
else if (type === 'url') {
safeShow(contentGroup);
safeShow(dynamicQRSection);
safeShow(urlPreview);
const qrContent = document.getElementById('qr-content');
if(qrContent) {
qrContent.disabled = false;
qrContent.placeholder = "https://www.exemplo.com.br";
}
}
else {
// Text (default fallback)
safeShow(contentGroup);
const qrContent = document.getElementById('qr-content');
if(qrContent) {
qrContent.disabled = false;
qrContent.placeholder = "Digite seu texto aqui...";
}
}
}
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();
const protocolOnly = this.isProtocolOnly(trimmedContent);
if (!trimmedContent || protocolOnly) {
this.clearValidationError();
if (contentField) {
contentField.classList.remove('is-valid', '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() !== '';
} else if (type === 'pix') {
const errors = window.pixGenerator.validatePixData();
isValid = errors.length === 0;
}
// Controle do botão principal "Gerar QR Code"
if (generateBtn) {
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');
}
}
}
// Validação mais permissiva para o botão "Próximo"
isValidForNext(type) {
if (!type) return false;
if (type === 'url') {
const contentField = document.getElementById('qr-content');
const content = contentField?.value || '';
const trimmedContent = content.trim();
// Validação mais simples: pelo menos 1 letra, um ponto, e mais 1 letra
const simpleUrlPattern = /\w+\.\w+/;
return trimmedContent.length >= 4 && simpleUrlPattern.test(trimmedContent);
} else if (type === 'text') {
const contentField = document.getElementById('qr-content');
const content = contentField?.value || '';
return content.trim().length >= 1; // Mais permissivo para texto
} else if (type === 'vcard') {
// Verificar apenas campos obrigatórios básicos
const name = document.getElementById('vcard-name')?.value || '';
const mobile = document.getElementById('vcard-mobile')?.value || '';
const email = document.getElementById('vcard-email')?.value || '';
return name.trim().length >= 2 &&
(mobile.trim().length >= 8 || email.includes('@'));
} else if (type === 'wifi') {
const ssid = document.getElementById('wifi-ssid')?.value || '';
return ssid.trim().length >= 2;
} else if (type === 'sms') {
const number = document.getElementById('sms-number')?.value || '';
const message = document.getElementById('sms-message')?.value || '';
return number.trim().length >= 8 && message.trim().length >= 1;
} else if (type === 'email') {
const to = document.getElementById('email-to')?.value || '';
const subject = document.getElementById('email-subject')?.value || '';
return to.includes('@') && subject.trim().length >= 1;
} else if (type === 'pix') {
const key = document.getElementById('pix-key')?.value || '';
const name = document.getElementById('pix-name')?.value || '';
const city = document.getElementById('pix-city')?.value || '';
return key.trim().length >= 1 && name.trim().length >= 1 && city.trim().length >= 1;
}
return false;
}
// Método para lidar com o clique do botão "Próximo"
handleNextButtonClick() {
// Abrir o accordion de personalização avançada
const customizationPanel = document.getElementById('customization-panel');
const customizationAccordion = document.getElementById('customization-accordion');
if (customizationPanel && !customizationPanel.classList.contains('show')) {
const bsCollapse = new bootstrap.Collapse(customizationPanel, {
show: true
});
}
// Scroll suave até a personalização avançada
if (customizationAccordion) {
customizationAccordion.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
// Opcionalmente mostrar um toast de orientação
this.showAdvancedCustomizationToast();
}
// Toast informativo sobre personalização avançada
showAdvancedCustomizationToast() {
const message = '✨ Personalização Avançada!
Customize cores, tamanho, bordas e adicione seu logo. Quando estiver pronto, clique em "Gerar QR Code".';
const toast = this.createEducationalToast(message);
this.showGuidanceToast(toast, 8000); // 8 segundos
}
// Scroll suave para o preview após geração do QR Code
scrollToPreview() {
setTimeout(() => {
const isMobile = window.innerWidth <= 768;
if (isMobile) {
// No mobile, fazer scroll para o preview específico dentro da sidebar
const previewDiv = document.getElementById('qr-preview');
if (previewDiv) {
const offsetTop = previewDiv.offsetTop - 60; // 60px de margem do topo
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
}
} else {
// No desktop, manter o comportamento atual
const previewCard = document.querySelector('.col-lg-4');
if (previewCard) {
previewCard.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
}, 500); // Pequeno delay para garantir que o QR foi renderizado
}
// 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)',
'pix': document.querySelector('[data-type-guide-pix]')?.textContent || '💸 Para PIX, preencha a chave, nome e cidade do recebedor',
'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"]');
const mainElement = document.getElementById('customization-accordion');
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) {
// 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'
];
testStrings.forEach(str => {
const encoded = this.prepareContentForQR(str, 'text');
});
// Teste específico para vCard
if (window.vcardGenerator) {
const testName = 'João Gonçalves';
const encoded = window.vcardGenerator.encodeQuotedPrintable(testName);
}
}
// 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');
if (userStatus && (userStatus.value === 'logged-in' || userStatus.value === 'premium')) {
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);
let currentData = { date: today, count: 0 };
if (rateLimitData) {
try {
currentData = JSON.parse(rateLimitData);
// Reset count if it's a new day
if (currentData.date !== today) {
currentData = { date: today, count: 0 };
}
} catch (e) {
currentData = { date: today, count: 0 };
}
} else {
console.log('🆕 No cookie found, starting fresh');
}
// Check if limit exceeded (don't increment here)
if (currentData.count >= 3) {
this.showRateLimitError();
return false;
}
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')?.value;
if (userStatus === 'logged-in' || userStatus === 'premium') {
// Logged users use the Credit Display logic
this.initializeUserCounter();
return;
}
// --- ANONYMOUS USERS ---
const today = new Date().toDateString();
const cookieName = 'qr_daily_count';
const rateLimitData = this.getCookie(cookieName);
let count = 0;
if (rateLimitData) {
try {
const currentData = JSON.parse(rateLimitData);
if (currentData.date === today) {
count = currentData.count;
}
} catch (e) {
count = 0;
}
}
// New limit is 1
const remaining = Math.max(0, 1 - count);
const counterElements = document.querySelectorAll('.qr-counter');
if (remaining > 0) {
counterElements.forEach(el => {
el.textContent = 'Um QRCode grátis';
el.className = 'badge bg-success qr-counter';
});
this.unlockInterface();
} else {
counterElements.forEach(el => {
el.textContent = '0 QRCodes grátis';
el.className = 'badge bg-danger qr-counter';
});
this.lockInterfaceForAnonymous();
}
}
lockInterfaceForAnonymous() {
const form = document.getElementById('qr-speed-form');
const generateBtn = document.getElementById('generate-btn');
const qrType = document.getElementById('qr-type');
const qrContent = document.getElementById('qr-content');
// Disable main controls
if (generateBtn) generateBtn.disabled = true;
if (qrType) qrType.disabled = true;
if (qrContent) qrContent.disabled = true;
// Disable all inputs in form
if (form) {
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => input.disabled = true);
form.style.opacity = '0.5';
form.style.pointerEvents = 'none'; // Prevent clicks
}
// Show large CTA overlay if not already present
const container = document.querySelector('.card-body'); // Assuming form is in a card-body
if (container && !document.getElementById('anonymous-lock-overlay')) {
const overlay = document.createElement('div');
overlay.id = 'anonymous-lock-overlay';
overlay.className = 'text-center p-4 position-absolute top-50 start-50 translate-middle w-100 h-100 d-flex flex-column justify-content-center align-items-center bg-white bg-opacity-75';
overlay.style.zIndex = '1000';
overlay.style.backdropFilter = 'blur(2px)';
overlay.innerHTML = `
Cota Grátis Esgotada!
Você já gerou seu QR Code gratuito de hoje.
`;
// Make parent relative so absolute positioning works
if (getComputedStyle(container).position === 'static') {
container.style.position = 'relative';
}
container.appendChild(overlay);
}
}
unlockInterface() {
const form = document.getElementById('qr-speed-form');
const overlay = document.getElementById('anonymous-lock-overlay');
// Remove overlay
if (overlay) overlay.remove();
// Enable form
if (form) {
form.style.opacity = '1';
form.style.pointerEvents = 'auto';
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => input.disabled = false);
}
}
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();
}
}
// 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.pixGenerator = new PixQRGenerator();
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);
}
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
});
}
};