fix: ajustes de links
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 2s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m34s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 2m0s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s

This commit is contained in:
Ricardo Carneiro 2025-09-14 18:19:23 -03:00
parent 2eada5f44c
commit 378bcf54b6
2 changed files with 262 additions and 74 deletions

View File

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Security.Claims; using System.Security.Claims;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace BCards.Web.Controllers; namespace BCards.Web.Controllers;
@ -203,7 +204,6 @@ public class AdminController : Controller
string userId = ""; string userId = "";
try try
{ {
ViewBag.IsHomePage = false; ViewBag.IsHomePage = false;
var user = await _authService.GetCurrentUserAsync(User); 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) // Limpar campos de redes sociais que são apenas espaços (tratados como vazios)
CleanSocialMediaFields(model); CleanSocialMediaFields(model);
AdjustModelState(ModelState, model);
_logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}");
ModelState.Remove<ManagePageViewModel>(x => x.InstagramUrl);
ModelState.Remove<ManagePageViewModel>(x => x.FacebookUrl);
ModelState.Remove<ManagePageViewModel>(x => x.TwitterUrl);
ModelState.Remove<ManagePageViewModel>(x => x.WhatsAppNumber);
_logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}"); _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) if (!ModelState.IsValid)
{ {
_logger.LogWarning("ModelState is invalid:"); var sbError = new StringBuilder();
sbError.AppendLine("ModelState is invalid!");
foreach (var error in ModelState) 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 // Repopulate dropdowns
var slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName); var slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName);
model.Slug = slug; model.Slug = slug;
@ -1143,4 +1144,35 @@ public class AdminController : Controller
_logger.LogInformation("KeepAlive endpoint triggered for user {User}", User.Identity?.Name ?? "Anonymous"); _logger.LogInformation("KeepAlive endpoint triggered for user {User}", User.Identity?.Name ?? "Anonymous");
return Json(new { status = "session_extended" }); return Json(new { status = "session_extended" });
} }
private void AdjustModelState(ModelStateDictionary modelState, ManagePageViewModel model)
{
modelState.Remove<ManagePageViewModel>(x => x.InstagramUrl);
modelState.Remove<ManagePageViewModel>(x => x.FacebookUrl);
modelState.Remove<ManagePageViewModel>(x => x.TwitterUrl);
modelState.Remove<ManagePageViewModel>(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);
}
}
}
}
}
} }

View File

