diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index 822aec0..8bdab6d 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using System.Security.Claims; +using System.Text; using System.Text.Json; namespace BCards.Web.Controllers; @@ -203,7 +204,6 @@ public class AdminController : Controller string userId = ""; try { - ViewBag.IsHomePage = false; var user = await _authService.GetCurrentUserAsync(User); @@ -214,13 +214,7 @@ public class AdminController : Controller // Limpar campos de redes sociais que são apenas espaços (tratados como vazios) CleanSocialMediaFields(model); - - _logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}"); - - ModelState.Remove(x => x.InstagramUrl); - ModelState.Remove(x => x.FacebookUrl); - ModelState.Remove(x => x.TwitterUrl); - ModelState.Remove(x => x.WhatsAppNumber); + AdjustModelState(ModelState, model); _logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}"); @@ -276,12 +270,19 @@ public class AdminController : Controller if (!ModelState.IsValid) { - _logger.LogWarning("ModelState is invalid:"); + var sbError = new StringBuilder(); + sbError.AppendLine("ModelState is invalid!"); foreach (var error in ModelState) { - _logger.LogWarning($"Key: {error.Key}, Errors: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}"); + var erroMsg = string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage)); + if (!string.IsNullOrEmpty(erroMsg)) + { + sbError.AppendLine($"Key: {error.Key}, Errors: {erroMsg}"); + } } + _logger.LogWarning(sbError.ToString()); + // Repopulate dropdowns var slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName); model.Slug = slug; @@ -1143,4 +1144,35 @@ public class AdminController : Controller _logger.LogInformation("KeepAlive endpoint triggered for user {User}", User.Identity?.Name ?? "Anonymous"); return Json(new { status = "session_extended" }); } + + private void AdjustModelState(ModelStateDictionary modelState, ManagePageViewModel model) + { + modelState.Remove(x => x.InstagramUrl); + modelState.Remove(x => x.FacebookUrl); + modelState.Remove(x => x.TwitterUrl); + modelState.Remove(x => x.WhatsAppNumber); + + // Remover validação de 'Description' para links do tipo 'Normal' + if (model.Links != null) + { + for (int i = 0; i < model.Links.Count; i++) + { + if (model.Links[i].Type == LinkType.Normal) + { + string key = $"Links[{i}].Description"; + if (ModelState.ContainsKey(key)) + { + ModelState.Remove(key); + ModelState.MarkFieldValid(key); + } + key = $"Links[{i}].Url"; + if (ModelState.ContainsKey(key)) + { + ModelState.Remove(key); + ModelState.MarkFieldValid(key); + } + } + } + } + } } \ 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 7e8c26d..25634de 100644 --- a/src/BCards.Web/Views/Admin/ManagePage.cshtml +++ b/src/BCards.Web/Views/Admin/ManagePage.cshtml @@ -381,10 +381,10 @@ var twitter = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("twitter")).FirstOrDefault(); var whatsapp = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("whatsapp")).FirstOrDefault(); var instagram = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("instagram")).FirstOrDefault(); - var facebookUrl = facebook !=null ? facebook.Url : ""; - var twitterUrl = twitter !=null ? twitter.Url : ""; - var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","") : ""; - var instagramUrl = instagram !=null ? instagram.Url : ""; + var facebookUrl = facebook !=null ? facebook.Url.Replace("https://facebook.com/","").Replace("https://www.facebook.com/","").Replace("https://fb.com/","") : ""; + var twitterUrl = twitter !=null ? twitter.Url.Replace("https://x.com/","").Replace("https://twitter.com/","").Replace("https://www.twitter.com/","") : ""; + var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","").Replace("whatsapp://","") : ""; + var instagramUrl = instagram !=null ? instagram.Url.Replace("https://instagram.com/","").Replace("https://www.instagram.com/","") : ""; }
@@ -579,29 +579,13 @@
Nome que aparecerá no botão
- +
- -
- https:// - -
- -
Digite apenas o domínio e caminho (sem https://)
-
- -
- - -
Texto adicional que aparece abaixo do título
-
- -
- - + + + @@ -613,6 +597,23 @@ +
Escolha o tipo para obter instruções específicas
+
+ +
+ +
+ https:// + +
+ +
Selecione primeiro o tipo de link acima
+
+ +
+ + +
Texto adicional que aparece abaixo do título
@@ -995,11 +996,17 @@ // Add link functionality via modal $('#addLinkBtn').on('click', function() { const maxlinks = @Model.MaxLinksAllowed; - if (linkCount >= maxlinks+4) { - alert('Você atingiu o limite de links para seu plano atual.'); + // Links do modal são limitados pelo MaxLinksAllowed (redes sociais são separadas e opcionais) + if (linkCount >= maxlinks) { + alert('Você atingiu o limite de ' + maxlinks + ' links para seu plano atual. As redes sociais do Passo 4 não contam neste limite.'); return false; } }); + + // Dynamic link type functionality + $('#linkIcon').on('change', function() { + updateLinkUrlField($(this).val()); + }); // Toggle between link types $('input[name="linkType"]').on('change', function() { @@ -1606,6 +1613,25 @@ } // Social Media Functions + function cleanSocialPrefix(value, socialType) { + const prefixes = { + 'Facebook': ['https://facebook.com/', 'https://www.facebook.com/', 'https://fb.com/', 'https://www.fb.com/'], + 'Instagram': ['https://instagram.com/', 'https://www.instagram.com/', 'https://instagr.am/'], + 'Twitter': ['https://x.com/', 'https://twitter.com/', 'https://www.twitter.com/', 'https://www.x.com/'], + 'WhatsApp': ['https://wa.me/', 'whatsapp://', 'https://api.whatsapp.com/', 'wa.me/'] + }; + + const typePrefixes = prefixes[socialType] || []; + + for (let prefix of typePrefixes) { + if (value.startsWith(prefix)) { + return value.replace(prefix, ''); + } + } + + return value; + } + function initializeSocialMedia() { // WhatsApp setupSocialField('WhatsApp', 'WhatsAppNumber', 'https://wa.me/', true); @@ -1626,25 +1652,21 @@ const group = $(`#${groupName}Group`); const userInput = isWhatsApp ? $(`#${groupName}Number`) : $(`#${groupName}User`); const hiddenField = $(`input[name="${hiddenFieldName}"]`); - + // SEMPRE garantir que hidden field tenha um valor (espaço se vazio) if (!hiddenField.val() || hiddenField.val().trim() === '') { hiddenField.val(' '); // Espaço em branco para evitar null } - + // Verificar se já tem valor e configurar estado inicial const currentValue = hiddenField.val(); if (currentValue && currentValue.trim() !== '') { checkbox.prop('checked', true); group.show(); - - // Extrair username/número do valor atual - if (currentValue.startsWith(prefix)) { - const userPart = currentValue.replace(prefix, ''); - userInput.val(userPart); - } else { - userInput.val(currentValue); - } + + // Limpar qualquer prefixo conhecido do valor atual + const cleanValue = cleanSocialPrefix(currentValue, name); + userInput.val(cleanValue); } else { // Se não tem valor, garantir que o hidden field seja espaço hiddenField.val(' '); @@ -1665,11 +1687,12 @@ // Atualizar campo hidden em tempo real userInput.on('input', function() { let value = $(this).val().trim(); - - // Se o usuário colou uma URL completa, extrair apenas a parte do usuário - if (value.startsWith(prefix)) { - value = value.replace(prefix, ''); - $(this).val(value); + + // Limpar qualquer prefixo conhecido + const cleanValue = cleanSocialPrefix(value, name); + if (cleanValue !== value) { + $(this).val(cleanValue); + value = cleanValue; } if (isWhatsApp) { @@ -1713,6 +1736,129 @@ } } + // Dynamic Link Type Functions + function updateLinkUrlField(iconValue) { + const $prefix = $('#urlPrefix'); + const $input = $('#linkUrlInput'); + const $instructions = $('#urlInstructions'); + + // Configurações por tipo de ícone + const linkTypes = { + 'fas fa-envelope': { + prefix: 'mailto:', + placeholder: 'seuemail@@exemplo.com', + instructions: 'Digite apenas o email (sem mailto:)', + color: 'bg-success' + }, + 'fas fa-phone': { + prefix: 'tel:', + placeholder: '5511999999999', + instructions: 'Digite o telefone com código do país e DDD (apenas números)', + color: 'bg-success' + }, + 'fab fa-youtube': { + prefix: 'https://youtube.com/', + placeholder: 'watch?v=VIDEO_ID ou @@usuario ou c/CANAL', + instructions: 'Digite o ID do vídeo, @@usuário ou c/canal', + color: 'bg-danger' + }, + 'fab fa-linkedin': { + prefix: 'https://linkedin.com/in/', + placeholder: 'seu-perfil-linkedin', + instructions: 'Digite apenas seu nome de usuário do LinkedIn', + color: 'bg-primary' + }, + 'fab fa-github': { + prefix: 'https://github.com/', + placeholder: 'usuario ou usuario/repositorio', + instructions: 'Digite seu usuário ou usuário/repositório', + color: 'bg-dark' + }, + 'fas fa-map-marker-alt': { + prefix: 'https://maps.google.com/?q=', + placeholder: 'Rua das Flores, 123 - São Paulo, SP', + instructions: 'Digite o endereço completo (acentos e espaços serão codificados automaticamente)', + color: 'bg-warning' + }, + 'fas fa-globe': { + prefix: 'https://', + placeholder: 'exemplo.com', + instructions: 'Digite apenas o domínio e caminho (sem https://)', + color: 'bg-primary' + }, + 'fas fa-shopping-cart': { + prefix: 'https://', + placeholder: 'minhaloja.com/produto', + instructions: 'Digite apenas o domínio e caminho da sua loja', + color: 'bg-success' + }, + 'fas fa-briefcase': { + prefix: 'https://', + placeholder: 'meuportifolio.com', + instructions: 'Digite apenas o domínio do seu portfólio', + color: 'bg-info' + }, + 'fas fa-download': { + prefix: 'https://', + placeholder: 'exemplo.com/arquivo.pdf', + instructions: 'Digite o link direto para download', + color: 'bg-secondary' + }, + 'fas fa-calendar': { + prefix: 'https://', + placeholder: 'calendly.com/seunome', + instructions: 'Digite o link do seu calendário (Calendly, etc.)', + color: 'bg-info' + }, + 'fas fa-heart': { + prefix: 'https://', + placeholder: 'exemplo.com', + instructions: 'Digite qualquer link especial', + color: 'bg-danger' + } + }; + + if (iconValue && linkTypes[iconValue]) { + const config = linkTypes[iconValue]; + $prefix.text(config.prefix) + .removeClass('bg-primary bg-success bg-danger bg-warning bg-info bg-secondary bg-dark') + .addClass(config.color); + $input.attr('placeholder', config.placeholder); + $instructions.text(config.instructions); + } else { + // Default + $prefix.text('https://') + .removeClass('bg-success bg-danger bg-warning bg-info bg-secondary bg-dark') + .addClass('bg-primary'); + $input.attr('placeholder', 'exemplo.com'); + $instructions.text('Selecione primeiro o tipo de link acima'); + } + + // Limpar o campo de input quando trocar tipo + $input.val(''); + $('#linkUrl').val(''); + } + + function cleanAnyUrlPrefix(value) { + const prefixes = [ + 'https://', 'http://', 'mailto:', 'tel:', 'whatsapp://', + 'https://wa.me/', 'https://youtube.com/', 'https://linkedin.com/in/', + 'https://github.com/', 'https://instagram.com/', 'https://facebook.com/', + 'https://x.com/', 'https://twitter.com/', 'https://maps.google.com/?q=', + 'https://www.youtube.com/', 'https://www.linkedin.com/in/', + 'https://www.github.com/', 'https://www.instagram.com/', + 'https://www.facebook.com/', 'https://www.twitter.com/' + ]; + + for (let prefix of prefixes) { + if (value.startsWith(prefix)) { + return value.replace(prefix, ''); + } + } + + return value; + } + // URL Input Functions function initializeUrlInputs() { // Setup para link normal @@ -1727,23 +1873,40 @@ function setupUrlField(inputSelector, hiddenSelector) { const $input = $(inputSelector); const $hidden = $(hiddenSelector); - + // Eventos para tratar entrada do usuário $input.on('input paste keyup', function() { - let value = $(this).val().trim(); - - // Remover https:// ou http:// se o usuário digitou - if (value.startsWith('https://')) { - value = value.substring(8); - $(this).val(value); - } else if (value.startsWith('http://')) { - value = value.substring(7); - $(this).val(value); + let value = $(this).val(); + const currentPrefix = $('#urlPrefix').text() || 'https://'; + + // Apenas processar/limpar a URL para tipos que não sejam de mapa + if (currentPrefix !== 'https://maps.google.com/?q=') { + let processedValue = value.trim(); + processedValue = cleanAnyUrlPrefix(processedValue); + + // Apenas atualiza o DOM se o valor foi alterado para não atrapalhar o cursor + if (processedValue !== value) { + $(this).val(processedValue); + } + value = processedValue; } - + // Atualizar campo hidden com URL completa if (value) { - $hidden.val('https://' + value); + // Tratar casos especiais + if (currentPrefix === 'https://maps.google.com/?q=') { + // Para mapas, o usuário digita normalmente. O encoding é feito apenas na URL final. + const encodedValue = encodeURIComponent(value); + $hidden.val(currentPrefix + encodedValue); + } else if (currentPrefix === 'tel:') { + // Para telefone, apenas números + const telValue = value.replace(/\D/g, ''); + $(this).val(telValue); + $hidden.val(currentPrefix + telValue); + } else { + // Para outros tipos, a URL é concatenada diretamente + $hidden.val(currentPrefix + value); + } } else { $hidden.val(''); } @@ -1752,15 +1915,8 @@ // Para modal de edição - detectar se já tem URL e separar if ($hidden.val()) { const existingUrl = $hidden.val(); - if (existingUrl.startsWith('https://')) { - $input.val(existingUrl.substring(8)); - } else if (existingUrl.startsWith('http://')) { - $input.val(existingUrl.substring(7)); - $hidden.val('https://' + existingUrl.substring(7)); // Converter para https - } else { - $input.val(existingUrl); - $hidden.val('https://' + existingUrl); - } + const cleanValue = cleanAnyUrlPrefix(existingUrl); + $input.val(cleanValue); } }