fix: ajustes de navegação
All checks were successful
Deploy QR Rapido / test (push) Successful in 52s
Deploy QR Rapido / build-and-push (push) Successful in 12m53s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m40s

This commit is contained in:
Ricardo Carneiro 2025-09-04 12:31:56 -03:00
parent 71c575f879
commit 49da9f874a
5 changed files with 206 additions and 43 deletions

View File

@ -103,6 +103,8 @@ jobs:
--name qrrapido-staging \ --name qrrapido-staging \
--restart unless-stopped \ --restart unless-stopped \
-p 5000:8080 \ -p 5000:8080 \
--add-host=host.docker.internal:host-gateway \
-e Serilog__SeqUrl="http://host.docker.internal:5343" \
-e ASPNETCORE_ENVIRONMENT=Staging \ -e ASPNETCORE_ENVIRONMENT=Staging \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
EOF EOF
@ -124,6 +126,8 @@ jobs:
--name qrrapido-staging \ --name qrrapido-staging \
--restart unless-stopped \ --restart unless-stopped \
-p 5000:8080 \ -p 5000:8080 \
--add-host=host.docker.internal:host-gateway \
-e Serilog__SeqUrl="http://host.docker.internal:5342" \
-e ASPNETCORE_ENVIRONMENT=Staging \ -e ASPNETCORE_ENVIRONMENT=Staging \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
EOF EOF

View File

@ -6,7 +6,7 @@ namespace QRRapidoApp.Middleware
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly ILogger<LanguageRedirectionMiddleware> _logger; private readonly ILogger<LanguageRedirectionMiddleware> _logger;
private readonly string[] _supportedCultures = { "pt-BR", "es-PY", "es" }; private readonly string[] _supportedCultures = { "pt-BR", "es-PY" };
private const string DefaultCulture = "pt-BR"; private const string DefaultCulture = "pt-BR";
public LanguageRedirectionMiddleware(RequestDelegate next, ILogger<LanguageRedirectionMiddleware> logger) public LanguageRedirectionMiddleware(RequestDelegate next, ILogger<LanguageRedirectionMiddleware> logger)
@ -18,31 +18,34 @@ namespace QRRapidoApp.Middleware
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
var path = context.Request.Path.Value?.TrimStart('/') ?? ""; var path = context.Request.Path.Value?.TrimStart('/') ?? "";
// Skip if already has culture in path, or if it's an API, static file, or special route
if (HasCultureInPath(path) || IsSpecialRoute(path)) if (HasCultureInPath(path) || IsSpecialRoute(path))
{ {
await _next(context); await _next(context);
return; return;
} }
// Detect browser language
var detectedCulture = DetectBrowserLanguage(context); var detectedCulture = DetectBrowserLanguage(context);
// Build redirect URL with culture // If the detected culture is the default, do not redirect.
if (detectedCulture == DefaultCulture)
{
await _next(context);
return;
}
var redirectUrl = $"/{detectedCulture}"; var redirectUrl = $"/{detectedCulture}";
if (!string.IsNullOrEmpty(path)) if (!string.IsNullOrEmpty(path))
{ {
redirectUrl += $"/{path}"; redirectUrl += $"/{path}";
} }
// Add query string if present
if (context.Request.QueryString.HasValue) if (context.Request.QueryString.HasValue)
{ {
redirectUrl += context.Request.QueryString.Value; redirectUrl += context.Request.QueryString.Value;
} }
_logger.LogInformation("Redirecting to localized URL: {RedirectUrl} (detected culture: {Culture})", _logger.LogInformation("Redirecting to localized URL: {RedirectUrl} (detected culture: {Culture})",
redirectUrl, detectedCulture); redirectUrl, detectedCulture);
context.Response.Redirect(redirectUrl, permanent: false); context.Response.Redirect(redirectUrl, permanent: false);
@ -64,7 +67,7 @@ namespace QRRapidoApp.Middleware
{ {
var specialRoutes = new[] var specialRoutes = new[]
{ {
"api/", "health", "_framework/", "lib/", "css/", "js/", "images/", "api/", "health", "_framework/", "lib/", "css/", "js/", "images/",
"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",
@ -80,41 +83,30 @@ namespace QRRapidoApp.Middleware
if (acceptLanguage != null && acceptLanguage.Any()) if (acceptLanguage != null && acceptLanguage.Any())
{ {
// Check for exact matches first
foreach (var lang in acceptLanguage.OrderByDescending(x => x.Quality ?? 1.0)) foreach (var lang in acceptLanguage.OrderByDescending(x => x.Quality ?? 1.0))
{ {
var langCode = lang.Value.Value; var langCode = lang.Value.Value;
// Special case: es-PY should redirect to pt-BR // Exact match for es-PY
if (string.Equals(langCode, "es-PY", StringComparison.OrdinalIgnoreCase)) if (string.Equals(langCode, "es-PY", StringComparison.OrdinalIgnoreCase))
{ {
return DefaultCulture; return "es-PY";
} }
// Check exact match // Generic 'es' maps to 'es-PY'
if (_supportedCultures.Contains(langCode)) if (langCode.StartsWith("es-", StringComparison.OrdinalIgnoreCase) || string.Equals(langCode, "es", StringComparison.OrdinalIgnoreCase))
{
return langCode;
}
// Check language part only (e.g., 'es' from 'es-AR')
var languagePart = langCode.Split('-')[0];
// Map 'es' to 'es-PY' specifically
if (string.Equals(languagePart, "es", StringComparison.OrdinalIgnoreCase))
{ {
return "es-PY"; return "es-PY";
} }
var matchingCulture = _supportedCultures.FirstOrDefault(c => c.StartsWith(languagePart + "-") || c == languagePart); // Check for pt-BR
if (matchingCulture != null) if (langCode.StartsWith("pt-", StringComparison.OrdinalIgnoreCase) || string.Equals(langCode, "pt", StringComparison.OrdinalIgnoreCase))
{ {
return matchingCulture; return "pt-BR";
} }
} }
} }
// Default fallback
return DefaultCulture; return DefaultCulture;
} }
} }

