feat/live-preview #8
@ -138,6 +138,29 @@ public class AdminController : Controller
|
|||||||
|
|
||||||
_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}");
|
||||||
|
|
||||||
|
// Filtrar links vazios ANTES da validação
|
||||||
|
if (model.Links != null)
|
||||||
|
{
|
||||||
|
model.Links = model.Links.Where(l => !l.IsEmpty).ToList();
|
||||||
|
|
||||||
|
// Reordenar os links restantes
|
||||||
|
for (int i = 0; i < model.Links.Count; i++)
|
||||||
|
{
|
||||||
|
model.Links[i].Order = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var link in model.Links)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(link.Id))
|
||||||
|
{
|
||||||
|
link.Id = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpar ModelState de campos que foram removidos
|
||||||
|
RemoveEmptyLinkValidations();
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("ModelState is invalid:");
|
_logger.LogWarning("ModelState is invalid:");
|
||||||
@ -169,16 +192,6 @@ public class AdminController : Controller
|
|||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user can create the requested number of links
|
|
||||||
var activeLinksCount = model.Links?.Count ?? 0;
|
|
||||||
if (activeLinksCount > model.MaxLinksAllowed)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("", $"Você excedeu o limite de {model.MaxLinksAllowed} links do seu plano atual.");
|
|
||||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create new page
|
// Create new page
|
||||||
@ -214,6 +227,27 @@ public class AdminController : Controller
|
|||||||
return RedirectToAction("Dashboard");
|
return RedirectToAction("Dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RemoveEmptyLinkValidations()
|
||||||
|
{
|
||||||
|
var keysToRemove = ModelState.Keys
|
||||||
|
.Where(key => key.StartsWith("Links[") && (
|
||||||
|
key.Contains(".Title") ||
|
||||||
|
key.Contains(".Url") ||
|
||||||
|
key.Contains(".Description") ||
|
||||||
|
key.Contains(".ProductTitle") ||
|
||||||
|
key.Contains(".ProductDescription") ||
|
||||||
|
key.Contains(".ProductPrice") ||
|
||||||
|
key.Contains(".ProductImage")
|
||||||
|
))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var key in keysToRemove)
|
||||||
|
{
|
||||||
|
ModelState.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("CreatePage")]
|
[Route("CreatePage")]
|
||||||
public async Task<IActionResult> CreatePage(CreatePageViewModel model)
|
public async Task<IActionResult> CreatePage(CreatePageViewModel model)
|
||||||
@ -475,6 +509,39 @@ public class AdminController : Controller
|
|||||||
|
|
||||||
private ManagePageViewModel MapToManageViewModel(UserPage page, List<Category> categories, List<PageTheme> themes, PlanType userPlanType)
|
private ManagePageViewModel MapToManageViewModel(UserPage page, List<Category> categories, List<PageTheme> themes, PlanType userPlanType)
|
||||||
{
|
{
|
||||||
|
// Separar links de redes sociais dos links normais
|
||||||
|
var socialMediaLinks = page.Links?.Where(l =>
|
||||||
|
l.Icon.Contains("whatsapp") ||
|
||||||
|
l.Icon.Contains("facebook") ||
|
||||||
|
l.Icon.Contains("twitter") ||
|
||||||
|
l.Icon.Contains("instagram")
|
||||||
|
).ToList() ?? new List<LinkItem>();
|
||||||
|
|
||||||
|
var regularLinks = page.Links?.Where(l =>
|
||||||
|
!l.Icon.Contains("whatsapp") &&
|
||||||
|
!l.Icon.Contains("facebook") &&
|
||||||
|
!l.Icon.Contains("twitter") &&
|
||||||
|
!l.Icon.Contains("instagram")
|
||||||
|
).ToList() ?? new List<LinkItem>();
|
||||||
|
|
||||||
|
// Extrair URLs das redes sociais
|
||||||
|
var whatsappLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("whatsapp"));
|
||||||
|
var facebookLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("facebook"));
|
||||||
|
var twitterLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("twitter"));
|
||||||
|
var instagramLink = socialMediaLinks.FirstOrDefault(l => l.Icon.Contains("instagram"));
|
||||||
|
|
||||||
|
// Extrair número do WhatsApp da URL
|
||||||
|
string whatsappNumber = "";
|
||||||
|
if (whatsappLink != null && whatsappLink.Url.Contains("wa.me/"))
|
||||||
|
{
|
||||||
|
var numberPart = whatsappLink.Url.Split("wa.me/").LastOrDefault();
|
||||||
|
if (!string.IsNullOrEmpty(numberPart))
|
||||||
|
{
|
||||||
|
// Formatar número para exibição (adicionar espaços, parênteses, etc.)
|
||||||
|
whatsappNumber = FormatWhatsAppNumber(numberPart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new ManagePageViewModel
|
return new ManagePageViewModel
|
||||||
{
|
{
|
||||||
Id = page.Id,
|
Id = page.Id,
|
||||||
@ -485,9 +552,17 @@ public class AdminController : Controller
|
|||||||
Bio = page.Bio,
|
Bio = page.Bio,
|
||||||
Slug = page.Slug,
|
Slug = page.Slug,
|
||||||
SelectedTheme = page.Theme?.Name ?? "minimalist",
|
SelectedTheme = page.Theme?.Name ?? "minimalist",
|
||||||
Links = page.Links?.Select((l, index) => new ManageLinkViewModel
|
|
||||||
|
// Redes sociais
|
||||||
|
WhatsAppNumber = whatsappNumber,
|
||||||
|
FacebookUrl = facebookLink?.Url ?? "",
|
||||||
|
TwitterUrl = twitterLink?.Url ?? "",
|
||||||
|
InstagramUrl = instagramLink?.Url ?? "",
|
||||||
|
|
||||||
|
// Links regulares
|
||||||
|
Links = regularLinks.Select((l, index) => new ManageLinkViewModel
|
||||||
{
|
{
|
||||||
Id = $"link_{index}",
|
Id = page.Id + "_" + index, // ID único para identificação
|
||||||
Title = l.Title,
|
Title = l.Title,
|
||||||
Url = l.Url,
|
Url = l.Url,
|
||||||
Description = l.Description,
|
Description = l.Description,
|
||||||
@ -500,7 +575,8 @@ public class AdminController : Controller
|
|||||||
ProductPrice = l.ProductPrice,
|
ProductPrice = l.ProductPrice,
|
||||||
ProductDescription = l.ProductDescription,
|
ProductDescription = l.ProductDescription,
|
||||||
ProductDataCachedAt = l.ProductDataCachedAt
|
ProductDataCachedAt = l.ProductDataCachedAt
|
||||||
}).ToList() ?? new List<ManageLinkViewModel>(),
|
}).ToList(),
|
||||||
|
|
||||||
AvailableCategories = categories,
|
AvailableCategories = categories,
|
||||||
AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(),
|
AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(),
|
||||||
MaxLinksAllowed = userPlanType.GetMaxLinksPerPage()
|
MaxLinksAllowed = userPlanType.GetMaxLinksPerPage()
|
||||||
@ -524,44 +600,47 @@ public class AdminController : Controller
|
|||||||
Links = new List<LinkItem>()
|
Links = new List<LinkItem>()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add regular links
|
// Add regular links (apenas links não vazios)
|
||||||
if (model.Links?.Any() == true)
|
if (model.Links?.Any() == true)
|
||||||
{
|
{
|
||||||
userPage.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url))
|
var validLinks = model.Links.Where(l => !l.IsEmpty).ToList();
|
||||||
.Select((l, index) => new LinkItem
|
|
||||||
|
userPage.Links.AddRange(validLinks.Select((l, index) => new LinkItem
|
||||||
{
|
{
|
||||||
Title = l.Title,
|
Title = l.Title,
|
||||||
Url = l.Url.ToLower(),
|
Url = l.Url,
|
||||||
Description = l.Description,
|
Description = l.Description ?? string.Empty,
|
||||||
Icon = l.Icon,
|
Icon = l.Icon ?? string.Empty,
|
||||||
IsActive = l.IsActive,
|
IsActive = l.IsActive,
|
||||||
Order = index,
|
Order = index,
|
||||||
Type = l.Type,
|
Type = l.Type,
|
||||||
ProductTitle = l.ProductTitle,
|
ProductTitle = l.ProductTitle ?? string.Empty,
|
||||||
ProductImage = l.ProductImage,
|
ProductImage = l.ProductImage ?? string.Empty,
|
||||||
ProductPrice = l.ProductPrice,
|
ProductPrice = l.ProductPrice ?? string.Empty,
|
||||||
ProductDescription = l.ProductDescription,
|
ProductDescription = l.ProductDescription ?? string.Empty,
|
||||||
ProductDataCachedAt = l.ProductDataCachedAt
|
ProductDataCachedAt = l.ProductDataCachedAt
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add social media links
|
// Add social media links apenas se preenchidos
|
||||||
var socialLinks = new List<LinkItem>();
|
var socialLinks = new List<LinkItem>();
|
||||||
var currentOrder = userPage.Links.Count;
|
var currentOrder = userPage.Links.Count;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.WhatsAppNumber))
|
if (!string.IsNullOrWhiteSpace(model.WhatsAppNumber))
|
||||||
{
|
{
|
||||||
|
var cleanNumber = model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "");
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
Title = "WhatsApp",
|
Title = "WhatsApp",
|
||||||
Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}",
|
Url = $"https://wa.me/{cleanNumber}",
|
||||||
Icon = "fab fa-whatsapp",
|
Icon = "fab fa-whatsapp",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.FacebookUrl))
|
if (!string.IsNullOrWhiteSpace(model.FacebookUrl))
|
||||||
{
|
{
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
@ -569,11 +648,12 @@ public class AdminController : Controller
|
|||||||
Url = model.FacebookUrl,
|
Url = model.FacebookUrl,
|
||||||
Icon = "fab fa-facebook",
|
Icon = "fab fa-facebook",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.TwitterUrl))
|
if (!string.IsNullOrWhiteSpace(model.TwitterUrl))
|
||||||
{
|
{
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
@ -581,11 +661,12 @@ public class AdminController : Controller
|
|||||||
Url = model.TwitterUrl,
|
Url = model.TwitterUrl,
|
||||||
Icon = "fab fa-x-twitter",
|
Icon = "fab fa-x-twitter",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.InstagramUrl))
|
if (!string.IsNullOrWhiteSpace(model.InstagramUrl))
|
||||||
{
|
{
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
@ -593,7 +674,8 @@ public class AdminController : Controller
|
|||||||
Url = model.InstagramUrl,
|
Url = model.InstagramUrl,
|
||||||
Icon = "fab fa-instagram",
|
Icon = "fab fa-instagram",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,47 +692,52 @@ public class AdminController : Controller
|
|||||||
page.Slug = model.Slug;
|
page.Slug = model.Slug;
|
||||||
page.UpdatedAt = DateTime.UtcNow;
|
page.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
// Update links
|
// Limpar todos os links existentes
|
||||||
page.Links = new List<LinkItem>();
|
page.Links.Clear();
|
||||||
|
|
||||||
// Add regular links
|
// Adicionar apenas links válidos (não vazios)
|
||||||
if (model.Links?.Any() == true)
|
if (model.Links?.Any() == true)
|
||||||
{
|
{
|
||||||
page.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url))
|
var validLinks = model.Links.Where(l => !l.IsEmpty).ToList();
|
||||||
.Select((l, index) => new LinkItem
|
|
||||||
|
page.Links.AddRange(validLinks.Select((l, index) => new LinkItem
|
||||||
{
|
{
|
||||||
Title = l.Title,
|
Title = l.Title,
|
||||||
Url = l.Url,
|
Url = l.Url,
|
||||||
Description = l.Description,
|
Description = l.Description ?? string.Empty,
|
||||||
Icon = l.Icon,
|
Icon = l.Icon ?? string.Empty,
|
||||||
IsActive = l.IsActive,
|
IsActive = l.IsActive,
|
||||||
Order = index,
|
Order = index,
|
||||||
Type = l.Type,
|
Type = l.Type,
|
||||||
ProductTitle = l.ProductTitle,
|
ProductTitle = l.ProductTitle ?? string.Empty,
|
||||||
ProductImage = l.ProductImage,
|
ProductImage = l.ProductImage ?? string.Empty,
|
||||||
ProductPrice = l.ProductPrice,
|
ProductPrice = l.ProductPrice ?? string.Empty,
|
||||||
ProductDescription = l.ProductDescription,
|
ProductDescription = l.ProductDescription ?? string.Empty,
|
||||||
ProductDataCachedAt = l.ProductDataCachedAt
|
ProductDataCachedAt = l.ProductDataCachedAt,
|
||||||
|
CreatedAt = DateTime.UtcNow // Para novos links ou manter o existente se for uma atualização
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add social media links (same logic as create)
|
// Adicionar links de redes sociais apenas se preenchidos
|
||||||
var socialLinks = new List<LinkItem>();
|
var socialLinks = new List<LinkItem>();
|
||||||
var currentOrder = page.Links.Count;
|
var currentOrder = page.Links.Count;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.WhatsAppNumber))
|
if (!string.IsNullOrWhiteSpace(model.WhatsAppNumber))
|
||||||
{
|
{
|
||||||
|
var cleanNumber = model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "");
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
Title = "WhatsApp",
|
Title = "WhatsApp",
|
||||||
Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}",
|
Url = $"https://wa.me/{cleanNumber}",
|
||||||
Icon = "fab fa-whatsapp",
|
Icon = "fab fa-whatsapp",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.FacebookUrl))
|
if (!string.IsNullOrWhiteSpace(model.FacebookUrl))
|
||||||
{
|
{
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
@ -658,11 +745,13 @@ public class AdminController : Controller
|
|||||||
Url = model.FacebookUrl,
|
Url = model.FacebookUrl,
|
||||||
Icon = "fab fa-facebook",
|
Icon = "fab fa-facebook",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.TwitterUrl))
|
if (!string.IsNullOrWhiteSpace(model.TwitterUrl))
|
||||||
{
|
{
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
@ -670,11 +759,13 @@ public class AdminController : Controller
|
|||||||
Url = model.TwitterUrl,
|
Url = model.TwitterUrl,
|
||||||
Icon = "fab fa-x-twitter",
|
Icon = "fab fa-x-twitter",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.InstagramUrl))
|
if (!string.IsNullOrWhiteSpace(model.InstagramUrl))
|
||||||
{
|
{
|
||||||
socialLinks.Add(new LinkItem
|
socialLinks.Add(new LinkItem
|
||||||
{
|
{
|
||||||
@ -682,10 +773,41 @@ public class AdminController : Controller
|
|||||||
Url = model.InstagramUrl,
|
Url = model.InstagramUrl,
|
||||||
Icon = "fab fa-instagram",
|
Icon = "fab fa-instagram",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Order = currentOrder++
|
Order = currentOrder++,
|
||||||
|
Type = LinkType.Normal,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
page.Links.AddRange(socialLinks);
|
page.Links.AddRange(socialLinks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Método helper para formatar número do WhatsApp
|
||||||
|
private string FormatWhatsAppNumber(string cleanNumber)
|
||||||
|
{
|
||||||
|
// Remove caracteres não numéricos
|
||||||
|
cleanNumber = new string(cleanNumber.Where(char.IsDigit).ToArray());
|
||||||
|
|
||||||
|
if (cleanNumber.Length >= 10)
|
||||||
|
{
|
||||||
|
// Formato brasileiro: +55 11 99999-9999
|
||||||
|
if (cleanNumber.StartsWith("55") && cleanNumber.Length >= 12)
|
||||||
|
{
|
||||||
|
var countryCode = cleanNumber.Substring(0, 2);
|
||||||
|
var areaCode = cleanNumber.Substring(2, 2);
|
||||||
|
var firstPart = cleanNumber.Substring(4, cleanNumber.Length - 8);
|
||||||
|
var lastPart = cleanNumber.Substring(cleanNumber.Length - 4);
|
||||||
|
|
||||||
|
return $"+{countryCode} {areaCode} {firstPart}-{lastPart}";
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Formato genérico
|
||||||
|
return "+" + cleanNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ using BCards.Web.Models;
|
|||||||
|
|
||||||
namespace BCards.Web.ViewModels;
|
namespace BCards.Web.ViewModels;
|
||||||
|
|
||||||
public class ManagePageViewModel
|
public class ManagePageViewModel : IValidatableObject
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
public bool IsNewPage { get; set; } = true;
|
public bool IsNewPage { get; set; } = true;
|
||||||
@ -26,12 +26,10 @@ public class ManagePageViewModel
|
|||||||
[Required(ErrorMessage = "Tema é obrigatório")]
|
[Required(ErrorMessage = "Tema é obrigatório")]
|
||||||
public string SelectedTheme { get; set; } = "minimalist";
|
public string SelectedTheme { get; set; } = "minimalist";
|
||||||
|
|
||||||
|
// Redes sociais sem [Required] - serão validadas no método Validate
|
||||||
public string WhatsAppNumber { get; set; } = string.Empty;
|
public string WhatsAppNumber { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string FacebookUrl { get; set; } = string.Empty;
|
public string FacebookUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string TwitterUrl { get; set; } = string.Empty;
|
public string TwitterUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string InstagramUrl { get; set; } = string.Empty;
|
public string InstagramUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
public List<ManageLinkViewModel> Links { get; set; } = new();
|
public List<ManageLinkViewModel> Links { get; set; } = new();
|
||||||
@ -43,44 +41,150 @@ public class ManagePageViewModel
|
|||||||
// Plan limitations
|
// Plan limitations
|
||||||
public int MaxLinksAllowed { get; set; } = 3;
|
public int MaxLinksAllowed { get; set; } = 3;
|
||||||
public bool CanUseTheme(string themeName) => AvailableThemes.Any(t => t.Name.ToLower() == themeName.ToLower());
|
public bool CanUseTheme(string themeName) => AvailableThemes.Any(t => t.Name.ToLower() == themeName.ToLower());
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var results = new List<ValidationResult>();
|
||||||
|
|
||||||
|
// Validar URLs de redes sociais apenas se preenchidas
|
||||||
|
if (!string.IsNullOrWhiteSpace(FacebookUrl) && !Uri.TryCreate(FacebookUrl, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult("URL do Facebook inválida", new[] { nameof(FacebookUrl) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ManageLinkViewModel
|
if (!string.IsNullOrWhiteSpace(TwitterUrl) && !Uri.TryCreate(TwitterUrl, UriKind.Absolute, out _))
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = "new";
|
results.Add(new ValidationResult("URL do Twitter inválida", new[] { nameof(TwitterUrl) }));
|
||||||
|
}
|
||||||
|
|
||||||
[Required(ErrorMessage = "Título é obrigatório")]
|
if (!string.IsNullOrWhiteSpace(InstagramUrl) && !Uri.TryCreate(InstagramUrl, UriKind.Absolute, out _))
|
||||||
[StringLength(50, ErrorMessage = "Título deve ter no máximo 50 caracteres")]
|
{
|
||||||
|
results.Add(new ValidationResult("URL do Instagram inválida", new[] { nameof(InstagramUrl) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar número do WhatsApp apenas se preenchido
|
||||||
|
if (!string.IsNullOrWhiteSpace(WhatsAppNumber))
|
||||||
|
{
|
||||||
|
var cleanNumber = WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "");
|
||||||
|
if (cleanNumber.Length < 10 || !cleanNumber.All(char.IsDigit))
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult("Número do WhatsApp inválido", new[] { nameof(WhatsAppNumber) }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar links ativos
|
||||||
|
var activeLinks = Links?.Where(l => !l.IsEmpty).ToList() ?? new List<ManageLinkViewModel>();
|
||||||
|
if (activeLinks.Count > MaxLinksAllowed)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult($"Você pode ter no máximo {MaxLinksAllowed} links ativos", new[] { nameof(Links) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class ManageLinkViewModel : IValidatableObject
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Required(ErrorMessage = "URL é obrigatória")]
|
|
||||||
[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")]
|
|
||||||
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;
|
||||||
public int Order { get; set; } = 0;
|
public int Order { get; set; } = 0;
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
// 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")]
|
|
||||||
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")]
|
|
||||||
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")]
|
|
||||||
public string ProductDescription { get; set; } = string.Empty;
|
public string ProductDescription { get; set; } = string.Empty;
|
||||||
|
|
||||||
public DateTime? ProductDataCachedAt { get; set; }
|
public DateTime? ProductDataCachedAt { get; set; }
|
||||||
|
|
||||||
|
// Validação customizada
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var results = new List<ValidationResult>();
|
||||||
|
|
||||||
|
// Se tem título OU URL, então ambos são obrigatórios
|
||||||
|
bool hasTitle = !string.IsNullOrWhiteSpace(Title);
|
||||||
|
bool hasUrl = !string.IsNullOrWhiteSpace(Url);
|
||||||
|
|
||||||
|
if (hasTitle || hasUrl)
|
||||||
|
{
|
||||||
|
if (!hasTitle)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"Título é obrigatório quando URL é preenchida",
|
||||||
|
new[] { nameof(Title) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasUrl)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"URL é obrigatória quando título é preenchido",
|
||||||
|
new[] { nameof(Url) }));
|
||||||
|
}
|
||||||
|
else if (!Uri.TryCreate(Url, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"URL inválida",
|
||||||
|
new[] { nameof(Url) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validações específicas para links de produto
|
||||||
|
if (Type == LinkType.Product && hasTitle && hasUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ProductTitle))
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"Título do produto é obrigatório para links de produto",
|
||||||
|
new[] { nameof(ProductTitle) }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validações de tamanho apenas se os campos estão preenchidos
|
||||||
|
if (hasTitle && Title.Length > 50)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"Título deve ter no máximo 50 caracteres",
|
||||||
|
new[] { nameof(Title) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Description) && Description.Length > 100)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"Descrição deve ter no máximo 100 caracteres",
|
||||||
|
new[] { nameof(Description) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ProductTitle) && ProductTitle.Length > 100)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"Título do produto deve ter no máximo 100 caracteres",
|
||||||
|
new[] { nameof(ProductTitle) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ProductPrice) && ProductPrice.Length > 50)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"Preço deve ter no máximo 50 caracteres",
|
||||||
|
new[] { nameof(ProductPrice) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ProductDescription) && ProductDescription.Length > 200)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult(
|
||||||
|
"Descrição do produto deve ter no máximo 200 caracteres",
|
||||||
|
new[] { nameof(ProductDescription) }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propriedade helper para verificar se o link está vazio
|
||||||
|
public bool IsEmpty => string.IsNullOrWhiteSpace(Title) && string.IsNullOrWhiteSpace(Url);
|
||||||
|
}
|
||||||
public class DashboardViewModel
|
public class DashboardViewModel
|
||||||
{
|
{
|
||||||
public User CurrentUser { get; set; } = new();
|
public User CurrentUser { get; set; } = new();
|
||||||
|
|||||||
@ -622,6 +622,9 @@
|
|||||||
let currentStep = 1;
|
let currentStep = 1;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
// Inicializar contador baseado nos links existentes não vazios
|
||||||
|
linkCount = $('.link-input-group').length;
|
||||||
|
|
||||||
// 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();
|
||||||
@ -643,6 +646,7 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
resetModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle between link types
|
// Toggle between link types
|
||||||
@ -666,7 +670,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
if (!isValidUrl(url)) {
|
||||||
alert('Por favor, insira uma URL válida que comece com http:// ou https://');
|
alert('Por favor, insira uma URL válida que comece com http:// ou https://');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -675,7 +679,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Save link from modal
|
// Save link from modal
|
||||||
$(document).on('click', '#saveLinkBtn', function() {
|
$('#saveLinkBtn').on('click', function() {
|
||||||
const linkType = $('input[name="linkType"]:checked').val();
|
const linkType = $('input[name="linkType"]:checked').val();
|
||||||
|
|
||||||
if (linkType === 'Product') {
|
if (linkType === 'Product') {
|
||||||
@ -687,19 +691,385 @@
|
|||||||
|
|
||||||
// Remove link functionality
|
// Remove link functionality
|
||||||
$(document).on('click', '.remove-link-btn', function() {
|
$(document).on('click', '.remove-link-btn', function() {
|
||||||
|
if (confirm('Tem certeza que deseja remover este link?')) {
|
||||||
$(this).closest('.link-input-group').remove();
|
$(this).closest('.link-input-group').remove();
|
||||||
linkCount--;
|
reindexLinks();
|
||||||
updateLinkNumbers();
|
updateAddLinkButton();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form validation
|
// Form validation and submission
|
||||||
$('#managePageForm').on('submit', function(e) {
|
$('#managePageForm').on('submit', function(e) {
|
||||||
console.log('Form submitted');
|
console.log('Form sendo enviado...');
|
||||||
// Allow submission but add loading state
|
|
||||||
$(this).find('button[type="submit"]').prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>Criando...');
|
// Remover containers de links completamente vazios antes do envio
|
||||||
});
|
removeEmptyLinkContainers();
|
||||||
|
|
||||||
|
// Reindexar todos os links restantes
|
||||||
|
reindexLinks();
|
||||||
|
|
||||||
|
// Validar formulário
|
||||||
|
if (!validateForm()) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Por favor, corrija os campos destacados em vermelho.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desabilitar botão de submit para evitar duplo clique
|
||||||
|
const $submitBtn = $(this).find('button[type="submit"]');
|
||||||
|
$submitBtn.prop('disabled', true);
|
||||||
|
|
||||||
|
if (@Model.IsNewPage) {
|
||||||
|
$submitBtn.html('<i class="fas fa-spinner fa-spin me-2"></i>Criando...');
|
||||||
|
} else {
|
||||||
|
$submitBtn.html('<i class="fas fa-spinner fa-spin me-2"></i>Salvando...');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validação em tempo real para links
|
||||||
|
$(document).on('input', '.link-title, .link-url', function() {
|
||||||
|
validateLinkInputs($(this).closest('.link-input-group'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicializar estado dos botões e validações
|
||||||
|
updateAddLinkButton();
|
||||||
|
|
||||||
|
// Marcar passos já completos se editando
|
||||||
|
@if (!Model.IsNewPage)
|
||||||
|
{
|
||||||
|
<text>
|
||||||
|
markStepComplete(1);
|
||||||
|
markStepComplete(2);
|
||||||
|
if ($('.link-input-group').length > 0) {
|
||||||
|
markStepComplete(3);
|
||||||
|
}
|
||||||
|
</text>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para validar URL
|
||||||
|
function isValidUrl(string) {
|
||||||
|
try {
|
||||||
|
new URL(string);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para remover containers de links vazios
|
||||||
|
function removeEmptyLinkContainers() {
|
||||||
|
$('.link-input-group').each(function() {
|
||||||
|
const $container = $(this);
|
||||||
|
const title = $container.find('input[name$=".Title"]').val()?.trim() || '';
|
||||||
|
const url = $container.find('input[name$=".Url"]').val()?.trim() || '';
|
||||||
|
|
||||||
|
// Se tanto título quanto URL estão vazios, remover o container
|
||||||
|
if (!title && !url) {
|
||||||
|
$container.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para reindexar links
|
||||||
|
function reindexLinks() {
|
||||||
|
$('.link-input-group').each(function(newIndex) {
|
||||||
|
const $container = $(this);
|
||||||
|
|
||||||
|
// Atualizar data-link
|
||||||
|
$container.attr('data-link', newIndex);
|
||||||
|
|
||||||
|
// Atualizar título visual
|
||||||
|
const $header = $container.find('h6').first();
|
||||||
|
const currentText = $header.html();
|
||||||
|
|
||||||
|
if (currentText.includes('Produto')) {
|
||||||
|
$header.html(currentText.replace(/Produto \d+/, `Produto ${newIndex + 1}`));
|
||||||
|
} else {
|
||||||
|
$header.html(currentText.replace(/Link \d+/, `Link ${newIndex + 1}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar nomes dos inputs
|
||||||
|
$container.find('input, select, textarea').each(function() {
|
||||||
|
const $input = $(this);
|
||||||
|
const name = $input.attr('name');
|
||||||
|
if (name && name.includes('Links[')) {
|
||||||
|
const newName = name.replace(/Links\[\d+\]/, `Links[${newIndex}]`);
|
||||||
|
$input.attr('name', newName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atualizar Order value
|
||||||
|
$container.find('input[name$=".Order"]').val(newIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atualizar contador global
|
||||||
|
linkCount = $('.link-input-group').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para atualizar estado do botão de adicionar link
|
||||||
|
function updateAddLinkButton() {
|
||||||
|
const canAddMore = linkCount < @Model.MaxLinksAllowed;
|
||||||
|
$('#addLinkBtn').prop('disabled', !canAddMore);
|
||||||
|
|
||||||
|
if (canAddMore) {
|
||||||
|
$('#addLinkBtn').html('<i class="fas fa-plus"></i> Adicionar Link');
|
||||||
|
} else {
|
||||||
|
$('#addLinkBtn').html(`Limite de ${@Model.MaxLinksAllowed} links atingido`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para validar inputs de um link específico
|
||||||
|
function validateLinkInputs($container) {
|
||||||
|
const $titleInput = $container.find('input[name$=".Title"]');
|
||||||
|
const $urlInput = $container.find('input[name$=".Url"]');
|
||||||
|
|
||||||
|
const title = $titleInput.val()?.trim() || '';
|
||||||
|
const url = $urlInput.val()?.trim() || '';
|
||||||
|
|
||||||
|
// Limpar classes de erro
|
||||||
|
$titleInput.removeClass('is-invalid');
|
||||||
|
$urlInput.removeClass('is-invalid');
|
||||||
|
|
||||||
|
// Se um está preenchido, o outro deve estar também
|
||||||
|
if (title && !url) {
|
||||||
|
$urlInput.addClass('is-invalid');
|
||||||
|
} else if (url && !title) {
|
||||||
|
$titleInput.addClass('is-invalid');
|
||||||
|
} else if (url && !isValidUrl(url)) {
|
||||||
|
$urlInput.addClass('is-invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função melhorada para adicionar links normais
|
||||||
|
function addLinkInput(title = '', url = '', description = '', icon = '', linkType = 'Normal', linkId = 'new') {
|
||||||
|
if (linkCount >= @Model.MaxLinksAllowed) {
|
||||||
|
alert('Você atingiu o limite de links para seu plano atual.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconHtml = icon ? `<i class="${icon} me-2"></i>` : '';
|
||||||
|
|
||||||
|
const linkHtml = `
|
||||||
|
<div class="link-input-group" data-link="${linkCount}">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
${iconHtml}Link ${linkCount + 1}: ${title || 'Novo Link'}
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-2">
|
||||||
|
<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" maxlength="50">
|
||||||
|
</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">
|
||||||
|
</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" maxlength="100">
|
||||||
|
</div>
|
||||||
|
<!-- Hidden fields -->
|
||||||
|
<input type="hidden" name="Links[${linkCount}].Id" value="${linkId}">
|
||||||
|
<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">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$('#linksContainer').append(linkHtml);
|
||||||
|
linkCount++;
|
||||||
|
markStepComplete(3);
|
||||||
|
updateAddLinkButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função melhorada para adicionar links de produtos
|
||||||
|
function addProductLinkInput(title, url, description, price, image, linkId = 'new') {
|
||||||
|
if (linkCount >= @Model.MaxLinksAllowed) {
|
||||||
|
alert('Você atingiu o limite de links para seu plano atual.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Produto ${linkCount + 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">
|
||||||
|
${image ? `<img src="${image}" 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>';">` : '<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">${title}</h6>
|
||||||
|
${price ? `<p class="card-text"><strong class="text-success">${price}</strong></p>` : ''}
|
||||||
|
${description ? `<p class="card-text small text-muted">${description}</p>` : ''}
|
||||||
|
<small class="text-muted d-block">
|
||||||
|
<i class="fas fa-external-link-alt me-1"></i>
|
||||||
|
${url.length > 50 ? url.substring(0, 50) + '...' : url}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden fields para form submission -->
|
||||||
|
<input type="hidden" name="Links[${linkCount}].Id" value="${linkId}">
|
||||||
|
<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">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$('#linksContainer').append(linkHtml);
|
||||||
|
linkCount++;
|
||||||
|
markStepComplete(3);
|
||||||
|
updateAddLinkButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para salvar link normal do modal
|
||||||
|
function saveNormalLink() {
|
||||||
|
const title = $('#linkTitle').val().trim();
|
||||||
|
const url = $('#linkUrl').val().trim();
|
||||||
|
const description = $('#linkDescription').val().trim();
|
||||||
|
const icon = $('#linkIcon').val();
|
||||||
|
|
||||||
|
if (!title || !url) {
|
||||||
|
alert('Por favor, preencha pelo menos o título e a URL do link.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidUrl(url)) {
|
||||||
|
alert('Por favor, insira uma URL válida que comece com http:// ou https://');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addLinkInput(title, url, description, icon, 'Normal');
|
||||||
|
closeModalAndReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para salvar link de produto do modal
|
||||||
|
function saveProductLink() {
|
||||||
|
const url = $('#productUrl').val().trim();
|
||||||
|
const title = $('#productTitle').val().trim();
|
||||||
|
const description = $('#productDescription').val().trim();
|
||||||
|
const price = $('#productPrice').val().trim();
|
||||||
|
const image = $('#productImage').val();
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
alert('Por favor, insira a URL do produto.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
alert('Por favor, preencha o título do produto.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidUrl(url)) {
|
||||||
|
alert('Por favor, insira uma URL válida que comece com http:// ou https://');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addProductLinkInput(title, url, description, price, image);
|
||||||
|
closeModalAndReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para extrair dados do produto
|
||||||
|
function extractProductData(url) {
|
||||||
|
$('#extractProductBtn').prop('disabled', true);
|
||||||
|
$('#extractLoading').show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/Product/extract',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ url: url }),
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$('#productTitle').val(response.title || '');
|
||||||
|
$('#productDescription').val(response.description || '');
|
||||||
|
$('#productPrice').val(response.price || '');
|
||||||
|
|
||||||
|
if (response.image) {
|
||||||
|
$('#productImage').val(response.image);
|
||||||
|
$('#productImagePreview').attr('src', response.image).show();
|
||||||
|
$('#productImagePlaceholder').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Dados extraídos com sucesso!', 'success');
|
||||||
|
} else {
|
||||||
|
alert('Erro: ' + (response.message || 'Não foi possível extrair os dados do produto.'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
let errorMessage = 'Erro ao extrair dados do produto.';
|
||||||
|
|
||||||
|
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||||
|
errorMessage = xhr.responseJSON.message;
|
||||||
|
} else if (xhr.status === 429) {
|
||||||
|
errorMessage = 'Aguarde 1 minuto antes de extrair dados de outro produto.';
|
||||||
|
} else if (xhr.status === 401) {
|
||||||
|
errorMessage = 'Você precisa estar logado para usar esta funcionalidade.';
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(errorMessage);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$('#extractProductBtn').prop('disabled', false);
|
||||||
|
$('#extractLoading').hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para fechar modal e resetar
|
||||||
|
function closeModalAndReset() {
|
||||||
|
var modal = bootstrap.Modal.getInstance(document.getElementById('addLinkModal'));
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
resetModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para resetar modal
|
||||||
|
function resetModal() {
|
||||||
|
$('#addLinkForm')[0].reset();
|
||||||
|
$('#productImagePreview').hide();
|
||||||
|
$('#productImagePlaceholder').show();
|
||||||
|
$('#productImage').val('');
|
||||||
|
$('#normalLinkSection').show();
|
||||||
|
$('#productLinkSection').hide();
|
||||||
|
$('#linkTypeNormal').prop('checked', true);
|
||||||
|
$('#extractProductBtn').prop('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funções de navegação do wizard
|
||||||
function nextStep(step) {
|
function nextStep(step) {
|
||||||
if (validateCurrentStep()) {
|
if (validateCurrentStep()) {
|
||||||
markStepComplete(currentStep);
|
markStepComplete(currentStep);
|
||||||
@ -727,7 +1097,6 @@
|
|||||||
|
|
||||||
function skipStep(step) {
|
function skipStep(step) {
|
||||||
markStepComplete(step);
|
markStepComplete(step);
|
||||||
// Show create button or next step
|
|
||||||
updateProgress();
|
updateProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,221 +1147,73 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLinkInput(title = '', url = '', description = '', icon = '', linkType = 'Normal') {
|
function validateForm() {
|
||||||
const iconHtml = icon ? `<i class="${icon} me-2"></i>` : '';
|
let isValid = true;
|
||||||
|
|
||||||
const linkHtml = `
|
// Limpar classes de erro anteriores
|
||||||
<div class="link-input-group" data-link="${linkCount}">
|
$('.is-invalid').removeClass('is-invalid');
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<h6 class="mb-0">
|
|
||||||
${iconHtml}Link ${linkCount + 1}: ${title || 'Novo Link'}
|
|
||||||
</h6>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="Links[${linkCount}].Id" value="">
|
|
||||||
<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">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$('#linksContainer').append(linkHtml);
|
// Validar campos obrigatórios principais
|
||||||
linkCount++;
|
const requiredFields = [
|
||||||
|
{ field: '#DisplayName', message: 'Nome é obrigatório' },
|
||||||
|
{ field: '#Category', message: 'Categoria é obrigatória' }
|
||||||
|
];
|
||||||
|
|
||||||
|
requiredFields.forEach(item => {
|
||||||
|
const $field = $(item.field);
|
||||||
|
if (!$field.val()?.trim()) {
|
||||||
|
$field.addClass('is-invalid');
|
||||||
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLinkNumbers() {
|
|
||||||
$('.link-input-group').each(function(index) {
|
|
||||||
$(this).find('h6').text('Link ' + (index + 1));
|
|
||||||
$(this).attr('data-link', index);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function saveNormalLink() {
|
// Validar links
|
||||||
const title = $('#linkTitle').val().trim();
|
$('.link-input-group').each(function() {
|
||||||
const url = $('#linkUrl').val().trim();
|
const $container = $(this);
|
||||||
const description = $('#linkDescription').val().trim();
|
const title = $container.find('input[name$=".Title"]').val()?.trim() || '';
|
||||||
const icon = $('#linkIcon').val();
|
const url = $container.find('input[name$=".Url"]').val()?.trim() || '';
|
||||||
|
|
||||||
if (!title || !url) {
|
|
||||||
alert('Por favor, preencha pelo menos o título e a URL do link.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
alert('Por favor, insira uma URL válida que comece com http:// ou https://');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addLinkInput(title, url, description, icon, 'Normal');
|
|
||||||
closeModalAndReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveProductLink() {
|
|
||||||
const url = $('#productUrl').val().trim();
|
|
||||||
const title = $('#productTitle').val().trim();
|
|
||||||
const description = $('#productDescription').val().trim();
|
|
||||||
const price = $('#productPrice').val().trim();
|
|
||||||
const image = $('#productImage').val();
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
alert('Por favor, insira a URL do produto.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Se tem título ou URL, ambos são obrigatórios
|
||||||
|
if (title || url) {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
alert('Por favor, preencha o título do produto.');
|
$container.find('input[name$=".Title"]').addClass('is-invalid');
|
||||||
return;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
if (!url) {
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
$container.find('input[name$=".Url"]').addClass('is-invalid');
|
||||||
alert('Por favor, insira uma URL válida que comece com http:// ou https://');
|
isValid = false;
|
||||||
return;
|
} else if (!isValidUrl(url)) {
|
||||||
|
$container.find('input[name$=".Url"]').addClass('is-invalid');
|
||||||
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
addProductLinkInput(title, url, description, price, image);
|
|
||||||
closeModalAndReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractProductData(url) {
|
|
||||||
$('#extractProductBtn').prop('disabled', true);
|
|
||||||
$('#extractLoading').show();
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/Product/extract',
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify({ url: url }),
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
$('#productTitle').val(response.title || '');
|
|
||||||
$('#productDescription').val(response.description || '');
|
|
||||||
$('#productPrice').val(response.price || '');
|
|
||||||
|
|
||||||
if (response.image) {
|
|
||||||
$('#productImage').val(response.image);
|
|
||||||
$('#productImagePreview').attr('src', response.image).show();
|
|
||||||
$('#productImagePlaceholder').hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast('Dados extraídos com sucesso!', 'success');
|
|
||||||
} else {
|
|
||||||
alert('Erro: ' + (response.message || 'Não foi possível extrair os dados do produto.'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(xhr) {
|
|
||||||
let errorMessage = 'Erro ao extrair dados do produto.';
|
|
||||||
|
|
||||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
|
||||||
errorMessage = xhr.responseJSON.message;
|
|
||||||
} else if (xhr.status === 429) {
|
|
||||||
errorMessage = 'Aguarde 1 minuto antes de extrair dados de outro produto.';
|
|
||||||
} else if (xhr.status === 401) {
|
|
||||||
errorMessage = 'Você precisa estar logado para usar esta funcionalidade.';
|
|
||||||
}
|
|
||||||
|
|
||||||
alert(errorMessage);
|
|
||||||
},
|
|
||||||
complete: function() {
|
|
||||||
$('#extractProductBtn').prop('disabled', false);
|
|
||||||
$('#extractLoading').hide();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validar URLs de redes sociais se preenchidas
|
||||||
|
const socialFields = ['#FacebookUrl', '#TwitterUrl', '#InstagramUrl'];
|
||||||
|
socialFields.forEach(field => {
|
||||||
|
const $field = $(field);
|
||||||
|
const value = $field.val()?.trim();
|
||||||
|
if (value && !isValidUrl(value)) {
|
||||||
|
$field.addClass('is-invalid');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validar WhatsApp se preenchido
|
||||||
|
const whatsapp = $('#WhatsAppNumber').val()?.trim();
|
||||||
|
if (whatsapp) {
|
||||||
|
const cleanNumber = whatsapp.replace(/[+\s\-()]/g, '');
|
||||||
|
if (cleanNumber.length < 10 || !/^\d+$/.test(cleanNumber)) {
|
||||||
|
$('#WhatsAppNumber').addClass('is-invalid');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addProductLinkInput(title, url, description, price, image) {
|
return isValid;
|
||||||
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">
|
|
||||||
<h6 class="mb-0">
|
|
||||||
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Produto ${linkCount + 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">
|
|
||||||
${image ? `<img src="${image}" 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>';">` : '<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">${title}</h6>
|
|
||||||
${price ? `<p class="card-text"><strong class="text-success">${price}</strong></p>` : ''}
|
|
||||||
${description ? `<p class="card-text small text-muted">${description}</p>` : ''}
|
|
||||||
<small class="text-muted d-block">
|
|
||||||
<i class="fas fa-external-link-alt me-1"></i>
|
|
||||||
${url.length > 50 ? url.substring(0, 50) + '...' : url}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden fields for form submission -->
|
|
||||||
<input type="hidden" name="Links[${linkCount}].Id" value="">
|
|
||||||
<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">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$('#linksContainer').append(linkHtml);
|
|
||||||
linkCount++;
|
|
||||||
markStepComplete(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModalAndReset() {
|
|
||||||
// Clear modal form
|
|
||||||
$('#addLinkForm')[0].reset();
|
|
||||||
$('#productImagePreview').hide();
|
|
||||||
$('#productImagePlaceholder').show();
|
|
||||||
$('#productImage').val('');
|
|
||||||
$('#normalLinkSection').show();
|
|
||||||
$('#productLinkSection').hide();
|
|
||||||
$('#linkTypeNormal').prop('checked', true);
|
|
||||||
|
|
||||||
// Close modal
|
|
||||||
var modal = bootstrap.Modal.getInstance(document.getElementById('addLinkModal'));
|
|
||||||
if (modal) {
|
|
||||||
modal.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
// Simple toast notification
|
|
||||||
const toastHtml = `
|
const toastHtml = `
|
||||||
<div class="toast align-items-center text-white bg-${type === 'success' ? 'success' : 'primary'} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
<div class="toast align-items-center text-white bg-${type === 'success' ? 'success' : 'primary'} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user