feat/live-preview #8

Merged
ricardo merged 43 commits from feat/live-preview into main 2025-08-18 00:50:03 +00:00
4 changed files with 469 additions and 81 deletions
Showing only changes of commit c6129a1c63 - Show all commits

View File

@ -4,6 +4,7 @@ using BCards.Web.Utils;
using BCards.Web.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Security.Claims;
namespace BCards.Web.Controllers;
@ -159,8 +160,16 @@ public class AdminController : Controller
if (user == null)
return RedirectToAction("Login", "Auth");
// 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);
if (!ModelState.IsValid)
{
_logger.LogWarning("ModelState is invalid:");
@ -958,4 +967,20 @@ public class AdminController : Controller
});
}
}
private void CleanSocialMediaFields(ManagePageViewModel model)
{
// Tratar espaço em branco como campo vazio para redes sociais
if (string.IsNullOrWhiteSpace(model.WhatsAppNumber) || model.WhatsAppNumber.Trim().Length <= 1)
model.WhatsAppNumber = string.Empty;
if (string.IsNullOrWhiteSpace(model.FacebookUrl) || model.FacebookUrl.Trim().Length <= 1)
model.FacebookUrl = string.Empty;
if (string.IsNullOrWhiteSpace(model.InstagramUrl) || model.InstagramUrl.Trim().Length <= 1)
model.InstagramUrl = string.Empty;
if (string.IsNullOrWhiteSpace(model.TwitterUrl) || model.TwitterUrl.Trim().Length <= 1)
model.TwitterUrl = string.Empty;
}
}

View File

@ -20,16 +20,12 @@ public class CreatePageViewModel
[Required(ErrorMessage = "Tema é obrigatório")]
public string SelectedTheme { get; set; } = "minimalist";
[Phone(ErrorMessage = "Número de WhatsApp inválido")]
public string WhatsAppNumber { get; set; } = string.Empty;
[Url(ErrorMessage = "URL do Facebook inválida")]
public string FacebookUrl { get; set; } = string.Empty;
[Url(ErrorMessage = "URL do X/Twitter inválida")]
public string TwitterUrl { get; set; } = string.Empty;
[Url(ErrorMessage = "URL do Instagram inválida")]
public string InstagramUrl { get; set; } = string.Empty;
public List<CreateLinkViewModel> Links { get; set; } = new();

View File

