From 2b3d9f308d34297bbd4c769a7845e50314aeea58 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Thu, 26 Jun 2025 00:25:04 -0300 Subject: [PATCH] fix: paginas de links funcionando --- src/BCards.Web/Controllers/AdminController.cs | 272 ++--- .../ViewModels/ManagePageViewModel.cs | 158 +-- src/BCards.Web/Views/Admin/ManagePage.cshtml | 951 +++++++----------- 3 files changed, 467 insertions(+), 914 deletions(-) diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index e64c444..5c82e6c 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -138,29 +138,6 @@ public class AdminController : Controller _logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}"); - // Filtrar links vazios ANTES da validação - if (model.Links != null) - { - model.Links = model.Links.Where(l => !l.IsEmpty).ToList(); - - // Reordenar os links restantes - for (int i = 0; i < model.Links.Count; i++) - { - model.Links[i].Order = i; - } - } - - foreach (var link in model.Links) - { - if (string.IsNullOrEmpty(link.Id)) - { - link.Id = Guid.NewGuid().ToString(); - } - } - - // Limpar ModelState de campos que foram removidos - RemoveEmptyLinkValidations(); - if (!ModelState.IsValid) { _logger.LogWarning("ModelState is invalid:"); @@ -168,7 +145,7 @@ public class AdminController : Controller { _logger.LogWarning($"Key: {error.Key}, Errors: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}"); } - + // Repopulate dropdowns model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); @@ -192,15 +169,25 @@ public class AdminController : Controller return View(model); } + // Check if user can create the requested number of links + var activeLinksCount = model.Links?.Count ?? 0; + if (activeLinksCount > model.MaxLinksAllowed) + { + ModelState.AddModelError("", $"Você excedeu o limite de {model.MaxLinksAllowed} links do seu plano atual."); + model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); + model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); + return View(model); + } + try { // Create new page var userPage = await MapToUserPage(model, user.Id); _logger.LogInformation($"Mapped to UserPage: {userPage.DisplayName}, Category: {userPage.Category}, Slug: {userPage.Slug}"); - + await _userPageService.CreatePageAsync(userPage); _logger.LogInformation("Page created successfully!"); - + TempData["Success"] = "Página criada com sucesso!"; } catch (Exception ex) @@ -227,27 +214,6 @@ public class AdminController : Controller return RedirectToAction("Dashboard"); } - private void RemoveEmptyLinkValidations() - { - var keysToRemove = ModelState.Keys - .Where(key => key.StartsWith("Links[") && ( - key.Contains(".Title") || - key.Contains(".Url") || - key.Contains(".Description") || - key.Contains(".ProductTitle") || - key.Contains(".ProductDescription") || - key.Contains(".ProductPrice") || - key.Contains(".ProductImage") - )) - .ToList(); - - foreach (var key in keysToRemove) - { - ModelState.Remove(key); - } - } - - [HttpPost] [Route("CreatePage")] public async Task CreatePage(CreatePageViewModel model) @@ -509,39 +475,6 @@ public class AdminController : Controller private ManagePageViewModel MapToManageViewModel(UserPage page, List categories, List themes, PlanType userPlanType) { - // Separar links de redes sociais dos links normais - var socialMediaLinks = page.Links?.Where(l => - l.Icon.Contains("whatsapp") || - l.Icon.Contains("facebook") || - l.Icon.Contains("twitter") || - l.Icon.Contains("instagram") - ).ToList() ?? new List(); - - var regularLinks = page.Links?.Where(l => - !l.Icon.Contains("whatsapp") && - !l.Icon.Contains("facebook") && - !l.Icon.Contains("twitter") && - !l.Icon.Contains("instagram") - ).ToList() ?? new List(); - - // Extrair URLs das redes sociais - var whatsappLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("whatsapp")); - var facebookLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("facebook")); - var twitterLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("twitter")); - var instagramLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("instagram")); - - // Extrair número do WhatsApp da URL - string whatsappNumber = ""; - if (whatsappLink != null && whatsappLink.Url.Contains("wa.me/")) - { - var numberPart = whatsappLink.Url.Split("wa.me/").LastOrDefault(); - if (!string.IsNullOrEmpty(numberPart)) - { - // Formatar número para exibição (adicionar espaços, parênteses, etc.) - whatsappNumber = FormatWhatsAppNumber(numberPart); - } - } - return new ManagePageViewModel { Id = page.Id, @@ -552,17 +485,9 @@ public class AdminController : Controller Bio = page.Bio, Slug = page.Slug, SelectedTheme = page.Theme?.Name ?? "minimalist", - - // Redes sociais - WhatsAppNumber = whatsappNumber, - FacebookUrl = facebookLink?.Url ?? "", - TwitterUrl = twitterLink?.Url ?? "", - InstagramUrl = instagramLink?.Url ?? "", - - // Links regulares - Links = regularLinks.Select((l, index) => new ManageLinkViewModel + Links = page.Links?.Select((l, index) => new ManageLinkViewModel { - Id = page.Id + "_" + index, // ID único para identificação + Id = $"link_{index}", Title = l.Title, Url = l.Url, Description = l.Description, @@ -575,8 +500,7 @@ public class AdminController : Controller ProductPrice = l.ProductPrice, ProductDescription = l.ProductDescription, ProductDataCachedAt = l.ProductDataCachedAt - }).ToList(), - + }).ToList() ?? new List(), AvailableCategories = categories, AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(), MaxLinksAllowed = userPlanType.GetMaxLinksPerPage() @@ -586,7 +510,7 @@ public class AdminController : Controller private async Task MapToUserPage(ManagePageViewModel model, string userId) { var theme = await _themeService.GetThemeByNameAsync(model.SelectedTheme) ?? _themeService.GetDefaultTheme(); - + var userPage = new UserPage { UserId = userId, @@ -600,47 +524,44 @@ public class AdminController : Controller Links = new List() }; - // Add regular links (apenas links não vazios) + // Add regular links if (model.Links?.Any() == true) { - var validLinks = model.Links.Where(l => !l.IsEmpty).ToList(); - - userPage.Links.AddRange(validLinks.Select((l, index) => new LinkItem - { - Title = l.Title, - Url = l.Url, - Description = l.Description ?? string.Empty, - Icon = l.Icon ?? string.Empty, - IsActive = l.IsActive, - Order = index, - Type = l.Type, - ProductTitle = l.ProductTitle ?? string.Empty, - ProductImage = l.ProductImage ?? string.Empty, - ProductPrice = l.ProductPrice ?? string.Empty, - ProductDescription = l.ProductDescription ?? string.Empty, - ProductDataCachedAt = l.ProductDataCachedAt - })); + userPage.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url)) + .Select((l, index) => new LinkItem + { + Title = l.Title, + Url = l.Url.ToLower(), + Description = l.Description, + Icon = l.Icon, + IsActive = l.IsActive, + Order = index, + Type = l.Type, + ProductTitle = l.ProductTitle, + ProductImage = l.ProductImage, + ProductPrice = l.ProductPrice, + ProductDescription = l.ProductDescription, + ProductDataCachedAt = l.ProductDataCachedAt + })); } - // Add social media links apenas se preenchidos + // Add social media links var socialLinks = new List(); var currentOrder = userPage.Links.Count; - if (!string.IsNullOrWhiteSpace(model.WhatsAppNumber)) + if (!string.IsNullOrEmpty(model.WhatsAppNumber)) { - var cleanNumber = model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", ""); socialLinks.Add(new LinkItem { Title = "WhatsApp", - Url = $"https://wa.me/{cleanNumber}", + Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}", Icon = "fab fa-whatsapp", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal + Order = currentOrder++ }); } - if (!string.IsNullOrWhiteSpace(model.FacebookUrl)) + if (!string.IsNullOrEmpty(model.FacebookUrl)) { socialLinks.Add(new LinkItem { @@ -648,12 +569,11 @@ public class AdminController : Controller Url = model.FacebookUrl, Icon = "fab fa-facebook", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal + Order = currentOrder++ }); } - if (!string.IsNullOrWhiteSpace(model.TwitterUrl)) + if (!string.IsNullOrEmpty(model.TwitterUrl)) { socialLinks.Add(new LinkItem { @@ -661,12 +581,11 @@ public class AdminController : Controller Url = model.TwitterUrl, Icon = "fab fa-x-twitter", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal + Order = currentOrder++ }); } - if (!string.IsNullOrWhiteSpace(model.InstagramUrl)) + if (!string.IsNullOrEmpty(model.InstagramUrl)) { socialLinks.Add(new LinkItem { @@ -674,8 +593,7 @@ public class AdminController : Controller Url = model.InstagramUrl, Icon = "fab fa-instagram", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal + Order = currentOrder++ }); } @@ -692,52 +610,47 @@ public class AdminController : Controller page.Slug = model.Slug; page.UpdatedAt = DateTime.UtcNow; - // Limpar todos os links existentes - page.Links.Clear(); - - // Adicionar apenas links válidos (não vazios) + // Update links + page.Links = new List(); + + // Add regular links if (model.Links?.Any() == true) { - var validLinks = model.Links.Where(l => !l.IsEmpty).ToList(); - - page.Links.AddRange(validLinks.Select((l, index) => new LinkItem - { - Title = l.Title, - Url = l.Url, - Description = l.Description ?? string.Empty, - Icon = l.Icon ?? string.Empty, - IsActive = l.IsActive, - Order = index, - Type = l.Type, - ProductTitle = l.ProductTitle ?? string.Empty, - ProductImage = l.ProductImage ?? string.Empty, - ProductPrice = l.ProductPrice ?? string.Empty, - ProductDescription = l.ProductDescription ?? string.Empty, - ProductDataCachedAt = l.ProductDataCachedAt, - CreatedAt = DateTime.UtcNow // Para novos links ou manter o existente se for uma atualização - })); + page.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url)) + .Select((l, index) => new LinkItem + { + Title = l.Title, + Url = l.Url, + Description = l.Description, + Icon = l.Icon, + IsActive = l.IsActive, + Order = index, + Type = l.Type, + ProductTitle = l.ProductTitle, + ProductImage = l.ProductImage, + ProductPrice = l.ProductPrice, + ProductDescription = l.ProductDescription, + ProductDataCachedAt = l.ProductDataCachedAt + })); } - // Adicionar links de redes sociais apenas se preenchidos + // Add social media links (same logic as create) var socialLinks = new List(); var currentOrder = page.Links.Count; - if (!string.IsNullOrWhiteSpace(model.WhatsAppNumber)) + if (!string.IsNullOrEmpty(model.WhatsAppNumber)) { - var cleanNumber = model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", ""); socialLinks.Add(new LinkItem { Title = "WhatsApp", - Url = $"https://wa.me/{cleanNumber}", + Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}", Icon = "fab fa-whatsapp", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal, - CreatedAt = DateTime.UtcNow + Order = currentOrder++ }); } - if (!string.IsNullOrWhiteSpace(model.FacebookUrl)) + if (!string.IsNullOrEmpty(model.FacebookUrl)) { socialLinks.Add(new LinkItem { @@ -745,13 +658,11 @@ public class AdminController : Controller Url = model.FacebookUrl, Icon = "fab fa-facebook", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal, - CreatedAt = DateTime.UtcNow + Order = currentOrder++ }); } - if (!string.IsNullOrWhiteSpace(model.TwitterUrl)) + if (!string.IsNullOrEmpty(model.TwitterUrl)) { socialLinks.Add(new LinkItem { @@ -759,13 +670,11 @@ public class AdminController : Controller Url = model.TwitterUrl, Icon = "fab fa-x-twitter", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal, - CreatedAt = DateTime.UtcNow + Order = currentOrder++ }); } - if (!string.IsNullOrWhiteSpace(model.InstagramUrl)) + if (!string.IsNullOrEmpty(model.InstagramUrl)) { socialLinks.Add(new LinkItem { @@ -773,41 +682,10 @@ public class AdminController : Controller Url = model.InstagramUrl, Icon = "fab fa-instagram", IsActive = true, - Order = currentOrder++, - Type = LinkType.Normal, - CreatedAt = DateTime.UtcNow + Order = currentOrder++ }); } page.Links.AddRange(socialLinks); } - - // Método helper para formatar número do WhatsApp - private string FormatWhatsAppNumber(string cleanNumber) - { - // Remove caracteres não numéricos - cleanNumber = new string(cleanNumber.Where(char.IsDigit).ToArray()); - - if (cleanNumber.Length >= 10) - { - // Formato brasileiro: +55 11 99999-9999 - if (cleanNumber.StartsWith("55") && cleanNumber.Length >= 12) - { - var countryCode = cleanNumber.Substring(0, 2); - var areaCode = cleanNumber.Substring(2, 2); - var firstPart = cleanNumber.Substring(4, cleanNumber.Length - 8); - var lastPart = cleanNumber.Substring(cleanNumber.Length - 4); - - return $"+{countryCode} {areaCode} {firstPart}-{lastPart}"; - } - else - { - // Formato genérico - return "+" + cleanNumber; - } - } - - return cleanNumber; - } -} - +} \ No newline at end of file diff --git a/src/BCards.Web/ViewModels/ManagePageViewModel.cs b/src/BCards.Web/ViewModels/ManagePageViewModel.cs index c59479f..f3e460c 100644 --- a/src/BCards.Web/ViewModels/ManagePageViewModel.cs +++ b/src/BCards.Web/ViewModels/ManagePageViewModel.cs @@ -3,7 +3,7 @@ using BCards.Web.Models; namespace BCards.Web.ViewModels; -public class ManagePageViewModel : IValidatableObject +public class ManagePageViewModel { public string Id { get; set; } = string.Empty; public bool IsNewPage { get; set; } = true; @@ -26,165 +26,61 @@ public class ManagePageViewModel : IValidatableObject [Required(ErrorMessage = "Tema é obrigatório")] public string SelectedTheme { get; set; } = "minimalist"; - // Redes sociais sem [Required] - serão validadas no método Validate public string WhatsAppNumber { get; set; } = string.Empty; + public string FacebookUrl { get; set; } = string.Empty; + public string TwitterUrl { get; set; } = string.Empty; + public string InstagramUrl { get; set; } = string.Empty; public List Links { get; set; } = new(); - + // Data for dropdowns and selections public List AvailableCategories { get; set; } = new(); public List AvailableThemes { get; set; } = new(); - + // Plan limitations public int MaxLinksAllowed { get; set; } = 3; public bool CanUseTheme(string themeName) => AvailableThemes.Any(t => t.Name.ToLower() == themeName.ToLower()); - - public IEnumerable Validate(ValidationContext validationContext) - { - var results = new List(); - - // Validar URLs de redes sociais apenas se preenchidas - if (!string.IsNullOrWhiteSpace(FacebookUrl) && !Uri.TryCreate(FacebookUrl, UriKind.Absolute, out _)) - { - results.Add(new ValidationResult("URL do Facebook inválida", new[] { nameof(FacebookUrl) })); - } - - if (!string.IsNullOrWhiteSpace(TwitterUrl) && !Uri.TryCreate(TwitterUrl, UriKind.Absolute, out _)) - { - results.Add(new ValidationResult("URL do Twitter inválida", new[] { nameof(TwitterUrl) })); - } - - if (!string.IsNullOrWhiteSpace(InstagramUrl) && !Uri.TryCreate(InstagramUrl, UriKind.Absolute, out _)) - { - results.Add(new ValidationResult("URL do Instagram inválida", new[] { nameof(InstagramUrl) })); - } - - // Validar número do WhatsApp apenas se preenchido - if (!string.IsNullOrWhiteSpace(WhatsAppNumber)) - { - var cleanNumber = WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", ""); - if (cleanNumber.Length < 10 || !cleanNumber.All(char.IsDigit)) - { - results.Add(new ValidationResult("Número do WhatsApp inválido", new[] { nameof(WhatsAppNumber) })); - } - } - - // Validar links ativos - var activeLinks = Links?.Where(l => !l.IsEmpty).ToList() ?? new List(); - if (activeLinks.Count > MaxLinksAllowed) - { - results.Add(new ValidationResult($"Você pode ter no máximo {MaxLinksAllowed} links ativos", new[] { nameof(Links) })); - } - - return results; - } } -public class ManageLinkViewModel : IValidatableObject + +public class ManageLinkViewModel { - public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Id { get; set; } = "new"; + + [Required(ErrorMessage = "Título é obrigatório")] + [StringLength(50, ErrorMessage = "Título deve ter no máximo 50 caracteres")] public string Title { get; set; } = string.Empty; + + [Required(ErrorMessage = "URL é obrigatória")] + [Url(ErrorMessage = "URL inválida")] public string Url { get; set; } = string.Empty; + + [StringLength(100, ErrorMessage = "Descrição deve ter no máximo 100 caracteres")] public string Description { get; set; } = string.Empty; + public string Icon { get; set; } = string.Empty; public int Order { get; set; } = 0; public bool IsActive { get; set; } = true; // Campos para Links de Produto public LinkType Type { get; set; } = LinkType.Normal; + + [StringLength(100, ErrorMessage = "Título do produto deve ter no máximo 100 caracteres")] public string ProductTitle { get; set; } = string.Empty; + public string ProductImage { get; set; } = string.Empty; + + [StringLength(50, ErrorMessage = "Preço deve ter no máximo 50 caracteres")] public string ProductPrice { get; set; } = string.Empty; + + [StringLength(200, ErrorMessage = "Descrição do produto deve ter no máximo 200 caracteres")] public string ProductDescription { get; set; } = string.Empty; + public DateTime? ProductDataCachedAt { get; set; } - - // Validação customizada - public IEnumerable Validate(ValidationContext validationContext) - { - var results = new List(); - - // Se tem título OU URL, então ambos são obrigatórios - bool hasTitle = !string.IsNullOrWhiteSpace(Title); - bool hasUrl = !string.IsNullOrWhiteSpace(Url); - - if (hasTitle || hasUrl) - { - if (!hasTitle) - { - results.Add(new ValidationResult( - "Título é obrigatório quando URL é preenchida", - new[] { nameof(Title) })); - } - - if (!hasUrl) - { - results.Add(new ValidationResult( - "URL é obrigatória quando título é preenchido", - new[] { nameof(Url) })); - } - else if (!Uri.TryCreate(Url, UriKind.Absolute, out _)) - { - results.Add(new ValidationResult( - "URL inválida", - new[] { nameof(Url) })); - } - - // Validações específicas para links de produto - if (Type == LinkType.Product && hasTitle && hasUrl) - { - if (string.IsNullOrWhiteSpace(ProductTitle)) - { - results.Add(new ValidationResult( - "Título do produto é obrigatório para links de produto", - new[] { nameof(ProductTitle) })); - } - } - - // Validações de tamanho apenas se os campos estão preenchidos - if (hasTitle && Title.Length > 50) - { - results.Add(new ValidationResult( - "Título deve ter no máximo 50 caracteres", - new[] { nameof(Title) })); - } - - if (!string.IsNullOrWhiteSpace(Description) && Description.Length > 100) - { - results.Add(new ValidationResult( - "Descrição deve ter no máximo 100 caracteres", - new[] { nameof(Description) })); - } - - if (!string.IsNullOrWhiteSpace(ProductTitle) && ProductTitle.Length > 100) - { - results.Add(new ValidationResult( - "Título do produto deve ter no máximo 100 caracteres", - new[] { nameof(ProductTitle) })); - } - - if (!string.IsNullOrWhiteSpace(ProductPrice) && ProductPrice.Length > 50) - { - results.Add(new ValidationResult( - "Preço deve ter no máximo 50 caracteres", - new[] { nameof(ProductPrice) })); - } - - if (!string.IsNullOrWhiteSpace(ProductDescription) && ProductDescription.Length > 200) - { - results.Add(new ValidationResult( - "Descrição do produto deve ter no máximo 200 caracteres", - new[] { nameof(ProductDescription) })); - } - } - - return results; - } - - // Propriedade helper para verificar se o link está vazio - public bool IsEmpty => string.IsNullOrWhiteSpace(Title) && string.IsNullOrWhiteSpace(Url); } + public class DashboardViewModel { public User CurrentUser { get; set; } = new(); diff --git a/src/BCards.Web/Views/Admin/ManagePage.cshtml b/src/BCards.Web/Views/Admin/ManagePage.cshtml index 9b0fa9a..f2f1140 100644 --- a/src/BCards.Web/Views/Admin/ManagePage.cshtml +++ b/src/BCards.Web/Views/Admin/ManagePage.cshtml @@ -618,624 +618,403 @@ @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } \ No newline at end of file