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",
"signin-microsoft", "signin-google", "signout-callback-oidc",
"Account/ExternalLoginCallback", "Account/Logout", "Pagamento/CreateCheckout",
"Pagamento/StripeWebhook"
"Pagamento/StripeWebhook", "api/QR"
};
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));

View File

@ -681,6 +681,15 @@
<data name="TypeGuideText" xml:space="preserve">
<value>📝 Para texto libre, ingresa cualquier contenido que desees</value>
</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">
<value>Ñe'ẽ pya'épe ha porãve (Consejos para QR Más Rápidos)</value>
</data>

View File

@ -681,6 +681,15 @@
<data name="TypeGuideText" xml:space="preserve">
<value>📝 Para texto livre, digite qualquer conteúdo que desejar</value>
</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">
<value>Dicas para QR Mais Rápidos</value>
</data>

View File

@ -25,7 +25,9 @@ namespace QRRapidoApp.Services
{
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)
{

View File

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

View File

@ -2,7 +2,7 @@
"ApplicationName": "QRRapido-Prod",
"Environment": "Prod",
"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": {
"SeqUrl": "http://localhost:5341",

View File

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

View File

@ -1123,3 +1123,75 @@ html[data-theme="dark"] .form-control:valid {
border-color: #22c55e !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.contentValid = false;
// UX Improvements - Intelligent delay system
this.contentDelayTimer = null;
this.hasShownContentToast = false;
this.buttonReadyState = false;
this.initializeEvents();
this.checkAdFreeStatus();
this.updateLanguage();
@ -111,9 +116,25 @@ class QRRapidoGenerator {
let timer;
qrContent.addEventListener('input', (e) => {
clearTimeout(timer);
// Clear URL validation timeout if exists
if (qrContent.validationTimeout) {
clearTimeout(qrContent.validationTimeout);
}
timer = setTimeout(() => {
this.handleContentChange(e.target.value);
this.updateGenerateButton();
// Handle URL-specific validation
const type = document.getElementById('qr-type')?.value;
if (type === 'url') {
qrContent.validationTimeout = setTimeout(() => {
this.updateGenerateButton();
}, 200); // Additional URL validation delay
}
// Trigger UX improvements with delay
this.handleContentInputWithDelay(e.target.value);
}, 300);
});
}
@ -391,7 +412,6 @@ class QRRapidoGenerator {
}
// Sending request to backend
const response = await fetch(requestData.endpoint, fetchOptions);
if (!response.ok) {
@ -1057,8 +1077,10 @@ class QRRapidoGenerator {
console.log('Calling showUnlimitedCounter directly for logged user');
this.showUnlimitedCounter();
} else {
if (response.status !== 401) {
console.log('GetUserStats response not ok:', response.status);
}
}
} catch (error) {
// If not authenticated or error, keep the default "Carregando..." text
console.debug('User not authenticated or error loading stats:', error);
@ -1582,41 +1604,40 @@ class QRRapidoGenerator {
const contentField = document.getElementById('qr-content');
if (!contentField) return;
// Auto-fix URL on blur (when user leaves the field)
contentField.addEventListener('blur', () => {
const type = document.getElementById('qr-type')?.value;
//const contentField = document.getElementById('qr-content');
//if (!contentField) return;
if (type === 'url' && contentField.value.trim()) {
const originalValue = contentField.value.trim();
const fixedValue = this.autoFixURL(originalValue);
//// Auto-fix URL on blur (when user leaves the field)
//contentField.addEventListener('blur', () => {
// const type = document.getElementById('qr-type')?.value;
if (originalValue !== fixedValue) {
contentField.value = fixedValue;
console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue);
// if (type === 'url' && contentField.value.trim()) {
// const originalValue = contentField.value.trim();
// const fixedValue = this.autoFixURL(originalValue);
// Revalidar após correção
this.updateGenerateButton();
}
}
});
// if (originalValue !== fixedValue) {
// contentField.value = fixedValue;
// console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue);
// Real-time validation with debounce
contentField.addEventListener('input', () => {
const type = document.getElementById('qr-type')?.value;
// // Revalidar após correção
// this.updateGenerateButton();
// }
// }
//});
if (type === 'url') {
// Debounce para não validar a cada caractere
clearTimeout(contentField.validationTimeout);
contentField.validationTimeout = setTimeout(() => {
this.updateGenerateButton();
}, 500);
}
});
// Note: Real-time validation is now handled in initializeEvents() to avoid duplicate listeners
}
handleTypeSelection(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) {
this.removeInitialHighlight();
// Sempre habilitar campos de conteúdo após selecionar tipo
@ -1647,6 +1668,139 @@ class QRRapidoGenerator {
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) {
const contentGroup = document.getElementById('content-group');
const vcardInterface = document.getElementById('vcard-interface');
@ -1966,7 +2120,7 @@ class QRRapidoGenerator {
// In a real implementation, these would come from server-side localization
// For now, we'll use the JavaScript language strings or fallback to Portuguese
return {
'url': document.querySelector('[data-type-guide-url]')?.textContent || '🌐 Para gerar QR de URL, digite o endereço completo (ex: https://google.com)',
//'url': document.querySelector('[data-type-guide-url]')?.textContent || '🌐 Para gerar QR de URL, digite o endereço completo (ex: https://google.com)',
'vcard': document.querySelector('[data-type-guide-vcard]')?.textContent || '👤 Para cartão de visita, preencha nome, telefone e email nos campos abaixo',
'wifi': document.querySelector('[data-type-guide-wifi]')?.textContent || '📶 Para WiFi, informe nome da rede, senha e tipo de segurança',
'sms': document.querySelector('[data-type-guide-sms]')?.textContent || '💬 Para SMS, digite o número do destinatário e a mensagem',
@ -2007,7 +2161,9 @@ class QRRapidoGenerator {
toastContainer = existingContainer;
} else {
// 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) {
// Insert at the beginning of main content
mainElement.insertBefore(toastContainer, mainElement.firstChild);