feat: adsense
All checks were successful
Deploy QR Rapido / test (push) Successful in 48s
Deploy QR Rapido / build-and-push (push) Successful in 13m55s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m53s

This commit is contained in:
Ricardo Carneiro 2025-08-11 12:23:01 -03:00
parent e172969996
commit bcf9f659b4
9 changed files with 290 additions and 39 deletions

View File

@ -68,7 +68,7 @@ namespace QRRapidoApp.Middleware
"favicon.ico", "robots.txt", "sitemap.xml", "favicon.ico", "robots.txt", "sitemap.xml",
"signin-microsoft", "signin-google", "signout-callback-oidc", "signin-microsoft", "signin-google", "signout-callback-oidc",
"Account/ExternalLoginCallback", "Account/Logout", "Pagamento/CreateCheckout", "Account/ExternalLoginCallback", "Account/Logout", "Pagamento/CreateCheckout",
"Pagamento/StripeWebhook" "Pagamento/StripeWebhook", "api/QR"
}; };
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase)); return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));

View File

@ -681,6 +681,15 @@
<data name="TypeGuideText" xml:space="preserve"> <data name="TypeGuideText" xml:space="preserve">
<value>📝 Para texto libre, ingresa cualquier contenido que desees</value> <value>📝 Para texto libre, ingresa cualquier contenido que desees</value>
</data> </data>
<data name="ContentAddedToastTitle" xml:space="preserve">
<value>✅ ¡Contenido ñemboguapy! (Contenido agregado!)</value>
</data>
<data name="ContentAddedToastMessage" xml:space="preserve">
<value>Opciones avanzadas ojeporukuaa guýpe. Eity "Generar QR Code" emoĩmbyre jave.</value>
</data>
<data name="GenerateButtonReady" xml:space="preserve">
<value>✨ ¡Emoĩmbyma! (¡Listo para generar!)</value>
</data>
<data name="TipsFasterQR" xml:space="preserve"> <data name="TipsFasterQR" xml:space="preserve">
<value>Ñe'ẽ pya'épe ha porãve (Consejos para QR Más Rápidos)</value> <value>Ñe'ẽ pya'épe ha porãve (Consejos para QR Más Rápidos)</value>
</data> </data>

View File

@ -681,6 +681,15 @@
<data name="TypeGuideText" xml:space="preserve"> <data name="TypeGuideText" xml:space="preserve">
<value>📝 Para texto livre, digite qualquer conteúdo que desejar</value> <value>📝 Para texto livre, digite qualquer conteúdo que desejar</value>
</data> </data>
<data name="ContentAddedToastTitle" xml:space="preserve">
<value>✅ Conteúdo adicionado!</value>
</data>
<data name="ContentAddedToastMessage" xml:space="preserve">
<value>Opções avançadas disponíveis abaixo. Clique em "Gerar QR Code" quando estiver pronto.</value>
</data>
<data name="GenerateButtonReady" xml:space="preserve">
<value>✨ Pronto para gerar!</value>
</data>
<data name="TipsFasterQR" xml:space="preserve"> <data name="TipsFasterQR" xml:space="preserve">
<value>Dicas para QR Mais Rápidos</value> <value>Dicas para QR Mais Rápidos</value>
</data> </data>

View File

@ -25,7 +25,9 @@ namespace QRRapidoApp.Services
{ {
if (_context.Users == null) return null; // Development mode without MongoDB if (_context.Users == null) return null; // Development mode without MongoDB
return await _context.Users.Find(u => u.Id == userId).FirstOrDefaultAsync(); var userData = await _context.Users.Find(u => u.Id == userId).FirstOrDefaultAsync();
return userData ?? new User();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -151,7 +151,10 @@
validationContentMinLength: '@Localizer["ValidationContentMinLength"]', validationContentMinLength: '@Localizer["ValidationContentMinLength"]',
errorSavingHistory: '@Localizer["ErrorSavingHistory"]', errorSavingHistory: '@Localizer["ErrorSavingHistory"]',
rateLimitReached: '@Localizer["RateLimitReached"]', rateLimitReached: '@Localizer["RateLimitReached"]',
premiumLogoRequired: '@Localizer["PremiumLogoRequired"]' premiumLogoRequired: '@Localizer["PremiumLogoRequired"]',
contentAddedToastTitle: '@Localizer["ContentAddedToastTitle"]',
contentAddedToastMessage: '@Localizer["ContentAddedToastMessage"]',
generateButtonReady: '@Localizer["GenerateButtonReady"]'
}; };
</script> </script>

View File