@ -548,18 +548,26 @@ function generateLinksData() {
}
});
// Create hidden inputs for links
$('#linksContainer').append('<div id="linksData"></div>');
$('#linksData').empty();
// Remove existing hidden link inputs
$('input[name^="Links["]').remove();
// Create hidden inputs for links directly in the form
links.forEach((link, index) => {
$('#linksData').append(`
$('#createPageForm').append(`
<input type="hidden" name="Links[${index}].Title" value="${link.Title}" />
<input type="hidden" name="Links[${index}].Url" value="${link.Url}" />
<input type="hidden" name="Links[${index}].Description" value="${link.Description}" />
<input type="hidden" name="Links[${index}].Icon" value="${link.Icon}" />
`);
});
// Debug: Log what we're sending
console.log('=== DEBUG GENERATELINKSDATA ===');
console.log('Links found:', links.length);
links.forEach((link, index) => {
console.log(`Link ${index}:`, link);
});
console.log('=== FIM DEBUG ===');
}
function generatePreview() {

View File

@ -354,53 +354,96 @@
</h2>
<div id="collapseSocial" class="accordion-collapse collapse" aria-labelledby="headingSocial" data-bs-parent="#pageWizard">
<div class="accordion-body">
<p class="text-muted mb-4">Conecte suas redes sociais (todos os campos são opcionais):</p>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="WhatsAppNumber" class="form-label">
<i class="fab fa-whatsapp text-success"></i>
WhatsApp
</label>
<input asp-for="WhatsAppNumber" class="form-control" placeholder="+55 11 99999-9999" value="@whatsappUrl">
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="FacebookUrl" class="form-label">
<i class="fab fa-facebook text-primary"></i>
Facebook
</label>
<input asp-for="FacebookUrl" class="form-control" placeholder="https://facebook.com/seu-perfil" value="@facebookUrl">
<span asp-validation-for="FacebookUrl" class="text-danger"></span>
</div>
</div>
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle me-2"></i>
<strong>Redes Sociais Opcionais</strong>
<p class="mb-0 mt-1">Marque apenas as redes sociais que você quer conectar. Todas são opcionais e você pode pular esta etapa.</p>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="TwitterUrl" class="form-label">
<i class="fab fa-x-twitter"></i>
X / Twitter
</label>
<input asp-for="TwitterUrl" class="form-control" placeholder="https://x.com/seu-perfil" value="@twitterUrl">
<span asp-validation-for="TwitterUrl" class="text-danger"></span>
<div class="col-lg-6">
<!-- WhatsApp -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableWhatsApp">
<label class="form-check-label" for="enableWhatsApp">
<i class="fab fa-whatsapp text-success me-2"></i>
<strong>Conectar WhatsApp</strong>
</label>
</div>
<div class="input-group social-input-group" id="whatsappGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-whatsapp text-success me-2"></i>
https://wa.me/
</span>
<input type="text" class="form-control" id="whatsappNumber" placeholder="5511999999999">
</div>
<small class="form-text text-muted">Exemplo: 5511999999999 (código do país + DDD + número)</small>
<input asp-for="WhatsAppNumber" type="hidden" value="@(whatsappUrl ?? "")">
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
</div>
<!-- Facebook -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableFacebook">
<label class="form-check-label" for="enableFacebook">
<i class="fab fa-facebook text-primary me-2"></i>
<strong>Conectar Facebook</strong>
</label>
</div>
<div class="input-group social-input-group" id="facebookGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-facebook text-primary me-2"></i>
https://facebook.com/
</span>
<input type="text" class="form-control" id="facebookUser" placeholder="seu-usuario">
</div>
<input asp-for="FacebookUrl" type="hidden" value="@(facebookUrl ?? "")">
<span asp-validation-for="FacebookUrl" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="InstagramUrl" class="form-label">
<i class="fab fa-instagram text-danger"></i>
Instagram
</label>
<input asp-for="InstagramUrl" class="form-control" placeholder="https://instagram.com/seu-perfil" value="@instagramUrl">
<div class="col-lg-6">
<!-- Instagram -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableInstagram">
<label class="form-check-label" for="enableInstagram">
<i class="fab fa-instagram text-danger me-2"></i>
<strong>Conectar Instagram</strong>
</label>
</div>
<div class="input-group social-input-group" id="instagramGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-instagram text-danger me-2"></i>
https://instagram.com/
</span>
<input type="text" class="form-control" id="instagramUser" placeholder="seu-usuario">
</div>
<input asp-for="InstagramUrl" type="hidden" value="@(instagramUrl ?? "")">
<span asp-validation-for="InstagramUrl" class="text-danger"></span>
</div>
<!-- X / Twitter -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableTwitter">
<label class="form-check-label" for="enableTwitter">
<i class="fab fa-x-twitter me-2"></i>
<strong>Conectar X / Twitter</strong>
</label>
</div>
<div class="input-group social-input-group" id="twitterGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-x-twitter me-2"></i>
https://x.com/
</span>
<input type="text" class="form-control" id="twitterUser" placeholder="seu-usuario">
</div>
<input asp-for="TwitterUrl" type="hidden" value="@(twitterUrl ?? "")">
<span asp-validation-for="TwitterUrl" class="text-danger"></span>
</div>
</div>
</div>
@ -408,15 +451,10 @@
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(3)">
<i class="fas fa-arrow-left me-1"></i> Anterior
</button>
<div>
<button type="button" class="btn btn-outline-success me-2" onclick="skipStep(4)">
Pular Etapa
</button>
<button type="submit" class="btn btn-success">
<i class="fas fa-@(Model.IsNewPage ? "rocket" : "save") me-2"></i>
@(Model.IsNewPage ? "Criar Página" : "Salvar Alterações")
</button>
</div>
<button type="submit" class="btn btn-success">
<i class="fas fa-@(Model.IsNewPage ? "rocket" : "save") me-2"></i>
@(Model.IsNewPage ? "Criar Página" : "Salvar Alterações")
</button>
</div>
</div>
</div>
@ -674,6 +712,60 @@
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Social Media Input Groups */
.social-input-group .input-group-text {
min-width: 180px;
justify-content: flex-start;
background-color: #f8f9fa;
border-right: none;
font-size: 0.9rem;
font-weight: 500;
}
.social-input-group .form-control {
border-left: none;
}
.social-input-group .form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.form-check-label {
cursor: pointer;
font-weight: 500;
transition: color 0.2s ease;
}
.form-check-label:hover {
color: #0056b3;
}
.form-check-input:checked + .form-check-label {
color: #198754;
}
.social-input-group {
transition: all 0.3s ease;
}
/* Estados de validação */
.form-control.is-valid {
border-color: #198754;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='m2.3 6.73.99-.99 1.99-1.99L6.98 2.99l-.99-.99L4.49 3.5 3.5 2.51 2.51 3.5z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-control.is-invalid {
border-color: #dc3545;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath d='m5.8 4.6 1.4 1.4m0 0 1.4 1.4m-1.4-1.4L5.8 8.4m1.4-1.4L8.6 5.6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
/* Product Link Preview Styles */
.product-link-preview {
background: rgba(25, 135, 84, 0.05);
@ -709,6 +801,27 @@
let currentStep = 1;
$(document).ready(function() {
// Initialize social media fields
initializeSocialMedia();
// Check for validation errors and show toast + open accordion
checkValidationErrors();
// Garantir que campos não marcados sejam string vazia ao submeter
$('form').on('submit', function() {
ensureUncheckedFieldsAreEmpty();
// Debug: Verificar quais campos de links estão sendo enviados
console.log('=== DEBUG FORM SUBMISSION ===');
const formData = new FormData(this);
for (let [key, value] of formData.entries()) {
if (key.includes('Links[')) {
console.log(`${key}: ${value}`);
}
}
console.log('=== FIM DEBUG ===');
});
// Generate slug when name or category changes
$('#DisplayName, #Category').on('input change', function() {
generateSlug();
@ -867,13 +980,27 @@
}
function addLinkInput(title = '', url = '', description = '', icon = '', linkType = 'Normal', id='new') {
// Encontrar o próximo índice disponível baseado em todos os campos Links[] existentes
const existingIndexes = [];
$('input[name^="Links["]').each(function() {
const name = $(this).attr('name');
const match = name.match(/Links\[(\d+)\]/);
if (match) {
existingIndexes.push(parseInt(match[1]));
}
});
// Encontrar o próximo índice disponível
const nextIndex = existingIndexes.length > 0 ? Math.max(...existingIndexes) + 1 : 0;
const iconHtml = icon ? `<i class="${icon} me-2"></i>` : '';
const displayCount = $('.link-input-group').length + 1;
const linkHtml = `
<div class="link-input-group" data-link="${linkCount}">
<div class="link-input-group" data-link="${nextIndex}">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">
${iconHtml}Link ${linkCount + 1}: ${title || 'Novo Link'}
${iconHtml}Link ${displayCount}: ${title || 'Novo Link'}
</h6>
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
<i class="fas fa-trash"></i>
@ -882,27 +1009,26 @@
<div class="row">
<div class="col-md-6">
<div class="mb-2">
<input type="hidden" name="Links[${linkCount}].Id" value="new">
<label class="form-label">Título</label>
<input type="text" name="Links[${linkCount}].Title" class="form-control link-title" value="${title}" placeholder="Ex: Meu Site" readonly>
<input type="text" name="Links[${nextIndex}].Title" class="form-control link-title" value="${title}" placeholder="Ex: Meu Site" readonly>
</div>
</div>
<div class="col-md-6">
<div class="mb-2">
<label class="form-label">URL</label>
<input type="url" name="Links[${linkCount}].Url" class="form-control link-url" value="${url}" placeholder="https://exemplo.com" readonly>
<input type="url" name="Links[${nextIndex}].Url" class="form-control link-url" value="${url}" placeholder="https://exemplo.com" readonly>
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label">Descrição (opcional)</label>
<input type="text" name="Links[${linkCount}].Description" class="form-control link-description" value="${description}" placeholder="Breve descrição do link" readonly>
<input type="text" name="Links[${nextIndex}].Description" class="form-control link-description" value="${description}" placeholder="Breve descrição do link" readonly>
</div>
<input type="hidden" name="Links[${linkCount}].Id" value="${id}">
<input type="hidden" name="Links[${linkCount}].Type" value="${linkType}">
<input type="hidden" name="Links[${linkCount}].Icon" value="${icon}">
<input type="hidden" name="Links[${linkCount}].Order" value="${linkCount}">
<input type="hidden" name="Links[${linkCount}].IsActive" value="true">
<input type="hidden" name="Links[${nextIndex}].Id" value="${id}">
<input type="hidden" name="Links[${nextIndex}].Type" value="${linkType}">
<input type="hidden" name="Links[${nextIndex}].Icon" value="${icon}">
<input type="hidden" name="Links[${nextIndex}].Order" value="${nextIndex}">
<input type="hidden" name="Links[${nextIndex}].IsActive" value="true">
</div>
`;
@ -1010,11 +1136,25 @@
}
function addProductLinkInput(title, url, description, price, image, id='new') {
// Encontrar o próximo índice disponível baseado em todos os campos Links[] existentes
const existingIndexes = [];
$('input[name^="Links["]').each(function() {
const name = $(this).attr('name');
const match = name.match(/Links\[(\d+)\]/);
if (match) {
existingIndexes.push(parseInt(match[1]));
}
});
// Encontrar o próximo índice disponível
const nextIndex = existingIndexes.length > 0 ? Math.max(...existingIndexes) + 1 : 0;
const displayCount = $('.link-input-group').length + 1;
const linkHtml = `
<div class="link-input-group product-link-preview" data-link="${linkCount}">
<div class="link-input-group product-link-preview" data-link="${nextIndex}">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Produto ${linkCount + 1}
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Produto ${displayCount}
</h6>
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
<i class="fas fa-trash"></i>
@ -1042,18 +1182,18 @@
</div>
<!-- Hidden fields for form submission -->
<input type="hidden" name="Links[${linkCount}].Id" value="${id}">
<input type="hidden" name="Links[${linkCount}].Title" value="${title}">
<input type="hidden" name="Links[${linkCount}].Url" value="${url}">
<input type="hidden" name="Links[${linkCount}].Description" value="${description}">
<input type="hidden" name="Links[${linkCount}].Type" value="Product">
<input type="hidden" name="Links[${linkCount}].ProductTitle" value="${title}">
<input type="hidden" name="Links[${linkCount}].ProductDescription" value="${description}">
<input type="hidden" name="Links[${linkCount}].ProductPrice" value="${price}">
<input type="hidden" name="Links[${linkCount}].ProductImage" value="${image}">
<input type="hidden" name="Links[${linkCount}].Icon" value="fas fa-shopping-bag">
<input type="hidden" name="Links[${linkCount}].Order" value="${linkCount}">
<input type="hidden" name="Links[${linkCount}].IsActive" value="true">
<input type="hidden" name="Links[${nextIndex}].Id" value="${id}">
<input type="hidden" name="Links[${nextIndex}].Title" value="${title}">
<input type="hidden" name="Links[${nextIndex}].Url" value="${url}">
<input type="hidden" name="Links[${nextIndex}].Description" value="${description}">
<input type="hidden" name="Links[${nextIndex}].Type" value="Product">
<input type="hidden" name="Links[${nextIndex}].ProductTitle" value="${title}">
<input type="hidden" name="Links[${nextIndex}].ProductDescription" value="${description}">
<input type="hidden" name="Links[${nextIndex}].ProductPrice" value="${price}">
<input type="hidden" name="Links[${nextIndex}].ProductImage" value="${image}">
<input type="hidden" name="Links[${nextIndex}].Icon" value="fas fa-shopping-bag">
<input type="hidden" name="Links[${nextIndex}].Order" value="${nextIndex}">
<input type="hidden" name="Links[${nextIndex}].IsActive" value="true">
</div>
`;
@ -1104,6 +1244,225 @@
$toast.remove();
}, 5000);
}
// Validation Error Handling
function checkValidationErrors() {
// Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado)
// Verificamos se existe algum input com validation-error class ou summary de erros
const hasValidationSummary = $('.validation-summary-errors').length > 0;
const hasFieldErrors = $('.input-validation-error').length > 0;
if (!hasValidationSummary && !hasFieldErrors) {
return; // Não há erros reais de validação, sair
}
const errorElements = $('.text-danger:not(:empty)').filter(function() {
// Filtrar apenas spans que realmente têm mensagens de erro
const text = $(this).text().trim();
return text.length > 0;
});
if (errorElements.length > 0) {
// Find which accordion steps have errors
const stepsWithErrors = [];
errorElements.each(function() {
const $error = $(this);
const $accordion = $error.closest('.accordion-item');
if ($accordion.length > 0) {
const stepNumber = getStepNumber($accordion);
if (stepNumber && !stepsWithErrors.includes(stepNumber)) {
stepsWithErrors.push(stepNumber);
}
}
});
if (stepsWithErrors.length > 0) {
// Show validation error toast
showValidationErrorToast(stepsWithErrors);
// Open first accordion with error
const firstErrorStep = Math.min(...stepsWithErrors);
openAccordionStep(firstErrorStep);
}
}
}
function getStepNumber($accordion) {
const id = $accordion.find('.accordion-collapse').attr('id');
const stepMap = {
'collapseBasic': 1,
'collapseLinks': 3,
'collapseSocial': 4
};
return stepMap[id] || null;
}
function openAccordionStep(stepNumber) {
const stepMap = {
1: '#collapseBasic',
3: '#collapseLinks',
4: '#collapseSocial'
};
const targetId = stepMap[stepNumber];
if (targetId) {
$(targetId).collapse('show');
}
}
function showValidationErrorToast(stepsWithErrors) {
const stepNames = {
1: 'Informações Básicas',
3: 'Links',
4: 'Redes Sociais'
};
const errorStepNames = stepsWithErrors.map(step => stepNames[step]).join(', ');
const toastHtml = `
<div class="toast align-items-center text-bg-warning border-0" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999;">
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Erro de Validação</strong><br>
Verifique os campos em: ${errorStepNames}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
const $toast = $(toastHtml);
$('body').append($toast);
const toast = new bootstrap.Toast($toast[0], { delay: 4000 });
toast.show();
// Remove toast after it's hidden
$toast.on('hidden.bs.toast', function() {
$(this).remove();
});
}
// Garantir que campos não selecionados sejam string vazia
function ensureUncheckedFieldsAreEmpty() {
const socialFields = [
{ checkbox: '#enableWhatsApp', hidden: 'input[name="WhatsAppNumber"]' },
{ checkbox: '#enableFacebook', hidden: 'input[name="FacebookUrl"]' },
{ checkbox: '#enableInstagram', hidden: 'input[name="InstagramUrl"]' },
{ checkbox: '#enableTwitter', hidden: 'input[name="TwitterUrl"]' }
];
socialFields.forEach(field => {
if (!$(field.checkbox).is(':checked')) {
$(field.hidden).val(' '); // Forçar espaço para campos não marcados
}
});
}
// Social Media Functions
function initializeSocialMedia() {
// WhatsApp
setupSocialField('WhatsApp', 'WhatsAppNumber', 'https://wa.me/', true);
// Facebook
setupSocialField('Facebook', 'FacebookUrl', 'https://facebook.com/', false);
// Instagram
setupSocialField('Instagram', 'InstagramUrl', 'https://instagram.com/', false);
// Twitter
setupSocialField('Twitter', 'TwitterUrl', 'https://x.com/', false);
}
function setupSocialField(name, hiddenFieldName, prefix, isWhatsApp) {
const checkbox = $(`#enable${name}`);
const groupName = name.toLowerCase();
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);
}
} else {
// Se não tem valor, garantir que o hidden field seja espaço
hiddenField.val(' ');
}
// Toggle visibility
checkbox.on('change', function() {
if ($(this).is(':checked')) {
group.slideDown(200);
userInput.focus();
} else {
group.slideUp(200);
userInput.val('');
hiddenField.val(' '); // Espaço em branco para evitar null - sobrescreve valor existente
}
});
// Atualizar campo hidden em tempo real
userInput.on('input', function() {
let value = $(this).val().trim();
if (isWhatsApp) {
// WhatsApp: apenas números
value = value.replace(/\D/g, '');
$(this).val(value);
}
// Atualizar campo hidden - SEMPRE string, nunca null
if (value) {
hiddenField.val(prefix + value);
} else {
hiddenField.val(' '); // Espaço em branco para evitar null
}
// Feedback visual
updateSocialFieldFeedback(userInput, value, isWhatsApp);
});
}
function updateSocialFieldFeedback(input, value, isWhatsApp) {
// Remover classes anteriores
input.removeClass('is-valid is-invalid');
if (!value) return;
if (isWhatsApp) {
// Validar WhatsApp (mínimo 10 dígitos)
if (value.length >= 10) {
input.addClass('is-valid');
} else {
input.addClass('is-invalid');
}
} else {
// Validar username (mínimo 3 caracteres, sem espaços)
if (value.length >= 3 && !/\s/.test(value)) {
input.addClass('is-valid');
} else {
input.addClass('is-invalid');
}
}
}
</script>
}