fix: alterar link de produto
This commit is contained in:
parent
7884a8f13b
commit
1cc8665176
72
src/BCards.Web/Utils/ConditionalRequiredAttribute.cs
Normal file
72
src/BCards.Web/Utils/ConditionalRequiredAttribute.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using BCards.Web.Models;
|
||||
using BCards.Web.ViewModels;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
// Atributo de validação customizado para links
|
||||
public class ConditionalRequiredAttribute : ValidationAttribute
|
||||
{
|
||||
private readonly string _dependentProperty;
|
||||
private readonly object _targetValue;
|
||||
|
||||
public ConditionalRequiredAttribute(string dependentProperty, object targetValue)
|
||||
{
|
||||
_dependentProperty = dependentProperty;
|
||||
_targetValue = targetValue;
|
||||
}
|
||||
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
var dependentProperty = validationContext.ObjectType.GetProperty(_dependentProperty);
|
||||
if (dependentProperty == null)
|
||||
return ValidationResult.Success;
|
||||
|
||||
var dependentValue = dependentProperty.GetValue(validationContext.ObjectInstance);
|
||||
|
||||
// Se o valor dependente não é o target, não valida
|
||||
if (!Equals(dependentValue, _targetValue))
|
||||
return ValidationResult.Success;
|
||||
|
||||
// Se é o target value e o campo está vazio, retorna erro
|
||||
if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
|
||||
return new ValidationResult(ErrorMessage ?? $"{validationContext.DisplayName} é obrigatório.");
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
// Método de extensão para validação personalizada no Controller
|
||||
public static class ModelStateExtensions
|
||||
{
|
||||
public static void ValidateLinks(this ModelStateDictionary modelState, List<ManageLinkViewModel> links)
|
||||
{
|
||||
for (int i = 0; i < links.Count; i++)
|
||||
{
|
||||
var link = links[i];
|
||||
|
||||
// Validação condicional baseada no tipo
|
||||
if (link.Type == LinkType.Product)
|
||||
{
|
||||
// Para links de produto, ProductTitle é obrigatório
|
||||
if (string.IsNullOrWhiteSpace(link.ProductTitle))
|
||||
{
|
||||
modelState.AddModelError($"Links[{i}].ProductTitle", "Título do produto é obrigatório");
|
||||
}
|
||||
|
||||
// Title pode ser vazio para links de produto (será preenchido automaticamente)
|
||||
modelState.Remove($"Links[{i}].Title");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Para links normais, Title é obrigatório
|
||||
if (string.IsNullOrWhiteSpace(link.Title))
|
||||
{
|
||||
modelState.AddModelError($"Links[{i}].Title", "Título é obrigatório");
|
||||
}
|
||||
|
||||
// Campos de produto podem ser vazios para links normais
|
||||
modelState.Remove($"Links[{i}].ProductTitle");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,14 +50,14 @@ public class ManageLinkViewModel
|
||||
public string Id { get; set; } = "new";
|
||||
|
||||
[Required(ErrorMessage = "Título é obrigatório")]
|
||||
[StringLength(50, ErrorMessage = "Título deve ter no máximo 50 caracteres")]
|
||||
[StringLength(200, ErrorMessage = "Título deve ter no máximo 50 caracteres")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "URL é obrigatória")]
|
||||
[Url(ErrorMessage = "URL inválida")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(100, ErrorMessage = "Descrição deve ter no máximo 100 caracteres")]
|
||||
[StringLength(3000, ErrorMessage = "Descrição deve ter no máximo 100 caracteres")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
@ -67,15 +67,15 @@ public class ManageLinkViewModel
|
||||
// Campos para Links de Produto
|
||||
public LinkType Type { get; set; } = LinkType.Normal;
|
||||
|
||||
[StringLength(100, ErrorMessage = "Título do produto deve ter no máximo 100 caracteres")]
|
||||
[StringLength(200, ErrorMessage = "Título do produto deve ter no máximo 100 caracteres")]
|
||||
public string ProductTitle { get; set; } = string.Empty;
|
||||
|
||||
public string ProductImage { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50, ErrorMessage = "Preço deve ter no máximo 50 caracteres")]
|
||||
public string ProductPrice { get; set; } = string.Empty;
|
||||
public string? ProductPrice { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(200, ErrorMessage = "Descrição do produto deve ter no máximo 200 caracteres")]
|
||||
[StringLength(3000, ErrorMessage = "Descrição do produto deve ter no máximo 200 caracteres")]
|
||||
public string ProductDescription { get; set; } = string.Empty;
|
||||
|
||||
public DateTime? ProductDataCachedAt { get; set; }
|
||||
|
||||
@ -129,11 +129,22 @@
|
||||
<div class="accordion-body">
|
||||
<p class="text-muted mb-4">Escolha um tema que combine com sua personalidade ou marca:</p>
|
||||
|
||||
<div class="row">
|
||||
@{
|
||||
var themeCount = 0;
|
||||
}
|
||||
@foreach (var theme in Model.AvailableThemes)
|
||||
{
|
||||
@if (themeCount % 4 == 0)
|
||||
{
|
||||
@if (themeCount > 0)
|
||||
{
|
||||
@:</div>
|
||||
}
|
||||
@:<div class="row">
|
||||
}
|
||||
|
||||
<div class="col-md-4 col-lg-3 mb-3">
|
||||
<div class="theme-card @(Model.SelectedTheme == theme.Name.ToLower() ? "selected" : "")" data-theme="@theme.Name.ToLower()">
|
||||
<div class="theme-card @(Model.SelectedTheme.ToLower() == theme.Name.ToLower() ? "selected" : "")" data-theme="@theme.Name.ToLower()">
|
||||
<div class="theme-preview" style="background: @theme.BackgroundColor; color: @theme.TextColor;">
|
||||
<div class="theme-header" style="background-color: @theme.PrimaryColor;">
|
||||
<div class="theme-avatar"></div>
|
||||
@ -153,8 +164,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
themeCount++;
|
||||
}
|
||||
@if (Model.AvailableThemes.Any())
|
||||
{
|
||||
@:</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<input asp-for="SelectedTheme" type="hidden">
|
||||
|
||||
@ -194,7 +210,10 @@
|
||||
"instagram"
|
||||
};
|
||||
var match = myList.FirstOrDefault(stringToCheck => Model.Links[i].Icon.Contains(stringToCheck));
|
||||
if (match==null) {
|
||||
if (match==null)
|
||||
{
|
||||
if (Model.Links[i].Type==LinkType.Normal)
|
||||
{
|
||||
<div class="link-input-group" data-link="@i">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Link @(i + 1)</h6>
|
||||
@ -228,6 +247,73 @@
|
||||
<input asp-for="Links[i].IsActive" type="hidden" value="true">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="link-input-group product-link-preview" data-link="@i">
|
||||
<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 @(i + 1)
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card border-success">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-3">
|
||||
<div class="p-3 text-center">
|
||||
@if (!string.IsNullOrEmpty(Model.Links[i].ProductImage))
|
||||
{
|
||||
<img src="@Model.Links[i].ProductImage"
|
||||
class="img-fluid rounded"
|
||||
style="max-height: 80px; max-width: 100%;"
|
||||
onerror="this.style.display='none'; this.parentNode.innerHTML='<i class=\'fas fa-image text-muted\'></i><br><small class=\'text-muted\'>Sem imagem</small>';" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-image text-muted fa-2x"></i>
|
||||
<br>
|
||||
<small class="text-muted">Sem imagem</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-success">@Model.Links[i].Title</h6>
|
||||
@if (!string.IsNullOrEmpty(Model.Links[i].ProductPrice))
|
||||
{
|
||||
<p class="card-text"><strong class="text-success">@Model.Links[i].ProductPrice</strong></p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.Links[i].ProductDescription))
|
||||
{
|
||||
<p class="card-text small text-muted">@Model.Links[i].ProductDescription</p>
|
||||
}
|
||||
<small class="text-muted d-block">
|
||||
<i class="fas fa-external-link-alt me-1"></i>
|
||||
@(Model.Links[i].Url.Length > 50 ? $"{Model.Links[i].Url.Substring(0, 50)}..." : Model.Links[i].Url)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden fields for form submission -->
|
||||
<input type="hidden" name="Links[@i].Id" value="@Model.Links[i].Id">
|
||||
<input type="hidden" name="Links[@i].Title" value="@Model.Links[i].Title">
|
||||
<input type="hidden" name="Links[@i].Url" value="@Model.Links[i].Url">
|
||||
<input type="hidden" name="Links[@i].Description" value="@Model.Links[i].Description">
|
||||
<input type="hidden" name="Links[@i].Type" value="Product">
|
||||
<input type="hidden" name="Links[@i].ProductTitle" value="@Model.Links[i].Title">
|
||||
<input type="hidden" name="Links[@i].ProductDescription" value="@Model.Links[i].ProductDescription">
|
||||
<input type="hidden" name="Links[@i].ProductPrice" value="@Model.Links[i].ProductPrice">
|
||||
<input type="hidden" name="Links[@i].ProductImage" value="@Model.Links[i].ProductImage">
|
||||
<input type="hidden" name="Links[@i].Icon" value="fas fa-shopping-bag">
|
||||
<input type="hidden" name="Links[@i].Order" value="@i">
|
||||
<input type="hidden" name="Links[@i].IsActive" value="true">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -639,7 +725,8 @@
|
||||
|
||||
// Add link functionality via modal
|
||||
$('#addLinkBtn').on('click', function() {
|
||||
if (linkCount >= @Model.MaxLinksAllowed) {
|
||||
const maxlinks = @Model.MaxLinksAllowed;
|
||||
if (linkCount >= maxlinks+4) {
|
||||
alert('Você atingiu o limite de links para seu plano atual.');
|
||||
return false;
|
||||
}
|
||||
@ -778,7 +865,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function addLinkInput(title = '', url = '', description = '', icon = '', linkType = 'Normal') {
|
||||
function addLinkInput(title = '', url = '', description = '', icon = '', linkType = 'Normal', id='new') {
|
||||
const iconHtml = icon ? `<i class="${icon} me-2"></i>` : '';
|
||||
|
||||
const linkHtml = `
|
||||
@ -810,7 +897,7 @@
|
||||
<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>
|
||||
</div>
|
||||
<input type="hidden" name="Links[${linkCount}].Id" value="">
|
||||
<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}">
|
||||
@ -921,7 +1008,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function addProductLinkInput(title, url, description, price, image) {
|
||||
function addProductLinkInput(title, url, description, price, image, id='new') {
|
||||
const linkHtml = `
|
||||
<div class="link-input-group product-link-preview" data-link="${linkCount}">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
@ -954,7 +1041,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hidden fields for form submission -->
|
||||
<input type="hidden" name="Links[${linkCount}].Id" value="">
|
||||
<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}">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user