@ -2,7 +2,7 @@
"ApplicationName": "QRRapido-Prod", "ApplicationName": "QRRapido-Prod",
"Environment": "Prod", "Environment": "Prod",
"ConnectionStrings": { "ConnectionStrings": {
"MongoDB": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/seu_banco?replicaSet=rs0&authSource=admin" "MongoDB": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin"
}, },
"Serilog": { "Serilog": {
"SeqUrl": "http://localhost:5341", "SeqUrl": "http://localhost:5341",

View File

@ -27,7 +27,7 @@
"PriceId": "prod_SnfQTxwE3i8r5L" "PriceId": "prod_SnfQTxwE3i8r5L"
}, },
"AdSense": { "AdSense": {
"ClientId": "ca-pub-XXXXXXXXXX", "ClientId": "ca-pub-3475956393038764",
"Enabled": true "Enabled": true
}, },
"Performance": { "Performance": {

View File

@ -1123,3 +1123,75 @@ html[data-theme="dark"] .form-control:valid {
border-color: #22c55e !important; border-color: #22c55e !important;
box-shadow: 0 0 0 0.25rem rgba(34, 197, 94, 0.25) !important; box-shadow: 0 0 0 0.25rem rgba(34, 197, 94, 0.25) !important;
} }
/* =================================
UX IMPROVEMENTS - BUTTON READY STATES
================================= */
.btn-pulse {
animation: buttonPulse 1.5s ease-in-out infinite;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.4) !important;
}
.btn-ready {
background: linear-gradient(45deg, #007bff, #00d4ff) !important;
border-color: #007bff !important;
position: relative;
overflow: hidden;
transform: scale(1.02);
transition: all 0.3s ease;
}
.btn-ready::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.6s ease;
}
.btn-ready:hover::before {
left: 100%;
}
@keyframes buttonPulse {
0% {
transform: scale(1.02);
box-shadow: 0 0 10px rgba(0, 123, 255, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 20px rgba(0, 123, 255, 0.6);
}
100% {
transform: scale(1.02);
box-shadow: 0 0 10px rgba(0, 123, 255, 0.4);
}
}
/* Dark mode adjustments for button ready states */
html[data-theme="dark"] .btn-pulse {
box-shadow: 0 0 10px rgba(96, 165, 250, 0.4) !important;
}
html[data-theme="dark"] .btn-ready {
background: linear-gradient(45deg, #3b82f6, #60a5fa) !important;
}
html[data-theme="dark"] @keyframes buttonPulse {
0% {
transform: scale(1.02);
box-shadow: 0 0 10px rgba(96, 165, 250, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 20px rgba(96, 165, 250, 0.6);
}
100% {
transform: scale(1.02);
box-shadow: 0 0 10px rgba(96, 165, 250, 0.4);
}
}

View File

@ -45,6 +45,11 @@ class QRRapidoGenerator {
this.selectedStyle = 'classic'; // Estilo padrão this.selectedStyle = 'classic'; // Estilo padrão
this.contentValid = false; this.contentValid = false;
// UX Improvements - Intelligent delay system
this.contentDelayTimer = null;
this.hasShownContentToast = false;
this.buttonReadyState = false;
this.initializeEvents(); this.initializeEvents();
this.checkAdFreeStatus(); this.checkAdFreeStatus();
this.updateLanguage(); this.updateLanguage();
@ -111,9 +116,25 @@ class QRRapidoGenerator {
let timer; let timer;
qrContent.addEventListener('input', (e) => { qrContent.addEventListener('input', (e) => {
clearTimeout(timer); clearTimeout(timer);
// Clear URL validation timeout if exists
if (qrContent.validationTimeout) {
clearTimeout(qrContent.validationTimeout);
}
timer = setTimeout(() => { timer = setTimeout(() => {
this.handleContentChange(e.target.value); this.handleContentChange(e.target.value);
this.updateGenerateButton(); this.updateGenerateButton();
// Handle URL-specific validation
const type = document.getElementById('qr-type')?.value;
if (type === 'url') {
qrContent.validationTimeout = setTimeout(() => {
this.updateGenerateButton();
}, 200); // Additional URL validation delay
}
// Trigger UX improvements with delay
this.handleContentInputWithDelay(e.target.value);
}, 300); }, 300);
}); });
} }
@ -391,7 +412,6 @@ class QRRapidoGenerator {
} }
// Sending request to backend // Sending request to backend
const response = await fetch(requestData.endpoint, fetchOptions); const response = await fetch(requestData.endpoint, fetchOptions);
if (!response.ok) { if (!response.ok) {
@ -1057,8 +1077,10 @@ class QRRapidoGenerator {
console.log('Calling showUnlimitedCounter directly for logged user'); console.log('Calling showUnlimitedCounter directly for logged user');
this.showUnlimitedCounter(); this.showUnlimitedCounter();
} else { } else {
if (response.status !== 401) {
console.log('GetUserStats response not ok:', response.status); console.log('GetUserStats response not ok:', response.status);
} }
}
} catch (error) { } catch (error) {
// If not authenticated or error, keep the default "Carregando..." text // If not authenticated or error, keep the default "Carregando..." text
console.debug('User not authenticated or error loading stats:', error); console.debug('User not authenticated or error loading stats:', error);
@ -1200,9 +1222,9 @@ class QRRapidoGenerator {
// Create SVG with embedded base64 image // Create SVG with embedded base64 image
const svgData = `<?xml version="1.0" encoding="UTF-8"?> const svgData = `<?xml version="1.0" encoding="UTF-8"?>
<svg width="${img.width}" height="${img.height}" xmlns="http://www.w3.org/2000/svg"> <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}"/> <image width="${img.width}" height="${img.height}" href="data:image/png;base64,${base64Data}"/>
</svg>`; </svg>`;
resolve(svgData); resolve(svgData);
}; };
img.src = `data:image/png;base64,${base64Data}`; img.src = `data:image/png;base64,${base64Data}`;
@ -1582,41 +1604,40 @@ class QRRapidoGenerator {
const contentField = document.getElementById('qr-content'); const contentField = document.getElementById('qr-content');
if (!contentField) return; if (!contentField) return;
// Auto-fix URL on blur (when user leaves the field) //const contentField = document.getElementById('qr-content');
contentField.addEventListener('blur', () => { //if (!contentField) return;
const type = document.getElementById('qr-type')?.value;
if (type === 'url' && contentField.value.trim()) { //// Auto-fix URL on blur (when user leaves the field)
const originalValue = contentField.value.trim(); //contentField.addEventListener('blur', () => {
const fixedValue = this.autoFixURL(originalValue); // const type = document.getElementById('qr-type')?.value;
if (originalValue !== fixedValue) { // if (type === 'url' && contentField.value.trim()) {
contentField.value = fixedValue; // const originalValue = contentField.value.trim();
console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue); // const fixedValue = this.autoFixURL(originalValue);
// Revalidar após correção // if (originalValue !== fixedValue) {
this.updateGenerateButton(); // contentField.value = fixedValue;
} // console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue);
}
});
// Real-time validation with debounce // // Revalidar após correção
contentField.addEventListener('input', () => { // this.updateGenerateButton();
const type = document.getElementById('qr-type')?.value; // }
// }
//});
if (type === 'url') { // Note: Real-time validation is now handled in initializeEvents() to avoid duplicate listeners
// Debounce para não validar a cada caractere
clearTimeout(contentField.validationTimeout);
contentField.validationTimeout = setTimeout(() => {
this.updateGenerateButton();
}, 500);
}
});
} }
handleTypeSelection(type) { handleTypeSelection(type) {
this.selectedType = type; this.selectedType = type;
// Reset UX improvements flags for new type selection
this.hasShownContentToast = false;
if (this.contentDelayTimer) {
clearTimeout(this.contentDelayTimer);
}
this.removeButtonReadyState();
if (type) { if (type) {
this.removeInitialHighlight(); this.removeInitialHighlight();
// Sempre habilitar campos de conteúdo após selecionar tipo // Sempre habilitar campos de conteúdo após selecionar tipo
@ -1647,6 +1668,139 @@ class QRRapidoGenerator {
this.updateGenerateButton(); this.updateGenerateButton();
} }
// UX Improvements - Intelligent delay system
handleContentInputWithDelay(content) {
// Clear existing timer
if (this.contentDelayTimer) {
clearTimeout(this.contentDelayTimer);
}
// Only proceed with delay logic if we have valid content and selected type
if (this.selectedType && this.validateContent(content) && !this.hasShownContentToast) {
this.contentDelayTimer = setTimeout(() => {
this.triggerContentReadyUX();
}, 2000); // 2 seconds delay after the initial 300ms debounce
}
}
triggerContentReadyUX() {
if (this.selectedType && this.contentValid && !this.hasShownContentToast) {
// Mark as shown to prevent multiple toasts
this.hasShownContentToast = true;
const contentField = document.getElementById('qr-content');
const originalValue = contentField.value.trim();
const fixedValue = this.autoFixURL(originalValue);
if (originalValue !== fixedValue) {
contentField.value = fixedValue;
this.updateGenerateButton();
}
// Show educational toast
this.showContentAddedToast();
// Update button state with ready indicator
this.updateGenerateButtonToReady();
// Auto scroll to QR generation area
this.smoothScrollToQRArea();
}
}
showContentAddedToast() {
const toastTitle = window.QRRapidoTranslations?.contentAddedToastTitle || '✅ Conteúdo adicionado!';
const toastMessage = window.QRRapidoTranslations?.contentAddedToastMessage ||
'Use as avançadas abaixo para personalizar o QR. \n Clique em "Gerar QR Code" quando estiver pronto.';
const fullMessage = `<strong>${toastTitle}</strong><br/>${toastMessage}`;
// Create educational toast similar to existing system but with longer duration
const toast = this.createEducationalToast(fullMessage);
this.showGuidanceToast(toast, 10000); // 10 seconds
}
createEducationalToast(message) {
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-bg-success border-0';
toast.setAttribute('role', 'alert');
toast.style.minWidth = '400px';
toast.id = 'content-added-toast';
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body fw-medium">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
return toast;
}
updateGenerateButtonToReady() {
const generateBtn = document.getElementById('generate-btn');
if (generateBtn && this.contentValid) {
this.buttonReadyState = true;
// Add visual ready state
generateBtn.classList.add('btn-pulse', 'btn-ready');
// Update button text with ready indicator
const readyText = window.QRRapidoTranslations?.generateButtonReady || '✨ Pronto para gerar!';
const originalHtml = generateBtn.innerHTML;
// Store original for restoration
if (!generateBtn.dataset.originalHtml) {
generateBtn.dataset.originalHtml = originalHtml;
}
generateBtn.innerHTML = `<i class="fas fa-bolt"></i> ${readyText}`;
// Remove ready state after 5 seconds
setTimeout(() => {
this.removeButtonReadyState();
}, 5000);
}
}
removeButtonReadyState() {
const generateBtn = document.getElementById('generate-btn');
if (generateBtn && generateBtn.dataset.originalHtml) {
generateBtn.classList.remove('btn-pulse', 'btn-ready');
generateBtn.innerHTML = generateBtn.dataset.originalHtml;
this.buttonReadyState = false;
}
}
smoothScrollToQRArea() {
// Wait a bit for the toast to appear, then scroll
setTimeout(() => {
let targetElement;
// Find the QR preview area or advanced options
const qrPreview = document.getElementById('qr-result');
const advancedSection = document.querySelector('.advanced-options');
const generateBtn = document.getElementById('generate-btn');
const toast = document.getElementById('content-added-toast');
// Priority: QR result > Advanced options > Generate button
targetElement = qrPreview || advancedSection || toast || generateBtn;
if (targetElement) {
// Calculate offset - less aggressive on mobile
const isMobile = window.innerWidth <= 768;
const offset = isMobile ? 20 : 100;
const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementPosition - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}, 1000); // 1 second delay to let toast show first
}
enableContentFields(type) { enableContentFields(type) {
const contentGroup = document.getElementById('content-group'); const contentGroup = document.getElementById('content-group');
const vcardInterface = document.getElementById('vcard-interface'); const vcardInterface = document.getElementById('vcard-interface');
@ -1966,7 +2120,7 @@ class QRRapidoGenerator {
// In a real implementation, these would come from server-side localization // In a real implementation, these would come from server-side localization
// For now, we'll use the JavaScript language strings or fallback to Portuguese // For now, we'll use the JavaScript language strings or fallback to Portuguese
return { return {
'url': document.querySelector('[data-type-guide-url]')?.textContent || '🌐 Para gerar QR de URL, digite o endereço completo (ex: https://google.com)', //'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', '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', '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', 'sms': document.querySelector('[data-type-guide-sms]')?.textContent || '💬 Para SMS, digite o número do destinatário e a mensagem',
@ -2007,7 +2161,9 @@ class QRRapidoGenerator {
toastContainer = existingContainer; toastContainer = existingContainer;
} else { } else {
// Find the main content area and insert after hero section // Find the main content area and insert after hero section
const mainElement = document.querySelector('main[role="main"]'); // const mainElement = document.querySelector('main[role="main"]');
const mainElement = document.getElementById('customization-accordion');
if (mainElement) { if (mainElement) {
// Insert at the beginning of main content // Insert at the beginning of main content
mainElement.insertBefore(toastContainer, mainElement.firstChild); mainElement.insertBefore(toastContainer, mainElement.firstChild);