View File

@ -147,7 +147,6 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
{ {
new CultureInfo("pt-BR"), new CultureInfo("pt-BR"),
new CultureInfo("es-PY"), new CultureInfo("es-PY"),
new CultureInfo("es")
}; };
options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR"); options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR");
@ -284,7 +283,7 @@ app.MapHealthChecks("/healthcheck");
// Language routes (must be first) // Language routes (must be first)
app.MapControllerRoute( app.MapControllerRoute(
name: "localized", name: "localized",
pattern: "{culture:regex(^(pt-BR|es-PY|es)$)}/{controller=Home}/{action=Index}/{id?}"); pattern: "{culture:regex(^(es-PY)$)}/{controller=Home}/{action=Index}/{id?}");
// API routes // API routes
app.MapControllerRoute( app.MapControllerRoute(

View File

@ -137,6 +137,23 @@
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="d-grid opacity-controlled disabled-state" id="next-button-group">
<button type="button" class="btn btn-success" id="next-btn">
<i class="fas fa-arrow-right"></i> Personalizar
</button>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="d-grid opacity-controlled disabled-state" id="button-gerar-quick-div">
<button type="submit" class="btn btn-primary" id="generate-quick-btn">
<i class="fas fa-qrcode"></i> Gerar Rápido
</button>
</div>
</div>
</div>
<!-- Dynamic QR Section (Premium) --> <!-- Dynamic QR Section (Premium) -->
<div id="dynamic-qr-section" style="display: none;" class="opacity-controlled disabled-state"> <div id="dynamic-qr-section" style="display: none;" class="opacity-controlled disabled-state">
<div class="premium-feature-box"> <div class="premium-feature-box">

View File

@ -183,6 +183,21 @@ class QRRapidoGenerator {
if (saveBtn) { if (saveBtn) {
saveBtn.addEventListener('click', this.saveToHistory.bind(this)); saveBtn.addEventListener('click', this.saveToHistory.bind(this));
} }
// Next button navigation
const nextBtn = document.getElementById('next-btn');
if (nextBtn) {
nextBtn.addEventListener('click', this.handleNextButtonClick.bind(this));
}
// Generate quick button (same functionality as main generate button)
const generateQuickBtn = document.getElementById('generate-quick-btn');
if (generateQuickBtn) {
generateQuickBtn.addEventListener('click', (e) => {
e.preventDefault();
this.generateQRWithTimer(e);
});
}
} }
setupDownloadButtons() { setupDownloadButtons() {
@ -718,6 +733,9 @@ class QRRapidoGenerator {
if (result.readabilityInfo && typeof this.displayReadabilityAnalysis === 'function') { if (result.readabilityInfo && typeof this.displayReadabilityAnalysis === 'function') {
this.displayReadabilityAnalysis(result.readabilityInfo); this.displayReadabilityAnalysis(result.readabilityInfo);
} }
// Scroll suave para o preview após a geração
this.scrollToPreview();
// CORREÇÃO: Log para debug - verificar se QR code tem logo // CORREÇÃO: Log para debug - verificar se QR code tem logo
const logoUpload = document.getElementById('logo-upload'); const logoUpload = document.getElementById('logo-upload');
@ -1629,7 +1647,7 @@ class QRRapidoGenerator {
if (this.selectedType && this.validateContent(content) && !this.hasShownContentToast) { if (this.selectedType && this.validateContent(content) && !this.hasShownContentToast) {
this.contentDelayTimer = setTimeout(() => { this.contentDelayTimer = setTimeout(() => {
this.triggerContentReadyUX(); this.triggerContentReadyUX();
}, 2000); // 2 seconds delay after the initial 300ms debounce }, 7000); // 7 seconds delay after the initial 300ms debounce
} }
} }
@ -1651,9 +1669,6 @@ class QRRapidoGenerator {
// Update button state with ready indicator // Update button state with ready indicator
this.updateGenerateButtonToReady(); this.updateGenerateButtonToReady();
// Auto scroll to QR generation area
this.smoothScrollToQRArea();
} }
} }
@ -1966,7 +1981,12 @@ class QRRapidoGenerator {
updateGenerateButton() { updateGenerateButton() {
const generateBtn = document.getElementById('generate-btn'); const generateBtn = document.getElementById('generate-btn');
if (!generateBtn) return; const generateQuickBtn = document.getElementById('generate-quick-btn');
const nextGroup = document.getElementById('next-button-group');
const nextBtn = document.getElementById('next-btn');
const quickGroup = document.getElementById('button-gerar-quick-div');
if (!generateBtn && !generateQuickBtn) return;
let isValid = false; let isValid = false;
const type = this.selectedType; const type = this.selectedType;
@ -2022,15 +2042,146 @@ class QRRapidoGenerator {
isValid = data.to.trim() !== '' && data.subject.trim() !== ''; isValid = data.to.trim() !== '' && data.subject.trim() !== '';
} }
generateBtn.disabled = !isValid; // Controle do botão principal "Gerar QR Code"
if (generateBtn) {
if (isValid) { generateBtn.disabled = !isValid;
generateBtn.classList.remove('btn-secondary', 'disabled'); if (isValid) {
generateBtn.classList.add('btn-primary'); generateBtn.classList.remove('btn-secondary', 'disabled');
} else { generateBtn.classList.add('btn-primary');
generateBtn.classList.remove('btn-primary'); } else {
generateBtn.classList.add('btn-secondary', 'disabled'); generateBtn.classList.remove('btn-primary');
generateBtn.classList.add('btn-secondary', 'disabled');
}
} }
// Controle do botão "Gerar Rápido" com a mesma validação
if (generateQuickBtn) {
generateQuickBtn.disabled = !isValid;
if (quickGroup) {
if (!isValid) {
quickGroup.classList.add('disabled-state');
} else {
quickGroup.classList.remove('disabled-state');
}
}
}
// Controle do botão "Próximo/Personalizar" com validação mais permissiva
const isNextValid = this.isValidForNext(type);
if (nextBtn) {
nextBtn.disabled = !isNextValid;
if (nextGroup) {
if (!isNextValid) {
nextGroup.classList.add('disabled-state');
} else {
nextGroup.classList.remove('disabled-state');
}
}
}
}
// Validação mais permissiva para o botão "Próximo"
isValidForNext(type) {
if (!type) return false;
if (type === 'url') {
const contentField = document.getElementById('qr-content');
const content = contentField?.value || '';
const trimmedContent = content.trim();
// Validação mais simples: pelo menos 1 letra, um ponto, e mais 1 letra
const simpleUrlPattern = /\w+\.\w+/;
return trimmedContent.length >= 4 && simpleUrlPattern.test(trimmedContent);
} else if (type === 'text') {
const contentField = document.getElementById('qr-content');
const content = contentField?.value || '';
return content.trim().length >= 1; // Mais permissivo para texto
} else if (type === 'vcard') {
// Verificar apenas campos obrigatórios básicos
const name = document.getElementById('vcard-name')?.value || '';
const mobile = document.getElementById('vcard-mobile')?.value || '';
const email = document.getElementById('vcard-email')?.value || '';
return name.trim().length >= 2 &&
(mobile.trim().length >= 8 || email.includes('@'));
} else if (type === 'wifi') {
const ssid = document.getElementById('wifi-ssid')?.value || '';
return ssid.trim().length >= 2;
} else if (type === 'sms') {
const number = document.getElementById('sms-number')?.value || '';
const message = document.getElementById('sms-message')?.value || '';
return number.trim().length >= 8 && message.trim().length >= 1;
} else if (type === 'email') {
const to = document.getElementById('email-to')?.value || '';
const subject = document.getElementById('email-subject')?.value || '';
return to.includes('@') && subject.trim().length >= 1;
}
return false;
}
// Método para lidar com o clique do botão "Próximo"
handleNextButtonClick() {
// Abrir o accordion de personalização avançada
const customizationPanel = document.getElementById('customization-panel');
const customizationAccordion = document.getElementById('customization-accordion');
if (customizationPanel && !customizationPanel.classList.contains('show')) {
const bsCollapse = new bootstrap.Collapse(customizationPanel, {
show: true
});
}
// Scroll suave até a personalização avançada
if (customizationAccordion) {
customizationAccordion.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
// Opcionalmente mostrar um toast de orientação
this.showAdvancedCustomizationToast();
}
// Toast informativo sobre personalização avançada
showAdvancedCustomizationToast() {
const message = '✨ <strong>Personalização Avançada!</strong><br/>Customize cores, tamanho, bordas e adicione seu logo. Quando estiver pronto, clique em "Gerar QR Code".';
const toast = this.createEducationalToast(message);
this.showGuidanceToast(toast, 8000); // 8 segundos
}
// Scroll suave para o preview após geração do QR Code
scrollToPreview() {
setTimeout(() => {
const isMobile = window.innerWidth <= 768;
if (isMobile) {
// No mobile, fazer scroll para o preview específico dentro da sidebar
const previewDiv = document.getElementById('qr-preview');
if (previewDiv) {
const offsetTop = previewDiv.offsetTop - 60; // 60px de margem do topo
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
}
} else {
// No desktop, manter o comportamento atual
const previewCard = document.querySelector('.col-lg-4');
if (previewCard) {
previewCard.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
}, 500); // Pequeno delay para garantir que o QR foi renderizado
} }
// Remove destaque inicial quando tipo for selecionado // Remove destaque inicial quando tipo for selecionado