Compare commits

..

2 Commits

Author SHA1 Message Date
acb5bbf74b feat: remover createpage
All checks were successful
BCards Multi-Tenant Deployment Pipeline / Run Tests (push) Successful in 9s
BCards Multi-Tenant Deployment Pipeline / PR Validation (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Build and Push Image (push) Successful in 7m44s
BCards Multi-Tenant Deployment Pipeline / Deploy bcards.site (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deploy spicylinks.site (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deploy luzlinks.site (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Successful in 16s
BCards Multi-Tenant Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deployment Summary (push) Successful in 0s
2026-05-01 11:12:57 -03:00
cff7962cc5 fix: ajustes na edição 2026-04-30 23:24:17 -03:00
9 changed files with 176 additions and 865 deletions

View File

@ -53,7 +53,9 @@
"Bash(git log:*)", "Bash(git log:*)",
"Bash(git mv:*)", "Bash(git mv:*)",
"Bash(python3 -c \"import sys,json; [print\\(json.loads\\(l\\).get\\('MessageTemplate',''\\) + ' ' + str\\(json.loads\\(l\\).get\\('Level',''\\)\\)\\) for l in sys.stdin if 'categ' in l.lower\\(\\) or 'Error' in l or 'error' in l.lower\\(\\)]\")", "Bash(python3 -c \"import sys,json; [print\\(json.loads\\(l\\).get\\('MessageTemplate',''\\) + ' ' + str\\(json.loads\\(l\\).get\\('Level',''\\)\\)\\) for l in sys.stdin if 'categ' in l.lower\\(\\) or 'Error' in l or 'error' in l.lower\\(\\)]\")",
"Bash(python3 -c \" import sys, json for line in sys.stdin: line = line.strip\\(\\).rstrip\\(','\\) try: obj = json.loads\\(line\\) lvl = obj.get\\('Level',''\\) msg = obj.get\\('MessageTemplate',''\\) or obj.get\\('message',''\\) if lvl in \\('Error','Warning','Fatal'\\) or 'categ' in msg.lower\\(\\) or 'initial' in msg.lower\\(\\): print\\(f'[{lvl}] {msg}'\\) except: pass \")" "Bash(python3 -c \" import sys, json for line in sys.stdin: line = line.strip\\(\\).rstrip\\(','\\) try: obj = json.loads\\(line\\) lvl = obj.get\\('Level',''\\) msg = obj.get\\('MessageTemplate',''\\) or obj.get\\('message',''\\) if lvl in \\('Error','Warning','Fatal'\\) or 'categ' in msg.lower\\(\\) or 'initial' in msg.lower\\(\\): print\\(f'[{lvl}] {msg}'\\) except: pass \")",
"Bash(grep -E \"\\\\.\\(cs|csproj\\)$\")",
"Bash(git -C /c/vscode/bcards log --oneline --follow src/BCards.Web/Views/Admin/CreatePage.cshtml)"
] ]
}, },
"enableAllProjectMcpServers": false "enableAllProjectMcpServers": false

View File

