From ffac8a787bd05f18793d13fa8b8d17a039ee660b Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Wed, 10 Sep 2025 18:42:50 -0300 Subject: [PATCH] fix: ajuste de tamanho de imagem e toast de mensagens para ajustes --- .claude/settings.local.json | 3 +- categorias.json | 452 ++++++++++++++++++ src/BCards.Web/Controllers/AdminController.cs | 35 +- src/BCards.Web/Program.cs | 40 +- src/BCards.Web/Services/GridFSImageStorage.cs | 2 +- src/BCards.Web/Views/Admin/ManagePage.cshtml | 112 ++++- 6 files changed, 613 insertions(+), 31 deletions(-) create mode 100644 categorias.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 52271af..d62d903 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,8 @@ "Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)", "Bash(sed:*)", "Bash(./clean-build.sh:*)", - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(scp:*)" ] }, "enableAllProjectMcpServers": false diff --git a/categorias.json b/categorias.json new file mode 100644 index 0000000..3e6c700 --- /dev/null +++ b/categorias.json @@ -0,0 +1,452 @@ +[ + { + "name": "Artesanato", + "slug": "artesanato", + "icon": "🎨", + "seoKeywords": [ + "artesanato", + "artesão", + "feito à mão", + "personalizado", + "criativo", + "decoração" + ], + "description": "Artesãos e criadores de produtos feitos à mão, decoração e arte personalizada", + "isActive": true + }, + { + "name": "Papelaria", + "slug": "papelaria", + "icon": "📝", + "seoKeywords": [ + "papelaria", + "escritório", + "material escolar", + "impressão", + "convites", + "personalização" + ], + "description": "Lojas de papelaria, material de escritório, impressão e produtos personalizados", + "isActive": true + }, + { + "name": "Coaching", + "slug": "coaching", + "icon": "🎯", + "seoKeywords": [ + "coaching", + "mentoria", + "desenvolvimento pessoal", + "life coach", + "business coach", + "liderança" + ], + "description": "Coaches, mentores e profissionais de desenvolvimento pessoal e empresarial", + "isActive": true + }, + { + "name": "Fitness", + "slug": "fitness", + "icon": "💪", + "seoKeywords": [ + "fitness", + "academia", + "personal trainer", + "musculação", + "treinamento", + "exercícios" + ], + "description": "Personal trainers, academias, estúdios de pilates e profissionais fitness", + "isActive": true + }, + { + "name": "Psicologia", + "slug": "psicologia", + "icon": "🧠", + "seoKeywords": [ + "psicólogo", + "terapia", + "psicologia", + "saúde mental", + "consultório", + "atendimento" + ], + "description": "Psicólogos, terapeutas e profissionais de saúde mental", + "isActive": true + }, + { + "name": "Nutrição", + "slug": "nutricao", + "icon": "🥗", + "seoKeywords": [ + "nutricionista", + "dieta", + "nutrição", + "alimentação saudável", + "consultoria nutricional", + "emagrecimento" + ], + "description": "Nutricionistas, consultores em alimentação e profissionais da nutrição", + "isActive": true + }, + { + "name": "Moda e Vestuário", + "slug": "moda", + "icon": "👗", + "seoKeywords": [ + "moda", + "vestuário", + "roupas", + "fashion", + "estilista", + "costureira" + ], + "description": "Lojas de roupas, estilistas, costureiras e profissionais da moda", + "isActive": true + }, + { + "name": "Fotografia", + "slug": "fotografia", + "icon": "📸", + "seoKeywords": [ + "fotógrafo", + "fotografia", + "ensaio", + "casamento", + "eventos", + "retratos" + ], + "description": "Fotógrafos profissionais, estúdios fotográficos e serviços de fotografia", + "isActive": true + }, + { + "name": "Marketing Digital", + "slug": "marketing-digital", + "icon": "📱", + "seoKeywords": [ + "marketing digital", + "social media", + "publicidade", + "SEO", + "gestão de redes", + "digital" + ], + "description": "Agências de marketing digital, gestores de redes sociais e consultores digitais", + "isActive": true + }, + { + "name": "Contabilidade", + "slug": "contabilidade", + "icon": "📊", + "seoKeywords": [ + "contador", + "contabilidade", + "fiscal", + "imposto de renda", + "MEI", + "consultoria contábil" + ], + "description": "Contadores, escritórios contábeis e consultoria fiscal", + "isActive": true + }, + { + "name": "Design", + "slug": "design", + "icon": "🎨", + "seoKeywords": [ + "designer", + "design gráfico", + "identidade visual", + "logo", + "criativo", + "branding" + ], + "description": "Designers gráficos, criativos e profissionais de identidade visual", + "isActive": true + }, + { + "name": "Consultoria", + "slug": "consultoria", + "icon": "🤝", + "seoKeywords": [ + "consultor", + "consultoria", + "assessoria", + "especialista", + "negócios", + "estratégia" + ], + "description": "Consultores especializados, assessoria empresarial e serviços de consultoria", + "isActive": true + }, + { + "name": "Pets", + "slug": "pets", + "icon": "🐕", + "seoKeywords": [ + "veterinário", + "pet shop", + "animais", + "cuidados", + "petshop", + "adestramento" + ], + "description": "Veterinários, pet shops, adestradores e serviços para animais", + "isActive": true + }, + { + "name": "Casa e Jardim", + "slug": "casa-jardim", + "icon": "🏡", + "seoKeywords": [ + "paisagismo", + "jardinagem", + "decoração", + "casa", + "jardim", + "plantas" + ], + "description": "Paisagistas, jardineiros, decoradores e serviços para casa e jardim", + "isActive": true + }, + { + "name": "Automóveis", + "slug": "automoveis", + "icon": "🚗", + "seoKeywords": [ + "mecânico", + "automóveis", + "carros", + "oficina", + "manutenção", + "peças" + ], + "description": "Mecânicos, oficinas, lojas de peças e serviços automotivos", + "isActive": true + }, + { + "name": "Turismo", + "slug": "turismo", + "icon": "✈️", + "seoKeywords": [ + "turismo", + "viagem", + "agência", + "guia turístico", + "passeios", + "hospedagem" + ], + "description": "Agências de turismo, guias, pousadas e prestadores de serviços turísticos", + "isActive": true + }, + { + "name": "Música", + "slug": "musica", + "icon": "🎵", + "seoKeywords": [ + "músico", + "professor de música", + "instrumentos", + "aulas", + "banda", + "eventos musicais" + ], + "description": "Músicos, professores de música, bandas e profissionais do entretenimento", + "isActive": true + }, + { + "name": "Idiomas", + "slug": "idiomas", + "icon": "🗣️", + "seoKeywords": [ + "professor de idiomas", + "inglês", + "espanhol", + "tradutor", + "aulas particulares", + "curso de idiomas" + ], + "description": "Professores de idiomas, tradutores e escolas de línguas", + "isActive": true + }, + { + "name": "Limpeza", + "slug": "limpeza", + "icon": "🧽", + "seoKeywords": [ + "limpeza", + "faxina", + "diarista", + "higienização", + "empresa de limpeza", + "doméstica" + ], + "description": "Empresas de limpeza, diaristas e serviços de higienização", + "isActive": true + }, + { + "name": "Segurança", + "slug": "seguranca", + "icon": "🛡️", + "seoKeywords": [ + "segurança", + "vigilante", + "porteiro", + "alarmes", + "monitoramento", + "proteção" + ], + "description": "Empresas de segurança, vigilantes e serviços de proteção", + "isActive": true + }, + { + "name": "Eventos", + "slug": "eventos", + "icon": "🎉", + "seoKeywords": [ + "eventos", + "festa", + "casamento", + "buffet", + "decoração de festas", + "cerimonial" + ], + "description": "Organizadores de eventos, buffets, decoração e cerimonial", + "isActive": true + }, + { + "name": "Transporte", + "slug": "transporte", + "icon": "🚐", + "seoKeywords": [ + "transporte", + "frete", + "mudança", + "delivery", + "motorista", + "logística" + ], + "description": "Empresas de transporte, fretes, mudanças e serviços de entrega", + "isActive": true + }, + { + "name": "Construção", + "slug": "construcao", + "icon": "🔨", + "seoKeywords": [ + "construção", + "pedreiro", + "pintor", + "eletricista", + "encanador", + "reforma" + ], + "description": "Profissionais da construção civil, reformas e manutenção predial", + "isActive": true + }, + { + "name": "Joias e Acessórios", + "slug": "joias", + "icon": "💎", + "seoKeywords": [ + "joias", + "bijuterias", + "acessórios", + "ourives", + "relógios", + "semijoias" + ], + "description": "Joalherias, bijuterias, ourives e lojas de acessórios", + "isActive": true + }, + { + "name": "Odontologia", + "slug": "odontologia", + "icon": "🦷", + "seoKeywords": [ + "dentista", + "odontologia", + "clínica dentária", + "ortodontia", + "implante", + "oral" + ], + "description": "Dentistas, clínicas odontológicas e profissionais da área bucal", + "isActive": true + }, + { + "name": "Fisioterapia", + "slug": "fisioterapia", + "icon": "🏥", + "seoKeywords": [ + "fisioterapeuta", + "fisioterapia", + "reabilitação", + "RPG", + "massagem", + "terapia" + ], + "description": "Fisioterapeutas, clínicas de reabilitação e terapias corporais", + "isActive": true + }, + { + "name": "Livraria", + "slug": "livraria", + "icon": "📚", + "seoKeywords": [ + "livraria", + "livros", + "sebo", + "literatura", + "editora", + "publicação" + ], + "description": "Livrarias, sebos, editoras e comércio de livros e publicações", + "isActive": true + }, + { + "name": "Floricultura", + "slug": "floricultura", + "icon": "🌸", + "seoKeywords": [ + "floricultura", + "flores", + "buquê", + "plantas", + "arranjos", + "casamento" + ], + "description": "Floriculturas, arranjos florais e comércio de plantas ornamentais", + "isActive": true + }, + { + "name": "Farmácia", + "slug": "farmacia", + "icon": "💊", + "seoKeywords": [ + "farmácia", + "farmacêutico", + "medicamentos", + "drogaria", + "manipulação", + "remédios" + ], + "description": "Farmácias, drogarias e farmacêuticos especializados", + "isActive": true + }, + { + "name": "Delivery", + "slug": "delivery", + "icon": "🛵", + "seoKeywords": [ + "delivery", + "entrega", + "motoboy", + "comida", + "aplicativo", + "rápido" + ], + "description": "Serviços de delivery, entregadores e aplicativos de entrega", + "isActive": true + } +] \ No newline at end of file diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index 71f5195..ba7179f 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -186,6 +186,8 @@ public class AdminController : Controller [HttpPost] [Route("ManagePage")] + [RequestSizeLimit(5 * 1024 * 1024)] // Allow 5MB uploads + [RequestFormLimits(MultipartBodyLengthLimit = 5 * 1024 * 1024)] public async Task ManagePage(ManagePageViewModel model) { ViewBag.IsHomePage = false; @@ -231,10 +233,27 @@ public class AdminController : Controller { _logger.LogError(ex, "Error uploading profile image"); ModelState.AddModelError("ProfileImageFile", "Erro ao fazer upload da imagem. Tente novamente."); + TempData["ImageError"] = "Erro ao processar a imagem. Verifique o formato e tamanho."; + + // Preservar dados do form e repopular dropdowns + var userPlanType = Enum.TryParse(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial; + var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString()); - // Repopulate dropdowns model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); + model.MaxLinksAllowed = userPlanType.GetMaxLinksPerPage(); + model.AllowProductLinks = planLimitations.AllowProductLinks; + + // Preservar ProfileImageId existente se estava editando + if (!model.IsNewPage && !string.IsNullOrEmpty(model.Id)) + { + var existingPage = await _userPageService.GetPageByIdAsync(model.Id); + if (existingPage != null) + { + model.ProfileImageId = existingPage.ProfileImageId; + } + } + return View(model); } } @@ -328,10 +347,20 @@ public class AdminController : Controller return RedirectToAction("Dashboard"); } - // IMPORTANTE: Preservar ProfileImageId da página existente se não houver novo upload + // IMPORTANTE: Tratar remoção de imagem ou preservar existente se não houver novo upload if (model.ProfileImageFile == null || model.ProfileImageFile.Length == 0) { - model.ProfileImageId = existingPage.ProfileImageId; + if (model.ProfileImageId == "REMOVE_IMAGE") + { + // Usuário quer remover a imagem existente + model.ProfileImageId = null; + _logger.LogInformation("Profile image removed by user request"); + } + else + { + // Preservar imagem existente + model.ProfileImageId = existingPage.ProfileImageId; + } } await UpdateUserPageFromModel(existingPage, model); diff --git a/src/BCards.Web/Program.cs b/src/BCards.Web/Program.cs index e36a0fd..dd1a386 100644 --- a/src/BCards.Web/Program.cs +++ b/src/BCards.Web/Program.cs @@ -4,12 +4,10 @@ using BCards.Web.Repositories; using BCards.Web.HealthChecks; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; -using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Options; using MongoDB.Driver; using System.Globalization; -using Stripe; using Microsoft.AspNetCore.Authentication.OAuth; using SendGrid; using BCards.Web.Middleware; @@ -22,11 +20,9 @@ using Serilog.Sinks.OpenSearch; var builder = WebApplication.CreateBuilder(args); -// Configure Serilog with environment-specific settings var isDevelopment = builder.Environment.IsDevelopment(); var hostname = Environment.MachineName; -// 🔥 CORREÇÃO 1: Habilitar SelfLog ANTES de configurar o logger Serilog.Debugging.SelfLog.Enable(msg => { Console.WriteLine($"[SERILOG SELF] {DateTime.Now:HH:mm:ss} {msg}"); @@ -112,7 +108,7 @@ else .MinimumLevel.Override("Microsoft.AspNetCore.StaticFiles", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning) - // 🔥 GARANTIR CONSOLE EM PRODUÇÃO TAMBÉM + .WriteTo.Console( restrictedToMinimumLevel: LogEventLevel.Information, outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") @@ -161,7 +157,6 @@ else } } -// 🔥 REMOVER OS Task.Delay E Log.CloseAndFlush desnecessários var logger = loggerConfig.CreateLogger(); Log.Logger = logger; @@ -176,7 +171,6 @@ builder.Host.UseSerilog(); // Log startup information Log.Information("Starting BCards application on {Hostname} in {Environment} mode", hostname, builder.Environment.EnvironmentName); -// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; @@ -186,7 +180,6 @@ builder.Services.Configure(options => options.ForwardLimit = null; }); -// 🔥 OTIMIZAÇÃO: Sistema de Compressão de Response (Brotli + Gzip) builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; @@ -410,12 +403,25 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Configure upload limits for file handling (images up to 5MB) builder.Services.Configure(options => { - options.MultipartBodyLengthLimit = 10 * 1024 * 1024; + options.MultipartBodyLengthLimit = 5 * 1024 * 1024; // 5MB options.ValueLengthLimit = int.MaxValue; options.ValueCountLimit = int.MaxValue; options.KeyLengthLimit = int.MaxValue; + options.BufferBody = true; + options.BufferBodyLengthLimit = 5 * 1024 * 1024; // 5MB + options.MultipartBodyLengthLimit = 5 * 1024 * 1024; // 5MB + options.MultipartHeadersLengthLimit = 16384; +}); + +// Configure Kestrel server limits for larger requests +builder.Services.Configure(options => +{ + options.Limits.MaxRequestBodySize = 5 * 1024 * 1024; // 5MB + options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2); + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); }); builder.Services.AddScoped(); @@ -555,11 +561,11 @@ app.UseRequestLocalization(); app.UseAuthentication(); app.UseAuthorization(); -app.UseMiddleware(); -app.UseMiddleware(); -app.UseMiddleware(); -app.UseMiddleware(); -app.UseMiddleware(); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseMiddleware(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) @@ -640,7 +646,6 @@ app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); -// Initialize default data using (var scope = app.Services.CreateScope()) { var themeService = scope.ServiceProvider.GetRequiredService(); @@ -660,12 +665,10 @@ using (var scope = app.Services.CreateScope()) await categoryService.InitializeDefaultCategoriesAsync(); } - // 🔥 CORREÇÃO 12: Logs adicionais após inicialização Log.Information("Default themes and categories initialized successfully"); } catch (Exception ex) { - //var logger = scope.ServiceProvider.GetRequiredService>(); Log.Error(ex, "Error initializing default data"); } } @@ -674,7 +677,6 @@ try { Log.Information("BCards application started successfully on {Hostname}", hostname); - // 🔥 CORREÇÃO 13: Aguardar um pouco para logs serem enviados antes da aplicação rodar if (isDevelopment) { Console.WriteLine("[DEBUG] Aguardando envio de logs iniciais..."); @@ -693,10 +695,8 @@ finally { Log.Information("BCards application shutting down on {Hostname}", hostname); - // 🔥 CORREÇÃO 14: Aguardar logs finais serem enviados await Task.Delay(5000); Log.CloseAndFlush(); } -// Make Program accessible for integration tests public partial class Program { } \ No newline at end of file diff --git a/src/BCards.Web/Services/GridFSImageStorage.cs b/src/BCards.Web/Services/GridFSImageStorage.cs index 9dbb85b..b50297f 100644 --- a/src/BCards.Web/Services/GridFSImageStorage.cs +++ b/src/BCards.Web/Services/GridFSImageStorage.cs @@ -222,4 +222,4 @@ public class GridFSImageStorage : IImageStorageService } }); } -} \ No newline at end of file +} diff --git a/src/BCards.Web/Views/Admin/ManagePage.cshtml b/src/BCards.Web/Views/Admin/ManagePage.cshtml index d04e9ee..e952437 100644 --- a/src/BCards.Web/Views/Admin/ManagePage.cshtml +++ b/src/BCards.Web/Views/Admin/ManagePage.cshtml @@ -119,9 +119,12 @@
- Formatos aceitos: JPG, PNG, GIF. Máximo: 2MB e 4000x4000px. Será redimensionada para 400x400px. + Formatos aceitos: JPG, PNG, GIF. Máximo: 2MB e 4000x4000px.
+
@@ -946,6 +949,9 @@ // Check for validation errors and show toast + open accordion checkValidationErrors(); + // Check for server-side image errors + checkServerImageErrors(); + // Garantir que campos não marcados sejam string vazia ao submeter $('form').on('submit', function() { ensureUncheckedFieldsAreEmpty(); @@ -1376,6 +1382,42 @@ }, 5000); } + // Nova função para mostrar toast de erro com múltiplas mensagens + function showErrorToast(errors) { + if (!Array.isArray(errors)) { + errors = [errors]; + } + + const errorList = errors.map(error => `
  • ${error}
  • `).join(''); + const toastHtml = ` + + `; + + if (!$('#toastContainer').length) { + $('body').append('
    '); + } + + const $toast = $(toastHtml); + $('#toastContainer').append($toast); + + const toast = new bootstrap.Toast($toast[0], { delay: 6000 }); // 6 segundos para erro + toast.show(); + + setTimeout(() => { + $toast.remove(); + }, 7000); + } + // Validation Error Handling function checkValidationErrors() { // Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado) @@ -1663,19 +1705,24 @@ // Preview da imagem selecionada fileInput.on('change', function(e) { const file = e.target.files[0]; - errorSpan.text(''); + const feedbackDiv = $('#imageErrorFeedback'); + + // Limpar estados de erro anteriores + clearImageError(); if (!file) return; - // Validações client-side + // Validações client-side com feedback visual melhorado if (!file.type.match(/^image\/(jpeg|jpg|png|gif)$/i)) { - errorSpan.text('Formato inválido. Use apenas JPG, PNG ou GIF.'); + setImageError('Formato inválido. Use apenas JPG, PNG ou GIF.'); + showErrorToast(['Formato de imagem inválido. Use apenas JPG, PNG ou GIF.']); fileInput.val(''); return; } if (file.size > 2 * 1024 * 1024) { // 2MB - errorSpan.text('Arquivo muito grande. Máximo 2MB.'); + setImageError('Arquivo muito grande. Máximo 2MB.'); + showErrorToast(['Arquivo muito grande. O tamanho máximo é de 2MB.']); fileInput.val(''); return; } @@ -1694,7 +1741,7 @@ removeBtn.on('click', function() { if (confirm('Tem certeza que deseja remover a imagem de perfil?')) { fileInput.val(''); - hiddenField.val(''); + hiddenField.val('REMOVE_IMAGE'); // Valor específico para indicar remoção preview.attr('src', '/images/default-avatar.svg'); removeBtn.hide(); errorSpan.text(''); @@ -1706,6 +1753,59 @@ removeBtn.show(); } } + + // Funções auxiliares para gerenciar estados de erro da imagem + function setImageError(message) { + const fileInput = $('#profileImageInput'); + const errorSpan = $('#imageError'); + const feedbackDiv = $('#imageErrorFeedback'); + + // Adicionar classes de erro + fileInput.addClass('is-invalid'); + + // Mostrar mensagens de erro + errorSpan.text(message); + feedbackDiv.text(message).show(); + + // Adicionar borda vermelha na área de preview + $('.profile-image-preview').addClass('border-danger'); + } + + function clearImageError() { + const fileInput = $('#profileImageInput'); + const errorSpan = $('#imageError'); + const feedbackDiv = $('#imageErrorFeedback'); + + // Remover classes de erro + fileInput.removeClass('is-invalid is-valid'); + + // Limpar mensagens de erro + errorSpan.text(''); + feedbackDiv.hide(); + + // Remover borda vermelha da área de preview + $('.profile-image-preview').removeClass('border-danger'); + } + + // Função para verificar erros de imagem do servidor + function checkServerImageErrors() { + @if (TempData["ImageError"] != null) + { + @:const imageError = '@Html.Raw(TempData["ImageError"])'; + @:setImageError(imageError); + @:showErrorToast([imageError]); + } + + // Verificar ModelState errors para ProfileImageFile + @if (ViewData.ModelState.ContainsKey("ProfileImageFile")) + { + @:const modelStateError = '@Html.Raw(ViewData.ModelState["ProfileImageFile"].Errors.FirstOrDefault()?.ErrorMessage)'; + @:if (modelStateError) { + @:setImageError(modelStateError); + @:showErrorToast([modelStateError]); + @:} + } + } }