feat/live-preview #1

Merged
ricardo merged 16 commits from feat/live-preview into Release/V0.0.3 2025-07-23 02:24:34 +00:00
3 changed files with 220 additions and 61 deletions
Showing only changes of commit 1cc8665176 - Show all commits

View 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");
}
}
}
}

View File

@ -50,14 +50,14 @@ public class ManageLinkViewModel
public string Id { get; set; } = "new"; public string Id { get; set; } = "new";
[Required(ErrorMessage = "Título é obrigatório")] [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; public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "URL é obrigatória")] [Required(ErrorMessage = "URL é obrigatória")]
[Url(ErrorMessage = "URL inválida")] [Url(ErrorMessage = "URL inválida")]
public string Url { get; set; } = string.Empty; 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 Description { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty; public string Icon { get; set; } = string.Empty;
@ -67,15 +67,15 @@ public class ManageLinkViewModel
// Campos para Links de Produto // Campos para Links de Produto
public LinkType Type { get; set; } = LinkType.Normal; 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 ProductTitle { get; set; } = string.Empty;
public string ProductImage { get; set; } = string.Empty; public string ProductImage { get; set; } = string.Empty;
[StringLength(50, ErrorMessage = "Preço deve ter no máximo 50 caracteres")] [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 string ProductDescription { get; set; } = string.Empty;
public DateTime? ProductDataCachedAt { get; set; } public DateTime? ProductDataCachedAt { get; set; }

View File

@ -129,32 +129,48 @@
<div class="accordion-body"> <div class="accordion-body">
<p class="text-muted mb-4">Escolha um tema que combine com sua personalidade ou marca:</p> <p class="text-muted mb-4">Escolha um tema que combine com sua personalidade ou marca:</p>
<div class="row"> @{
@foreach (var theme in Model.AvailableThemes) var themeCount = 0;
}
@foreach (var theme in Model.AvailableThemes)
{
@if (themeCount % 4 == 0)
{ {
<div class="col-md-4 col-lg-3 mb-3"> @if (themeCount > 0)
<div class="theme-card @(Model.SelectedTheme == theme.Name.ToLower() ? "selected" : "")" data-theme="@theme.Name.ToLower()"> {
<div class="theme-preview" style="background: @theme.BackgroundColor; color: @theme.TextColor;"> @:</div>
<div class="theme-header" style="background-color: @theme.PrimaryColor;"> }
<div class="theme-avatar"></div> @:<div class="row">
<h6>@theme.Name</h6> }
</div>
<div class="theme-links"> <div class="col-md-4 col-lg-3 mb-3">
<div class="theme-link" style="background-color: @theme.PrimaryColor;"></div> <div class="theme-card @(Model.SelectedTheme.ToLower() == theme.Name.ToLower() ? "selected" : "")" data-theme="@theme.Name.ToLower()">
<div class="theme-link" style="background-color: @theme.SecondaryColor;"></div> <div class="theme-preview" style="background: @theme.BackgroundColor; color: @theme.TextColor;">
</div> <div class="theme-header" style="background-color: @theme.PrimaryColor;">
<div class="theme-avatar"></div>
<h6>@theme.Name</h6>
</div> </div>
<div class="theme-name"> <div class="theme-links">
@theme.Name <div class="theme-link" style="background-color: @theme.PrimaryColor;"></div>
@if (theme.IsPremium) <div class="theme-link" style="background-color: @theme.SecondaryColor;"></div>
{
<span class="badge bg-warning">Premium</span>
}
</div> </div>
</div> </div>
<div class="theme-name">
@theme.Name
@if (theme.IsPremium)
{
<span class="badge bg-warning">Premium</span>
}
</div>
</div> </div>
} </div>
</div>
themeCount++;
}
@if (Model.AvailableThemes.Any())
{
@:</div>
}
<input asp-for="SelectedTheme" type="hidden"> <input asp-for="SelectedTheme" type="hidden">
@ -194,39 +210,109 @@
"instagram" "instagram"
}; };
var match = myList.FirstOrDefault(stringToCheck => Model.Links[i].Icon.Contains(stringToCheck)); var match = myList.FirstOrDefault(stringToCheck => Model.Links[i].Icon.Contains(stringToCheck));
if (match==null) { if (match==null)
<div class="link-input-group" data-link="@i"> {
<div class="d-flex justify-content-between align-items-center mb-2"> if (Model.Links[i].Type==LinkType.Normal)
<h6 class="mb-0">Link @(i + 1)</h6> {
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn"> <div class="link-input-group" data-link="@i">
<i class="fas fa-trash"></i> <div class="d-flex justify-content-between align-items-center mb-2">
</button> <h6 class="mb-0">Link @(i + 1)</h6>
</div> <button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
<div class="row"> <i class="fas fa-trash"></i>
<div class="col-md-6"> </button>
<div class="mb-2">
<label class="form-label">Título</label>
<input asp-for="Links[i].Title" class="form-control link-title" placeholder="Ex: Meu Site">
<span asp-validation-for="Links[i].Title" class="text-danger"></span>
</div> </div>
</div> <div class="row">
<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">Título</label>
<input asp-for="Links[i].Url" class="form-control link-url" placeholder="https://exemplo.com"> <input asp-for="Links[i].Title" class="form-control link-title" placeholder="Ex: Meu Site">
<span asp-validation-for="Links[i].Url" class="text-danger"></span> <span asp-validation-for="Links[i].Title" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-2">
<label class="form-label">URL</label>
<input asp-for="Links[i].Url" class="form-control link-url" placeholder="https://exemplo.com">
<span asp-validation-for="Links[i].Url" class="text-danger"></span>
</div>
</div>
</div> </div>
<div class="mb-2">
<label class="form-label">Descrição (opcional)</label>
<input asp-for="Links[i].Description" class="form-control link-description" placeholder="Breve descrição do link">
</div>
<input asp-for="Links[i].Id" type="hidden">
<input asp-for="Links[i].Icon" type="hidden">
<input asp-for="Links[i].Order" type="hidden">
<input asp-for="Links[i].IsActive" type="hidden" value="true">
</div> </div>
</div> }
<div class="mb-2"> else
<label class="form-label">Descrição (opcional)</label> {
<input asp-for="Links[i].Description" class="form-control link-description" placeholder="Breve descrição do link"> <div class="link-input-group product-link-preview" data-link="@i">
</div> <div class="d-flex justify-content-between align-items-center mb-2">
<input asp-for="Links[i].Id" type="hidden"> <h6 class="mb-0">
<input asp-for="Links[i].Icon" type="hidden"> <i class="fas fa-shopping-bag me-2 text-success"></i>Link de Produto @(i + 1)
<input asp-for="Links[i].Order" type="hidden"> </h6>
<input asp-for="Links[i].IsActive" type="hidden" value="true"> <button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
</div> <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> </div>
@ -639,7 +725,8 @@
// Add link functionality via modal // Add link functionality via modal
$('#addLinkBtn').on('click', function() { $('#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.'); alert('Você atingiu o limite de links para seu plano atual.');
return false; 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 iconHtml = icon ? `<i class="${icon} me-2"></i>` : '';
const linkHtml = ` const linkHtml = `
@ -810,7 +897,7 @@
<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[${linkCount}].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=""> <input type="hidden" name="Links[${linkCount}].Id" value="${id}">
<input type="hidden" name="Links[${linkCount}].Type" value="${linkType}"> <input type="hidden" name="Links[${linkCount}].Type" value="${linkType}">
<input type="hidden" name="Links[${linkCount}].Icon" value="${icon}"> <input type="hidden" name="Links[${linkCount}].Icon" value="${icon}">
<input type="hidden" name="Links[${linkCount}].Order" value="${linkCount}"> <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 = ` const linkHtml = `
<div class="link-input-group product-link-preview" data-link="${linkCount}"> <div class="link-input-group product-link-preview" data-link="${linkCount}">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
@ -954,7 +1041,7 @@
</div> </div>
<!-- Hidden fields for form submission --> <!-- 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}].Title" value="${title}">
<input type="hidden" name="Links[${linkCount}].Url" value="${url}"> <input type="hidden" name="Links[${linkCount}].Url" value="${url}">
<input type="hidden" name="Links[${linkCount}].Description" value="${description}"> <input type="hidden" name="Links[${linkCount}].Description" value="${description}">