QrRapido/wwwroot/js/qr-speed-generator.js
Ricardo Carneiro 16a9720a12
All checks were successful
Deploy QR Rapido / test (push) Successful in 59s
Deploy QR Rapido / build-and-push (push) Successful in 9m57s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m11s
feat: qrcode por creditos.
2026-01-26 20:13:45 -03:00

3902 lines
149 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

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

// QR Rapido Speed Generator
class QRRapidoGenerator {
constructor() {
this.startTime = 0;
this.currentQR = null;
this.timerInterval = null;
// 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('<br>'));
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('<br>'));
return false;
}
return true;
} else if (qrType === 'sms') {
const errors = window.smsGenerator.validateSMSData();
if (errors.length > 0) {
this.showError(errors.join('<br>'));
return false;
}
return true;
} else if (qrType === 'email') {
const errors = window.emailGenerator.validateEmailData();
if (errors.length > 0) {
this.showError(errors.join('<br>'));
return false;
}
return true;
} else if (qrType === 'pix') {
const errors = window.pixGenerator.validatePixData();
if (errors.length > 0) {
this.showError(errors.join('<br>'));
return false;
}
return true;
}
// Normal validation for other types
const qrContent = document.getElementById('qr-content').value.trim();
if (!qrContent) {
this.showError(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 = `
<img src="${imageUrl}"
class="img-fluid border rounded shadow-sm"
alt="QR Code gerado em ${generationTime}s"
style="image-rendering: crisp-edges;">
`;
// Exibir análise de legibilidade do logo se disponível
if (result.readabilityInfo && typeof this.displayReadabilityAnalysis === 'function') {
this.displayReadabilityAnalysis(result.readabilityInfo);
}
// 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 = `
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning w-100 fw-bold shadow-sm">
<i class="fas fa-coins"></i> Adicionar Mais Créditos
</a>
<small class="text-muted d-block mt-1">Garanta o próximo QR Code!</small>
`;
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 = `
<div class="text-center p-4">
<div class="spinner-border text-primary mb-3" role="status"></div>
<p class="text-muted">${this.languageStrings[this.currentLang].generating}</p>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
style="width: 100%"></div>
</div>
</div>
`;
}
}
showGenerationStats(generationTime) {
const statsDiv = document.querySelector('.generation-stats');
const speedBadge = document.querySelector('.speed-badge');
if (statsDiv) {
statsDiv.classList.remove('d-none');
const timeSpan = statsDiv.querySelector('.generation-time');
if (timeSpan) {
timeSpan.textContent = `${generationTime}s`;
}
}
// Show speed badge
if (speedBadge) {
const strings = this.languageStrings[this.currentLang];
let badgeText = strings.normal;
let badgeClass = 'bg-secondary';
if (generationTime < 1.0) {
badgeText = strings.ultraFast;
badgeClass = 'bg-success';
} else if (generationTime < 2.0) {
badgeText = strings.fast;
badgeClass = 'bg-primary';
}
speedBadge.innerHTML = `
<span class="badge ${badgeClass}">
<i class="fas fa-bolt"></i> ${badgeText}
</span>
`;
speedBadge.classList.remove('d-none');
}
}
hideGenerationLoading() {
const button = document.getElementById('generate-btn');
const spinner = button?.querySelector('.spinner-border');
if (button) button.disabled = false;
if (spinner) spinner.classList.add('d-none');
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
updateContentHints() {
const type = document.getElementById('qr-type')?.value;
const hintsElement = document.getElementById('content-hints');
const vcardInterface = document.getElementById('vcard-interface');
const contentTextarea = document.getElementById('qr-content');
if (!hintsElement || !type) return;
// Show/hide VCard interface based on type
if (type === 'vcard' || 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 = `<i class="fas fa-stopwatch"></i> ${avgTime}s`;
}
});
// Update the generation timer in the header
const timerElement = document.querySelector('.generation-timer span');
if (timerElement) {
timerElement.textContent = `${generationTime}s`;
}
// Update performance statistics
if (!window.qrRapidoPerformance) {
window.qrRapidoPerformance = {
totalGenerations: 0,
totalTime: 0,
bestTime: Infinity,
worstTime: 0
};
}
const perf = window.qrRapidoPerformance;
perf.totalGenerations++;
perf.totalTime += timeFloat;
perf.bestTime = Math.min(perf.bestTime, timeFloat);
perf.worstTime = Math.max(perf.worstTime, timeFloat);
}
isPremiumUser() {
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
}
async downloadQR(format) {
if (!this.currentQR || !this.currentQR.qrCodeBase64) {
this.showError('Nenhum QR Code gerado para download.');
return;
}
try {
const timestamp = new Date().toISOString().slice(0,10);
const base64Data = this.currentQR.qrCodeBase64;
if (format === 'png') {
// Download PNG directly from base64
const link = document.createElement('a');
link.href = `data:image/png;base64,${base64Data}`;
link.download = `qrrapido-${timestamp}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else if (format === 'svg') {
// Convert PNG to SVG
const svgData = await this.convertPngToSvg(base64Data);
const link = document.createElement('a');
link.href = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgData)}`;
link.download = `qrrapido-${timestamp}.svg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else if (format === 'pdf') {
// Convert PNG to PDF
const pdfBlob = await this.convertPngToPdf(base64Data);
const url = window.URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
link.download = `qrrapido-${timestamp}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
} catch (error) {
console.error('Download error:', error);
this.showError(`Erro ao fazer download ${format.toUpperCase()}. Tente novamente.`);
}
}
async convertPngToSvg(base64Data) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// Create SVG with embedded base64 image
const svgData = `<?xml version="1.0" encoding="UTF-8"?>
<svg width="${img.width}" height="${img.height}" xmlns="http://www.w3.org/2000/svg">
<image width="${img.width}" height="${img.height}" href="data:image/png;base64,${base64Data}"/>
</svg>`;
resolve(svgData);
};
img.src = `data:image/png;base64,${base64Data}`;
});
}
async convertPngToPdf(base64Data) {
return new Promise((resolve, reject) => {
try {
// Try to use jsPDF if available, otherwise use a simpler approach
if (typeof window.jsPDF !== 'undefined') {
const pdf = new window.jsPDF();
const img = new Image();
img.onload = () => {
// Calculate dimensions to fit on page
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const imgRatio = img.width / img.height;
let width = Math.min(pdfWidth - 20, 150);
let height = width / imgRatio;
if (height > pdfHeight - 20) {
height = pdfHeight - 20;
width = height * imgRatio;
}
// Center on page
const x = (pdfWidth - width) / 2;
const y = (pdfHeight - height) / 2;
pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, width, height);
const pdfBlob = pdf.output('blob');
resolve(pdfBlob);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = `data:image/png;base64,${base64Data}`;
} else {
// Fallback: create a very simple PDF
this.createBasicPdf(base64Data).then(resolve).catch(reject);
}
} catch (error) {
reject(error);
}
});
}
async createBasicPdf(base64Data) {
// Load jsPDF dynamically if not available
if (typeof window.jsPDF === 'undefined') {
await this.loadJsPDF();
}
return new Promise((resolve) => {
const pdf = new window.jsPDF();
const img = new Image();
img.onload = () => {
// Add QR code to PDF
const pdfWidth = pdf.internal.pageSize.getWidth();
const size = Math.min(pdfWidth - 40, 100);
const x = (pdfWidth - size) / 2;
const y = 50;
pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, size, size);
pdf.text('QR Code - QR Rapido', pdfWidth / 2, 30, { align: 'center' });
const pdfBlob = pdf.output('blob');
resolve(pdfBlob);
};
img.src = `data:image/png;base64,${base64Data}`;
});
}
async loadJsPDF() {
return new Promise((resolve, reject) => {
if (typeof window.jsPDF !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
script.onload = () => {
window.jsPDF = window.jspdf.jsPDF;
resolve();
};
script.onerror = () => reject(new Error('Failed to load jsPDF'));
document.head.appendChild(script);
});
}
async saveToHistory() {
if (!this.currentQR) return;
try {
const response = await fetch('/api/QR/SaveToHistory', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
qrId: this.currentQR.id
})
});
if (response.ok) {
this.showSuccess(this.languageStrings[this.currentLang].success);
} else {
throw new Error('Failed to save');
}
} catch (error) {
console.error('Save error:', error);
this.showError(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 = `
<i class="fas fa-crown text-warning"></i>
<strong>Sessão sem anúncios ativa!</strong>
Tempo restante: <span class="ad-free-countdown">${this.formatTime(timeRemaining)}</span>
<a href="/Pagamento/SelecaoPlano" class="btn btn-sm btn-warning ms-2">Tornar Permanente</a>
`;
const container = document.querySelector('.container');
const row = container?.querySelector('.row');
if (container && row) {
container.insertBefore(message, row);
}
}
formatTime(minutes) {
if (minutes === 0) return '0m';
const days = Math.floor(minutes / 1440);
const hours = Math.floor((minutes % 1440) / 60);
const mins = minutes % 60;
if (days > 0) return `${days}d ${hours}h ${mins}m`;
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
showUpgradeModal(message) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title">
<i class="fas fa-crown"></i> Upgrade para Premium
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>${message}</p>
<div class="row">
<div class="col-md-6">
<h6>Plano Atual (Free)</h6>
<ul class="list-unstyled">
<li>❌ Limite de 10 QR/dia</li>
<li>❌ Anúncios</li>
<li>✅ QR básicos</li>
</ul>
</div>
<div class="col-md-6">
<h6>Premium (R$ 19,90/mês)</h6>
<ul class="list-unstyled">
<li>✅ QR ilimitados</li>
<li>✅ Sem anúncios</li>
<li>✅ QR dinâmicos</li>
<li>✅ Analytics</li>
<li>✅ Suporte prioritário</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning">
<i class="fas fa-crown"></i> Fazer Upgrade
</a>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
// Remove modal when closed
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
});
}
showCreditsModal(message) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">
<i class="fas fa-coins"></i> Seus créditos acabaram
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="lead">${message}</p>
<i class="fas fa-wallet fa-3x text-muted mb-3"></i>
<p>Adquira um novo pacote de créditos para continuar gerando QR Codes de alta qualidade.</p>
<p class="text-success small"><i class="fas fa-check"></i> Créditos não expiram!</p>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<a href="/Pagamento/SelecaoPlano" class="btn btn-primary px-4">
<i class="fas fa-shopping-cart"></i> Comprar Créditos
</a>
</div>
</div>
</div>
`;
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 = `
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-${type === 'danger' ? 'exclamation-triangle' : 'check-circle'}"></i>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
toastContainer.appendChild(toast);
// Show toast
const bsToast = new bootstrap.Toast(toast, {
delay: type === 'success' ? 3000 : 5000,
autohide: true
});
bsToast.show();
// Remove from DOM after hidden
toast.addEventListener('hidden.bs.toast', () => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
});
}
setupRealTimePreview() {
const contentField = document.getElementById('qr-content');
const typeField = document.getElementById('qr-type');
if (contentField && typeField) {
let previewTimeout;
const updatePreview = () => {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(() => {
if (contentField.value.trim() && typeField.value) {
// Could implement real-time preview for premium users
console.log('Real-time preview update');
}
}, 500);
};
contentField.addEventListener('input', updatePreview);
typeField.addEventListener('change', updatePreview);
}
}
// ============================================
// SISTEMA DE FLUXO ASCENDENTE PROGRESSIVO
// ============================================
initializeProgressiveFlow() {
// Estado inicial: apenas tipo habilitado, botão gerar desabilitado
const qrContent = document.getElementById('qr-content');
const vcardInterface = document.getElementById('vcard-interface');
// FIXED: Don't disable content field initially - let enableContentFields handle it
// The field should be available for typing once a type is selected
// Ocultar interface vCard
if (vcardInterface) {
vcardInterface.style.display = 'none';
}
this.updateGenerateButton();
// Setup URL validation event listeners
this.setupURLValidationListeners();
// Setup hint auto-hide timer
this.setupHintAutoHide();
}
setupHintAutoHide() {
const hint = document.getElementById('type-selection-hint');
if (hint) {
// Auto-hide after 30 seconds
setTimeout(() => {
if (hint && !hint.classList.contains('fade-out')) {
hint.classList.add('fade-out');
// Remove from DOM after animation
setTimeout(() => {
if (hint.parentNode) {
hint.style.display = 'none';
}
}, 1000); // 1s for fade-out animation
}
}, 30000); // 30 seconds
}
}
setupURLValidationListeners() {
const contentField = document.getElementById('qr-content');
if (!contentField) return;
//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 = `<strong>${toastTitle}</strong><br/>${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 = `
<div class="d-flex">
<div class="toast-body fw-medium">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
return toast;
}
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 = `<i class="fas fa-bolt"></i> ${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 = `<small><i class="fas fa-exclamation-triangle"></i> ${message}</small>`;
errorDiv.style.display = 'block';
}
clearValidationError() {
const errorDiv = document.getElementById('url-validation-error');
if (errorDiv) {
errorDiv.style.display = 'none';
}
}
updateGenerateButton() {
const generateBtn = document.getElementById('generate-btn');
if (!generateBtn) return;
let isValid = false;
const type = this.selectedType;
if (type === 'url') {
const contentField = document.getElementById('qr-content');
const content = contentField?.value || '';
const trimmedContent = content.trim();
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 = '✨ <strong>Personalização Avançada!</strong><br/>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 = `
<div class="d-flex">
<div class="toast-body fw-medium">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
return toast;
}
showGuidanceToast(toast, duration = 30000) {
// Create toast container if doesn't exist (positioned below header stats)
let toastContainer = document.getElementById('guidance-toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'guidance-toast-container';
toastContainer.className = 'toast-container position-relative w-100 d-flex justify-content-center mb-3';
toastContainer.style.zIndex = '1060';
// Try to find existing toast container first (if manually positioned)
const existingContainer = document.getElementById('guidance-toast-container');
if (existingContainer) {
// Use existing container that was manually positioned
toastContainer = existingContainer;
} else {
// Find the main content area and insert after hero section
// const mainElement = document.querySelector('main[role="main"]');
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 = `
<div class="bg-white p-4 rounded shadow border border-warning">
<i class="fas fa-lock fa-3x text-warning mb-3"></i>
<h4 class="fw-bold">Cota Grátis Esgotada!</h4>
<p class="text-muted mb-4">Você já gerou seu QR Code gratuito de hoje.</p>
<div class="d-grid gap-2">
<a href="/Account/Login" class="btn btn-primary btn-lg">
<i class="fas fa-sign-in-alt me-2"></i> Fazer Login e Ganhar +5
</a>
<a href="/Pagamento/SelecaoPlano" class="btn btn-outline-success">
<i class="fas fa-coins me-2"></i> Comprar Créditos
</a>
</div>
</div>
`;
// 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
? `<div class="collapse mt-2" id="readabilityTips">
<ul class="list-unstyled mb-0">
${readabilityInfo.tips.map(tip => `<li class="mb-1"><small>${tip}</small></li>`).join('')}
</ul>
</div>`
: '';
// Criar HTML da análise
const scoreClass = getScoreClass(readabilityInfo.readabilityScore);
const iconClass = getIconClass(readabilityInfo.readabilityScore);
analysisContainer.innerHTML = `
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<i class="${iconClass} ${scoreClass} me-2"></i>
<div>
<h6 class="mb-1 ${scoreClass}">Análise de Legibilidade</h6>
<p class="mb-0 small">${readabilityInfo.userMessage}</p>
</div>
</div>
<div class="text-end">
<div class="badge bg-secondary">${readabilityInfo.readabilityScore}/100</div>
${readabilityInfo.tips && readabilityInfo.tips.length > 0
? `<button class="btn btn-sm btn-outline-secondary ms-2" type="button"
data-bs-toggle="collapse" data-bs-target="#readabilityTips">
<i class="fas fa-lightbulb"></i> Dicas
</button>`
: ''}
</div>
</div>
${tipsHtml}
`;
// Animação de entrada suave
analysisContainer.style.opacity = '0';
analysisContainer.style.transform = 'translateY(-10px)';
setTimeout(() => {
analysisContainer.style.transition = 'all 0.3s ease';
analysisContainer.style.opacity = '1';
analysisContainer.style.transform = 'translateY(0)';
}, 100);
}
updateLogoReadabilityPreview() {
const logoUpload = document.getElementById('logo-upload');
const logoSizeSlider = document.getElementById('logo-size-slider');
// Só mostrar preview se há logo selecionado
if (!logoUpload || !logoUpload.files || logoUpload.files.length === 0) {
this.clearReadabilityPreview();
return;
}
const logoSize = parseInt(logoSizeSlider?.value || '20');
// Calcular score estimado baseado apenas no tamanho (simplificado para preview)
let estimatedScore = 100;
if (logoSize > 20) {
estimatedScore -= (logoSize - 20) * 2;
}
estimatedScore = Math.max(40, estimatedScore); // Mínimo de 40
// Simular análise básica para preview
const previewInfo = {
hasLogo: true,
logoSizePercent: logoSize,
readabilityScore: estimatedScore,
difficultyLevel: this.getDifficultyLevelFromScore(estimatedScore),
userMessage: this.generatePreviewMessage(estimatedScore, logoSize),
tips: this.generatePreviewTips(logoSize, estimatedScore)
};
this.displayReadabilityPreview(previewInfo);
}
getDifficultyLevelFromScore(score) {
if (score >= 85) return 'VeryEasy';
if (score >= 70) return 'Easy';
if (score >= 55) return 'Medium';
if (score >= 40) return 'Hard';
return 'VeryHard';
}
generatePreviewMessage(score, logoSize) {
if (score >= 85) {
return `✅ Estimativa: Excelente legibilidade com logo de ${logoSize}%`;
} else if (score >= 70) {
return `🟢 Estimativa: Boa legibilidade com logo de ${logoSize}%`;
} else if (score >= 55) {
return `⚠️ Estimativa: Legibilidade moderada com logo de ${logoSize}%`;
} else {
return `🟡 Estimativa: Legibilidade baixa com logo de ${logoSize}%`;
}
}
generatePreviewTips(logoSize, score) {
const tips = [];
if (logoSize > 22) {
tips.push(`📏 Considere reduzir para 18-20% (atual: ${logoSize}%)`);
}
if (score < 70) {
tips.push('💡 Use boa iluminação ao escanear');
}
tips.push('🎨 PNG transparente melhora a legibilidade');
tips.push('📱 Teste em vários apps de QR Code');
return tips;
}
displayReadabilityPreview(previewInfo) {
// Usar a mesma função de display, mas com ID diferente para preview
let previewContainer = document.getElementById('readability-preview');
if (!previewContainer) {
previewContainer = document.createElement('div');
previewContainer.id = 'readability-preview';
previewContainer.className = 'mt-2 p-2 border rounded bg-info bg-opacity-10 border-info border-opacity-25';
// Inserir após o slider de tamanho do logo
const logoSizeSlider = document.getElementById('logo-size-slider');
if (logoSizeSlider && logoSizeSlider.parentNode) {
logoSizeSlider.parentNode.insertBefore(previewContainer, logoSizeSlider.nextSibling);
}
}
const scoreClass = previewInfo.readabilityScore >= 85 ? 'text-success' :
previewInfo.readabilityScore >= 70 ? 'text-info' :
previewInfo.readabilityScore >= 55 ? 'text-warning' : 'text-danger';
const iconClass = previewInfo.readabilityScore >= 85 ? 'fas fa-check-circle' :
previewInfo.readabilityScore >= 70 ? 'fas fa-thumbs-up' :
previewInfo.readabilityScore >= 55 ? 'fas fa-exclamation-triangle' : 'fas fa-exclamation-circle';
previewContainer.innerHTML = `
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<i class="${iconClass} ${scoreClass} me-2"></i>
<small class="${scoreClass}">${previewInfo.userMessage}</small>
</div>
<div class="badge bg-secondary">${previewInfo.readabilityScore}/100</div>
</div>
`;
}
clearReadabilityPreview() {
const previewContainer = document.getElementById('readability-preview');
if (previewContainer) {
previewContainer.remove();
}
}
}
class SMSQRGenerator {
constructor() {
this.initializeSMSInterface();
}
initializeSMSInterface() {
// Atualizar preview em tempo real
const fieldsToWatch = ['sms-number', 'sms-message'];
fieldsToWatch.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
element.addEventListener('input', () => this.updatePreview());
element.addEventListener('change', () => this.updatePreview());
}
});
// Inicializar preview
this.updatePreview();
}
updatePreview() {
const smsString = this.generateSMSString();
const previewElement = document.getElementById('sms-preview-text');
if (previewElement) {
previewElement.textContent = smsString;
}
}
generateSMSString() {
const data = this.collectSMSData();
if (!data.number || !data.message) {
return 'SMSTO::';
}
return `SMSTO:${data.number}:${data.message}`;
}
collectSMSData() {
return {
number: document.getElementById('sms-number')?.value || '',
message: document.getElementById('sms-message')?.value || ''
};
}
validateSMSData() {
const data = this.collectSMSData();
const errors = [];
if (!data.number.trim()) {
errors.push('Número do celular é obrigatório');
}
if (!data.message.trim()) {
errors.push('Mensagem é obrigatória');
}
// Validação de telefone brasileiro básica
const phoneRegex = /^\d{10,11}$/;
if (data.number && !phoneRegex.test(data.number.replace(/\D/g, ''))) {
errors.push('Número deve ter 10 ou 11 dígitos (DDD + número)');
}
return errors;
}
}
class EmailQRGenerator {
constructor() {
this.initializeEmailInterface();
}
initializeEmailInterface() {
// Atualizar preview em tempo real
const fieldsToWatch = ['email-to', 'email-subject', 'email-body'];
fieldsToWatch.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
element.addEventListener('input', () => this.updatePreview());
element.addEventListener('change', () => this.updatePreview());
}
});
// Inicializar preview
this.updatePreview();
}
updatePreview() {
const emailString = this.generateEmailString();
const previewElement = document.getElementById('email-preview-text');
if (previewElement) {
previewElement.textContent = emailString;
}
}
generateEmailString() {
const data = this.collectEmailData();
if (!data.to) {
return 'mailto:?subject=&body=';
}
let emailString = `mailto:${data.to}`;
const params = [];
if (data.subject) {
params.push(`subject=${encodeURIComponent(data.subject)}`);
}
if (data.body) {
params.push(`body=${encodeURIComponent(data.body)}`);
}
if (params.length > 0) {
emailString += '?' + params.join('&');
}
return emailString;
}
collectEmailData() {
return {
to: document.getElementById('email-to')?.value || '',
subject: document.getElementById('email-subject')?.value || '',
body: document.getElementById('email-body')?.value || ''
};
}
validateEmailData() {
const data = this.collectEmailData();
const errors = [];
if (!data.to.trim()) {
errors.push('Email destinatário é obrigatório');
}
if (!data.subject.trim()) {
errors.push('Assunto é obrigatório');
}
// Validação básica de email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (data.to && !emailRegex.test(data.to)) {
errors.push('Email destinatário inválido');
}
return errors;
}
}
class WiFiQRGenerator {
constructor() {
this.initializeWiFiInterface();
}
initializeWiFiInterface() {
// Radio buttons: usar name para group
document.querySelectorAll('input[name="wifi-security"]').forEach(radio => {
radio.addEventListener('change', () => {
this.togglePasswordField();
this.updatePreview();
});
});
// Campos individuais: usar IDs únicos
const fieldsToWatch = ['wifi-ssid', 'wifi-password', 'wifi-hidden'];
fieldsToWatch.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
element.addEventListener('input', () => this.updatePreview());
element.addEventListener('change', () => this.updatePreview());
}
});
const togglePassword = document.getElementById('toggle-password');
if (togglePassword) {
togglePassword.addEventListener('click', () => {
this.togglePasswordVisibility();
});
}
// Inicializar estado
this.togglePasswordField();
this.updatePreview();
}
togglePasswordField() {
const selectedRadio = document.querySelector('input[name="wifi-security"]:checked');
const securityType = selectedRadio ? selectedRadio.value : 'WPA';
const passwordGroup = document.getElementById('wifi-password-group');
const passwordInput = document.getElementById('wifi-password');
if (securityType === 'nopass') {
passwordGroup.style.display = 'none';
passwordInput.removeAttribute('required');
passwordInput.value = '';
} else {
passwordGroup.style.display = 'block';
passwordInput.setAttribute('required', 'required');
}
}
togglePasswordVisibility() {
const passwordInput = document.getElementById('wifi-password');
const toggleIcon = document.getElementById('toggle-password');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.className = 'fas fa-eye-slash';
} else {
passwordInput.type = 'password';
toggleIcon.className = 'fas fa-eye';
}
}
updatePreview() {
const wifiString = this.generateWiFiString();
const previewText = document.getElementById('wifi-preview-text');
if(previewText) {
previewText.textContent = wifiString;
}
}
generateWiFiString() {
const data = this.collectWiFiData();
// Validar dados mínimos
if (!data.ssid) {
return 'WIFI:T:WPA;S:;P:;H:false;;';
}
// Escapar caracteres especiais
const ssid = this.escapeWiFiString(data.ssid);
const password = this.escapeWiFiString(data.password);
// Construir string WiFi
return `WIFI:T:${data.security};S:${ssid};P:${password};H:${data.hidden};;`;
}
collectWiFiData() {
return {
ssid: document.getElementById('wifi-ssid').value,
security: document.querySelector('input[name="wifi-security"]:checked')?.value || 'WPA',
password: document.getElementById('wifi-password').value,
hidden: document.getElementById('wifi-hidden').checked.toString()
};
}
escapeWiFiString(str) {
if (!str) return '';
// Escapar caracteres especiais do formato WiFi
return str.replace(/[\\;,:"]/g, '\\escapeWiFiString(str) { if (!str) return; }');
// Escapar caracteres especiais do formato WiFi
//return str.replace(/[\\;,:""]/g, '\\document.addEventListener('DOMContentLoaded', () => {
// window.qrGenerator = new QRRapidoGenerator();
// window.vcardGenerator = new VCardGenerator();
// // Initialize AdSense if necessary
// if (window.adsbygoogle && document.querySelector('.adsbygoogle')) {
// (adsbygoogle = window.adsbygoogle || []).push({});
// }
//});
return str.replace(/[\\;,:"']/g, '\\$&');
// VCard Generator Class');
}
validateWiFiData() {
const data = this.collectWiFiData();
const errors = [];
// Validações obrigatórias
if (!data.ssid.trim()) {
errors.push('Nome da rede (SSID) é obrigatório');
}
// Validação de senha conforme tipo de segurança
if ((data.security === 'WPA' || data.security === 'WEP') && !data.password.trim()) {
errors.push('Senha é obrigatória para redes protegidas');
}
// Validação de comprimento
if (data.ssid.length > 32) {
errors.push('Nome da rede deve ter no máximo 32 caracteres');
}
if (data.password.length > 63) {
errors.push('Senha deve ter no máximo 63 caracteres');
}
// Validação WEP específica
if (data.security === 'WEP' && data.password) {
const validWEPLengths = [5, 10, 13, 26]; // WEP 64/128 bit
if (!validWEPLengths.includes(data.password.length)) {
errors.push('Senha WEP deve ter 5, 10, 13 ou 26 caracteres');
}
}
return errors;
}
}
// VCard Generator Class
class VCardGenerator {
constructor() {
this.initializeVCardInterface();
}
// Função para codificar caracteres especiais usando Quoted-Printable
encodeQuotedPrintable(text) {
if (!text) return '';
return text.replace(/[^\u0020-\u007E]/g, (char) => {
// Para caracteres especiais comuns do português, usar codificação UTF-8
const utf8Bytes = new TextEncoder().encode(char);
return Array.from(utf8Bytes)
.map(byte => `=${byte.toString(16).toUpperCase().padStart(2, '0')}`)
.join('');
});
}
// Função para preparar texto com codificação adequada
prepareTextForVCard(text, needsEncoding = true) {
if (!text) return '';
const trimmedText = text.trim();
if (!needsEncoding) return trimmedText;
// Verifica se há caracteres especiais que precisam de codificação
const hasSpecialChars = /[^\u0020-\u007E]/.test(trimmedText);
if (hasSpecialChars) {
return `CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:${this.encodeQuotedPrintable(trimmedText)}`;
}
return trimmedText;
}
initializeVCardInterface() {
// Show/hide optional fields based on checkboxes
document.querySelectorAll('#vcard-interface .form-check-input').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const groupId = e.target.id.replace('enable-', '') + '-group';
const group = document.getElementById(groupId);
if (group) {
group.style.display = e.target.checked ? 'block' : 'none';
// Clear values when hiding fields
if (!e.target.checked) {
const inputs = group.querySelectorAll('input');
inputs.forEach(input => input.value = '');
}
}
this.updatePreview();
});
});
// Update preview in real-time
document.querySelectorAll('#vcard-interface input').forEach(input => {
input.addEventListener('input', () => this.updatePreview());
input.addEventListener('blur', () => this.validateField(input));
});
}
updatePreview() {
const vcard = this.generateVCardContent();
const previewElement = document.getElementById('vcard-preview-text');
if (previewElement) {
previewElement.textContent = vcard;
}
}
generateVCardContent() {
const data = this.collectVCardData();
let vcard = 'BEGIN:VCARD\nVERSION:3.0\nCHARSET=UTF-8\n';
// Nome (obrigatório)
if (data.name) {
const nameParts = data.name.trim().split(' ');
const firstName = nameParts[0] || '';
const lastName = nameParts.slice(1).join(' ') || '';
// Codificar nome se necessário
const encodedLastName = this.prepareTextForVCard(lastName);
const encodedFirstName = this.prepareTextForVCard(firstName);
const encodedFullName = this.prepareTextForVCard(data.name.trim());
vcard += `N:${encodedLastName};${encodedFirstName}\n`;
vcard += `FN:${encodedFullName}\n`;
}
// Empresa
if (data.company) {
const encodedCompany = this.prepareTextForVCard(data.company);
vcard += `ORG:${encodedCompany}\n`;
}
// Título
if (data.title) {
const encodedTitle = this.prepareTextForVCard(data.title);
vcard += `TITLE:${encodedTitle}\n`;
}
// Endereço
if (data.address || data.city || data.state || data.zip) {
const encodedAddress = this.prepareTextForVCard(data.address || '');
const encodedCity = this.prepareTextForVCard(data.city || '');
const encodedState = this.prepareTextForVCard(data.state || '');
const encodedZip = this.prepareTextForVCard(data.zip || '', false); // ZIP não precisa de codificação especial
const encodedCountry = this.prepareTextForVCard('Brasil');
const addr = `;;${encodedAddress};${encodedCity};${encodedState};${encodedZip};${encodedCountry}`;
vcard += `ADR:${addr}\n`;
}
// Telefones
if (data.phone) vcard += `TEL;WORK;VOICE:${data.phone}\n`;
if (data.mobile) vcard += `TEL;CELL:${data.mobile}\n`;
// Email
if (data.email) vcard += `EMAIL;WORK;INTERNET:${data.email}\n`;
// Website
if (data.website) vcard += `URL:${data.website}\n`;
vcard += 'END:VCARD';
return vcard;
}
collectVCardData() {
return {
name: document.getElementById('vcard-name')?.value?.trim() || '',
mobile: document.getElementById('vcard-mobile')?.value?.trim() || '',
email: document.getElementById('vcard-email')?.value?.trim() || '',
company: document.getElementById('enable-company')?.checked ?
(document.getElementById('vcard-company')?.value?.trim() || '') : '',
title: document.getElementById('enable-title')?.checked ?
(document.getElementById('vcard-title')?.value?.trim() || '') : '',
website: document.getElementById('enable-website')?.checked ?
(document.getElementById('vcard-website')?.value?.trim() || '') : '',
address: document.getElementById('enable-address')?.checked ?
(document.getElementById('vcard-address')?.value?.trim() || '') : '',
city: document.getElementById('enable-address')?.checked ?
(document.getElementById('vcard-city')?.value?.trim() || '') : '',
state: document.getElementById('enable-address')?.checked ?
(document.getElementById('vcard-state')?.value?.trim() || '') : '',
zip: document.getElementById('enable-address')?.checked ?
(document.getElementById('vcard-zip')?.value?.trim() || '') : '',
phone: document.getElementById('enable-phone')?.checked ?
(document.getElementById('vcard-phone')?.value?.trim() || '') : ''
};
}
validateVCardData() {
const data = this.collectVCardData();
const errors = [];
// Required field validations
if (!data.name) errors.push('Nome é obrigatório');
if (!data.mobile) errors.push('Telefone celular é obrigatório');
if (!data.email) errors.push('Email é obrigatório');
// Format validations
if (data.email && !this.isValidEmail(data.email)) {
errors.push('Email inválido');
}
if (data.website && !this.isValidURL(data.website)) {
errors.push('Website inválido (deve começar com http:// ou https://)');
}
if (data.mobile && !this.isValidPhone(data.mobile)) {
errors.push('Telefone celular inválido (deve ter 10-11 dígitos)');
}
if (data.phone && !this.isValidPhone(data.phone)) {
errors.push('Telefone fixo inválido (deve ter 10-11 dígitos)');
}
return errors;
}
validateField(input) {
const value = input.value.trim();
let isValid = true;
let message = '';
switch (input.id) {
case 'vcard-email':
if (value && !this.isValidEmail(value)) {
isValid = false;
message = 'Email inválido';
}
break;
case 'vcard-website':
if (value && !this.isValidURL(value)) {
isValid = false;
message = 'Website inválido (deve começar com http:// ou https://)';
}
break;
case 'vcard-mobile':
case 'vcard-phone':
if (value && !this.isValidPhone(value)) {
isValid = false;
message = 'Telefone inválido (apenas números, 10-11 dígitos)';
}
break;
}
// Update field validation state
if (isValid) {
input.classList.remove('is-invalid');
input.classList.add('is-valid');
} else {
input.classList.remove('is-valid');
input.classList.add('is-invalid');
// Show error message
const feedback = input.parentNode.querySelector('.invalid-feedback');
if (feedback) {
feedback.textContent = message;
}
}
return isValid;
}
isValidEmail(email) {
return /^[^ @]+@[^ @]+\.[^ @]+$/.test(email);
}
isValidURL(url) {
try {
new URL(url);
return url.startsWith('http://') || url.startsWith('https://');
} catch {
return false;
}
}
isValidPhone(phone) {
// Brazilian phone validation (DDD + number)
const digitsOnly = phone.replace(/\D/g, '');
return /^\d{10,11}$/.test(digitsOnly);
}
// Method to be called by main QR generator
getVCardContent() {
const errors = this.validateVCardData();
if (errors.length > 0) {
throw new Error('Erro na validação: ' + errors.join(', '));
}
return this.generateVCardContent();
}
}
// Global functions for ad control
window.QRApp = {
refreshAds: function() {
if (window.adsbygoogle) {
document.querySelectorAll('.adsbygoogle').forEach(ad => {
(adsbygoogle = window.adsbygoogle || []).push({});
});
}
},
hideAds: function() {
document.querySelectorAll('.ad-container').forEach(ad => {
ad.style.display = 'none';
});
}
};
// Google Analytics 4 Event Tracking
window.trackLanguageChange = function(from, to) {
if (typeof gtag !== 'undefined') {
gtag('event', 'language_change', {
'from_language': from,
'to_language': to
});
}
};
window.trackUpgradeClick = function(location) {
if (typeof gtag !== 'undefined') {
gtag('event', 'upgrade_click', {
'click_location': location
});
}
};