QrRapido/wwwroot/js/qr-speed-generator.js
2025-07-31 11:39:31 -03:00

1342 lines
51 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.initializeEvents();
this.checkAdFreeStatus();
this.updateLanguage();
this.updateStatsCounters();
}
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));
}
// 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
const qrType = document.getElementById('qr-type');
if (qrType) {
qrType.addEventListener('change', this.updateContentHints.bind(this));
}
// 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;
}
// 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';
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';
}
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);
}
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);
}
}
}
// 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';
});
}
};