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";
[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; }

View File

@ -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}">