3713 lines
142 KiB
JavaScript
3713 lines
142 KiB
JavaScript
// 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();
|
||
this.initializeProgressiveFlow();
|
||
this.initializeRateLimiting();
|
||
|
||
// Validar segurança dos dados após carregamento
|
||
setTimeout(() => {
|
||
this.validateDataSecurity();
|
||
this.testUTF8Encoding();
|
||
}, 1000);
|
||
}
|
||
|
||
initializeEvents() {
|
||
// Form submission with timer
|
||
const form = document.getElementById('qr-speed-form');
|
||
if (form) {
|
||
form.addEventListener('submit', this.generateQRWithTimer.bind(this));
|
||
}
|
||
|
||
// Quick style selection
|
||
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
|
||
radio.addEventListener('change', this.applyQuickStyle.bind(this));
|
||
});
|
||
|
||
// Logo upload feedback
|
||
const logoUpload = document.getElementById('logo-upload');
|
||
if (logoUpload) {
|
||
logoUpload.addEventListener('change', this.handleLogoSelection.bind(this));
|
||
}
|
||
|
||
// Logo size slider preview em tempo real
|
||
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||
if (logoSizeSlider && typeof this.updateLogoReadabilityPreview === 'function') {
|
||
logoSizeSlider.addEventListener('input', this.updateLogoReadabilityPreview.bind(this));
|
||
}
|
||
|
||
// Logo colorize toggle preview em tempo real
|
||
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
|
||
if (logoColorizeToggle && typeof this.updateLogoReadabilityPreview === 'function') {
|
||
logoColorizeToggle.addEventListener('change', this.updateLogoReadabilityPreview.bind(this));
|
||
}
|
||
|
||
// Corner style validation for non-premium users
|
||
const cornerStyle = document.getElementById('corner-style');
|
||
if (cornerStyle) {
|
||
cornerStyle.addEventListener('change', this.handleCornerStyleChange.bind(this));
|
||
}
|
||
|
||
// QR type change with hints and flow
|
||
const qrType = document.getElementById('qr-type');
|
||
if (qrType) {
|
||
qrType.addEventListener('change', (e) => {
|
||
this.handleTypeSelection(e.target.value);
|
||
this.updateContentHints();
|
||
});
|
||
}
|
||
|
||
// Content validation
|
||
const qrContent = document.getElementById('qr-content');
|
||
if (qrContent) {
|
||
let timer;
|
||
qrContent.addEventListener('input', (e) => {
|
||
clearTimeout(timer);
|
||
// 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 || 'QR codes limit reached!');
|
||
return;
|
||
}
|
||
|
||
if (response.status === 400 && errorData.requiresPremium) {
|
||
this.showUpgradeModal(errorData.error || window.QRRapidoTranslations?.premiumLogoRequired || 'Premium logo required.');
|
||
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';
|
||
}
|
||
|
||
// 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() {
|
||
// ✅ Check if user is logged in before making API request
|
||
const userStatus = document.getElementById('user-premium-status')?.value;
|
||
if (userStatus === 'anonymous') {
|
||
return; // Don't make request for anonymous users
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/QR/GetUserStats');
|
||
if (response.ok) {
|
||
const stats = await response.json();
|
||
this.showUnlimitedCounter();
|
||
} else {
|
||
if (response.status !== 401) {
|
||
console.log('GetUserStats response not ok:', response.status);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// If not authenticated or error, keep the default "Carregando..." text
|
||
console.debug('User not authenticated or error loading stats:', error);
|
||
}
|
||
}
|
||
|
||
trackGenerationEvent(type, time) {
|
||
// Google Analytics
|
||
if (typeof gtag !== 'undefined') {
|
||
gtag('event', 'qr_generated', {
|
||
'qr_type': type,
|
||
'generation_time': parseFloat(time),
|
||
'user_type': this.isPremiumUser() ? 'premium' : 'free',
|
||
'language': this.currentLang
|
||
});
|
||
}
|
||
|
||
// Internal statistics
|
||
if (!window.qrRapidoStats) {
|
||
window.qrRapidoStats = {
|
||
times: [],
|
||
getAverageTime: function() {
|
||
if (this.times.length === 0) return '1.2';
|
||
const avg = this.times.reduce((a, b) => a + b) / this.times.length;
|
||
return avg.toFixed(1);
|
||
}
|
||
};
|
||
}
|
||
window.qrRapidoStats.times.push(parseFloat(time));
|
||
}
|
||
|
||
updateSpeedStats(generationTime) {
|
||
// Update the live statistics display
|
||
const timeFloat = parseFloat(generationTime);
|
||
|
||
// Update average time display in the stats cards
|
||
// Find h5 elements in card bodies and look for one containing time info
|
||
const cardElements = document.querySelectorAll('.card-body h5');
|
||
cardElements.forEach(element => {
|
||
// Look for elements containing time format (e.g., "1.2s", "0.8s", etc.)
|
||
if (element.textContent.includes('s') && element.textContent.match(/\d+\.\d+s/)) {
|
||
const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime;
|
||
element.innerHTML = `<i class="fas fa-stopwatch"></i> ${avgTime}s`;
|
||
}
|
||
});
|
||
|
||
// Update the generation timer in the header
|
||
const timerElement = document.querySelector('.generation-timer span');
|
||
if (timerElement) {
|
||
timerElement.textContent = `${generationTime}s`;
|
||
}
|
||
|
||
// Update performance statistics
|
||
if (!window.qrRapidoPerformance) {
|
||
window.qrRapidoPerformance = {
|
||
totalGenerations: 0,
|
||
totalTime: 0,
|
||
bestTime: Infinity,
|
||
worstTime: 0
|
||
};
|
||
}
|
||
|
||
const perf = window.qrRapidoPerformance;
|
||
perf.totalGenerations++;
|
||
perf.totalTime += timeFloat;
|
||
perf.bestTime = Math.min(perf.bestTime, timeFloat);
|
||
perf.worstTime = Math.max(perf.worstTime, timeFloat);
|
||
}
|
||
|
||
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);
|
||
});
|
||
}
|
||
|
||
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'; };
|
||
const safeShow = (el) => { if (el) el.style.display = 'block'; };
|
||
|
||
// 1. Hide EVERYTHING specific first
|
||
safeHide(vcardInterface);
|
||
safeHide(wifiInterface);
|
||
safeHide(smsInterface);
|
||
safeHide(emailInterface);
|
||
safeHide(pixInterface);
|
||
safeHide(dynamicQRSection);
|
||
safeHide(urlPreview);
|
||
|
||
// 2. Default: Show content group (hidden later if specific)
|
||
if (contentGroup) contentGroup.style.display = 'block';
|
||
|
||
// 3. Specific logic
|
||
if (type === 'vcard') {
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
safeShow(vcardInterface);
|
||
this.enableVCardFields();
|
||
}
|
||
else if (type === 'wifi') {
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
safeShow(wifiInterface);
|
||
}
|
||
else if (type === 'sms') {
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
safeShow(smsInterface);
|
||
}
|
||
else if (type === 'email') {
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
safeShow(emailInterface);
|
||
}
|
||
else if (type === 'pix') {
|
||
console.log('Showing PIX interface');
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
safeShow(pixInterface);
|
||
}
|
||
else if (type === 'url') {
|
||
safeShow(dynamicQRSection);
|
||
safeShow(urlPreview);
|
||
// URL needs content field
|
||
const qrContent = document.getElementById('qr-content');
|
||
if(qrContent) qrContent.disabled = false;
|
||
}
|
||
else {
|
||
// Text or others - Keep content group
|
||
const qrContent = document.getElementById('qr-content');
|
||
if(qrContent) qrContent.disabled = false;
|
||
}
|
||
}
|
||
|
||
enableVCardFields() {
|
||
const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
|
||
requiredFields.forEach(fieldId => {
|
||
const field = document.getElementById(fieldId);
|
||
if (field) {
|
||
field.disabled = false;
|
||
field.setAttribute('required', 'required');
|
||
}
|
||
});
|
||
}
|
||
|
||
disableAllFields() {
|
||
// Resetar estado
|
||
const qrContent = document.getElementById('qr-content');
|
||
const vcardInterface = document.getElementById('vcard-interface');
|
||
|
||
if (qrContent) {
|
||
// FIXED: Don't disable content field, just clear it
|
||
qrContent.value = '';
|
||
}
|
||
|
||
if (vcardInterface) {
|
||
vcardInterface.style.display = 'none';
|
||
// Limpar campos vCard
|
||
const vcardFields = document.querySelectorAll('#vcard-interface input');
|
||
vcardFields.forEach(field => {
|
||
field.value = '';
|
||
field.disabled = true;
|
||
});
|
||
}
|
||
|
||
this.contentValid = false;
|
||
}
|
||
|
||
validateContent(content) {
|
||
if (!content) return false;
|
||
return content.trim().length >= 3;
|
||
}
|
||
|
||
validateVCardFields() {
|
||
const nameField = document.getElementById('vcard-name');
|
||
const mobileField = document.getElementById('vcard-mobile');
|
||
const emailField = document.getElementById('vcard-email');
|
||
|
||
const name = nameField?.value.trim() || '';
|
||
const mobile = mobileField?.value.trim() || '';
|
||
const email = emailField?.value.trim() || '';
|
||
|
||
// Validação individual dos campos com feedback visual
|
||
this.validateField(nameField, name !== '', 'Nome é obrigatório');
|
||
this.validateField(mobileField, mobile !== '' && mobile.length >= 10, 'Telefone deve ter pelo menos 10 dígitos');
|
||
this.validateField(emailField, email !== '' && email.includes('@'), 'Email válido é obrigatório');
|
||
|
||
const isValid = name !== '' && mobile !== '' && email !== '' && email.includes('@') && mobile.length >= 10;
|
||
this.contentValid = isValid;
|
||
|
||
return isValid;
|
||
}
|
||
|
||
validateField(field, isValid, errorMessage) {
|
||
if (!field) return;
|
||
|
||
const feedbackElement = field.parentNode.querySelector('.invalid-feedback');
|
||
|
||
if (isValid) {
|
||
field.classList.remove('is-invalid');
|
||
field.classList.add('is-valid');
|
||
if (feedbackElement) {
|
||
feedbackElement.style.display = 'none';
|
||
}
|
||
} else if (field.value.trim() !== '') {
|
||
// Só mostrar erro se o campo tem conteúdo
|
||
field.classList.remove('is-valid');
|
||
field.classList.add('is-invalid');
|
||
if (feedbackElement) {
|
||
feedbackElement.textContent = errorMessage;
|
||
feedbackElement.style.display = 'block';
|
||
}
|
||
} else {
|
||
// Campo vazio, remover validações visuais
|
||
field.classList.remove('is-valid', 'is-invalid');
|
||
if (feedbackElement) {
|
||
feedbackElement.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// URL VALIDATION FUNCTIONS
|
||
// ============================================
|
||
|
||
isValidURL(url) {
|
||
if (!url || typeof url !== 'string') return false;
|
||
|
||
const trimmedUrl = url.trim();
|
||
|
||
// Verificar se começa com http:// ou https://
|
||
const hasProtocol = trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://');
|
||
|
||
// Verificar se tem pelo menos um ponto
|
||
const hasDot = trimmedUrl.includes('.');
|
||
|
||
// Verificar se não é só o protocolo
|
||
const hasContent = trimmedUrl.length > 8; // mais que "https://"
|
||
|
||
// Regex mais robusta
|
||
const urlRegex = /^https?:\/\/.+\..+/i;
|
||
|
||
return hasProtocol && hasDot && hasContent && urlRegex.test(trimmedUrl);
|
||
}
|
||
|
||
autoFixURL(url) {
|
||
if (!url || typeof url !== 'string') return '';
|
||
|
||
let fixedUrl = url.trim();
|
||
|
||
// Se não tem protocolo, adicionar https://
|
||
if (!fixedUrl.startsWith('http://') && !fixedUrl.startsWith('https://')) {
|
||
fixedUrl = 'https://' + fixedUrl;
|
||
}
|
||
|
||
return fixedUrl;
|
||
}
|
||
|
||
showValidationError(message) {
|
||
let errorDiv = document.getElementById('url-validation-error');
|
||
|
||
if (!errorDiv) {
|
||
// Criar div de erro se não existir
|
||
errorDiv = document.createElement('div');
|
||
errorDiv.id = 'url-validation-error';
|
||
errorDiv.className = 'alert alert-danger mt-2';
|
||
errorDiv.style.display = 'none';
|
||
|
||
// Inserir após o campo de conteúdo
|
||
const contentGroup = document.getElementById('content-group');
|
||
if (contentGroup) {
|
||
contentGroup.appendChild(errorDiv);
|
||
}
|
||
}
|
||
|
||
errorDiv.innerHTML = `<small><i class="fas fa-exclamation-triangle"></i> ${message}</small>`;
|
||
errorDiv.style.display = 'block';
|
||
}
|
||
|
||
clearValidationError() {
|
||
const errorDiv = document.getElementById('url-validation-error');
|
||
if (errorDiv) {
|
||
errorDiv.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
updateGenerateButton() {
|
||
const generateBtn = document.getElementById('generate-btn');
|
||
|
||
if (!generateBtn) return;
|
||
|
||
let isValid = false;
|
||
const type = this.selectedType;
|
||
|
||
if (type === 'url') {
|
||
const contentField = document.getElementById('qr-content');
|
||
const content = contentField?.value || '';
|
||
const trimmedContent = content.trim();
|
||
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 = /[^ |