@ -381,10 +381,10 @@
var twitter = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("twitter")).FirstOrDefault(); 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 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 instagram = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("instagram")).FirstOrDefault();
var facebookUrl = facebook !=null ? facebook.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 : ""; 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/","") : ""; var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","").Replace("whatsapp://","") : "";
var instagramUrl = instagram !=null ? instagram.Url : ""; var instagramUrl = instagram !=null ? instagram.Url.Replace("https://instagram.com/","").Replace("https://www.instagram.com/","") : "";
} }
<!-- Passo 4: Redes Sociais (Opcional) --> <!-- Passo 4: Redes Sociais (Opcional) -->
<div class="accordion-item"> <div class="accordion-item">
@ -579,29 +579,13 @@
<input type="text" class="form-control" id="linkTitle" placeholder="Ex: Meu Site, Portfólio, Instagram..." required> <input type="text" class="form-control" id="linkTitle" placeholder="Ex: Meu Site, Portfólio, Instagram..." required>
<div class="form-text">Nome que aparecerá no botão</div> <div class="form-text">Nome que aparecerá no botão</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="linkUrl" class="form-label">URL <span class="text-danger">*</span></label> <label for="linkIcon" class="form-label">Tipo de Link <span class="text-danger">*</span></label>
<div class="input-group"> <select class="form-select" id="linkIcon" required>
<span class="input-group-text bg-primary text-white fw-bold">https://</span> <option value="">Selecione o tipo de link</option>
<input type="text" class="form-control" id="linkUrlInput" placeholder="exemplo.com" required> <option value="fas fa-globe">🌐 Site Geral</option>
</div> <option value="fas fa-shopping-cart">🛒 Loja/E-commerce</option>
<input type="hidden" id="linkUrl">
<div class="form-text">Digite apenas o domínio e caminho (sem https://)</div>
</div>
<div class="mb-3">
<label for="linkDescription" class="form-label">Descrição (opcional)</label>
<input type="text" class="form-control" id="linkDescription" placeholder="Breve descrição do link">
<div class="form-text">Texto adicional que aparece abaixo do título</div>
</div>
<div class="mb-3">
<label for="linkIcon" class="form-label">Ícone (opcional)</label>
<select class="form-select" id="linkIcon">
<option value="">Sem ícone</option>
<option value="fas fa-globe">🌐 Site</option>
<option value="fas fa-shopping-cart">🛒 Loja</option>
<option value="fas fa-briefcase">💼 Portfólio</option> <option value="fas fa-briefcase">💼 Portfólio</option>
<option value="fas fa-envelope">✉️ Email</option> <option value="fas fa-envelope">✉️ Email</option>
<option value="fas fa-phone">📞 Telefone</option> <option value="fas fa-phone">📞 Telefone</option>
@ -613,6 +597,23 @@
<option value="fas fa-calendar">📅 Agenda</option> <option value="fas fa-calendar">📅 Agenda</option>
<option value="fas fa-heart">❤️ Favorito</option> <option value="fas fa-heart">❤️ Favorito</option>
</select> </select>
<div class="form-text">Escolha o tipo para obter instruções específicas</div>
</div>
<div class="mb-3">
<label for="linkUrl" class="form-label">URL/Contato <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text bg-primary text-white fw-bold" id="urlPrefix">https://</span>
<input type="text" class="form-control" id="linkUrlInput" placeholder="exemplo.com" required>
</div>
<input type="hidden" id="linkUrl">
<div class="form-text" id="urlInstructions">Selecione primeiro o tipo de link acima</div>
</div>
<div class="mb-3">
<label for="linkDescription" class="form-label">Descrição (opcional)</label>
<input type="text" class="form-control" id="linkDescription" placeholder="Breve descrição do link">
<div class="form-text">Texto adicional que aparece abaixo do título</div>
</div> </div>
</div> </div>
@ -995,11 +996,17 @@
// Add link functionality via modal // Add link functionality via modal
$('#addLinkBtn').on('click', function() { $('#addLinkBtn').on('click', function() {
const maxlinks = @Model.MaxLinksAllowed; const maxlinks = @Model.MaxLinksAllowed;
if (linkCount >= maxlinks+4) { // Links do modal são limitados pelo MaxLinksAllowed (redes sociais são separadas e opcionais)
alert('Você atingiu o limite de links para seu plano atual.'); 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; return false;
} }
}); });
// Dynamic link type functionality
$('#linkIcon').on('change', function() {
updateLinkUrlField($(this).val());
});
// Toggle between link types // Toggle between link types
$('input[name="linkType"]').on('change', function() { $('input[name="linkType"]').on('change', function() {
@ -1606,6 +1613,25 @@
} }
// Social Media Functions // 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() { function initializeSocialMedia() {
// WhatsApp // WhatsApp
setupSocialField('WhatsApp', 'WhatsAppNumber', 'https://wa.me/', true); setupSocialField('WhatsApp', 'WhatsAppNumber', 'https://wa.me/', true);
@ -1626,25 +1652,21 @@
const group = $(`#${groupName}Group`); const group = $(`#${groupName}Group`);
const userInput = isWhatsApp ? $(`#${groupName}Number`) : $(`#${groupName}User`); const userInput = isWhatsApp ? $(`#${groupName}Number`) : $(`#${groupName}User`);
const hiddenField = $(`input[name="${hiddenFieldName}"]`); const hiddenField = $(`input[name="${hiddenFieldName}"]`);
// SEMPRE garantir que hidden field tenha um valor (espaço se vazio) // SEMPRE garantir que hidden field tenha um valor (espaço se vazio)
if (!hiddenField.val() || hiddenField.val().trim() === '') { if (!hiddenField.val() || hiddenField.val().trim() === '') {
hiddenField.val(' '); // Espaço em branco para evitar null hiddenField.val(' '); // Espaço em branco para evitar null
} }
// Verificar se já tem valor e configurar estado inicial // Verificar se já tem valor e configurar estado inicial
const currentValue = hiddenField.val(); const currentValue = hiddenField.val();
if (currentValue && currentValue.trim() !== '') { if (currentValue && currentValue.trim() !== '') {
checkbox.prop('checked', true); checkbox.prop('checked', true);
group.show(); group.show();
// Extrair username/número do valor atual // Limpar qualquer prefixo conhecido do valor atual
if (currentValue.startsWith(prefix)) { const cleanValue = cleanSocialPrefix(currentValue, name);
const userPart = currentValue.replace(prefix, ''); userInput.val(cleanValue);
userInput.val(userPart);
} else {
userInput.val(currentValue);
}
} else { } else {
// Se não tem valor, garantir que o hidden field seja espaço // Se não tem valor, garantir que o hidden field seja espaço
hiddenField.val(' '); hiddenField.val(' ');
@ -1665,11 +1687,12 @@
// Atualizar campo hidden em tempo real // Atualizar campo hidden em tempo real
userInput.on('input', function() { userInput.on('input', function() {
let value = $(this).val().trim(); let value = $(this).val().trim();
// Se o usuário colou uma URL completa, extrair apenas a parte do usuário // Limpar qualquer prefixo conhecido
if (value.startsWith(prefix)) { const cleanValue = cleanSocialPrefix(value, name);
value = value.replace(prefix, ''); if (cleanValue !== value) {
$(this).val(value); $(this).val(cleanValue);
value = cleanValue;
} }
if (isWhatsApp) { 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 // URL Input Functions
function initializeUrlInputs() { function initializeUrlInputs() {
// Setup para link normal // Setup para link normal
@ -1727,23 +1873,40 @@
function setupUrlField(inputSelector, hiddenSelector) { function setupUrlField(inputSelector, hiddenSelector) {
const $input = $(inputSelector); const $input = $(inputSelector);
const $hidden = $(hiddenSelector); const $hidden = $(hiddenSelector);
// Eventos para tratar entrada do usuário // Eventos para tratar entrada do usuário
$input.on('input paste keyup', function() { $input.on('input paste keyup', function() {
let value = $(this).val().trim(); let value = $(this).val();
const currentPrefix = $('#urlPrefix').text() || 'https://';
// Remover https:// ou http:// se o usuário digitou
if (value.startsWith('https://')) { // Apenas processar/limpar a URL para tipos que não sejam de mapa
value = value.substring(8); if (currentPrefix !== 'https://maps.google.com/?q=') {
$(this).val(value); let processedValue = value.trim();
} else if (value.startsWith('http://')) { processedValue = cleanAnyUrlPrefix(processedValue);
value = value.substring(7);
$(this).val(value); // 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 // Atualizar campo hidden com URL completa
if (value) { 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 { } else {
$hidden.val(''); $hidden.val('');
} }
@ -1752,15 +1915,8 @@
// Para modal de edição - detectar se já tem URL e separar // Para modal de edição - detectar se já tem URL e separar
if ($hidden.val()) { if ($hidden.val()) {
const existingUrl = $hidden.val(); const existingUrl = $hidden.val();
if (existingUrl.startsWith('https://')) { const cleanValue = cleanAnyUrlPrefix(existingUrl);
$input.val(existingUrl.substring(8)); $input.val(cleanValue);
} 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);
}
} }
} }