1678 lines
62 KiB
JavaScript
1678 lines
62 KiB
JavaScript
// QR Rapido Speed Generator
|
|
class QRRapidoGenerator {
|
|
constructor() {
|
|
this.startTime = 0;
|
|
this.currentQR = null;
|
|
this.timerInterval = null;
|
|
this.selectedType = null;
|
|
this.selectedStyle = null;
|
|
this.contentValid = false;
|
|
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.initializeEvents();
|
|
this.checkAdFreeStatus();
|
|
this.updateLanguage();
|
|
this.updateStatsCounters();
|
|
this.initializeProgressiveFlow();
|
|
}
|
|
|
|
initializeEvents() {
|
|
// Form submission with timer
|
|
const form = document.getElementById('qr-speed-form');
|
|
if (form) {
|
|
form.addEventListener('submit', this.generateQRWithTimer.bind(this));
|
|
}
|
|
|
|
// Quick style selection with flow control
|
|
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
|
|
radio.addEventListener('change', (e) => {
|
|
this.handleStyleSelection(e.target.value);
|
|
this.applyQuickStyle(e);
|
|
});
|
|
});
|
|
|
|
// Content field monitoring
|
|
const contentField = document.getElementById('qr-content');
|
|
if (contentField) {
|
|
contentField.addEventListener('input', (e) => {
|
|
this.handleContentChange(e.target.value);
|
|
});
|
|
}
|
|
|
|
// Logo upload feedback
|
|
const logoUpload = document.getElementById('logo-upload');
|
|
if (logoUpload) {
|
|
logoUpload.addEventListener('change', this.handleLogoSelection.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 control
|
|
const qrType = document.getElementById('qr-type');
|
|
if (qrType) {
|
|
qrType.addEventListener('change', (e) => {
|
|
this.handleTypeSelection(e.target.value);
|
|
this.updateContentHints();
|
|
});
|
|
}
|
|
|
|
// Language selector
|
|
document.querySelectorAll('[data-lang]').forEach(link => {
|
|
link.addEventListener('click', this.changeLanguage.bind(this));
|
|
});
|
|
|
|
// Real-time preview for premium users
|
|
if (this.isPremiumUser()) {
|
|
this.setupRealTimePreview();
|
|
}
|
|
|
|
// Download buttons
|
|
this.setupDownloadButtons();
|
|
|
|
// Share functionality
|
|
this.setupShareButtons();
|
|
|
|
// Save to history
|
|
const saveBtn = document.getElementById('save-to-history');
|
|
if (saveBtn) {
|
|
saveBtn.addEventListener('click', this.saveToHistory.bind(this));
|
|
}
|
|
|
|
}
|
|
|
|
setupDownloadButtons() {
|
|
const pngBtn = document.getElementById('download-png');
|
|
const svgBtn = document.getElementById('download-svg');
|
|
const pdfBtn = document.getElementById('download-pdf');
|
|
|
|
if (pngBtn) pngBtn.addEventListener('click', () => this.downloadQR('png'));
|
|
if (svgBtn) svgBtn.addEventListener('click', () => this.downloadQR('svg'));
|
|
if (pdfBtn) pdfBtn.addEventListener('click', () => this.downloadQR('pdf'));
|
|
}
|
|
|
|
setupShareButtons() {
|
|
// Check if Web Share API is supported and show/hide native share option
|
|
if (navigator.share && this.isMobileDevice()) {
|
|
const nativeShareOption = document.getElementById('native-share-option');
|
|
if (nativeShareOption) {
|
|
nativeShareOption.classList.remove('d-none');
|
|
}
|
|
}
|
|
|
|
// Show save to gallery option on mobile
|
|
if (this.isMobileDevice()) {
|
|
const saveGalleryOption = document.getElementById('save-gallery-option');
|
|
if (saveGalleryOption) {
|
|
saveGalleryOption.classList.remove('d-none');
|
|
}
|
|
}
|
|
|
|
// Add event listeners to share buttons
|
|
const shareButtons = {
|
|
'native-share': () => this.shareNative(),
|
|
'share-whatsapp': () => this.shareWhatsApp(),
|
|
'share-telegram': () => this.shareTelegram(),
|
|
'share-email': () => this.shareEmail(),
|
|
'copy-qr-link': () => this.copyToClipboard(),
|
|
'save-to-gallery': () => this.saveToGallery()
|
|
};
|
|
|
|
Object.entries(shareButtons).forEach(([id, handler]) => {
|
|
const button = document.getElementById(id);
|
|
if (button) {
|
|
button.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
handler();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
isMobileDevice() {
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
|
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
|
|
}
|
|
|
|
async shareNative() {
|
|
if (!this.currentQR || !navigator.share) return;
|
|
|
|
try {
|
|
// Create a blob from the base64 image
|
|
const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`);
|
|
const blob = await base64Response.blob();
|
|
const file = new File([blob], 'qrcode.png', { type: 'image/png' });
|
|
|
|
const shareData = {
|
|
title: 'QR Code - QR Rapido',
|
|
text: 'QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!',
|
|
url: window.location.origin,
|
|
files: [file]
|
|
};
|
|
|
|
// Check if files can be shared
|
|
if (navigator.canShare && navigator.canShare(shareData)) {
|
|
await navigator.share(shareData);
|
|
} else {
|
|
// Fallback without files
|
|
await navigator.share({
|
|
title: shareData.title,
|
|
text: shareData.text,
|
|
url: shareData.url
|
|
});
|
|
}
|
|
|
|
this.trackShareEvent('native');
|
|
} catch (error) {
|
|
console.error('Error sharing:', error);
|
|
if (error.name !== 'AbortError') {
|
|
this.showError('Erro ao compartilhar. Tente outro método.');
|
|
}
|
|
}
|
|
}
|
|
|
|
shareWhatsApp() {
|
|
if (!this.currentQR) return;
|
|
|
|
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil! ' + window.location.origin);
|
|
const url = `https://wa.me/?text=${text}`;
|
|
|
|
if (this.isMobileDevice()) {
|
|
window.open(url, '_blank');
|
|
} else {
|
|
window.open(`https://web.whatsapp.com/send?text=${text}`, '_blank');
|
|
}
|
|
|
|
this.trackShareEvent('whatsapp');
|
|
}
|
|
|
|
shareTelegram() {
|
|
if (!this.currentQR) return;
|
|
|
|
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!');
|
|
const url = encodeURIComponent(window.location.origin);
|
|
const telegramUrl = `https://t.me/share/url?url=${url}&text=${text}`;
|
|
|
|
window.open(telegramUrl, '_blank');
|
|
this.trackShareEvent('telegram');
|
|
}
|
|
|
|
shareEmail() {
|
|
if (!this.currentQR) return;
|
|
|
|
const subject = encodeURIComponent('QR Code - QR Rapido');
|
|
const body = encodeURIComponent(`Olá!\n\nCompartilho com você este QR Code gerado no QR Rapido, o gerador mais rápido do Brasil!\n\nAcesse: ${window.location.origin}\n\nAbraços!`);
|
|
const mailtoUrl = `mailto:?subject=${subject}&body=${body}`;
|
|
|
|
window.location.href = mailtoUrl;
|
|
this.trackShareEvent('email');
|
|
}
|
|
|
|
async copyToClipboard() {
|
|
if (!this.currentQR) return;
|
|
|
|
try {
|
|
const shareText = `QR Code gerado com QR Rapido - ${window.location.origin}`;
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(shareText);
|
|
} else {
|
|
// Fallback for older browsers
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = shareText;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
textArea.remove();
|
|
}
|
|
|
|
this.showSuccess('Link copiado para a área de transferência!');
|
|
this.trackShareEvent('copy');
|
|
} catch (error) {
|
|
console.error('Error copying to clipboard:', error);
|
|
this.showError('Erro ao copiar link. Tente novamente.');
|
|
}
|
|
}
|
|
|
|
async saveToGallery() {
|
|
if (!this.currentQR) return;
|
|
|
|
try {
|
|
// Create a blob from the base64 image
|
|
const base64Response = await fetch(`data:image/png;base64,${this.currentQR.base64}`);
|
|
const blob = await base64Response.blob();
|
|
|
|
// Create download link
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `qrrapido-${new Date().toISOString().slice(0,10)}-${Date.now()}.png`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
this.showSuccess('QR Code baixado! Verifique sua galeria/downloads.');
|
|
this.trackShareEvent('gallery');
|
|
} catch (error) {
|
|
console.error('Error saving to gallery:', error);
|
|
this.showError('Erro ao salvar na galeria. Tente novamente.');
|
|
}
|
|
}
|
|
|
|
trackShareEvent(method) {
|
|
// Google Analytics
|
|
if (typeof gtag !== 'undefined') {
|
|
gtag('event', 'qr_shared', {
|
|
'share_method': method,
|
|
'user_type': this.isPremiumUser() ? 'premium' : 'free',
|
|
'language': this.currentLang
|
|
});
|
|
}
|
|
|
|
// Internal tracking
|
|
console.log(`QR Code shared via ${method}`);
|
|
}
|
|
|
|
async generateQRWithTimer(e) {
|
|
e.preventDefault();
|
|
|
|
// 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'
|
|
};
|
|
}
|
|
|
|
const response = await fetch(requestData.endpoint, fetchOptions);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
|
|
if (response.status === 429) {
|
|
this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.');
|
|
return;
|
|
}
|
|
|
|
if (response.status === 400 && errorData.requiresPremium) {
|
|
this.showUpgradeModal(errorData.error || 'Logo personalizado é exclusivo do plano Premium.');
|
|
return;
|
|
}
|
|
|
|
throw new Error(errorData.error || 'Erro na geração');
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
if (result.requiresPremium) {
|
|
this.showUpgradeModal(result.error || 'Logo personalizado é exclusivo do plano Premium.');
|
|
return;
|
|
}
|
|
throw new Error(result.error || 'Erro desconhecido');
|
|
}
|
|
|
|
const generationTime = ((performance.now() - this.startTime) / 1000).toFixed(1);
|
|
|
|
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;
|
|
}
|
|
|
|
// Check if flow is properly completed
|
|
if (!this.selectedType || !this.selectedStyle || !this.contentValid) {
|
|
this.showError('Complete todas as etapas antes de gerar o 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('VCard generator não está disponível');
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.showError('Erro na validação do VCard: ' + error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Normal validation for other types
|
|
const qrContent = document.getElementById('qr-content').value.trim();
|
|
|
|
if (!qrContent) {
|
|
this.showError('Digite o conteúdo do QR code');
|
|
return false;
|
|
}
|
|
|
|
if (qrContent.length > 4000) {
|
|
this.showError('Conteúdo muito longo. Máximo 4000 caracteres.');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
collectFormData() {
|
|
const type = document.getElementById('qr-type').value;
|
|
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
|
|
const styleSettings = this.getStyleSettings(quickStyle);
|
|
|
|
// Handle VCard type
|
|
if (type === 'vcard') {
|
|
if (window.vcardGenerator) {
|
|
const vcardContent = window.vcardGenerator.getVCardContent();
|
|
return {
|
|
data: {
|
|
type: 'vcard', // Keep as vcard type for tracking
|
|
content: vcardContent,
|
|
quickStyle: quickStyle,
|
|
primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
|
|
backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
|
|
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
|
|
},
|
|
isMultipart: false,
|
|
endpoint: '/api/QR/GenerateRapid'
|
|
};
|
|
} else {
|
|
throw new Error('VCard generator não está disponível');
|
|
}
|
|
}
|
|
|
|
// Check if logo is selected for premium users
|
|
const logoUpload = document.getElementById('logo-upload');
|
|
const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0];
|
|
|
|
if (hasLogo) {
|
|
// Use FormData for premium users with logo
|
|
const formData = new FormData();
|
|
|
|
// Get user-selected colors with proper priority
|
|
const userPrimaryColor = document.getElementById('primary-color').value;
|
|
const userBackgroundColor = document.getElementById('bg-color').value;
|
|
|
|
// Priority: User selection > Style defaults > Fallback
|
|
// Always use user selection if it exists, regardless of what color it is
|
|
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
|
|
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
|
|
|
// Debug logging for color selection
|
|
console.log('🎨 Color Selection Debug (FormData):');
|
|
console.log(' Style:', quickStyle);
|
|
console.log(' Style Default Primary:', styleSettings.primaryColor);
|
|
console.log(' User Selected Primary:', userPrimaryColor);
|
|
console.log(' Final Primary Color:', finalPrimaryColor);
|
|
console.log(' Final Background Color:', finalBackgroundColor);
|
|
|
|
// Add basic form fields
|
|
formData.append('type', document.getElementById('qr-type').value);
|
|
formData.append('content', document.getElementById('qr-content').value);
|
|
formData.append('quickStyle', quickStyle);
|
|
formData.append('primaryColor', finalPrimaryColor);
|
|
formData.append('backgroundColor', finalBackgroundColor);
|
|
formData.append('size', parseInt(document.getElementById('qr-size').value));
|
|
formData.append('margin', parseInt(document.getElementById('qr-margin').value));
|
|
formData.append('cornerStyle', document.getElementById('corner-style')?.value || 'square');
|
|
formData.append('optimizeForSpeed', 'true');
|
|
formData.append('language', this.currentLang);
|
|
|
|
// Add logo file
|
|
formData.append('logo', logoUpload.files[0]);
|
|
console.log('Logo file added to form data:', logoUpload.files[0].name, logoUpload.files[0].size + ' bytes');
|
|
|
|
return { data: formData, isMultipart: true, endpoint: '/api/QR/GenerateRapidWithLogo' };
|
|
} else {
|
|
// Use JSON for basic QR generation (original working method)
|
|
// Get user-selected colors
|
|
const userPrimaryColor = document.getElementById('primary-color').value;
|
|
const userBackgroundColor = document.getElementById('bg-color').value;
|
|
|
|
// Priority: User selection > Style defaults > Fallback
|
|
// Always use user selection if it exists, regardless of what color it is
|
|
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
|
|
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
|
|
|
// Debug logging for color selection
|
|
console.log('🎨 Color Selection Debug (JSON):');
|
|
console.log(' Style:', quickStyle);
|
|
console.log(' Style Default Primary:', styleSettings.primaryColor);
|
|
console.log(' User Selected Primary:', userPrimaryColor);
|
|
console.log(' Final Primary Color:', finalPrimaryColor);
|
|
console.log(' Final Background Color:', finalBackgroundColor);
|
|
|
|
return {
|
|
data: {
|
|
type: document.getElementById('qr-type').value,
|
|
content: document.getElementById('qr-content').value,
|
|
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
|
|
},
|
|
isMultipart: false,
|
|
endpoint: '/api/QR/GenerateRapid'
|
|
};
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
previewDiv.innerHTML = `
|
|
<img src="data:image/png;base64,${result.qrCodeBase64}"
|
|
class="img-fluid border rounded shadow-sm"
|
|
alt="QR Code gerado em ${generationTime}s">
|
|
`;
|
|
|
|
// 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,
|
|
id: result.qrId,
|
|
generationTime: generationTime
|
|
};
|
|
|
|
// Update counter for free users
|
|
if (result.remainingQRs !== undefined) {
|
|
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';
|
|
// Add VCard input monitoring for progressive flow
|
|
this.setupVCardMonitoring();
|
|
}
|
|
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;Mensaje'
|
|
}
|
|
};
|
|
|
|
const langHints = hints[this.currentLang] || hints['pt-BR'];
|
|
hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado';
|
|
}
|
|
|
|
setupVCardMonitoring() {
|
|
// Monitor VCard required fields for progressive flow
|
|
const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
|
|
|
|
requiredFields.forEach(fieldId => {
|
|
const field = document.getElementById(fieldId);
|
|
if (field) {
|
|
field.addEventListener('input', () => {
|
|
// Use a small delay to allow for validation
|
|
setTimeout(() => {
|
|
const isValid = this.validateContent(''); // VCard validation is internal
|
|
this.contentValid = isValid;
|
|
|
|
if (isValid && this.selectedType && this.selectedStyle) {
|
|
this.showGenerateButton();
|
|
} else {
|
|
this.hideGenerateButton();
|
|
}
|
|
}, 100);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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');
|
|
|
|
if (file) {
|
|
// Validate file size (2MB max)
|
|
if (file.size > 2 * 1024 * 1024) {
|
|
this.showError('Logo muito grande. Máximo 2MB.');
|
|
e.target.value = ''; // Clear the input
|
|
logoPreview?.classList.add('d-none');
|
|
return;
|
|
}
|
|
|
|
// Validate file type
|
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
|
|
if (!allowedTypes.includes(file.type)) {
|
|
this.showError('Formato inválido. Use PNG ou JPG.');
|
|
e.target.value = ''; // Clear the input
|
|
logoPreview?.classList.add('d-none');
|
|
return;
|
|
}
|
|
|
|
// Show success feedback
|
|
if (logoFilename) {
|
|
const fileSizeKB = Math.round(file.size / 1024);
|
|
logoFilename.textContent = `${file.name} (${fileSizeKB}KB)`;
|
|
}
|
|
logoPreview?.classList.remove('d-none');
|
|
|
|
console.log('Logo selected:', file.name, file.size + ' bytes', file.type);
|
|
} else {
|
|
// Hide preview when no file selected
|
|
logoPreview?.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
handleCornerStyleChange(e) {
|
|
const selectedStyle = e.target.value;
|
|
const premiumStyles = ['rounded', 'circle', 'leaf'];
|
|
|
|
if (premiumStyles.includes(selectedStyle)) {
|
|
// Check if user is premium (we can detect this by checking if the option is disabled)
|
|
const option = e.target.options[e.target.selectedIndex];
|
|
if (option.disabled) {
|
|
// Reset to square
|
|
e.target.value = 'square';
|
|
this.showUpgradeModal('Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
console.log('Corner style selected:', selectedStyle);
|
|
}
|
|
|
|
applyQuickStyle(e) {
|
|
const style = e.target.value;
|
|
const settings = this.getStyleSettings(style);
|
|
|
|
const primaryColor = document.getElementById('primary-color');
|
|
const bgColor = document.getElementById('bg-color');
|
|
|
|
if (primaryColor) primaryColor.value = settings.primaryColor;
|
|
if (bgColor) bgColor.value = settings.backgroundColor;
|
|
}
|
|
|
|
updateStatsCounters() {
|
|
// Simulate real-time counters
|
|
setInterval(() => {
|
|
const totalElement = document.getElementById('total-qrs');
|
|
if (totalElement) {
|
|
const current = parseFloat(totalElement.textContent.replace('K', '')) || 10.5;
|
|
const newValue = (current + Math.random() * 0.1).toFixed(1);
|
|
totalElement.textContent = `${newValue}K`;
|
|
}
|
|
|
|
// Update average time based on real performance
|
|
const avgElement = document.getElementById('avg-generation-time');
|
|
if (avgElement && window.qrRapidoStats) {
|
|
const avg = window.qrRapidoStats.getAverageTime();
|
|
avgElement.textContent = `${avg}s`;
|
|
}
|
|
}, 30000); // Update every 30 seconds
|
|
}
|
|
|
|
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
|
|
const avgElement = document.querySelector('.card-body h5:contains("1.2s")');
|
|
if (avgElement) {
|
|
const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime;
|
|
avgElement.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) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/QR/Download/${this.currentQR.id}?format=${format}`);
|
|
if (!response.ok) throw new Error('Download failed');
|
|
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `qrrapido-${new Date().toISOString().slice(0,10)}.${format}`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
this.showError('Erro ao fazer download. Tente novamente.');
|
|
}
|
|
}
|
|
|
|
async saveToHistory() {
|
|
if (!this.currentQR) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/QR/SaveToHistory', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
qrId: this.currentQR.id
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
this.showSuccess(this.languageStrings[this.currentLang].success);
|
|
} else {
|
|
throw new Error('Failed to save');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Save error:', error);
|
|
this.showError('Erro ao salvar no histórico.');
|
|
}
|
|
}
|
|
|
|
async checkAdFreeStatus() {
|
|
try {
|
|
const response = await fetch('/Account/AdFreeStatus');
|
|
const status = await response.json();
|
|
|
|
if (status.isAdFree) {
|
|
this.hideAllAds();
|
|
this.showAdFreeMessage(status.timeRemaining);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error checking ad-free status:', error);
|
|
}
|
|
}
|
|
|
|
hideAllAds() {
|
|
document.querySelectorAll('.ad-container').forEach(ad => {
|
|
ad.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
showAdFreeMessage(timeRemaining) {
|
|
if (timeRemaining <= 0) return;
|
|
|
|
const existing = document.querySelector('.ad-free-notice');
|
|
if (existing) return; // Already shown
|
|
|
|
const message = document.createElement('div');
|
|
message.className = 'alert alert-success text-center mb-3 ad-free-notice';
|
|
message.innerHTML = `
|
|
<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) {
|
|
counterElement.textContent = `${remaining} QR codes restantes hoje`;
|
|
|
|
if (remaining <= 3) {
|
|
counterElement.className = 'badge bg-warning qr-counter';
|
|
}
|
|
if (remaining === 0) {
|
|
counterElement.className = 'badge bg-danger qr-counter';
|
|
}
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
this.showAlert(message, 'danger');
|
|
}
|
|
|
|
showSuccess(message) {
|
|
this.showAlert(message, 'success');
|
|
}
|
|
|
|
showAlert(message, type) {
|
|
const alert = document.createElement('div');
|
|
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
|
alert.innerHTML = `
|
|
<div>${message}</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
const container = document.querySelector('.container');
|
|
const row = container?.querySelector('.row');
|
|
if (container && row) {
|
|
container.insertBefore(alert, row);
|
|
}
|
|
|
|
// Auto-remove after delay
|
|
setTimeout(() => {
|
|
if (alert.parentNode) {
|
|
alert.parentNode.removeChild(alert);
|
|
}
|
|
}, type === 'success' ? 3000 : 5000);
|
|
}
|
|
|
|
initializeProgressiveFlow() {
|
|
// Reset all states to initial disabled state
|
|
this.selectedType = null;
|
|
this.selectedStyle = null;
|
|
this.contentValid = false;
|
|
|
|
// Ensure proper initial state
|
|
this.disableStyleSelection();
|
|
this.disableContentFields();
|
|
this.disableAdvancedOptions();
|
|
this.hideGenerateButton();
|
|
}
|
|
|
|
handleTypeSelection(type) {
|
|
this.selectedType = type;
|
|
|
|
if (type) {
|
|
// Remove highlight when type is selected
|
|
this.removeInitialHighlight();
|
|
|
|
// Enable style selection
|
|
this.enableStyleSelection();
|
|
// Reset subsequent selections
|
|
this.selectedStyle = null;
|
|
this.contentValid = false;
|
|
this.disableContentFields();
|
|
this.hideGenerateButton();
|
|
|
|
} else {
|
|
// Disable everything if no type selected
|
|
this.disableStyleSelection();
|
|
this.disableContentFields();
|
|
this.disableAdvancedOptions();
|
|
this.hideGenerateButton();
|
|
}
|
|
}
|
|
|
|
handleStyleSelection(style) {
|
|
this.selectedStyle = style;
|
|
|
|
if (style && this.selectedType) {
|
|
// Enable content fields
|
|
this.enableContentFields();
|
|
|
|
// Open advanced panel if 'advanced' style is selected (future enhancement)
|
|
if (style === 'advanced') {
|
|
this.openAdvancedCustomization();
|
|
}
|
|
|
|
// Reset content validation
|
|
this.contentValid = false;
|
|
this.hideGenerateButton();
|
|
|
|
}
|
|
}
|
|
|
|
handleContentChange(content) {
|
|
const isValid = this.validateContent(content);
|
|
this.contentValid = isValid;
|
|
|
|
if (isValid && this.selectedType && this.selectedStyle) {
|
|
this.showGenerateButton();
|
|
|
|
} else {
|
|
this.hideGenerateButton();
|
|
}
|
|
}
|
|
|
|
validateContent(content) {
|
|
if (!content) return false;
|
|
|
|
const trimmedContent = content.trim();
|
|
|
|
// VCard has its own validation
|
|
if (this.selectedType === 'vcard') {
|
|
try {
|
|
if (window.vcardGenerator) {
|
|
const errors = window.vcardGenerator.validateVCardData();
|
|
return errors.length === 0;
|
|
}
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// General validation: at least 3 characters
|
|
return trimmedContent.length >= 3;
|
|
}
|
|
|
|
enableStyleSelection() {
|
|
const styleContainer = document.getElementById('style-selector');
|
|
const styleInputs = document.querySelectorAll('input[name="quick-style"]');
|
|
const styleLabel = styleContainer?.parentElement.querySelector('.form-label');
|
|
|
|
if (styleContainer) {
|
|
styleContainer.classList.remove('qr-flow-disabled');
|
|
styleContainer.classList.add('qr-flow-enabled', 'qr-flow-ascending');
|
|
}
|
|
|
|
if (styleLabel) {
|
|
styleLabel.classList.remove('qr-flow-disabled');
|
|
styleLabel.classList.add('qr-flow-enabled');
|
|
}
|
|
|
|
styleInputs.forEach(input => {
|
|
input.disabled = false;
|
|
});
|
|
}
|
|
|
|
disableStyleSelection() {
|
|
const styleContainer = document.getElementById('style-selector');
|
|
const styleInputs = document.querySelectorAll('input[name="quick-style"]');
|
|
const styleLabel = styleContainer?.parentElement.querySelector('.form-label');
|
|
|
|
if (styleContainer) {
|
|
styleContainer.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
|
|
styleContainer.classList.add('qr-flow-disabled');
|
|
}
|
|
|
|
if (styleLabel) {
|
|
styleLabel.classList.remove('qr-flow-enabled');
|
|
styleLabel.classList.add('qr-flow-disabled');
|
|
}
|
|
|
|
styleInputs.forEach(input => {
|
|
input.disabled = true;
|
|
input.checked = false;
|
|
});
|
|
|
|
// Reset to default selection
|
|
const defaultStyle = document.getElementById('style-classic');
|
|
if (defaultStyle) {
|
|
defaultStyle.checked = true;
|
|
}
|
|
}
|
|
|
|
enableContentFields() {
|
|
const contentTextarea = document.getElementById('qr-content');
|
|
const contentLabel = contentTextarea?.parentElement.querySelector('.form-label');
|
|
const vcardInterface = document.getElementById('vcard-interface');
|
|
|
|
if (contentTextarea) {
|
|
contentTextarea.classList.remove('qr-flow-disabled');
|
|
contentTextarea.classList.add('qr-flow-enabled', 'qr-flow-ascending');
|
|
contentTextarea.disabled = false;
|
|
}
|
|
|
|
if (contentLabel) {
|
|
contentLabel.classList.remove('qr-flow-disabled');
|
|
contentLabel.classList.add('qr-flow-enabled');
|
|
}
|
|
|
|
if (vcardInterface && this.selectedType === 'vcard') {
|
|
vcardInterface.classList.remove('qr-flow-disabled');
|
|
vcardInterface.classList.add('qr-flow-enabled', 'qr-flow-ascending');
|
|
|
|
// Enable VCard form fields
|
|
const vcardInputs = vcardInterface.querySelectorAll('input, textarea, select');
|
|
vcardInputs.forEach(input => {
|
|
input.disabled = false;
|
|
});
|
|
}
|
|
|
|
// Enable advanced customization
|
|
this.enableAdvancedOptions();
|
|
}
|
|
|
|
disableContentFields() {
|
|
const contentTextarea = document.getElementById('qr-content');
|
|
const contentLabel = contentTextarea?.parentElement.querySelector('.form-label');
|
|
const vcardInterface = document.getElementById('vcard-interface');
|
|
|
|
if (contentTextarea) {
|
|
contentTextarea.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
|
|
contentTextarea.classList.add('qr-flow-disabled');
|
|
contentTextarea.disabled = true;
|
|
contentTextarea.value = '';
|
|
}
|
|
|
|
if (contentLabel) {
|
|
contentLabel.classList.remove('qr-flow-enabled');
|
|
contentLabel.classList.add('qr-flow-disabled');
|
|
}
|
|
|
|
if (vcardInterface) {
|
|
vcardInterface.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
|
|
vcardInterface.classList.add('qr-flow-disabled');
|
|
|
|
// Disable VCard form fields
|
|
const vcardInputs = vcardInterface.querySelectorAll('input, textarea, select');
|
|
vcardInputs.forEach(input => {
|
|
input.disabled = true;
|
|
input.value = '';
|
|
input.checked = false;
|
|
});
|
|
}
|
|
|
|
this.disableAdvancedOptions();
|
|
}
|
|
|
|
enableAdvancedOptions() {
|
|
const advancedAccordion = document.getElementById('customization-accordion');
|
|
|
|
if (advancedAccordion) {
|
|
advancedAccordion.classList.remove('qr-flow-disabled');
|
|
advancedAccordion.classList.add('qr-flow-enabled', 'qr-flow-ascending');
|
|
|
|
// Enable form controls inside accordion
|
|
const advancedInputs = advancedAccordion.querySelectorAll('input, select');
|
|
advancedInputs.forEach(input => {
|
|
input.disabled = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
disableAdvancedOptions() {
|
|
const advancedAccordion = document.getElementById('customization-accordion');
|
|
|
|
if (advancedAccordion) {
|
|
advancedAccordion.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
|
|
advancedAccordion.classList.add('qr-flow-disabled');
|
|
|
|
// Disable form controls inside accordion
|
|
const advancedInputs = advancedAccordion.querySelectorAll('input, select');
|
|
advancedInputs.forEach(input => {
|
|
input.disabled = true;
|
|
});
|
|
}
|
|
}
|
|
|
|
showGenerateButton() {
|
|
const buttonContainer = document.getElementById('generate-button-container');
|
|
const generateBtn = document.getElementById('generate-btn');
|
|
|
|
if (buttonContainer) {
|
|
buttonContainer.style.display = 'block';
|
|
buttonContainer.classList.add('show');
|
|
}
|
|
|
|
if (generateBtn) {
|
|
generateBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
hideGenerateButton() {
|
|
const buttonContainer = document.getElementById('generate-button-container');
|
|
const generateBtn = document.getElementById('generate-btn');
|
|
|
|
if (buttonContainer) {
|
|
buttonContainer.classList.remove('show');
|
|
setTimeout(() => {
|
|
if (!buttonContainer.classList.contains('show')) {
|
|
buttonContainer.style.display = 'none';
|
|
}
|
|
}, 400);
|
|
}
|
|
|
|
if (generateBtn) {
|
|
generateBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
openAdvancedCustomization() {
|
|
const advancedPanel = document.getElementById('customization-panel');
|
|
const accordionButton = document.querySelector('[data-bs-target="#customization-panel"]');
|
|
|
|
if (advancedPanel && accordionButton) {
|
|
// Use Bootstrap's collapse to open the panel
|
|
const collapse = new bootstrap.Collapse(advancedPanel, {
|
|
show: true
|
|
});
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Remove destaque inicial quando tipo for selecionado
|
|
removeInitialHighlight() {
|
|
const typeField = document.getElementById('qr-type');
|
|
if (typeField && typeField.classList.contains('qr-field-highlight')) {
|
|
typeField.classList.remove('qr-field-highlight');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.qrGenerator = new QRRapidoGenerator();
|
|
window.vcardGenerator = new VCardGenerator();
|
|
|
|
// Initialize AdSense if necessary
|
|
if (window.adsbygoogle && document.querySelector('.adsbygoogle')) {
|
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
|
}
|
|
});
|
|
|
|
// VCard Generator Class
|
|
class VCardGenerator {
|
|
constructor() {
|
|
this.initializeVCardInterface();
|
|
}
|
|
|
|
initializeVCardInterface() {
|
|
// Show/hide optional fields based on checkboxes
|
|
document.querySelectorAll('#vcard-interface .form-check-input').forEach(checkbox => {
|
|
checkbox.addEventListener('change', (e) => {
|
|
const groupId = e.target.id.replace('enable-', '') + '-group';
|
|
const group = document.getElementById(groupId);
|
|
if (group) {
|
|
group.style.display = e.target.checked ? 'block' : 'none';
|
|
// Clear values when hiding fields
|
|
if (!e.target.checked) {
|
|
const inputs = group.querySelectorAll('input');
|
|
inputs.forEach(input => input.value = '');
|
|
}
|
|
}
|
|
this.updatePreview();
|
|
});
|
|
});
|
|
|
|
// Update preview in real-time
|
|
document.querySelectorAll('#vcard-interface input').forEach(input => {
|
|
input.addEventListener('input', () => this.updatePreview());
|
|
input.addEventListener('blur', () => this.validateField(input));
|
|
});
|
|
}
|
|
|
|
updatePreview() {
|
|
const vcard = this.generateVCardContent();
|
|
const previewElement = document.getElementById('vcard-preview-text');
|
|
if (previewElement) {
|
|
previewElement.textContent = vcard;
|
|
}
|
|
}
|
|
|
|
generateVCardContent() {
|
|
const data = this.collectVCardData();
|
|
|
|
let vcard = 'BEGIN:VCARD\nVERSION:3.0\n';
|
|
|
|
// Nome (obrigatório)
|
|
if (data.name) {
|
|
const nameParts = data.name.trim().split(' ');
|
|
const firstName = nameParts[0] || '';
|
|
const lastName = nameParts.slice(1).join(' ') || '';
|
|
vcard += `N:${lastName};${firstName}\n`;
|
|
vcard += `FN:${data.name}\n`;
|
|
}
|
|
|
|
// Empresa
|
|
if (data.company) vcard += `ORG:${data.company}\n`;
|
|
|
|
// Título
|
|
if (data.title) vcard += `TITLE:${data.title}\n`;
|
|
|
|
// Endereço
|
|
if (data.address || data.city || data.state || data.zip) {
|
|
const addr = `;;${data.address || ''};${data.city || ''};${data.state || ''};${data.zip || ''};Brasil`;
|
|
vcard += `ADR:${addr}\n`;
|
|
}
|
|
|
|
// Telefones
|
|
if (data.phone) vcard += `TEL;WORK;VOICE:${data.phone}\n`;
|
|
if (data.mobile) vcard += `TEL;CELL:${data.mobile}\n`;
|
|
|
|
// Email
|
|
if (data.email) vcard += `EMAIL;WORK;INTERNET:${data.email}\n`;
|
|
|
|
// Website
|
|
if (data.website) vcard += `URL:${data.website}\n`;
|
|
|
|
vcard += 'END:VCARD';
|
|
|
|
return vcard;
|
|
}
|
|
|
|
collectVCardData() {
|
|
return {
|
|
name: document.getElementById('vcard-name')?.value?.trim() || '',
|
|
mobile: document.getElementById('vcard-mobile')?.value?.trim() || '',
|
|
email: document.getElementById('vcard-email')?.value?.trim() || '',
|
|
company: document.getElementById('enable-company')?.checked ?
|
|
(document.getElementById('vcard-company')?.value?.trim() || '') : '',
|
|
title: document.getElementById('enable-title')?.checked ?
|
|
(document.getElementById('vcard-title')?.value?.trim() || '') : '',
|
|
website: document.getElementById('enable-website')?.checked ?
|
|
(document.getElementById('vcard-website')?.value?.trim() || '') : '',
|
|
address: document.getElementById('enable-address')?.checked ?
|
|
(document.getElementById('vcard-address')?.value?.trim() || '') : '',
|
|
city: document.getElementById('enable-address')?.checked ?
|
|
(document.getElementById('vcard-city')?.value?.trim() || '') : '',
|
|
state: document.getElementById('enable-address')?.checked ?
|
|
(document.getElementById('vcard-state')?.value?.trim() || '') : '',
|
|
zip: document.getElementById('enable-address')?.checked ?
|
|
(document.getElementById('vcard-zip')?.value?.trim() || '') : '',
|
|
phone: document.getElementById('enable-phone')?.checked ?
|
|
(document.getElementById('vcard-phone')?.value?.trim() || '') : ''
|
|
};
|
|
}
|
|
|
|
validateVCardData() {
|
|
const data = this.collectVCardData();
|
|
const errors = [];
|
|
|
|
// Required field validations
|
|
if (!data.name) errors.push('Nome é obrigatório');
|
|
if (!data.mobile) errors.push('Telefone celular é obrigatório');
|
|
if (!data.email) errors.push('Email é obrigatório');
|
|
|
|
// Format validations
|
|
if (data.email && !this.isValidEmail(data.email)) {
|
|
errors.push('Email inválido');
|
|
}
|
|
|
|
if (data.website && !this.isValidURL(data.website)) {
|
|
errors.push('Website inválido (deve começar com http:// ou https://)');
|
|
}
|
|
|
|
if (data.mobile && !this.isValidPhone(data.mobile)) {
|
|
errors.push('Telefone celular inválido (deve ter 10-11 dígitos)');
|
|
}
|
|
|
|
if (data.phone && !this.isValidPhone(data.phone)) {
|
|
errors.push('Telefone fixo inválido (deve ter 10-11 dígitos)');
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
validateField(input) {
|
|
const value = input.value.trim();
|
|
let isValid = true;
|
|
let message = '';
|
|
|
|
switch (input.id) {
|
|
case 'vcard-email':
|
|
if (value && !this.isValidEmail(value)) {
|
|
isValid = false;
|
|
message = 'Email inválido';
|
|
}
|
|
break;
|
|
case 'vcard-website':
|
|
if (value && !this.isValidURL(value)) {
|
|
isValid = false;
|
|
message = 'Website inválido (deve começar com http:// ou https://)';
|
|
}
|
|
break;
|
|
case 'vcard-mobile':
|
|
case 'vcard-phone':
|
|
if (value && !this.isValidPhone(value)) {
|
|
isValid = false;
|
|
message = 'Telefone inválido (apenas números, 10-11 dígitos)';
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Update field validation state
|
|
if (isValid) {
|
|
input.classList.remove('is-invalid');
|
|
input.classList.add('is-valid');
|
|
} else {
|
|
input.classList.remove('is-valid');
|
|
input.classList.add('is-invalid');
|
|
// Show error message
|
|
const feedback = input.parentNode.querySelector('.invalid-feedback');
|
|
if (feedback) {
|
|
feedback.textContent = message;
|
|
}
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
isValidEmail(email) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
}
|
|
|
|
isValidURL(url) {
|
|
try {
|
|
new URL(url);
|
|
return url.startsWith('http://') || url.startsWith('https://');
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
isValidPhone(phone) {
|
|
// Brazilian phone validation (DDD + number)
|
|
const digitsOnly = phone.replace(/\D/g, '');
|
|
return /^\d{10,11}$/.test(digitsOnly);
|
|
}
|
|
|
|
// Method to be called by main QR generator
|
|
getVCardContent() {
|
|
const errors = this.validateVCardData();
|
|
if (errors.length > 0) {
|
|
throw new Error('Erro na validação: ' + errors.join(', '));
|
|
}
|
|
return this.generateVCardContent();
|
|
}
|
|
}
|
|
|
|
// Global functions for ad control
|
|
window.QRApp = {
|
|
refreshAds: function() {
|
|
if (window.adsbygoogle) {
|
|
document.querySelectorAll('.adsbygoogle').forEach(ad => {
|
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
|
});
|
|
}
|
|
},
|
|
|
|
hideAds: function() {
|
|
document.querySelectorAll('.ad-container').forEach(ad => {
|
|
ad.style.display = 'none';
|
|
});
|
|
}
|
|
}; |