3285 lines
125 KiB
JavaScript
3285 lines
125 KiB
JavaScript
// QR Rapido Speed Generator
|
||
class QRRapidoGenerator {
|
||
constructor() {
|
||
this.startTime = 0;
|
||
this.currentQR = null;
|
||
this.timerInterval = null;
|
||
this.languageStrings = {
|
||
'pt-BR': {
|
||
tagline: 'Gere QR codes em segundos!',
|
||
generating: 'Gerando...',
|
||
generated: 'Gerado em',
|
||
seconds: 's',
|
||
ultraFast: 'Geração ultra rápida!',
|
||
fast: 'Geração rápida!',
|
||
normal: 'Geração normal',
|
||
error: 'Erro na geração. Tente novamente.',
|
||
success: 'QR Code salvo no histórico!'
|
||
},
|
||
'es': {
|
||
tagline: '¡Genera códigos QR en segundos!',
|
||
generating: 'Generando...',
|
||
generated: 'Generado en',
|
||
seconds: 's',
|
||
ultraFast: '¡Generación ultra rápida!',
|
||
fast: '¡Generación rápida!',
|
||
normal: 'Generación normal',
|
||
error: 'Error en la generación. Inténtalo de nuevo.',
|
||
success: '¡Código QR guardado en el historial!'
|
||
},
|
||
'en': {
|
||
tagline: 'Generate QR codes in seconds!',
|
||
generating: 'Generating...',
|
||
generated: 'Generated in',
|
||
seconds: 's',
|
||
ultraFast: 'Ultra fast generation!',
|
||
fast: 'Fast generation!',
|
||
normal: 'Normal generation',
|
||
error: 'Generation error. Please try again.',
|
||
success: 'QR Code saved to history!'
|
||
}
|
||
};
|
||
this.currentLang = localStorage.getItem('qrrapido-lang') || 'pt-BR';
|
||
|
||
this.selectedType = null;
|
||
this.selectedStyle = 'classic'; // Estilo padrão
|
||
this.contentValid = false;
|
||
|
||
this.initializeEvents();
|
||
this.checkAdFreeStatus();
|
||
this.updateLanguage();
|
||
this.updateStatsCounters();
|
||
this.initializeUserCounter();
|
||
this.initializeProgressiveFlow();
|
||
this.initializeRateLimiting();
|
||
|
||
// Validar segurança dos dados após carregamento
|
||
setTimeout(() => {
|
||
this.validateDataSecurity();
|
||
this.testUTF8Encoding();
|
||
}, 1000);
|
||
}
|
||
|
||
initializeEvents() {
|
||
// Form submission with timer
|
||
const form = document.getElementById('qr-speed-form');
|
||
if (form) {
|
||
form.addEventListener('submit', this.generateQRWithTimer.bind(this));
|
||
}
|
||
|
||
// Quick style selection
|
||
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
|
||
radio.addEventListener('change', this.applyQuickStyle.bind(this));
|
||
});
|
||
|
||
// Logo upload feedback
|
||
const logoUpload = document.getElementById('logo-upload');
|
||
if (logoUpload) {
|
||
logoUpload.addEventListener('change', this.handleLogoSelection.bind(this));
|
||
}
|
||
|
||
// Logo size slider preview em tempo real
|
||
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||
if (logoSizeSlider && typeof this.updateLogoReadabilityPreview === 'function') {
|
||
logoSizeSlider.addEventListener('input', this.updateLogoReadabilityPreview.bind(this));
|
||
}
|
||
|
||
// Logo colorize toggle preview em tempo real
|
||
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
|
||
if (logoColorizeToggle && typeof this.updateLogoReadabilityPreview === 'function') {
|
||
logoColorizeToggle.addEventListener('change', this.updateLogoReadabilityPreview.bind(this));
|
||
}
|
||
|
||
// Corner style validation for non-premium users
|
||
const cornerStyle = document.getElementById('corner-style');
|
||
if (cornerStyle) {
|
||
cornerStyle.addEventListener('change', this.handleCornerStyleChange.bind(this));
|
||
}
|
||
|
||
// QR type change with hints and flow
|
||
const qrType = document.getElementById('qr-type');
|
||
if (qrType) {
|
||
qrType.addEventListener('change', (e) => {
|
||
this.handleTypeSelection(e.target.value);
|
||
this.updateContentHints();
|
||
});
|
||
}
|
||
|
||
// Content validation
|
||
const qrContent = document.getElementById('qr-content');
|
||
if (qrContent) {
|
||
let timer;
|
||
qrContent.addEventListener('input', (e) => {
|
||
clearTimeout(timer);
|
||
timer = setTimeout(() => {
|
||
this.handleContentChange(e.target.value);
|
||
this.updateGenerateButton();
|
||
}, 300);
|
||
});
|
||
}
|
||
|
||
// Style selection
|
||
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
|
||
radio.addEventListener('change', (e) => {
|
||
this.handleStyleSelection(e.target.value);
|
||
});
|
||
});
|
||
|
||
// Add listeners to all relevant fields to update button state
|
||
const fieldsToWatch = [
|
||
'qr-content', 'vcard-name', 'vcard-mobile', 'vcard-email',
|
||
'wifi-ssid', 'wifi-password', 'sms-number', 'sms-message',
|
||
'email-to', 'email-subject', 'email-body'
|
||
];
|
||
fieldsToWatch.forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) {
|
||
el.addEventListener('input', () => this.updateGenerateButton());
|
||
}
|
||
});
|
||
document.querySelectorAll('input[name="wifi-security"]').forEach(radio => {
|
||
radio.addEventListener('change', () => this.updateGenerateButton());
|
||
});
|
||
|
||
// Language selector
|
||
document.querySelectorAll('[data-lang]').forEach(link => {
|
||
link.addEventListener('click', this.changeLanguage.bind(this));
|
||
});
|
||
|
||
// Real-time preview for premium users
|
||
if (this.isPremiumUser()) {
|
||
this.setupRealTimePreview();
|
||
}
|
||
|
||
// Download buttons
|
||
this.setupDownloadButtons();
|
||
|
||
// Share functionality
|
||
this.setupShareButtons();
|
||
|
||
// Save to history
|
||
const saveBtn = document.getElementById('save-to-history');
|
||
if (saveBtn) {
|
||
saveBtn.addEventListener('click', this.saveToHistory.bind(this));
|
||
}
|
||
}
|
||
|
||
setupDownloadButtons() {
|
||
const pngBtn = document.getElementById('download-png');
|
||
const svgBtn = document.getElementById('download-svg');
|
||
const pdfBtn = document.getElementById('download-pdf');
|
||
|
||
if (pngBtn) pngBtn.addEventListener('click', () => this.downloadQR('png'));
|
||
if (svgBtn) svgBtn.addEventListener('click', () => this.downloadQR('svg'));
|
||
if (pdfBtn) pdfBtn.addEventListener('click', () => this.downloadQR('pdf'));
|
||
}
|
||
|
||
setupShareButtons() {
|
||
// Check if Web Share API is supported and show/hide native share option
|
||
if (navigator.share && this.isMobileDevice()) {
|
||
const nativeShareOption = document.getElementById('native-share-option');
|
||
if (nativeShareOption) {
|
||
nativeShareOption.classList.remove('d-none');
|
||
}
|
||
}
|
||
|
||
// Show save to gallery option on mobile
|
||
if (this.isMobileDevice()) {
|
||
const saveGalleryOption = document.getElementById('save-gallery-option');
|
||
if (saveGalleryOption) {
|
||
saveGalleryOption.classList.remove('d-none');
|
||
}
|
||
}
|
||
|
||
// Add event listeners to share buttons
|
||
const shareButtons = {
|
||
'native-share': () => this.shareNative(),
|
||
'share-whatsapp': () => this.shareWhatsApp(),
|
||
'share-telegram': () => this.shareTelegram(),
|
||
'share-email': () => this.shareEmail(),
|
||
'copy-qr-link': () => this.copyToClipboard(),
|
||
'save-to-gallery': () => this.saveToGallery()
|
||
};
|
||
|
||
Object.entries(shareButtons).forEach(([id, handler]) => {
|
||
const button = document.getElementById(id);
|
||
if (button) {
|
||
button.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
handler();
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
isMobileDevice() {
|
||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
|
||
}
|
||
|
||
async shareNative() {
|
||
if (!this.currentQR || !navigator.share) return;
|
||
|
||
try {
|
||
// Create a blob from the base64 image
|
||
const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`);
|
||
const blob = await base64Response.blob();
|
||
const file = new File([blob], 'qrcode.png', { type: 'image/png' });
|
||
|
||
const shareData = {
|
||
title: 'QR Code - QR Rapido',
|
||
text: 'QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!',
|
||
url: window.location.origin,
|
||
files: [file]
|
||
};
|
||
|
||
// Check if files can be shared
|
||
if (navigator.canShare && navigator.canShare(shareData)) {
|
||
await navigator.share(shareData);
|
||
} else {
|
||
// Fallback without files
|
||
await navigator.share({
|
||
title: shareData.title,
|
||
text: shareData.text,
|
||
url: shareData.url
|
||
});
|
||
}
|
||
|
||
this.trackShareEvent('native');
|
||
} catch (error) {
|
||
console.error('Error sharing:', error);
|
||
if (error.name !== 'AbortError') {
|
||
this.showError(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);
|
||
|
||
console.log('✅ QR code recebido do backend:', {
|
||
success: result.success,
|
||
hasBase64: !!result.qrCodeBase64,
|
||
base64Length: result.qrCodeBase64?.length || 0,
|
||
generationTime: generationTime + 's'
|
||
});
|
||
|
||
this.displayQRResult(result, generationTime);
|
||
this.updateSpeedStats(generationTime);
|
||
this.trackGenerationEvent(requestData.data.type || requestData.data.get('type'), generationTime);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erro ao gerar QR:', error);
|
||
this.showError(this.languageStrings[this.currentLang].error);
|
||
} finally {
|
||
this.hideGenerationLoading();
|
||
}
|
||
}
|
||
|
||
validateForm() {
|
||
const qrType = document.getElementById('qr-type').value;
|
||
|
||
if (!qrType) {
|
||
this.showError('Selecione o tipo de QR code');
|
||
return false;
|
||
}
|
||
|
||
// Special validation for VCard
|
||
if (qrType === 'vcard') {
|
||
try {
|
||
if (window.vcardGenerator) {
|
||
const errors = window.vcardGenerator.validateVCardData();
|
||
if (errors.length > 0) {
|
||
this.showError(errors.join('<br>'));
|
||
return false;
|
||
}
|
||
return true;
|
||
} else {
|
||
this.showError(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;
|
||
}
|
||
|
||
// 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 === 'vcard') {
|
||
if (!window.vcardGenerator) {
|
||
throw new Error('VCard generator não está disponível');
|
||
}
|
||
content = window.vcardGenerator.getVCardContent();
|
||
actualType = 'vcard'; // Keep as vcard type for tracking
|
||
} else {
|
||
// Default case - get content from input
|
||
content = document.getElementById('qr-content').value;
|
||
actualType = type;
|
||
}
|
||
|
||
// Prepare final content
|
||
const encodedContent = this.prepareContentForQR(content, actualType);
|
||
|
||
// Get colors
|
||
const userPrimaryColor = document.getElementById('primary-color').value;
|
||
const userBackgroundColor = document.getElementById('bg-color').value;
|
||
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
|
||
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
||
|
||
// Common data for both endpoints
|
||
const commonData = {
|
||
type: actualType,
|
||
content: encodedContent,
|
||
quickStyle: quickStyle,
|
||
primaryColor: finalPrimaryColor,
|
||
backgroundColor: finalBackgroundColor,
|
||
size: parseInt(document.getElementById('qr-size').value),
|
||
margin: parseInt(document.getElementById('qr-margin').value),
|
||
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
||
optimizeForSpeed: true,
|
||
language: this.currentLang
|
||
};
|
||
|
||
// Add dynamic QR data if it's a URL type
|
||
if (type === 'url') {
|
||
let dynamicData;
|
||
if (typeof this.getDynamicQRData === 'function') {
|
||
dynamicData = this.getDynamicQRData();
|
||
} else {
|
||
// Fallback se a função não existir
|
||
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
|
||
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
|
||
dynamicData = {
|
||
isDynamic: isDynamic,
|
||
originalUrl: document.getElementById('qr-content').value,
|
||
requiresPremium: !this.isPremium && isDynamic
|
||
};
|
||
}
|
||
commonData.isDynamic = dynamicData.isDynamic;
|
||
}
|
||
|
||
if (hasLogo) {
|
||
// Use FormData for requests with logo
|
||
const formData = new FormData();
|
||
|
||
// Add all common data to FormData
|
||
Object.keys(commonData).forEach(key => {
|
||
formData.append(key, commonData[key]);
|
||
});
|
||
|
||
// NOVOS PARÂMETROS DE LOGO APRIMORADO
|
||
const logoSettings = this.getLogoSettings();
|
||
formData.append('LogoSizePercent', logoSettings.logoSizePercent.toString());
|
||
formData.append('ApplyLogoColorization', logoSettings.applyColorization.toString());
|
||
|
||
// Add logo file
|
||
formData.append('logo', logoUpload.files[0]);
|
||
|
||
// CORREÇÃO: Log detalhado antes de enviar
|
||
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
|
||
|
||
console.log('🎨 Preparando FormData com logo:', {
|
||
logoFile: logoUpload.files[0].name,
|
||
logoSize: logoUpload.files[0].size + ' bytes',
|
||
LogoSizePercent: logoSettings.logoSizePercent,
|
||
ApplyLogoColorization: logoSettings.applyColorization,
|
||
checkboxChecked: logoColorizeToggle?.checked,
|
||
endpoint: '/api/QR/GenerateRapidWithLogo'
|
||
});
|
||
|
||
return {
|
||
data: formData,
|
||
isMultipart: true,
|
||
endpoint: '/api/QR/GenerateRapidWithLogo'
|
||
};
|
||
} else {
|
||
// Usar JSON para QR sem logo (método original)
|
||
console.log('📝 Preparando JSON sem logo - endpoint: /api/QR/GenerateRapid');
|
||
|
||
return {
|
||
data: commonData,
|
||
isMultipart: false,
|
||
endpoint: '/api/QR/GenerateRapid'
|
||
};
|
||
}
|
||
}
|
||
|
||
// Nova função para coletar configurações de logo
|
||
getLogoSettings() {
|
||
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
|
||
|
||
return {
|
||
logoSizePercent: parseInt(logoSizeSlider?.value || '20'),
|
||
applyColorization: logoColorizeToggle?.checked || false
|
||
};
|
||
}
|
||
|
||
// Função auxiliar para obter conteúdo baseado no tipo
|
||
getContentForType(type) {
|
||
if (type === 'url') {
|
||
let dynamicData;
|
||
if (typeof this.getDynamicQRData === 'function') {
|
||
dynamicData = this.getDynamicQRData();
|
||
} else {
|
||
// Fallback se a função não existir
|
||
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
|
||
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
|
||
dynamicData = {
|
||
isDynamic: isDynamic,
|
||
originalUrl: document.getElementById('qr-content').value,
|
||
requiresPremium: !this.isPremium && isDynamic
|
||
};
|
||
}
|
||
return dynamicData?.originalUrl || document.getElementById('qr-content').value;
|
||
} else if (type === 'wifi') {
|
||
return window.wifiGenerator?.generateWiFiString() || '';
|
||
} else if (type === 'vcard') {
|
||
return window.vcardGenerator?.getVCardContent() || '';
|
||
} else if (type === 'sms') {
|
||
return window.smsGenerator?.generateSMSString() || '';
|
||
} else if (type === 'email') {
|
||
return window.emailGenerator?.generateEmailString() || '';
|
||
} else {
|
||
return document.getElementById('qr-content').value || '';
|
||
}
|
||
}
|
||
|
||
getStyleSettings(style) {
|
||
const styles = {
|
||
classic: { primaryColor: '#000000', backgroundColor: '#FFFFFF' },
|
||
modern: { primaryColor: '#007BFF', backgroundColor: '#F8F9FA' },
|
||
colorful: { primaryColor: '#FF6B35', backgroundColor: '#FFF3E0' }
|
||
};
|
||
return styles[style] || styles.classic;
|
||
}
|
||
|
||
displayQRResult(result, generationTime) {
|
||
const previewDiv = document.getElementById('qr-preview');
|
||
if (!previewDiv) return;
|
||
|
||
// CORREÇÃO SIMPLES: Remover cache buster que quebrava a imagem e adicionar debug
|
||
const imageUrl = `data:image/png;base64,${result.qrCodeBase64}`;
|
||
|
||
previewDiv.innerHTML = `
|
||
<img src="${imageUrl}"
|
||
class="img-fluid border rounded shadow-sm"
|
||
alt="QR Code gerado em ${generationTime}s"
|
||
style="image-rendering: crisp-edges;">
|
||
`;
|
||
|
||
// Exibir análise de legibilidade do logo se disponível
|
||
if (result.readabilityInfo && typeof this.displayReadabilityAnalysis === 'function') {
|
||
this.displayReadabilityAnalysis(result.readabilityInfo);
|
||
}
|
||
|
||
// CORREÇÃO: Log para debug - verificar se QR code tem logo
|
||
const logoUpload = document.getElementById('logo-upload');
|
||
const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0;
|
||
console.log('✅ QR Code exibido:', {
|
||
hasLogo: hasLogo,
|
||
logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum',
|
||
generationTime: generationTime + 's',
|
||
imageSize: result.qrCodeBase64.length + ' chars',
|
||
readabilityScore: result.readabilityInfo?.readabilityScore
|
||
});
|
||
|
||
// Show generation statistics
|
||
this.showGenerationStats(generationTime);
|
||
|
||
// Show download buttons
|
||
const downloadSection = document.getElementById('download-section');
|
||
if (downloadSection) {
|
||
downloadSection.style.display = 'block';
|
||
}
|
||
|
||
// Save current data
|
||
this.currentQR = {
|
||
base64: result.qrCodeBase64,
|
||
qrCodeBase64: result.qrCodeBase64, // Both properties for compatibility
|
||
id: result.qrId,
|
||
generationTime: generationTime,
|
||
readabilityInfo: result.readabilityInfo
|
||
};
|
||
|
||
// Increment rate limit counter after successful generation
|
||
this.incrementRateLimit();
|
||
|
||
// Update counter for logged users
|
||
if (result.remainingQRs !== undefined) {
|
||
if (result.remainingQRs === -1 || result.remainingQRs === 2147483647 || result.remainingQRs >= 1000000) {
|
||
// Logged user - show unlimited
|
||
this.showUnlimitedCounter();
|
||
} else {
|
||
this.updateRemainingCounter(result.remainingQRs);
|
||
}
|
||
}
|
||
}
|
||
|
||
showGenerationStarted() {
|
||
const button = document.getElementById('generate-btn');
|
||
const spinner = button?.querySelector('.spinner-border');
|
||
const timer = document.querySelector('.generation-timer');
|
||
|
||
if (button) button.disabled = true;
|
||
if (spinner) spinner.classList.remove('d-none');
|
||
if (timer) timer.classList.remove('d-none');
|
||
|
||
// Update timer in real time
|
||
this.timerInterval = setInterval(() => {
|
||
const elapsed = ((performance.now() - this.startTime) / 1000).toFixed(1);
|
||
const timerSpan = timer?.querySelector('span');
|
||
if (timerSpan) {
|
||
timerSpan.textContent = `${elapsed}s`;
|
||
}
|
||
}, 100);
|
||
|
||
// Preview loading
|
||
const preview = document.getElementById('qr-preview');
|
||
if (preview) {
|
||
preview.innerHTML = `
|
||
<div class="text-center p-4">
|
||
<div class="spinner-border text-primary mb-3" role="status"></div>
|
||
<p class="text-muted">${this.languageStrings[this.currentLang].generating}</p>
|
||
<div class="progress">
|
||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||
style="width: 100%"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
showGenerationStats(generationTime) {
|
||
const statsDiv = document.querySelector('.generation-stats');
|
||
const speedBadge = document.querySelector('.speed-badge');
|
||
|
||
if (statsDiv) {
|
||
statsDiv.classList.remove('d-none');
|
||
const timeSpan = statsDiv.querySelector('.generation-time');
|
||
if (timeSpan) {
|
||
timeSpan.textContent = `${generationTime}s`;
|
||
}
|
||
}
|
||
|
||
// Show speed badge
|
||
if (speedBadge) {
|
||
const strings = this.languageStrings[this.currentLang];
|
||
let badgeText = strings.normal;
|
||
let badgeClass = 'bg-secondary';
|
||
|
||
if (generationTime < 1.0) {
|
||
badgeText = strings.ultraFast;
|
||
badgeClass = 'bg-success';
|
||
} else if (generationTime < 2.0) {
|
||
badgeText = strings.fast;
|
||
badgeClass = 'bg-primary';
|
||
}
|
||
|
||
speedBadge.innerHTML = `
|
||
<span class="badge ${badgeClass}">
|
||
<i class="fas fa-bolt"></i> ${badgeText}
|
||
</span>
|
||
`;
|
||
speedBadge.classList.remove('d-none');
|
||
}
|
||
}
|
||
|
||
hideGenerationLoading() {
|
||
const button = document.getElementById('generate-btn');
|
||
const spinner = button?.querySelector('.spinner-border');
|
||
|
||
if (button) button.disabled = false;
|
||
if (spinner) spinner.classList.add('d-none');
|
||
|
||
if (this.timerInterval) {
|
||
clearInterval(this.timerInterval);
|
||
this.timerInterval = null;
|
||
}
|
||
}
|
||
|
||
updateContentHints() {
|
||
const type = document.getElementById('qr-type')?.value;
|
||
const hintsElement = document.getElementById('content-hints');
|
||
const vcardInterface = document.getElementById('vcard-interface');
|
||
const contentTextarea = document.getElementById('qr-content');
|
||
|
||
if (!hintsElement || !type) return;
|
||
|
||
// Show/hide VCard interface based on type
|
||
if (type === 'vcard') {
|
||
if (vcardInterface) vcardInterface.style.display = 'block';
|
||
if (contentTextarea) {
|
||
contentTextarea.style.display = 'none';
|
||
contentTextarea.removeAttribute('required');
|
||
}
|
||
hintsElement.textContent = 'Preencha os campos acima para criar seu cartão de visita digital';
|
||
return; // Skip normal hints for VCard
|
||
} else {
|
||
if (vcardInterface) vcardInterface.style.display = 'none';
|
||
if (contentTextarea) {
|
||
contentTextarea.style.display = 'block';
|
||
contentTextarea.setAttribute('required', 'required');
|
||
}
|
||
}
|
||
|
||
const hints = {
|
||
'pt-BR': {
|
||
'url': 'Ex: https://www.exemplo.com.br',
|
||
'text': 'Digite qualquer texto que desejar',
|
||
'wifi': 'Nome da rede;Senha;Tipo de segurança (WPA/WEP)',
|
||
'vcard': 'Nome;Telefone;Email;Empresa',
|
||
'sms': 'Número;Mensagem',
|
||
'email': 'email@exemplo.com;Assunto;Mensagem'
|
||
},
|
||
'es': {
|
||
'url': 'Ej: https://www.ejemplo.com',
|
||
'text': 'Escribe cualquier texto que desees',
|
||
'wifi': 'Nombre de red;Contraseña;Tipo de seguridad (WPA/WEP)',
|
||
'vcard': 'Nombre;Teléfono;Email;Empresa',
|
||
'sms': 'Número;Mensaje',
|
||
'email': 'email@ejemplo.com;Asunto;Mensagem'
|
||
}
|
||
};
|
||
|
||
const langHints = hints[this.currentLang] || hints['pt-BR'];
|
||
hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado';
|
||
}
|
||
|
||
changeLanguage(e) {
|
||
e.preventDefault();
|
||
this.currentLang = e.target.dataset.lang;
|
||
this.updateLanguage();
|
||
this.updateContentHints();
|
||
|
||
// Save preference
|
||
localStorage.setItem('qrrapido-lang', this.currentLang);
|
||
|
||
// Track language change
|
||
window.trackLanguageChange && window.trackLanguageChange('pt-BR', this.currentLang);
|
||
}
|
||
|
||
updateLanguage() {
|
||
const strings = this.languageStrings[this.currentLang];
|
||
|
||
// Update tagline
|
||
const tagline = document.getElementById('tagline');
|
||
if (tagline) {
|
||
tagline.textContent = strings.tagline;
|
||
}
|
||
|
||
// Update language selector
|
||
const langMap = { 'pt-BR': 'PT', 'es': 'ES', 'en': 'EN' };
|
||
const currentLang = document.getElementById('current-lang');
|
||
if (currentLang) {
|
||
currentLang.textContent = langMap[this.currentLang];
|
||
}
|
||
|
||
// Update hints if type already selected
|
||
const qrType = document.getElementById('qr-type');
|
||
if (qrType?.value) {
|
||
this.updateContentHints();
|
||
}
|
||
}
|
||
|
||
handleLogoSelection(e) {
|
||
const file = e.target.files[0];
|
||
const logoPreview = document.getElementById('logo-preview');
|
||
const logoFilename = document.getElementById('logo-filename');
|
||
const logoPreviewImage = document.getElementById('logo-preview-image');
|
||
|
||
if (file) {
|
||
// Validate file size (2MB max)
|
||
if (file.size > 2 * 1024 * 1024) {
|
||
this.showError(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');
|
||
|
||
// CORREÇÃO: Log detalhado do logo selecionado
|
||
console.log('📁 Logo selecionado:', {
|
||
name: file.name,
|
||
size: Math.round(file.size / 1024) + 'KB',
|
||
type: file.type,
|
||
timestamp: new Date().toLocaleTimeString()
|
||
});
|
||
|
||
// Atualizar preview de legibilidade
|
||
if (typeof this.updateLogoReadabilityPreview === 'function') {
|
||
this.updateLogoReadabilityPreview();
|
||
}
|
||
};
|
||
reader.readAsDataURL(file);
|
||
} else {
|
||
// Limpar preview quando arquivo removido
|
||
logoPreview?.classList.add('d-none');
|
||
const logoVisualPreview = document.getElementById('logo-visual-preview');
|
||
if (logoVisualPreview) logoVisualPreview.style.display = 'none';
|
||
if (logoPreviewImage) logoPreviewImage.src = '';
|
||
|
||
// Limpar preview de legibilidade
|
||
if (typeof this.clearReadabilityPreview === 'function') {
|
||
this.clearReadabilityPreview();
|
||
}
|
||
|
||
console.log('🗑️ Logo removido');
|
||
}
|
||
}
|
||
|
||
handleCornerStyleChange(e) {
|
||
const selectedStyle = e.target.value;
|
||
const premiumStyles = ['rounded', 'circle', 'leaf'];
|
||
|
||
if (premiumStyles.includes(selectedStyle)) {
|
||
// Check if user is premium (we can detect this by checking if the option is disabled)
|
||
const option = e.target.options[e.target.selectedIndex];
|
||
if (option.disabled) {
|
||
// Reset to square
|
||
e.target.value = 'square';
|
||
this.showUpgradeModal(window.QRRapidoTranslations?.premiumCornerStyleRequired || 'Custom corner styles are exclusive to Premium plan.');
|
||
return;
|
||
}
|
||
}
|
||
|
||
console.log('Corner style selected:', selectedStyle);
|
||
}
|
||
|
||
applyQuickStyle(e) {
|
||
const style = e.target.value;
|
||
const settings = this.getStyleSettings(style);
|
||
|
||
const primaryColor = document.getElementById('primary-color');
|
||
const bgColor = document.getElementById('bg-color');
|
||
|
||
if (primaryColor) primaryColor.value = settings.primaryColor;
|
||
if (bgColor) bgColor.value = settings.backgroundColor;
|
||
}
|
||
|
||
updateStatsCounters() {
|
||
// Simulate real-time counters
|
||
setInterval(() => {
|
||
const totalElement = document.getElementById('total-qrs');
|
||
if (totalElement) {
|
||
const current = parseFloat(totalElement.textContent.replace('K', '')) || 10.5;
|
||
const newValue = (current + Math.random() * 0.1).toFixed(1);
|
||
totalElement.textContent = `${newValue}K`;
|
||
}
|
||
|
||
// Update average time based on real performance
|
||
const avgElement = document.getElementById('avg-generation-time');
|
||
if (avgElement && window.qrRapidoStats) {
|
||
const avg = window.qrRapidoStats.getAverageTime();
|
||
avgElement.textContent = `${avg}s`;
|
||
}
|
||
}, 30000); // Update every 30 seconds
|
||
}
|
||
|
||
async initializeUserCounter() {
|
||
try {
|
||
const response = await fetch('/api/QR/GetUserStats');
|
||
if (response.ok) {
|
||
const stats = await response.json();
|
||
console.log('User stats loaded:', stats);
|
||
console.log('remainingCount:', stats.remainingCount, 'type:', typeof stats.remainingCount);
|
||
console.log('isUnlimited:', stats.isUnlimited);
|
||
|
||
// For logged users, always show unlimited (they all have unlimited QR codes)
|
||
console.log('Calling showUnlimitedCounter directly for logged user');
|
||
this.showUnlimitedCounter();
|
||
} else {
|
||
console.log('GetUserStats response not ok:', response.status);
|
||
}
|
||
} catch (error) {
|
||
// If not authenticated or error, keep the default "Carregando..." text
|
||
console.debug('User not authenticated or error loading stats:', error);
|
||
}
|
||
}
|
||
|
||
trackGenerationEvent(type, time) {
|
||
// Google Analytics
|
||
if (typeof gtag !== 'undefined') {
|
||
gtag('event', 'qr_generated', {
|
||
'qr_type': type,
|
||
'generation_time': parseFloat(time),
|
||
'user_type': this.isPremiumUser() ? 'premium' : 'free',
|
||
'language': this.currentLang
|
||
});
|
||
}
|
||
|
||
// Internal statistics
|
||
if (!window.qrRapidoStats) {
|
||
window.qrRapidoStats = {
|
||
times: [],
|
||
getAverageTime: function() {
|
||
if (this.times.length === 0) return '1.2';
|
||
const avg = this.times.reduce((a, b) => a + b) / this.times.length;
|
||
return avg.toFixed(1);
|
||
}
|
||
};
|
||
}
|
||
window.qrRapidoStats.times.push(parseFloat(time));
|
||
}
|
||
|
||
updateSpeedStats(generationTime) {
|
||
// Update the live statistics display
|
||
const timeFloat = parseFloat(generationTime);
|
||
|
||
// Update average time display in the stats cards
|
||
// Find h5 elements in card bodies and look for one containing time info
|
||
const cardElements = document.querySelectorAll('.card-body h5');
|
||
cardElements.forEach(element => {
|
||
// Look for elements containing time format (e.g., "1.2s", "0.8s", etc.)
|
||
if (element.textContent.includes('s') && element.textContent.match(/\d+\.\d+s/)) {
|
||
const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime;
|
||
element.innerHTML = `<i class="fas fa-stopwatch"></i> ${avgTime}s`;
|
||
}
|
||
});
|
||
|
||
// Update the generation timer in the header
|
||
const timerElement = document.querySelector('.generation-timer span');
|
||
if (timerElement) {
|
||
timerElement.textContent = `${generationTime}s`;
|
||
}
|
||
|
||
// Update performance statistics
|
||
if (!window.qrRapidoPerformance) {
|
||
window.qrRapidoPerformance = {
|
||
totalGenerations: 0,
|
||
totalTime: 0,
|
||
bestTime: Infinity,
|
||
worstTime: 0
|
||
};
|
||
}
|
||
|
||
const perf = window.qrRapidoPerformance;
|
||
perf.totalGenerations++;
|
||
perf.totalTime += timeFloat;
|
||
perf.bestTime = Math.min(perf.bestTime, timeFloat);
|
||
perf.worstTime = Math.max(perf.worstTime, timeFloat);
|
||
|
||
// Log performance statistics for debugging
|
||
console.log('📊 Performance Update:', {
|
||
currentTime: `${generationTime}s`,
|
||
averageTime: `${(perf.totalTime / perf.totalGenerations).toFixed(1)}s`,
|
||
bestTime: `${perf.bestTime.toFixed(1)}s`,
|
||
totalGenerations: perf.totalGenerations
|
||
});
|
||
}
|
||
|
||
isPremiumUser() {
|
||
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
|
||
}
|
||
|
||
async downloadQR(format) {
|
||
if (!this.currentQR || !this.currentQR.qrCodeBase64) {
|
||
this.showError('Nenhum QR Code gerado para download.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const timestamp = new Date().toISOString().slice(0,10);
|
||
const base64Data = this.currentQR.qrCodeBase64;
|
||
|
||
if (format === 'png') {
|
||
// Download PNG directly from base64
|
||
const link = document.createElement('a');
|
||
link.href = `data:image/png;base64,${base64Data}`;
|
||
link.download = `qrrapido-${timestamp}.png`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
} else if (format === 'svg') {
|
||
// Convert PNG to SVG
|
||
const svgData = await this.convertPngToSvg(base64Data);
|
||
const link = document.createElement('a');
|
||
link.href = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgData)}`;
|
||
link.download = `qrrapido-${timestamp}.svg`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
} else if (format === 'pdf') {
|
||
// Convert PNG to PDF
|
||
const pdfBlob = await this.convertPngToPdf(base64Data);
|
||
const url = window.URL.createObjectURL(pdfBlob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = `qrrapido-${timestamp}.pdf`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(url);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Download error:', error);
|
||
this.showError(`Erro ao fazer download ${format.toUpperCase()}. Tente novamente.`);
|
||
}
|
||
}
|
||
|
||
async convertPngToSvg(base64Data) {
|
||
return new Promise((resolve) => {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
// Create SVG with embedded base64 image
|
||
const svgData = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<svg width="${img.width}" height="${img.height}" xmlns="http://www.w3.org/2000/svg">
|
||
<image width="${img.width}" height="${img.height}" href="data:image/png;base64,${base64Data}"/>
|
||
</svg>`;
|
||
resolve(svgData);
|
||
};
|
||
img.src = `data:image/png;base64,${base64Data}`;
|
||
});
|
||
}
|
||
|
||
async convertPngToPdf(base64Data) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// Try to use jsPDF if available, otherwise use a simpler approach
|
||
if (typeof window.jsPDF !== 'undefined') {
|
||
const pdf = new window.jsPDF();
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
// Calculate dimensions to fit on page
|
||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||
const imgRatio = img.width / img.height;
|
||
|
||
let width = Math.min(pdfWidth - 20, 150);
|
||
let height = width / imgRatio;
|
||
|
||
if (height > pdfHeight - 20) {
|
||
height = pdfHeight - 20;
|
||
width = height * imgRatio;
|
||
}
|
||
|
||
// Center on page
|
||
const x = (pdfWidth - width) / 2;
|
||
const y = (pdfHeight - height) / 2;
|
||
|
||
pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, width, height);
|
||
const pdfBlob = pdf.output('blob');
|
||
resolve(pdfBlob);
|
||
};
|
||
img.onerror = () => reject(new Error('Failed to load image'));
|
||
img.src = `data:image/png;base64,${base64Data}`;
|
||
} else {
|
||
// Fallback: create a very simple PDF
|
||
this.createBasicPdf(base64Data).then(resolve).catch(reject);
|
||
}
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
async createBasicPdf(base64Data) {
|
||
// Load jsPDF dynamically if not available
|
||
if (typeof window.jsPDF === 'undefined') {
|
||
await this.loadJsPDF();
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
const pdf = new window.jsPDF();
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
// Add QR code to PDF
|
||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||
const size = Math.min(pdfWidth - 40, 100);
|
||
const x = (pdfWidth - size) / 2;
|
||
const y = 50;
|
||
|
||
pdf.addImage(`data:image/png;base64,${base64Data}`, 'PNG', x, y, size, size);
|
||
pdf.text('QR Code - QR Rapido', pdfWidth / 2, 30, { align: 'center' });
|
||
|
||
const pdfBlob = pdf.output('blob');
|
||
resolve(pdfBlob);
|
||
};
|
||
img.src = `data:image/png;base64,${base64Data}`;
|
||
});
|
||
}
|
||
|
||
async loadJsPDF() {
|
||
return new Promise((resolve, reject) => {
|
||
if (typeof window.jsPDF !== 'undefined') {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
|
||
script.onload = () => {
|
||
window.jsPDF = window.jspdf.jsPDF;
|
||
resolve();
|
||
};
|
||
script.onerror = () => reject(new Error('Failed to load jsPDF'));
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
async saveToHistory() {
|
||
if (!this.currentQR) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/QR/SaveToHistory', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
qrId: this.currentQR.id
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
this.showSuccess(this.languageStrings[this.currentLang].success);
|
||
} else {
|
||
throw new Error('Failed to save');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Save error:', error);
|
||
this.showError(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="/Premium/Upgrade" class="btn btn-sm btn-warning ms-2">Tornar Permanente</a>
|
||
`;
|
||
|
||
const container = document.querySelector('.container');
|
||
const row = container?.querySelector('.row');
|
||
if (container && row) {
|
||
container.insertBefore(message, row);
|
||
}
|
||
}
|
||
|
||
formatTime(minutes) {
|
||
if (minutes === 0) return '0m';
|
||
|
||
const days = Math.floor(minutes / 1440);
|
||
const hours = Math.floor((minutes % 1440) / 60);
|
||
const mins = minutes % 60;
|
||
|
||
if (days > 0) return `${days}d ${hours}h ${mins}m`;
|
||
if (hours > 0) return `${hours}h ${mins}m`;
|
||
return `${mins}m`;
|
||
}
|
||
|
||
showUpgradeModal(message) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal fade';
|
||
modal.innerHTML = `
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-warning">
|
||
<h5 class="modal-title">
|
||
<i class="fas fa-crown"></i> Upgrade para Premium
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>${message}</p>
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<h6>Plano Atual (Free)</h6>
|
||
<ul class="list-unstyled">
|
||
<li>❌ Limite de 10 QR/dia</li>
|
||
<li>❌ Anúncios</li>
|
||
<li>✅ QR básicos</li>
|
||
</ul>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<h6>Premium (R$ 19,90/mês)</h6>
|
||
<ul class="list-unstyled">
|
||
<li>✅ QR ilimitados</li>
|
||
<li>✅ Sem anúncios</li>
|
||
<li>✅ QR dinâmicos</li>
|
||
<li>✅ Analytics</li>
|
||
<li>✅ Suporte prioritário</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||
<a href="/Premium/Upgrade" class="btn btn-warning">
|
||
<i class="fas fa-crown"></i> Fazer Upgrade
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
const bsModal = new bootstrap.Modal(modal);
|
||
bsModal.show();
|
||
|
||
// Remove modal when closed
|
||
modal.addEventListener('hidden.bs.modal', () => {
|
||
document.body.removeChild(modal);
|
||
});
|
||
}
|
||
|
||
updateRemainingCounter(remaining) {
|
||
const counterElement = document.querySelector('.qr-counter');
|
||
if (counterElement && remaining !== null && remaining !== undefined) {
|
||
// If it's unlimited (any special value indicating unlimited)
|
||
if (remaining === -1 || remaining >= 2147483647 || remaining > 1000000) {
|
||
this.showUnlimitedCounter();
|
||
return;
|
||
}
|
||
|
||
const remainingText = this.getLocalizedString('QRCodesRemainingToday');
|
||
counterElement.textContent = `${remaining} ${remainingText}`;
|
||
|
||
if (remaining <= 3) {
|
||
counterElement.className = 'badge bg-warning qr-counter';
|
||
}
|
||
if (remaining === 0) {
|
||
counterElement.className = 'badge bg-danger qr-counter';
|
||
}
|
||
}
|
||
}
|
||
|
||
showUnlimitedCounter() {
|
||
console.log('showUnlimitedCounter called');
|
||
const counterElement = document.querySelector('.qr-counter');
|
||
if (counterElement) {
|
||
counterElement.textContent = 'QR Codes ilimitados';
|
||
counterElement.className = 'badge bg-success qr-counter';
|
||
console.log('Set counter to: QR Codes ilimitados');
|
||
} else {
|
||
console.log('Counter element not found');
|
||
}
|
||
}
|
||
|
||
showError(message) {
|
||
this.showAlert(message, 'danger');
|
||
}
|
||
|
||
showSuccess(message) {
|
||
this.showAlert(message, 'success');
|
||
}
|
||
|
||
showAlert(message, type) {
|
||
// Create toast container if doesn't exist
|
||
let toastContainer = document.getElementById('toast-container');
|
||
if (!toastContainer) {
|
||
toastContainer = document.createElement('div');
|
||
toastContainer.id = 'toast-container';
|
||
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||
toastContainer.style.zIndex = '1060';
|
||
document.body.appendChild(toastContainer);
|
||
}
|
||
|
||
// Create toast element
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast align-items-center text-bg-${type} border-0`;
|
||
toast.setAttribute('role', 'alert');
|
||
toast.innerHTML = `
|
||
<div class="d-flex">
|
||
<div class="toast-body">
|
||
<i class="fas fa-${type === 'danger' ? 'exclamation-triangle' : 'check-circle'}"></i>
|
||
${message}
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||
</div>
|
||
`;
|
||
|
||
toastContainer.appendChild(toast);
|
||
|
||
// Show toast
|
||
const bsToast = new bootstrap.Toast(toast, {
|
||
delay: type === 'success' ? 3000 : 5000,
|
||
autohide: true
|
||
});
|
||
bsToast.show();
|
||
|
||
// Remove from DOM after hidden
|
||
toast.addEventListener('hidden.bs.toast', () => {
|
||
if (toast.parentNode) {
|
||
toast.parentNode.removeChild(toast);
|
||
}
|
||
});
|
||
}
|
||
|
||
setupRealTimePreview() {
|
||
const contentField = document.getElementById('qr-content');
|
||
const typeField = document.getElementById('qr-type');
|
||
|
||
if (contentField && typeField) {
|
||
let previewTimeout;
|
||
const updatePreview = () => {
|
||
clearTimeout(previewTimeout);
|
||
previewTimeout = setTimeout(() => {
|
||
if (contentField.value.trim() && typeField.value) {
|
||
// Could implement real-time preview for premium users
|
||
console.log('Real-time preview update');
|
||
}
|
||
}, 500);
|
||
};
|
||
|
||
contentField.addEventListener('input', updatePreview);
|
||
typeField.addEventListener('change', updatePreview);
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// SISTEMA DE FLUXO ASCENDENTE PROGRESSIVO
|
||
// ============================================
|
||
|
||
initializeProgressiveFlow() {
|
||
// Estado inicial: apenas tipo habilitado, botão gerar desabilitado
|
||
const qrContent = document.getElementById('qr-content');
|
||
const vcardInterface = document.getElementById('vcard-interface');
|
||
|
||
// FIXED: Don't disable content field initially - let enableContentFields handle it
|
||
// The field should be available for typing once a type is selected
|
||
|
||
// Ocultar interface vCard
|
||
if (vcardInterface) {
|
||
vcardInterface.style.display = 'none';
|
||
}
|
||
|
||
this.updateGenerateButton();
|
||
|
||
// Setup URL validation event listeners
|
||
this.setupURLValidationListeners();
|
||
|
||
// Setup hint auto-hide timer
|
||
this.setupHintAutoHide();
|
||
}
|
||
|
||
setupHintAutoHide() {
|
||
const hint = document.getElementById('type-selection-hint');
|
||
if (hint) {
|
||
// Auto-hide after 30 seconds
|
||
setTimeout(() => {
|
||
if (hint && !hint.classList.contains('fade-out')) {
|
||
hint.classList.add('fade-out');
|
||
// Remove from DOM after animation
|
||
setTimeout(() => {
|
||
if (hint.parentNode) {
|
||
hint.style.display = 'none';
|
||
}
|
||
}, 1000); // 1s for fade-out animation
|
||
}
|
||
}, 30000); // 30 seconds
|
||
}
|
||
}
|
||
|
||
setupURLValidationListeners() {
|
||
const contentField = document.getElementById('qr-content');
|
||
if (!contentField) return;
|
||
|
||
// Auto-fix URL on blur (when user leaves the field)
|
||
contentField.addEventListener('blur', () => {
|
||
const type = document.getElementById('qr-type')?.value;
|
||
|
||
if (type === 'url' && contentField.value.trim()) {
|
||
const originalValue = contentField.value.trim();
|
||
const fixedValue = this.autoFixURL(originalValue);
|
||
|
||
if (originalValue !== fixedValue) {
|
||
contentField.value = fixedValue;
|
||
console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue);
|
||
|
||
// Revalidar após correção
|
||
this.updateGenerateButton();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Real-time validation with debounce
|
||
contentField.addEventListener('input', () => {
|
||
const type = document.getElementById('qr-type')?.value;
|
||
|
||
if (type === 'url') {
|
||
// Debounce para não validar a cada caractere
|
||
clearTimeout(contentField.validationTimeout);
|
||
contentField.validationTimeout = setTimeout(() => {
|
||
this.updateGenerateButton();
|
||
}, 500);
|
||
}
|
||
});
|
||
}
|
||
|
||
handleTypeSelection(type) {
|
||
this.selectedType = type;
|
||
|
||
if (type) {
|
||
this.removeInitialHighlight();
|
||
// Sempre habilitar campos de conteúdo após selecionar tipo
|
||
this.enableContentFields(type);
|
||
// Show guidance toast for the selected type
|
||
this.showTypeGuidanceToast(type);
|
||
} else {
|
||
this.disableAllFields();
|
||
}
|
||
|
||
this.updateGenerateButton();
|
||
}
|
||
|
||
handleStyleSelection(style) {
|
||
this.selectedStyle = style;
|
||
this.updateGenerateButton();
|
||
}
|
||
|
||
handleContentChange(content) {
|
||
const contentField = document.getElementById('qr-content');
|
||
this.contentValid = this.validateContent(content);
|
||
|
||
// Feedback visual para campo de conteúdo
|
||
if (contentField) {
|
||
this.validateField(contentField, this.contentValid, window.QRRapidoTranslations?.validationContentMinLength || 'Content must have at least 3 characters');
|
||
}
|
||
|
||
this.updateGenerateButton();
|
||
}
|
||
|
||
enableContentFields(type) {
|
||
const contentGroup = document.getElementById('content-group');
|
||
const vcardInterface = document.getElementById('vcard-interface');
|
||
const wifiInterface = document.getElementById('wifi-interface');
|
||
const smsInterface = document.getElementById('sms-interface');
|
||
const emailInterface = document.getElementById('email-interface');
|
||
const dynamicQRSection = document.getElementById('dynamic-qr-section');
|
||
const urlPreview = document.getElementById('url-preview');
|
||
|
||
// Hide all interfaces by default
|
||
if (vcardInterface) vcardInterface.style.display = 'none';
|
||
if (wifiInterface) wifiInterface.style.display = 'none';
|
||
if (smsInterface) smsInterface.style.display = 'none';
|
||
if (emailInterface) emailInterface.style.display = 'none';
|
||
if (dynamicQRSection) dynamicQRSection.style.display = 'none';
|
||
if (urlPreview) urlPreview.style.display = 'none';
|
||
if (contentGroup) contentGroup.style.display = 'block';
|
||
|
||
if (type === 'vcard') {
|
||
// Para vCard, ocultar textarea e mostrar interface específica
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
if (vcardInterface) {
|
||
vcardInterface.style.display = 'block';
|
||
this.enableVCardFields();
|
||
}
|
||
} else if (type === 'wifi') {
|
||
// Para WiFi, ocultar textarea e mostrar interface específica
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
if (wifiInterface) {
|
||
wifiInterface.style.display = 'block';
|
||
}
|
||
} else if (type === 'sms') {
|
||
// Para SMS, ocultar textarea e mostrar interface específica
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
if (smsInterface) {
|
||
smsInterface.style.display = 'block';
|
||
}
|
||
} else if (type === 'email') {
|
||
// Para Email, ocultar textarea e mostrar interface específica
|
||
if (contentGroup) contentGroup.style.display = 'none';
|
||
if (emailInterface) {
|
||
emailInterface.style.display = 'block';
|
||
}
|
||
} else if (type === 'url') {
|
||
if (dynamicQRSection) dynamicQRSection.style.display = 'block';
|
||
if (urlPreview) urlPreview.style.display = 'block';
|
||
// CRITICAL FIX: Enable content field for URL type
|
||
const qrContent = document.getElementById('qr-content');
|
||
if(qrContent) {
|
||
qrContent.disabled = false;
|
||
}
|
||
} else {
|
||
// Para outros tipos, mostrar textarea
|
||
if (contentGroup) contentGroup.style.display = 'block';
|
||
const qrContent = document.getElementById('qr-content');
|
||
if(qrContent) {
|
||
qrContent.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
enableVCardFields() {
|
||
const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
|
||
requiredFields.forEach(fieldId => {
|
||
const field = document.getElementById(fieldId);
|
||
if (field) {
|
||
field.disabled = false;
|
||
field.setAttribute('required', 'required');
|
||
}
|
||
});
|
||
}
|
||
|
||
disableAllFields() {
|
||
// Resetar estado
|
||
const qrContent = document.getElementById('qr-content');
|
||
const vcardInterface = document.getElementById('vcard-interface');
|
||
|
||
if (qrContent) {
|
||
// FIXED: Don't disable content field, just clear it
|
||
qrContent.value = '';
|
||
}
|
||
|
||
if (vcardInterface) {
|
||
vcardInterface.style.display = 'none';
|
||
// Limpar campos vCard
|
||
const vcardFields = document.querySelectorAll('#vcard-interface input');
|
||
vcardFields.forEach(field => {
|
||
field.value = '';
|
||
field.disabled = true;
|
||
});
|
||
}
|
||
|
||
this.contentValid = false;
|
||
}
|
||
|
||
validateContent(content) {
|
||
if (!content) return false;
|
||
return content.trim().length >= 3;
|
||
}
|
||
|
||
validateVCardFields() {
|
||
const nameField = document.getElementById('vcard-name');
|
||
const mobileField = document.getElementById('vcard-mobile');
|
||
const emailField = document.getElementById('vcard-email');
|
||
|
||
const name = nameField?.value.trim() || '';
|
||
const mobile = mobileField?.value.trim() || '';
|
||
const email = emailField?.value.trim() || '';
|
||
|
||
// Validação individual dos campos com feedback visual
|
||
this.validateField(nameField, name !== '', 'Nome é obrigatório');
|
||
this.validateField(mobileField, mobile !== '' && mobile.length >= 10, 'Telefone deve ter pelo menos 10 dígitos');
|
||
this.validateField(emailField, email !== '' && email.includes('@'), 'Email válido é obrigatório');
|
||
|
||
const isValid = name !== '' && mobile !== '' && email !== '' && email.includes('@') && mobile.length >= 10;
|
||
this.contentValid = isValid;
|
||
|
||
return isValid;
|
||
}
|
||
|
||
validateField(field, isValid, errorMessage) {
|
||
if (!field) return;
|
||
|
||
const feedbackElement = field.parentNode.querySelector('.invalid-feedback');
|
||
|
||
if (isValid) {
|
||
field.classList.remove('is-invalid');
|
||
field.classList.add('is-valid');
|
||
if (feedbackElement) {
|
||
feedbackElement.style.display = 'none';
|
||
}
|
||
} else if (field.value.trim() !== '') {
|
||
// Só mostrar erro se o campo tem conteúdo
|
||
field.classList.remove('is-valid');
|
||
field.classList.add('is-invalid');
|
||
if (feedbackElement) {
|
||
feedbackElement.textContent = errorMessage;
|
||
feedbackElement.style.display = 'block';
|
||
}
|
||
} else {
|
||
// Campo vazio, remover validações visuais
|
||
field.classList.remove('is-valid', 'is-invalid');
|
||
if (feedbackElement) {
|
||
feedbackElement.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// URL VALIDATION FUNCTIONS
|
||
// ============================================
|
||
|
||
isValidURL(url) {
|
||
if (!url || typeof url !== 'string') return false;
|
||
|
||
const trimmedUrl = url.trim();
|
||
|
||
// Verificar se começa com http:// ou https://
|
||
const hasProtocol = trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://');
|
||
|
||
// Verificar se tem pelo menos um ponto
|
||
const hasDot = trimmedUrl.includes('.');
|
||
|
||
// Verificar se não é só o protocolo
|
||
const hasContent = trimmedUrl.length > 8; // mais que "https://"
|
||
|
||
// Regex mais robusta
|
||
const urlRegex = /^https?:\/\/.+\..+/i;
|
||
|
||
return hasProtocol && hasDot && hasContent && urlRegex.test(trimmedUrl);
|
||
}
|
||
|
||
autoFixURL(url) {
|
||
if (!url || typeof url !== 'string') return '';
|
||
|
||
let fixedUrl = url.trim();
|
||
|
||
// Se não tem protocolo, adicionar https://
|
||
if (!fixedUrl.startsWith('http://') && !fixedUrl.startsWith('https://')) {
|
||
fixedUrl = 'https://' + fixedUrl;
|
||
}
|
||
|
||
return fixedUrl;
|
||
}
|
||
|
||
showValidationError(message) {
|
||
let errorDiv = document.getElementById('url-validation-error');
|
||
|
||
if (!errorDiv) {
|
||
// Criar div de erro se não existir
|
||
errorDiv = document.createElement('div');
|
||
errorDiv.id = 'url-validation-error';
|
||
errorDiv.className = 'alert alert-danger mt-2';
|
||
errorDiv.style.display = 'none';
|
||
|
||
// Inserir após o campo de conteúdo
|
||
const contentGroup = document.getElementById('content-group');
|
||
if (contentGroup) {
|
||
contentGroup.appendChild(errorDiv);
|
||
}
|
||
}
|
||
|
||
errorDiv.innerHTML = `<small><i class="fas fa-exclamation-triangle"></i> ${message}</small>`;
|
||
errorDiv.style.display = 'block';
|
||
}
|
||
|
||
clearValidationError() {
|
||
const errorDiv = document.getElementById('url-validation-error');
|
||
if (errorDiv) {
|
||
errorDiv.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
updateGenerateButton() {
|
||
const generateBtn = document.getElementById('generate-btn');
|
||
if (!generateBtn) return;
|
||
|
||
let isValid = false;
|
||
const type = this.selectedType;
|
||
|
||
if (type === 'url') {
|
||
const contentField = document.getElementById('qr-content');
|
||
const content = contentField?.value || '';
|
||
const trimmedContent = content.trim();
|
||
|
||
if (!trimmedContent) {
|
||
this.showValidationError('URL é obrigatória');
|
||
if (contentField) {
|
||
contentField.classList.remove('is-valid');
|
||
contentField.classList.add('is-invalid');
|
||
}
|
||
isValid = false;
|
||
} else if (!this.isValidURL(trimmedContent)) {
|
||
this.showValidationError('URL deve começar com http:// ou https:// e conter pelo menos um ponto (ex: https://google.com)');
|
||
if (contentField) {
|
||
contentField.classList.remove('is-valid');
|
||
contentField.classList.add('is-invalid');
|
||
}
|
||
isValid = false;
|
||
} else {
|
||
this.clearValidationError();
|
||
if (contentField) {
|
||
contentField.classList.remove('is-invalid');
|
||
contentField.classList.add('is-valid');
|
||
}
|
||
isValid = true;
|
||
}
|
||
} else if (type === 'text') {
|
||
const contentField = document.getElementById('qr-content');
|
||
const content = contentField?.value || '';
|
||
isValid = content.trim().length >= 3;
|
||
this.clearValidationError(); // Clear any URL validation errors
|
||
if (contentField) {
|
||
contentField.classList.remove('is-valid', 'is-invalid');
|
||
}
|
||
} else if (type === 'vcard') {
|
||
isValid = this.validateVCardFields();
|
||
} else if (type === 'wifi') {
|
||
const data = window.wifiGenerator.collectWiFiData();
|
||
isValid = data.ssid.trim() !== '';
|
||
if (data.security !== 'nopass' && !data.password.trim()) {
|
||
isValid = false;
|
||
}
|
||
} else if (type === 'sms') {
|
||
const data = window.smsGenerator.collectSMSData();
|
||
isValid = data.number.trim() !== '' && data.message.trim() !== '';
|
||
} else if (type === 'email') {
|
||
const data = window.emailGenerator.collectEmailData();
|
||
isValid = data.to.trim() !== '' && data.subject.trim() !== '';
|
||
}
|
||
|
||
generateBtn.disabled = !isValid;
|
||
|
||
if (isValid) {
|
||
generateBtn.classList.remove('btn-secondary', 'disabled');
|
||
generateBtn.classList.add('btn-primary');
|
||
} else {
|
||
generateBtn.classList.remove('btn-primary');
|
||
generateBtn.classList.add('btn-secondary', 'disabled');
|
||
}
|
||
}
|
||
|
||
// Remove destaque inicial quando tipo for selecionado
|
||
removeInitialHighlight() {
|
||
const typeField = document.getElementById('qr-type');
|
||
const hint = document.getElementById('type-selection-hint');
|
||
|
||
if (typeField && typeField.classList.contains('qr-type-highlight')) {
|
||
typeField.classList.remove('qr-type-highlight');
|
||
}
|
||
|
||
if (hint && !hint.classList.contains('fade-out')) {
|
||
hint.classList.add('fade-out');
|
||
// Hide after animation
|
||
setTimeout(() => {
|
||
if (hint.parentNode) {
|
||
hint.style.display = 'none';
|
||
}
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
showTypeGuidanceToast(type) {
|
||
// Get localized message based on QR type
|
||
const messages = this.getTypeGuidanceMessages();
|
||
const message = messages[type];
|
||
|
||
if (message) {
|
||
// Create info toast with 30 second duration
|
||
const toast = this.createGuidanceToast(message);
|
||
this.showGuidanceToast(toast, 30000); // 30 seconds
|
||
}
|
||
}
|
||
|
||
getTypeGuidanceMessages() {
|
||
// These should match the resource file keys
|
||
// In a real implementation, these would come from server-side localization
|
||
// For now, we'll use the JavaScript language strings or fallback to Portuguese
|
||
return {
|
||
'url': document.querySelector('[data-type-guide-url]')?.textContent || '🌐 Para gerar QR de URL, digite o endereço completo (ex: https://google.com)',
|
||
'vcard': document.querySelector('[data-type-guide-vcard]')?.textContent || '👤 Para cartão de visita, preencha nome, telefone e email nos campos abaixo',
|
||
'wifi': document.querySelector('[data-type-guide-wifi]')?.textContent || '📶 Para WiFi, informe nome da rede, senha e tipo de segurança',
|
||
'sms': document.querySelector('[data-type-guide-sms]')?.textContent || '💬 Para SMS, digite o número do destinatário e a mensagem',
|
||
'email': document.querySelector('[data-type-guide-email]')?.textContent || '📧 Para email, preencha destinatário, assunto e mensagem (opcional)',
|
||
'text': document.querySelector('[data-type-guide-text]')?.textContent || '📝 Para texto livre, digite qualquer conteúdo que desejar'
|
||
};
|
||
}
|
||
|
||
createGuidanceToast(message) {
|
||
const toast = document.createElement('div');
|
||
toast.className = 'toast align-items-center text-bg-info border-0';
|
||
toast.setAttribute('role', 'alert');
|
||
toast.style.minWidth = '400px';
|
||
toast.innerHTML = `
|
||
<div class="d-flex">
|
||
<div class="toast-body fw-medium">
|
||
${message}
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||
</div>
|
||
`;
|
||
return toast;
|
||
}
|
||
|
||
showGuidanceToast(toast, duration = 30000) {
|
||
// Create toast container if doesn't exist (positioned below header stats)
|
||
let toastContainer = document.getElementById('guidance-toast-container');
|
||
if (!toastContainer) {
|
||
toastContainer = document.createElement('div');
|
||
toastContainer.id = 'guidance-toast-container';
|
||
toastContainer.className = 'toast-container position-relative w-100 d-flex justify-content-center mb-3';
|
||
toastContainer.style.zIndex = '1060';
|
||
|
||
// Try to find existing toast container first (if manually positioned)
|
||
const existingContainer = document.getElementById('guidance-toast-container');
|
||
if (existingContainer) {
|
||
// Use existing container that was manually positioned
|
||
toastContainer = existingContainer;
|
||
} else {
|
||
// Find the main content area and insert after hero section
|
||
const mainElement = document.querySelector('main[role="main"]');
|
||
if (mainElement) {
|
||
// Insert at the beginning of main content
|
||
mainElement.insertBefore(toastContainer, mainElement.firstChild);
|
||
} else {
|
||
// Fallback: find container and insert at top
|
||
const container = document.querySelector('.container');
|
||
if (container) {
|
||
container.insertBefore(toastContainer, container.firstChild);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
toastContainer.appendChild(toast);
|
||
|
||
// Show toast with custom duration
|
||
const bsToast = new bootstrap.Toast(toast, {
|
||
delay: duration,
|
||
autohide: true
|
||
});
|
||
bsToast.show();
|
||
|
||
// Remove from DOM after hidden
|
||
toast.addEventListener('hidden.bs.toast', () => {
|
||
if (toast.parentNode) {
|
||
toast.parentNode.removeChild(toast);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// FUNÇÕES DE CODIFICAÇÃO UTF-8
|
||
// ============================================
|
||
|
||
// Garantir codificação UTF-8 para todos os tipos de QR Code
|
||
prepareContentForQR(content, type) {
|
||
if (!content) return '';
|
||
|
||
// Garantir que o conteúdo seja tratado como UTF-8
|
||
try {
|
||
// Verificar se há caracteres especiais
|
||
const hasSpecialChars = /[^ |