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.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<ManagePageViewModel>(x => x.InstagramUrl);
ModelState.Remove<ManagePageViewModel>(x => x.FacebookUrl);
ModelState.Remove<ManagePageViewModel>(x => x.TwitterUrl);
ModelState.Remove<ManagePageViewModel>(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,11 +270,18 @@ 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);
@ -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<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 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/","") : "";
}
<!-- Passo 4: Redes Sociais (Opcional) -->
<div class="accordion-item">
@ -581,27 +581,11 @@
</div>
<div class="mb-3">
<label for="linkUrl" class="form-label">URL <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text bg-primary text-white fw-bold">https://</span>
<input type="text" class="form-control" id="linkUrlInput" placeholder="exemplo.com" required>
</div>
<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>
<label for="linkIcon" class="form-label">Tipo de Link <span class="text-danger">*</span></label>
<select class="form-select" id="linkIcon" required>
<option value="">Selecione o tipo de link</option>
<option value="fas fa-globe">🌐 Site Geral</option>
<option value="fas fa-shopping-cart">🛒 Loja/E-commerce</option>
<option value="fas fa-briefcase">💼 Portfólio</option>
<option value="fas fa-envelope">✉️ Email</option>
<option value="fas fa-phone">📞 Telefone</option>
@ -613,6 +597,23 @@
<option value="fas fa-calendar">📅 Agenda</option>
<option value="fas fa-heart">❤️ Favorito</option>
</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>
@ -995,12 +996,18 @@
// 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() {
const linkType = $(this).val();
@ -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);
@ -1638,13 +1664,9 @@
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(' ');
@ -1666,10 +1688,11 @@
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
@ -1730,20 +1876,37 @@
// Eventos para tratar entrada do usuário
$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://')) {
value = value.substring(8);
$(this).val(value);
} else if (value.startsWith('http://')) {
value = value.substring(7);
$(this).val(value);
// 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);
}
}