@ -482,182 +482,10 @@ public class AdminController : Controller
} }
} }
[HttpPost]
[Route("CreatePage")] [Route("CreatePage")]
public async Task<IActionResult> CreatePage(CreatePageViewModel model) public IActionResult CreatePage()
{ {
var user = await _authService.GetCurrentUserAsync(User); return RedirectToAction("ManagePage", new { id = "new" });
if (user == null)
return RedirectToAction("Login", "Auth");
// Check if user already has a page
var existingPage = await _userPageService.GetUserPageAsync(user.Id);
if (existingPage != null)
return RedirectToAction("EditPage");
if (!ModelState.IsValid)
{
var categories = await _categoryService.GetAllCategoriesAsync();
var themes = await _themeService.GetAvailableThemesAsync();
ViewBag.Categories = categories;
ViewBag.Themes = themes;
return View(model);
}
// Generate slug if not provided
if (string.IsNullOrEmpty(model.Slug))
{
model.Slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName);
}
// Check if slug is available
if (!await _userPageService.ValidateSlugAsync(model.Category, model.Slug))
{
ModelState.AddModelError("Slug", "Esta URL já está em uso. Tente outra.");
var categories = await _categoryService.GetAllCategoriesAsync();
var themes = await _themeService.GetAvailableThemesAsync();
ViewBag.Categories = categories;
ViewBag.Themes = themes;
return View(model);
}
// Check if user can create the requested number of links
var activeLinksCount = model.Links?.Count ?? 0;
if (!await _userPageService.CanCreateLinksAsync(user.Id, activeLinksCount))
{
ModelState.AddModelError("", "Você excedeu o limite de links do seu plano atual.");
var categories = await _categoryService.GetAllCategoriesAsync();
var themes = await _themeService.GetAvailableThemesAsync();
ViewBag.Categories = categories;
ViewBag.Themes = themes;
return View(model);
}
// Convert ViewModel to UserPage
var userPage = new UserPage
{
UserId = user.Id,
DisplayName = model.DisplayName,
Category = model.Category,
BusinessType = model.BusinessType,
Bio = model.Bio,
Slug = model.Slug,
Theme = await _themeService.GetThemeByNameAsync(model.SelectedTheme) ?? _themeService.GetDefaultTheme(),
Links = model.Links?.Select(l => new LinkItem
{
Title = l.Title,
Url = l.Url,
Description = l.Description,
Icon = l.Icon,
IsActive = true,
Order = model.Links.IndexOf(l)
}).ToList() ?? new List<LinkItem>()
};
// Add social media links
var socialLinks = new List<LinkItem>();
if (!string.IsNullOrEmpty(model.WhatsAppNumber))
{
socialLinks.Add(new LinkItem
{
Title = "WhatsApp",
Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}",
Icon = "fab fa-whatsapp",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.FacebookUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Facebook",
Url = model.FacebookUrl,
Icon = "fab fa-facebook",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.TwitterUrl))
{
socialLinks.Add(new LinkItem
{
Title = "X / Twitter",
Url = model.TwitterUrl,
Icon = "fab fa-x-twitter",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.InstagramUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Instagram",
Url = model.InstagramUrl,
Icon = "fab fa-instagram",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.TiktokUrl))
{
socialLinks.Add(new LinkItem
{
Title = "TikTok",
Url = model.TiktokUrl,
Icon = "fab fa-tiktok",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.PinterestUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Pinterest",
Url = model.PinterestUrl,
Icon = "fab fa-pinterest",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.DiscordUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Discord",
Url = model.DiscordUrl,
Icon = "fab fa-discord",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.KawaiUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Kawai",
Url = model.KawaiUrl,
Icon = "fas fa-heart",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
userPage.Links.AddRange(socialLinks);
await _userPageService.CreatePageAsync(userPage);
TempData["Success"] = "Página criada com sucesso!";
return RedirectToAction("Dashboard");
} }
[HttpGet] [HttpGet]
@ -837,6 +665,16 @@ public class AdminController : Controller
FileSize = d.FileSize, FileSize = d.FileSize,
UploadedAt = d.UploadedAt UploadedAt = d.UploadedAt
}).ToList() ?? new List<ManageDocumentViewModel>(), }).ToList() ?? new List<ManageDocumentViewModel>(),
// Social media fields — extracted from Links so the edit form pre-fills correctly
WhatsAppNumber = page.Links?.FirstOrDefault(l => l.Icon?.Contains("whatsapp") == true)?.Url
?.Replace("https://wa.me/", "").Replace("whatsapp://", "") ?? string.Empty,
FacebookUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("facebook") == true)?.Url ?? string.Empty,
InstagramUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("instagram") == true)?.Url ?? string.Empty,
TwitterUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("twitter") == true)?.Url ?? string.Empty,
TiktokUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("tiktok") == true)?.Url ?? string.Empty,
PinterestUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("pinterest") == true)?.Url ?? string.Empty,
DiscordUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("discord") == true)?.Url ?? string.Empty,
KawaiUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("kawai") == true)?.Url ?? string.Empty,
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(),
@ -892,10 +730,13 @@ public class AdminController : Controller
if (!string.IsNullOrEmpty(model.WhatsAppNumber)) if (!string.IsNullOrEmpty(model.WhatsAppNumber))
{ {
var whatsappDigits = model.WhatsAppNumber
.Replace("https://wa.me/", "").Replace("whatsapp://", "")
.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/{whatsappDigits}",
Icon = "fab fa-whatsapp", Icon = "fab fa-whatsapp",
IsActive = true, IsActive = true,
Order = currentOrder++ Order = currentOrder++
@ -1241,10 +1082,13 @@ public class AdminController : Controller
if (!string.IsNullOrEmpty(model.WhatsAppNumber)) if (!string.IsNullOrEmpty(model.WhatsAppNumber))
{ {
var whatsappDigits = model.WhatsAppNumber
.Replace("https://wa.me/", "").Replace("whatsapp://", "")
.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/{whatsappDigits}",
Icon = "fab fa-whatsapp", Icon = "fab fa-whatsapp",
IsActive = true, IsActive = true,
Order = currentOrder++ Order = currentOrder++

View File

@ -1,58 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace BCards.Web.ViewModels;
public class CreatePageViewModel
{
[Required(ErrorMessage = "Nome é obrigatório")]
[StringLength(50, ErrorMessage = "Nome deve ter no máximo 50 caracteres")]
public string DisplayName { get; set; } = string.Empty;
[Required(ErrorMessage = "Categoria é obrigatória")]
public string Category { get; set; } = string.Empty;
[Required(ErrorMessage = "Tipo de negócio é obrigatório")]
public string BusinessType { get; set; } = "individual";
[StringLength(3000, ErrorMessage = "Bio deve ter no máximo 3000 caracteres")]
public string Bio { get; set; } = string.Empty;
[Required(ErrorMessage = "Tema é obrigatório")]
public string SelectedTheme { get; set; } = "minimalist";
public string WhatsAppNumber { get; set; } = string.Empty;
public string FacebookUrl { get; set; } = string.Empty;
public string TwitterUrl { get; set; } = string.Empty;
public string InstagramUrl { get; set; } = string.Empty;
public string TiktokUrl { get; set; } = string.Empty;
public string PinterestUrl { get; set; } = string.Empty;
public string DiscordUrl { get; set; } = string.Empty;
public string KawaiUrl { get; set; } = string.Empty;
public List<CreateLinkViewModel> Links { get; set; } = new();
public string Slug { get; set; } = string.Empty;
}
public class CreateLinkViewModel
{
[Required(ErrorMessage = "Título é obrigatório")]
[StringLength(50, 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")]
public string Description { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
}

View File

@ -1,619 +0,0 @@
@model BCards.Web.ViewModels.CreatePageViewModel
@{
ViewData["Title"] = "Criar Página";
Layout = "_Layout";
}
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 mx-auto">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="fas fa-magic"></i>
Criar Sua Página de Links
</h4>
</div>
<div class="card-body">
<!-- Progress Bar -->
<div class="progress mb-4" style="height: 8px;">
<div class="progress-bar" role="progressbar" style="width: 20%" id="wizardProgress"></div>
</div>
<form asp-action="CreatePage" method="post" id="createPageForm">
<!-- Step 1: Informações Básicas -->
<div class="wizard-step" id="step1">
<h5 class="step-title">
<span class="step-number">1</span>
Informações Básicas
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="DisplayName" class="form-label">Nome da Página</label>
<input asp-for="DisplayName" class="form-control" placeholder="Ex: João Silva">
<span asp-validation-for="DisplayName" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="Category" class="form-label">Categoria</label>
<select asp-for="Category" class="form-select">
<option value="">Selecione uma categoria</option>
@foreach (var category in ViewBag.Categories as List<BCards.Web.Models.Category> ?? new List<BCards.Web.Models.Category>())
{
<option value="@category.Name">@category.Name</option>
}
</select>
<span asp-validation-for="Category" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="BusinessType" class="form-label">Tipo</label>
<select asp-for="BusinessType" class="form-select">
<option value="individual">Pessoa Física</option>
<option value="company">Empresa</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="slugPreview" class="form-label">URL da Página</label>
<div class="input-group">
<span class="input-group-text">page/</span>
<span class="input-group-text" id="categorySlug">categoria</span>
<span class="input-group-text">/</span>
<input type="text" class="form-control" id="slugPreview" readonly>
<input asp-for="Slug" type="hidden">
</div>
<small class="form-text text-muted">URL gerada automaticamente</small>
</div>
</div>
</div>
<div class="mb-3">
<label asp-for="Bio" class="form-label">Bio/Descrição</label>
<textarea asp-for="Bio" class="form-control" rows="8" maxlength="3000" placeholder="Uma breve descrição sobre você ou sua empresa..."></textarea>
<span asp-validation-for="Bio" class="text-danger"></span>
<div class="form-text">Máximo 3000 caracteres</div>
</div>
</div>
<!-- Step 2: Seleção de Tema -->
<div class="wizard-step d-none" id="step2">
<h5 class="step-title">
<span class="step-number">2</span>
Escolha Seu Tema Visual
</h5>
<div class="row">
@foreach (var theme in ViewBag.Themes as List<BCards.Web.Models.PageTheme> ?? new List<BCards.Web.Models.PageTheme>())
{
<div class="col-md-4 mb-3">
<div class="theme-card" 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>
<h6>@theme.Name</h6>
</div>
<div class="theme-links">
<div class="theme-link" style="background-color: @theme.PrimaryColor;"></div>
<div class="theme-link" style="background-color: @theme.SecondaryColor;"></div>
</div>
</div>
<div class="theme-name">
@theme.Name
@if (theme.IsPremium)
{
<span class="badge bg-warning">Premium</span>
}
</div>
</div>
</div>
}
</div>
<input asp-for="SelectedTheme" type="hidden">
</div>
<!-- Step 3: Links Principais -->
<div class="wizard-step d-none" id="step3">
<h5 class="step-title">
<span class="step-number">3</span>
Links Principais
</h5>
<div id="linksContainer">
<!-- Links will be added dynamically -->
</div>
<button type="button" class="btn btn-outline-primary" id="addLinkBtn">
<i class="fas fa-plus"></i> Adicionar Link
</button>
</div>
<!-- Step 4: Redes Sociais -->
<div class="wizard-step d-none" id="step4">
<h5 class="step-title">
<span class="step-number">4</span>
Redes Sociais
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="WhatsAppNumber" class="form-label">
<i class="fab fa-whatsapp text-success"></i>
WhatsApp
</label>
<input asp-for="WhatsAppNumber" class="form-control" placeholder="+55 11 99999-9999">
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="FacebookUrl" class="form-label">
<i class="fab fa-facebook text-primary"></i>
Facebook
</label>
<input asp-for="FacebookUrl" class="form-control" placeholder="https://facebook.com/seu-perfil">
<span asp-validation-for="FacebookUrl" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="TwitterUrl" class="form-label">
<i class="fab fa-x-twitter"></i>
X / Twitter
</label>
<input asp-for="TwitterUrl" class="form-control" placeholder="https://x.com/seu-perfil">
<span asp-validation-for="TwitterUrl" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="InstagramUrl" class="form-label">
<i class="fab fa-instagram text-danger"></i>
Instagram
</label>
<input asp-for="InstagramUrl" class="form-control" placeholder="https://instagram.com/seu-perfil">
<span asp-validation-for="InstagramUrl" class="text-danger"></span>
</div>
</div>
</div>
</div>
<!-- Step 5: Preview e Finalização -->
<div class="wizard-step d-none" id="step5">
<h5 class="step-title">
<span class="step-number">5</span>
Preview e Finalização
</h5>
<div class="preview-container">
<div class="preview-phone">
<div class="preview-screen" id="previewScreen">
<!-- Preview will be generated here -->
</div>
</div>
</div>
<div class="text-center mt-4">
<p class="text-muted">Sua página estará disponível em:</p>
<strong id="finalUrl">page/categoria/seu-slug</strong>
</div>
</div>
<!-- Navigation Buttons -->
<div class="wizard-navigation mt-4">
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
<i class="fas fa-arrow-left"></i> Anterior
</button>
<button type="button" class="btn btn-primary float-end" id="nextBtn">
Próximo <i class="fas fa-arrow-right"></i>
</button>
<button type="submit" class="btn btn-success float-end d-none" id="submitBtn">
<i class="fas fa-check"></i> Criar Página
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.wizard-step {
min-height: 400px;
}
.step-title {
color: #495057;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.step-number {
display: inline-block;
width: 30px;
height: 30px;
line-height: 30px;
background-color: #007bff;
color: white;
border-radius: 50%;
text-align: center;
margin-right: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
}
.theme-card {
cursor: pointer;
border: 2px solid transparent;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.theme-card:hover,
.theme-card.selected {
border-color: #007bff;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.theme-preview {
height: 120px;
position: relative;
padding: 1rem;
}
.theme-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.theme-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
}
.theme-header h6 {
margin: 0;
font-size: 0.75rem;
color: white;
}
.theme-links {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.theme-link {
height: 8px;
border-radius: 4px;
opacity: 0.8;
}
.theme-name {
padding: 0.75rem;
text-align: center;
font-weight: 500;
background-color: #f8f9fa;
}
.link-input-group {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.preview-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
.preview-phone {
width: 300px;
height: 400px;
border: 8px solid #333;
border-radius: 20px;
background-color: #000;
padding: 20px 10px;
position: relative;
}
.preview-screen {
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 12px;
overflow-y: auto;
padding: 1rem;
}
.wizard-navigation {
border-top: 1px solid #dee2e6;
padding-top: 1rem;
}
</style>
<script>
let currentStep = 1;
const totalSteps = 5;
let linkCount = 0;
$(document).ready(function() {
initializeWizard();
// Generate slug when name or category changes
$('#DisplayName, #Category').on('input change', function() {
generateSlug();
});
// Theme selection
$('.theme-card').on('click', function() {
$('.theme-card').removeClass('selected');
$(this).addClass('selected');
const themeName = $(this).data('theme');
$('#SelectedTheme').val(themeName);
});
// Navigation
$('#nextBtn').on('click', function() {
if (validateCurrentStep()) {
nextStep();
}
});
$('#prevBtn').on('click', function() {
prevStep();
});
// Add link functionality
$('#addLinkBtn').on('click', function() {
addLinkInput();
});
// Form submission
$('#createPageForm').on('submit', function(e) {
generateLinksData();
});
});
function initializeWizard() {
updateProgressBar();
updateNavigationButtons();
addLinkInput(); // Add first link input
}
function nextStep() {
if (currentStep < totalSteps) {
$(`#step${currentStep}`).addClass('d-none');
currentStep++;
$(`#step${currentStep}`).removeClass('d-none');
if (currentStep === 5) {
generatePreview();
}
updateProgressBar();
updateNavigationButtons();
}
}
function prevStep() {
if (currentStep > 1) {
$(`#step${currentStep}`).addClass('d-none');
currentStep--;
$(`#step${currentStep}`).removeClass('d-none');
updateProgressBar();
updateNavigationButtons();
}
}
function updateProgressBar() {
const progress = (currentStep / totalSteps) * 100;
$('#wizardProgress').css('width', progress + '%');
}
function updateNavigationButtons() {
$('#prevBtn').toggle(currentStep > 1);
if (currentStep === totalSteps) {
$('#nextBtn').addClass('d-none');
$('#submitBtn').removeClass('d-none');
} else {
$('#nextBtn').removeClass('d-none');
$('#submitBtn').addClass('d-none');
}
}
function validateCurrentStep() {
let isValid = true;
switch (currentStep) {
case 1:
if (!$('#DisplayName').val() || !$('#Category').val()) {
alert('Por favor, preencha o nome e a categoria.');
isValid = false;
}
break;
case 2:
if (!$('#SelectedTheme').val()) {
alert('Por favor, selecione um tema.');
isValid = false;
}
break;
}
return isValid;
}
function generateSlug() {
const name = $('#DisplayName').val();
const category = $('#Category').val();
if (name && category) {
$.post('/Admin/GenerateSlug', { category: category, name: name })
.done(function(data) {
$('#Slug').val(data.slug);
$('#slugPreview').val(data.slug);
$('#categorySlug').text(category);
$('#finalUrl').text(`page/${category}/${data.slug}`);
});
}
}
function addLinkInput() {
linkCount++;
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">Link ${linkCount}</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" class="form-control link-title" placeholder="Ex: Meu Site">
</div>
</div>
<div class="col-md-6">
<div class="mb-2">
<label class="form-label">URL</label>
<input type="url" class="form-control link-url" placeholder="https://exemplo.com">
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label">Descrição (opcional)</label>
<input type="text" class="form-control link-description" placeholder="Breve descrição do link">
</div>
</div>
`;
$('#linksContainer').append(linkHtml);
// Add remove functionality
$('.remove-link-btn').off('click').on('click', function() {
$(this).closest('.link-input-group').remove();
});
}
function generateLinksData() {
const links = [];
$('.link-input-group').each(function() {
const title = $(this).find('.link-title').val();
const url = $(this).find('.link-url').val();
const description = $(this).find('.link-description').val();
if (title && url) {
links.push({
Title: title,
Url: url,
Description: description,
Icon: ''
});
}
});
// Remove existing hidden link inputs
$('input[name^="Links["]').remove();
// Create hidden inputs for links directly in the form
links.forEach((link, index) => {
$('#createPageForm').append(`
<input type="hidden" name="Links[${index}].Title" value="${link.Title}" />
<input type="hidden" name="Links[${index}].Url" value="${link.Url}" />
<input type="hidden" name="Links[${index}].Description" value="${link.Description}" />
<input type="hidden" name="Links[${index}].Icon" value="${link.Icon}" />
`);
});
// Debug: Log what we're sending
console.log('=== DEBUG GENERATELINKSDATA ===');
console.log('Links found:', links.length);
links.forEach((link, index) => {
console.log(`Link ${index}:`, link);
});
console.log('=== FIM DEBUG ===');
}
function generatePreview() {
const name = $('#DisplayName').val();
const bio = $('#Bio').val();
const selectedTheme = $('#SelectedTheme').val();
let previewHtml = `
<div class="text-center">
<div class="mb-3">
<div style="width: 60px; height: 60px; background-color: #ddd; border-radius: 50%; margin: 0 auto;"></div>
</div>
<h5 class="mb-2">${name}</h5>
<p class="text-muted small mb-3">${bio}</p>
<div class="d-grid gap-2">
`;
// Add links preview
$('.link-input-group').each(function() {
const title = $(this).find('.link-title').val();
if (title) {
previewHtml += `<div class="btn btn-primary btn-sm">${title}</div>`;
}
});
// Add social media preview
if ($('#WhatsAppNumber').val()) {
previewHtml += `<div class="btn btn-success btn-sm"><i class="fab fa-whatsapp"></i> WhatsApp</div>`;
}
if ($('#FacebookUrl').val()) {
previewHtml += `<div class="btn btn-primary btn-sm"><i class="fab fa-facebook"></i> Facebook</div>`;
}
if ($('#TwitterUrl').val()) {
previewHtml += `<div class="btn btn-dark btn-sm"><i class="fab fa-x-twitter"></i> X / Twitter</div>`;
}
if ($('#InstagramUrl').val()) {
previewHtml += `<div class="btn btn-danger btn-sm"><i class="fab fa-instagram"></i> Instagram</div>`;
}
previewHtml += `</div></div>`;
$('#previewScreen').html(previewHtml);
}
</script>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@ -115,9 +115,54 @@
<div class="mb-3"> <div class="mb-3">
<label asp-for="Bio" class="form-label">Bio/Descrição</label> <label asp-for="Bio" class="form-label">Bio/Descrição</label>
<textarea asp-for="Bio" class="form-control" rows="8" maxlength="3000" placeholder="Uma breve descrição sobre você ou sua empresa..."></textarea> <div class="md-toolbar border rounded-top border-bottom-0 bg-light px-2 py-1 d-flex gap-1 flex-wrap position-relative">
<button type="button" class="btn btn-sm btn-outline-secondary md-btn" data-target="Bio" data-wrap="**" title="Negrito"><b>B</b></button>
<button type="button" class="btn btn-sm btn-outline-secondary md-btn" data-target="Bio" data-wrap="*" title="Itálico"><i>I</i></button>
<button type="button" class="btn btn-sm btn-outline-secondary md-list-btn" data-target="Bio" title="Lista">&#8226; Lista</button>
<button type="button" class="btn btn-sm btn-outline-secondary md-link-btn" data-target="Bio" title="Link">&#128279; Link</button>
<button type="button" class="btn btn-sm btn-outline-secondary md-icon-picker-btn" data-target="Bio" title="Inserir ícone">&#9881; Ícone</button>
<div class="md-icon-picker-panel shadow border rounded bg-white p-2" style="display:none; position:absolute; top:100%; left:0; z-index:9999; width:300px; max-height:260px; overflow-y:auto;">
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Status</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="OK">✅</span><span class="md-icon-item" title="Erro">❌</span><span class="md-icon-item" title="Atenção">⚠️</span><span class="md-icon-item" title="Info"></span><span class="md-icon-item" title="Check">✔</span><span class="md-icon-item" title="X">✘</span><span class="md-icon-item" title="Seta">➤</span><span class="md-icon-item" title="Seta direita">→</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Formas &amp; Bullets</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Bullet">•</span><span class="md-icon-item" title="Círculo vazio">○</span><span class="md-icon-item" title="Círculo cheio">●</span><span class="md-icon-item" title="Quadrado cheio">■</span><span class="md-icon-item" title="Quadrado vazio">□</span><span class="md-icon-item" title="Quadrado com check">☑</span><span class="md-icon-item" title="Losango cheio">◆</span><span class="md-icon-item" title="Losango vazio">◇</span><span class="md-icon-item" title="Triângulo">▶</span><span class="md-icon-item" title="Estrela">★</span><span class="md-icon-item" title="Estrela vazia">☆</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Círculos coloridos</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Vermelho">🔴</span><span class="md-icon-item" title="Laranja">🟠</span><span class="md-icon-item" title="Amarelo">🟡</span><span class="md-icon-item" title="Verde">🟢</span><span class="md-icon-item" title="Azul">🔵</span><span class="md-icon-item" title="Roxo">🟣</span><span class="md-icon-item" title="Preto">⚫</span><span class="md-icon-item" title="Branco">⚪</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Negócios &amp; Escritório</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Gráfico barras">📊</span><span class="md-icon-item" title="Gráfico alta">📈</span><span class="md-icon-item" title="Gráfico baixa">📉</span><span class="md-icon-item" title="Prancheta">📋</span><span class="md-icon-item" title="Pin">📌</span><span class="md-icon-item" title="Pasta">📁</span><span class="md-icon-item" title="Maleta">💼</span><span class="md-icon-item" title="Nota">📝</span><span class="md-icon-item" title="Email">📧</span><span class="md-icon-item" title="Telefone">📞</span><span class="md-icon-item" title="Alvo">🎯</span><span class="md-icon-item" title="Troféu">🏆</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Tecnologia</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Notebook">💻</span><span class="md-icon-item" title="Monitor">🖥️</span><span class="md-icon-item" title="Celular">📱</span><span class="md-icon-item" title="Teclado">⌨️</span><span class="md-icon-item" title="Impressora">🖨️</span><span class="md-icon-item" title="Engrenagem">⚙️</span><span class="md-icon-item" title="Lupa">🔍</span><span class="md-icon-item" title="Link">🔗</span>
</div>
</div>
<div class="md-icon-section">
<div class="text-muted small mb-1 fw-semibold">Finanças &amp; Outros</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Dinheiro">💰</span><span class="md-icon-item" title="Cartão">💳</span><span class="md-icon-item" title="Banco">🏦</span><span class="md-icon-item" title="Chave">🔑</span><span class="md-icon-item" title="Cadeado">🔒</span><span class="md-icon-item" title="Sino">🔔</span><span class="md-icon-item" title="Lâmpada">💡</span><span class="md-icon-item" title="Raio">⚡</span><span class="md-icon-item" title="Pessoa">👤</span><span class="md-icon-item" title="Pessoas">👥</span>
</div>
</div>
</div>
</div>
<textarea asp-for="Bio" id="Bio" class="form-control rounded-0 rounded-bottom" rows="5" maxlength="3000" placeholder="Uma breve descrição sobre você ou sua empresa..." style="font-family: monospace; font-size: 0.9rem;"></textarea>
<span asp-validation-for="Bio" class="text-danger"></span> <span asp-validation-for="Bio" class="text-danger"></span>
<div class="form-text">Máximo 3000 caracteres</div> <div class="form-text">Máximo 3000 caracteres. Use **negrito**, *itálico*, - item para listas.</div>
</div> </div>
<!-- Profile Image Upload --> <!-- Profile Image Upload -->
@ -1210,7 +1255,6 @@
@section Scripts { @section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");} @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script> <script>
const LINK_TYPES_CONFIG = @Html.Raw(linkTypesJson); const LINK_TYPES_CONFIG = @Html.Raw(linkTypesJson);
let linkCount = @Model.Links.Count; let linkCount = @Model.Links.Count;
@ -1219,6 +1263,9 @@
const totalSteps = 5; const totalSteps = 5;
$(document).ready(function() { $(document).ready(function() {
// Initialize Markdown toolbar
initMarkdownToolbar();
// Initialize social media fields // Initialize social media fields
initializeSocialMedia(); initializeSocialMedia();
@ -1337,11 +1384,11 @@
updateLinkNumbers(); updateLinkNumbers();
}); });
// Form validation // Loading state — só desabilita se a validação JS não bloqueou o submit
$('#managePageForm').on('submit', function(e) { $('#managePageForm').on('submit', function(e) {
console.log('Form submitted'); if (!e.isDefaultPrevented()) {
// 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>Salvando...');
$(this).find('button[type="submit"]').prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>Criando...'); }
}); });
}); });
@ -1943,6 +1990,84 @@
}, 7000); }, 7000);
} }
// Markdown Toolbar
function initMarkdownToolbar() {
// Icon picker toggle
document.querySelectorAll('.md-icon-picker-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var panel = this.parentElement.querySelector('.md-icon-picker-panel');
var isVisible = panel.style.display !== 'none';
document.querySelectorAll('.md-icon-picker-panel').forEach(function(p) { p.style.display = 'none'; });
if (!isVisible) panel.style.display = 'block';
});
});
// Insert icon on click
document.querySelectorAll('.md-icon-item').forEach(function(item) {
item.addEventListener('click', function(e) {
e.stopPropagation();
var panel = this.closest('.md-icon-picker-panel');
var btn = panel.parentElement.querySelector('.md-icon-picker-btn');
var targetId = btn ? btn.dataset.target : 'Bio';
var ta = document.getElementById(targetId);
var icon = this.textContent;
var start = ta.selectionStart, end = ta.selectionEnd;
ta.value = ta.value.substring(0, start) + icon + ta.value.substring(end);
ta.selectionStart = ta.selectionEnd = start + icon.length;
ta.focus();
panel.style.display = 'none';
});
});
// Close picker on outside click
document.addEventListener('click', function() {
document.querySelectorAll('.md-icon-picker-panel').forEach(function(p) { p.style.display = 'none'; });
});
document.querySelectorAll('.md-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var targetId = this.dataset.target;
var wrap = this.dataset.wrap;
var ta = document.getElementById(targetId);
var start = ta.selectionStart, end = ta.selectionEnd;
var sel = ta.value.substring(start, end) || 'texto';
var before = ta.value.substring(0, start);
var after = ta.value.substring(end);
ta.value = before + wrap + sel + wrap + after;
ta.selectionStart = start + wrap.length;
ta.selectionEnd = start + wrap.length + sel.length;
ta.focus();
});
});
document.querySelectorAll('.md-list-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var targetId = this.dataset.target;
var ta = document.getElementById(targetId);
var start = ta.selectionStart;
var lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
var before = ta.value.substring(0, lineStart);
var after = ta.value.substring(lineStart);
ta.value = before + '- ' + after;
ta.selectionStart = ta.selectionEnd = start + 2;
ta.focus();
});
});
document.querySelectorAll('.md-link-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var targetId = this.dataset.target;
var ta = document.getElementById(targetId);
var start = ta.selectionStart, end = ta.selectionEnd;
var sel = ta.value.substring(start, end) || 'texto do link';
var url = prompt('URL do link:') || 'https://';
var before = ta.value.substring(0, start);
var after = ta.value.substring(end);
var md = '[' + sel + '](' + url + ')';
ta.value = before + md + after;
ta.selectionStart = ta.selectionEnd = start + md.length;
ta.focus();
});
});
}
// Validation Error Handling // Validation Error Handling
function checkValidationErrors() { function checkValidationErrors() {
// Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado) // Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado)
@ -2262,8 +2387,10 @@
} }
// Atualizar campo hidden - SEMPRE string, nunca null // Atualizar campo hidden - SEMPRE string, nunca null
// WhatsApp: armazena só o número (servidor adiciona https://wa.me/ ao salvar)
// Outros: armazena URL completa (servidor usa diretamente)
if (value) { if (value) {
hiddenField.val(prefix + value); hiddenField.val(isWhatsApp ? value : prefix + value);
} else { } else {
hiddenField.val(' '); // Espaço em branco para evitar null hiddenField.val(' '); // Espaço em branco para evitar null
} }
@ -2606,6 +2733,16 @@
@section Styles { @section Styles {
<style> <style>
.md-icon-item {
cursor: pointer;
font-size: 1.2rem;
padding: 2px 4px;
border-radius: 4px;
line-height: 1.4;
transition: background 0.1s;
}
.md-icon-item:hover { background: #e9ecef; }
/* Estilo customizado para o scroll dos temas */ /* Estilo customizado para o scroll dos temas */
.themes-container { .themes-container {
scrollbar-width: thin; scrollbar-width: thin;

View File

@ -111,7 +111,7 @@
<div class="card-footer bg-transparent p-4"> <div class="card-footer bg-transparent p-4">
@if (User.Identity?.IsAuthenticated == true) @if (User.Identity?.IsAuthenticated == true)
{ {
<a asp-controller="Admin" asp-action="CreatePage" class="btn btn-success w-100">Começar Grátis</a> <a asp-controller="Admin" asp-action="ManagePage" asp-route-id="new" class="btn btn-success w-100">Começar Grátis</a>
} }
else else
{ {

View File

@ -103,9 +103,11 @@
<div class="card-body"> <div class="card-body">
@if (!string.IsNullOrEmpty(Model.Page.Bio)) @if (!string.IsNullOrEmpty(Model.Page.Bio))
{ {
var bioPipeline = new Markdig.MarkdownPipelineBuilder().UseAutoLinks().DisableHtml().Build();
var bioHtml = Markdig.Markdown.ToHtml(Model.Page.Bio, bioPipeline);
<div class="mb-3"> <div class="mb-3">
<strong>Biografia:</strong> <strong>Biografia:</strong>
<p>@Model.Page.Bio</p> <div>@Html.Raw(bioHtml)</div>
</div> </div>
} }

View File

@ -367,7 +367,9 @@
@if (!string.IsNullOrEmpty(Model.Bio)) @if (!string.IsNullOrEmpty(Model.Bio))
{ {
<p class="profile-bio">@Model.Bio</p> var bioPipeline = new Markdig.MarkdownPipelineBuilder().UseAutoLinks().DisableHtml().Build();
var bioHtml = Markdig.Markdown.ToHtml(Model.Bio, bioPipeline);
<div class="profile-bio">@Html.Raw(bioHtml)</div>
} }
<!-- Links Container --> <!-- Links Container -->

View File

@ -1,3 +1,4 @@
@using BCards.Web @using BCards.Web
@using BCards.Web.Models @using BCards.Web.Models
@using Markdig
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers