feat: links sociais opcionais
This commit is contained in:
parent
2449a617ca
commit
c6129a1c63
@ -4,6 +4,7 @@ using BCards.Web.Utils;
|
|||||||
using BCards.Web.ViewModels;
|
using BCards.Web.ViewModels;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace BCards.Web.Controllers;
|
namespace BCards.Web.Controllers;
|
||||||
@ -159,8 +160,16 @@ public class AdminController : Controller
|
|||||||
if (user == null)
|
if (user == null)
|
||||||
return RedirectToAction("Login", "Auth");
|
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}");
|
_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)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("ModelState is invalid:");
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -20,16 +20,12 @@ public class CreatePageViewModel
|
|||||||
[Required(ErrorMessage = "Tema é obrigatório")]
|
[Required(ErrorMessage = "Tema é obrigatório")]
|
||||||
public string SelectedTheme { get; set; } = "minimalist";
|
public string SelectedTheme { get; set; } = "minimalist";
|
||||||
|
|
||||||
[Phone(ErrorMessage = "Número de WhatsApp inválido")]
|
|
||||||
public string WhatsAppNumber { get; set; } = string.Empty;
|
public string WhatsAppNumber { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Url(ErrorMessage = "URL do Facebook inválida")]
|
|
||||||
public string FacebookUrl { get; set; } = string.Empty;
|
public string FacebookUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Url(ErrorMessage = "URL do X/Twitter inválida")]
|
|
||||||
public string TwitterUrl { get; set; } = string.Empty;
|
public string TwitterUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Url(ErrorMessage = "URL do Instagram inválida")]
|
|
||||||
public string InstagramUrl { get; set; } = string.Empty;
|
public string InstagramUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
public List<CreateLinkViewModel> Links { get; set; } = new();
|
public List<CreateLinkViewModel> Links { get; set; } = new();
|
||||||
|
|||||||
@ -548,18 +548,26 @@ function generateLinksData() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create hidden inputs for links
|
// Remove existing hidden link inputs
|
||||||
$('#linksContainer').append('<div id="linksData"></div>');
|
$('input[name^="Links["]').remove();
|
||||||
$('#linksData').empty();
|
|
||||||
|
|
||||||
|
// Create hidden inputs for links directly in the form
|
||||||
links.forEach((link, index) => {
|
links.forEach((link, index) => {
|
||||||
$('#linksData').append(`
|
$('#createPageForm').append(`
|
||||||
<input type="hidden" name="Links[${index}].Title" value="${link.Title}" />
|
<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}].Url" value="${link.Url}" />
|
||||||
<input type="hidden" name="Links[${index}].Description" value="${link.Description}" />
|
<input type="hidden" name="Links[${index}].Description" value="${link.Description}" />
|
||||||
<input type="hidden" name="Links[${index}].Icon" value="${link.Icon}" />
|
<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() {
|
function generatePreview() {
|
||||||
|
|||||||
@ -354,53 +354,96 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div id="collapseSocial" class="accordion-collapse collapse" aria-labelledby="headingSocial" data-bs-parent="#pageWizard">
|
<div id="collapseSocial" class="accordion-collapse collapse" aria-labelledby="headingSocial" data-bs-parent="#pageWizard">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<p class="text-muted mb-4">Conecte suas redes sociais (todos os campos são opcionais):</p>
|
<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="row">
|
||||||
<div class="col-md-6">
|
<div class="col-lg-6">
|
||||||
<div class="mb-3">
|
<!-- WhatsApp -->
|
||||||
<label asp-for="WhatsAppNumber" class="form-label">
|
<div class="mb-4">
|
||||||
<i class="fab fa-whatsapp text-success"></i>
|
<div class="form-check mb-3">
|
||||||
WhatsApp
|
<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>
|
</label>
|
||||||
<input asp-for="WhatsAppNumber" class="form-control" placeholder="+55 11 99999-9999" value="@whatsappUrl">
|
</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>
|
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
<!-- Facebook -->
|
||||||
<div class="mb-3">
|
<div class="mb-4">
|
||||||
<label asp-for="FacebookUrl" class="form-label">
|
<div class="form-check mb-3">
|
||||||
<i class="fab fa-facebook text-primary"></i>
|
<input class="form-check-input" type="checkbox" id="enableFacebook">
|
||||||
Facebook
|
<label class="form-check-label" for="enableFacebook">
|
||||||
|
<i class="fab fa-facebook text-primary me-2"></i>
|
||||||
|
<strong>Conectar Facebook</strong>
|
||||||
</label>
|
</label>
|
||||||
<input asp-for="FacebookUrl" class="form-control" placeholder="https://facebook.com/seu-perfil" value="@facebookUrl">
|
</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>
|
<span asp-validation-for="FacebookUrl" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="col-lg-6">
|
||||||
<div class="col-md-6">
|
<!-- Instagram -->
|
||||||
<div class="mb-3">
|
<div class="mb-4">
|
||||||
<label asp-for="TwitterUrl" class="form-label">
|
<div class="form-check mb-3">
|
||||||
<i class="fab fa-x-twitter"></i>
|
<input class="form-check-input" type="checkbox" id="enableInstagram">
|
||||||
X / Twitter
|
<label class="form-check-label" for="enableInstagram">
|
||||||
|
<i class="fab fa-instagram text-danger me-2"></i>
|
||||||
|
<strong>Conectar Instagram</strong>
|
||||||
</label>
|
</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>
|
</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>
|
</div>
|
||||||
|
<input asp-for="InstagramUrl" type="hidden" value="@(instagramUrl ?? "")">
|
||||||
<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">
|
|
||||||
<span asp-validation-for="InstagramUrl" class="text-danger"></span>
|
<span asp-validation-for="InstagramUrl" class="text-danger"></span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -408,10 +451,6 @@
|
|||||||
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(3)">
|
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(3)">
|
||||||
<i class="fas fa-arrow-left me-1"></i> Anterior
|
<i class="fas fa-arrow-left me-1"></i> Anterior
|
||||||
</button>
|
</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">
|
<button type="submit" class="btn btn-success">
|
||||||
<i class="fas fa-@(Model.IsNewPage ? "rocket" : "save") me-2"></i>
|
<i class="fas fa-@(Model.IsNewPage ? "rocket" : "save") me-2"></i>
|
||||||
@(Model.IsNewPage ? "Criar Página" : "Salvar Alterações")
|
@(Model.IsNewPage ? "Criar Página" : "Salvar Alterações")
|
||||||
@ -421,7 +460,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -674,6 +712,60 @@
|
|||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
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 Styles */
|
||||||
.product-link-preview {
|
.product-link-preview {
|
||||||
background: rgba(25, 135, 84, 0.05);
|
background: rgba(25, 135, 84, 0.05);
|
||||||
@ -709,6 +801,27 @@
|
|||||||
let currentStep = 1;
|
let currentStep = 1;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(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
|
// Generate slug when name or category changes
|
||||||
$('#DisplayName, #Category').on('input change', function() {
|
$('#DisplayName, #Category').on('input change', function() {
|
||||||
generateSlug();
|
generateSlug();
|
||||||
@ -867,13 +980,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addLinkInput(title = '', url = '', description = '', icon = '', linkType = 'Normal', id='new') {
|
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 iconHtml = icon ? `<i class="${icon} me-2"></i>` : '';
|
||||||
|
const displayCount = $('.link-input-group').length + 1;
|
||||||
|
|
||||||
const linkHtml = `
|
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">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
${iconHtml}Link ${linkCount + 1}: ${title || 'Novo Link'}
|
${iconHtml}Link ${displayCount}: ${title || 'Novo Link'}
|
||||||
</h6>
|
</h6>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@ -882,27 +1009,26 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<input type="hidden" name="Links[${linkCount}].Id" value="new">
|
|
||||||
<label class="form-label">Título</label>
|
<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>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">URL</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Descrição (opcional)</label>
|
<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>
|
</div>
|
||||||
<input type="hidden" name="Links[${linkCount}].Id" value="${id}">
|
<input type="hidden" name="Links[${nextIndex}].Id" value="${id}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Type" value="${linkType}">
|
<input type="hidden" name="Links[${nextIndex}].Type" value="${linkType}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Icon" value="${icon}">
|
<input type="hidden" name="Links[${nextIndex}].Icon" value="${icon}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Order" value="${linkCount}">
|
<input type="hidden" name="Links[${nextIndex}].Order" value="${nextIndex}">
|
||||||
<input type="hidden" name="Links[${linkCount}].IsActive" value="true">
|
<input type="hidden" name="Links[${nextIndex}].IsActive" value="true">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -1010,11 +1136,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addProductLinkInput(title, url, description, price, image, id='new') {
|
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 = `
|
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">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h6 class="mb-0">
|
<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>
|
</h6>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@ -1042,18 +1182,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden fields for form submission -->
|
<!-- Hidden fields for form submission -->
|
||||||
<input type="hidden" name="Links[${linkCount}].Id" value="${id}">
|
<input type="hidden" name="Links[${nextIndex}].Id" value="${id}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Title" value="${title}">
|
<input type="hidden" name="Links[${nextIndex}].Title" value="${title}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Url" value="${url}">
|
<input type="hidden" name="Links[${nextIndex}].Url" value="${url}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Description" value="${description}">
|
<input type="hidden" name="Links[${nextIndex}].Description" value="${description}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Type" value="Product">
|
<input type="hidden" name="Links[${nextIndex}].Type" value="Product">
|
||||||
<input type="hidden" name="Links[${linkCount}].ProductTitle" value="${title}">
|
<input type="hidden" name="Links[${nextIndex}].ProductTitle" value="${title}">
|
||||||
<input type="hidden" name="Links[${linkCount}].ProductDescription" value="${description}">
|
<input type="hidden" name="Links[${nextIndex}].ProductDescription" value="${description}">
|
||||||
<input type="hidden" name="Links[${linkCount}].ProductPrice" value="${price}">
|
<input type="hidden" name="Links[${nextIndex}].ProductPrice" value="${price}">
|
||||||
<input type="hidden" name="Links[${linkCount}].ProductImage" value="${image}">
|
<input type="hidden" name="Links[${nextIndex}].ProductImage" value="${image}">
|
||||||
<input type="hidden" name="Links[${linkCount}].Icon" value="fas fa-shopping-bag">
|
<input type="hidden" name="Links[${nextIndex}].Icon" value="fas fa-shopping-bag">
|
||||||
<input type="hidden" name="Links[${linkCount}].Order" value="${linkCount}">
|
<input type="hidden" name="Links[${nextIndex}].Order" value="${nextIndex}">
|
||||||
<input type="hidden" name="Links[${linkCount}].IsActive" value="true">
|
<input type="hidden" name="Links[${nextIndex}].IsActive" value="true">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -1104,6 +1244,225 @@
|
|||||||
$toast.remove();
|
$toast.remove();
|
||||||
}, 5000);
|
}, 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>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user