BCards/src/BCards.Web/Views/Admin/ManagePage.cshtml

2765 lines
142 KiB
Plaintext

@using BCards.Web.Utils
@model BCards.Web.ViewModels.ManagePageViewModel
@{
ViewData["Title"] = Model.IsNewPage ? "Criar Página" : "Editar 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-@(Model.IsNewPage ? "plus" : "edit")"></i>
@(Model.IsNewPage ? "Assistente de Criação de Página" : "Editar Página")
</h4>
</div>
<div class="card-body">
<form asp-action="ManagePage" method="post" id="managePageForm" enctype="multipart/form-data" novalidate>
<input asp-for="Id" type="hidden">
<input asp-for="IsNewPage" type="hidden">
<!-- Progress Bar -->
@if (Model.IsNewPage)
{
<div class="mb-4">
<div class="progress" style="height: 6px;">
<div class="progress-bar" role="progressbar" style="width: 25%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between mt-2">
<small class="text-muted">Passo 1 de 5</small>
<small class="text-muted">Informações Básicas</small>
</div>
</div>
}
<!-- Accordion -->
<div class="accordion" id="pageWizard">
<!-- Passo 1: Informações Básicas -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingBasic">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseBasic" aria-expanded="true" aria-controls="collapseBasic">
<i class="fas fa-info-circle me-2"></i>
Passo 1: Informações Básicas
<span class="badge bg-success ms-auto me-3" id="step1Status" style="display: none;">✓</span>
</button>
</h2>
<div id="collapseBasic" class="accordion-collapse collapse show" aria-labelledby="headingBasic" data-bs-parent="#pageWizard">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="DisplayName" class="form-label">Nome da Página <span class="text-danger">*</span></label>
<input asp-for="DisplayName" class="form-control" placeholder="Ex: João Silva" required>
<span asp-validation-for="DisplayName" class="text-danger"></span>
<div class="form-text">Nome que aparecerá no topo da sua página</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="Category" class="form-label">Categoria <span class="text-danger">*</span></label>
<select asp-for="Category" class="form-select" required>
<option value="">Selecione uma categoria</option>
@foreach (var category in Model.AvailableCategories)
{
<option value="@category.Name">@category.Name</option>
}
</select>
<span asp-validation-for="Category" class="text-danger"></span>
<div class="form-text">Categoria define o tipo da sua página</div>
</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" selected>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">@SlugHelper.CreateCategorySlug(Model.Category)</span>
<span class="input-group-text">/</span>
<input type="text" class="form-control" id="slugPreview" value="@Model.Slug" readonly>
<span class="input-group-text" id="slugValidationIcon" style="display: none;">
<i id="slugIcon" class=""></i>
</span>
<input asp-for="Slug" type="hidden">
</div>
<small class="form-text" id="slugValidationMessage">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="3" 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 200 caracteres</div>
</div>
<!-- Profile Image Upload -->
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label class="form-label">
<i class="fas fa-camera me-2"></i>
Foto de Perfil (Opcional)
</label>
<input type="file" class="form-control" id="profileImageInput" name="ProfileImageFile" accept="image/*">
<div class="form-text">
<i class="fas fa-info-circle me-1"></i>
Formatos aceitos: JPG, PNG, GIF. Máximo: 2MB e 4000x4000px.
</div>
<span class="text-danger" id="imageError"></span>
<div class="invalid-feedback" id="imageErrorFeedback" style="display: none;">
Erro com a imagem selecionada
</div>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<div class="profile-image-preview">
<img id="imagePreview" src="@Model.ProfileImageUrl" alt="Preview" class="img-thumbnail profile-preview-img">
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-danger" id="removeImageBtn" style="@(string.IsNullOrEmpty(Model.ProfileImageId) ? "display: none;" : "")">
<i class="fas fa-trash"></i> Remover
</button>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" asp-for="ProfileImageId" id="profileImageId">
<div class="text-end">
<button type="button" class="btn btn-primary" onclick="nextStep(2)">
Próximo <i class="fas fa-arrow-right ms-1"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Passo 2: Tema Visual -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingTheme">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTheme" aria-expanded="false" aria-controls="collapseTheme">
<i class="fas fa-palette me-2"></i>
Passo 2: Tema Visual
<span class="badge bg-success ms-auto me-3" id="step2Status" style="display: none;">✓</span>
</button>
</h2>
<div id="collapseTheme" class="accordion-collapse collapse" aria-labelledby="headingTheme" data-bs-parent="#pageWizard">
<div class="accordion-body">
<p class="text-muted mb-4">Escolha um tema que combine com sua personalidade ou marca:</p>
<!-- Container com scroll para os temas -->
<div class="themes-container" style="max-height: 400px; overflow-y: auto; overflow-x: hidden; padding-right: 10px;">
@{
var themeCount = 0;
}
@foreach (var theme in Model.AvailableThemes)
{
@if (themeCount % 4 == 0)
{
@if (themeCount > 0)
{
@:</div>
}
@:<div class="row">
}
<div class="col-md-4 col-lg-3 mb-3">
<div class="theme-card @(Model.SelectedTheme.ToLower() == theme.Name.ToLower() ? "selected" : "")" data-theme="@theme.Name.ToLower()">
<div class="theme-preview" style="background: @theme.BackgroundColor; color: @theme.TextColor;">
<div class="theme-header" style="background-color: @theme.PrimaryColor;">
<div class="theme-avatar"></div>
<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>
themeCount++;
}
@if (Model.AvailableThemes.Any())
{
@:</div>
}
</div> <!-- /themes-container -->
<input asp-for="SelectedTheme" type="hidden">
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(1)">
<i class="fas fa-arrow-left me-1"></i> Anterior
</button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)">
Próximo <i class="fas fa-arrow-right ms-1"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Passo 3: Documentos PDF (Premium) -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingDocuments">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDocuments" aria-expanded="false" aria-controls="collapseDocuments">
<i class="fas fa-file-pdf me-2"></i>
Passo 3: Documentos PDF (Premium)
<span class="badge bg-success ms-auto me-3" id="step3Status" style="display: none;">✓</span>
</button>
</h2>
<div id="collapseDocuments" class="accordion-collapse collapse" aria-labelledby="headingDocuments" data-bs-parent="#pageWizard">
<div class="accordion-body">
@if (!Model.AllowDocumentUpload)
{
<!-- Alerta amarelo APENAS para quem NÃO tem Premium -->
<div class="alert alert-warning border-start border-4 border-warning d-flex align-items-start mb-4">
<i class="fas fa-crown me-3 mt-1 text-warning" style="font-size: 1.2rem;"></i>
<div class="flex-grow-1">
<h6 class="mb-1 fw-bold">
<i class="fas fa-file-pdf me-1"></i> Anexar PDFs é exclusivo dos planos Premium
</h6>
<p class="mb-2 small text-muted">
Compartilhe apresentações, catálogos, portfólios e documentos diretamente na sua página profissional.
</p>
<div class="d-flex align-items-center gap-2 flex-wrap mb-3">
<span class="badge bg-light text-dark border">
<i class="fas fa-check-circle text-success me-1"></i> Plano Premium: até 5 PDFs
</span>
<span class="badge bg-light text-dark border">
<i class="fas fa-check-circle text-success me-1"></i> Plano Premium + Afiliados: até 10 PDFs
</span>
</div>
<a asp-controller="Home" asp-action="Pricing" class="btn btn-warning btn-sm">
<i class="fas fa-arrow-up me-1"></i>Fazer upgrade e desbloquear PDFs
</a>
</div>
</div>
}
@if (Model.AllowDocumentUpload)
{
<p class="text-muted mb-3">Anexe PDFs com apresentações, catálogos ou materiais exclusivos para quem acessar sua página Premium.</p>
@if (Model.MaxDocumentsAllowed > 0)
{
<div class="alert alert-light border-start border-3 border-primary py-2 small">
<i class="fas fa-info-circle me-2 text-primary"></i>
Você pode anexar até <strong>@Model.MaxDocumentsAllowed</strong> documento(s) no seu plano atual.
</div>
}
<div id="documentsContainer">
@if (Model.Documents != null && Model.Documents.Count > 0)
{
for (var i = 0; i < Model.Documents.Count; i++)
{
<div class="document-input-group border rounded p-3 mb-3" data-document="@i">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="mb-1">Documento @(i + 1)</h6>
@if (Model.Documents[i].UploadedAt.HasValue)
{
<small class="text-muted">Atualizado em @Model.Documents[i].UploadedAt.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</small>
}
</div>
<div class="btn-group">
@if (!string.IsNullOrEmpty(Model.Documents[i].DocumentId))
{
<a class="btn btn-sm btn-outline-primary" href="/api/document/@Model.Documents[i].DocumentId" target="_blank">
<i class="fas fa-file-pdf me-1"></i> Ver PDF
</a>
}
<button type="button" class="btn btn-sm btn-outline-danger remove-document-btn">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Título <span class="text-danger">*</span></label>
<input asp-for="Documents[i].Title" class="form-control" placeholder="Ex: Apresentação de Serviços">
<span asp-validation-for="Documents[i].Title" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-label">Descrição (opcional)</label>
<textarea asp-for="Documents[i].Description" class="form-control" rows="2" placeholder="Resumo do conteúdo"></textarea>
<span asp-validation-for="Documents[i].Description" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-label">Arquivo PDF</label>
@if (!string.IsNullOrEmpty(Model.Documents[i].DocumentId))
{
<div class="bg-light border rounded p-3 mb-2 document-file-info">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>@Model.Documents[i].FileName</strong>
<div class="small text-muted">@((Model.Documents[i].FileSize / 1024.0).ToString("0.#")) KB</div>
</div>
<span class="badge bg-primary-subtle text-primary">PDF</span>
</div>
</div>
<input asp-for="Documents[i].DocumentFile" class="form-control" type="file" accept="application/pdf">
<small class="form-text text-muted">Envie outro PDF para substituir o arquivo atual (máx. 10MB).</small>
}
else
{
<input asp-for="Documents[i].DocumentFile" class="form-control" type="file" accept="application/pdf">
<small class="form-text text-muted">Envie um arquivo PDF (máx. 10MB).</small>
}
<span asp-validation-for="Documents[i].DocumentFile" class="text-danger"></span>
</div>
<input asp-for="Documents[i].Id" type="hidden">
<input asp-for="Documents[i].DocumentId" type="hidden">
<input asp-for="Documents[i].FileName" type="hidden">
<input asp-for="Documents[i].FileSize" type="hidden">
<input asp-for="Documents[i].UploadedAt" type="hidden">
<input asp-for="Documents[i].MarkForRemoval" type="hidden">
</div>
}
<div class="alert alert-info" id="documentsEmptyState" style="display: none;">
<i class="fas fa-folder-open me-2"></i>Nenhum documento adicionado ainda.
</div>
}
else
{
<div class="alert alert-info" id="documentsEmptyState">
<i class="fas fa-folder-open me-2"></i>Nenhum documento adicionado ainda.
</div>
}
</div>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-3">
<small class="text-muted">Os documentos são exibidos em ordem de cadastro. Utilize títulos claros para facilitar o acesso.</small>
<button type="button" class="btn btn-outline-primary" id="addDocumentBtn">
<i class="fas fa-plus me-2"></i>Adicionar Documento
</button>
</div>
}
else
{
<div class="alert alert-warning d-flex align-items-start">
<i class="fas fa-crown me-2 mt-1"></i>
<div>
Upload de PDFs disponível apenas nos planos <strong>@Model.DocumentUploadPlansDisplay</strong>.
<div class="mt-2">
<a asp-controller="Home" asp-action="Pricing" class="btn btn-warning btn-sm">
<i class="fas fa-arrow-up me-1"></i>Fazer upgrade agora
</a>
</div>
</div>
</div>
}
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(2)">
<i class="fas fa-arrow-left me-1"></i> Anterior
</button>
<button type="button" class="btn btn-primary" onclick="nextStep(4)">
Próximo <i class="fas fa-arrow-right ms-1"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Passo 4: Links Principais -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingLinks">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLinks" aria-expanded="false" aria-controls="collapseLinks">
<i class="fas fa-link me-2"></i>
Passo 4: Links Principais
<span class="badge bg-success ms-auto me-3" id="step4Status" style="display: none;">✓</span>
</button>
</h2>
<div id="collapseLinks" class="accordion-collapse collapse" aria-labelledby="headingLinks" data-bs-parent="#pageWizard">
<div class="accordion-body">
<p class="text-muted mb-4">Adicione os links mais importantes (Máximo: @Model.MaxLinksAllowed):</p>
<div id="linksContainer">
@for (int i = 0; i < Model.Links.Count; i++)
{
var myList = new List<string>()
{
"facebook",
"whatsapp",
"twitter",
"instagram",
"tiktok",
"pinterest",
"discord",
"kawai"
};
var match = myList.FirstOrDefault(stringToCheck =>
!string.IsNullOrEmpty(Model.Links[i].Icon) &&
Model.Links[i].Icon.Contains(stringToCheck));
if (match==null)
{
if (Model.Links[i].Type==LinkType.Normal)
{
<div class="link-input-group" data-link="@i">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Link @(i + 1)</h6>
<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 asp-for="Links[i].Title" class="form-control link-title" placeholder="Ex: Meu Site" readonly>
<span asp-validation-for="Links[i].Title" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-2">
<label class="form-label">URL</label>
<input asp-for="Links[i].Url" class="form-control link-url" placeholder="https://exemplo.com" readonly>
<span asp-validation-for="Links[i].Url" class="text-danger"></span>
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label">Descrição (opcional)</label>
<input asp-for="Links[i].Description" class="form-control link-description" placeholder="Breve descrição do link" readonly>
</div>
<input asp-for="Links[i].Id" type="hidden">
<input asp-for="Links[i].Icon" type="hidden">
<input asp-for="Links[i].Order" type="hidden">
<input asp-for="Links[i].IsActive" type="hidden" value="true">
</div>
}
else
{
<div class="link-input-group product-link-preview" data-link="@i">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Afiliado @(i + 1)
</h6>
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="card border-success">
<div class="row g-0">
<div class="col-md-3">
<div class="p-3 text-center">
@if (!string.IsNullOrEmpty(Model.Links[i].ProductImage))
{
<img src="@Model.Links[i].ProductImage"
class="img-fluid rounded"
style="max-height: 80px; max-width: 100%;"
onerror="this.style.display='none'; this.parentNode.innerHTML='<i class=\'fas fa-image text-muted\'></i><br><small class=\'text-muted\'>Sem imagem</small>';" />
}
else
{
<i class="fas fa-image text-muted fa-2x"></i>
<br>
<small class="text-muted">Sem imagem</small>
}
</div>
</div>
<div class="col-md-9">
<div class="card-body">
<h6 class="card-title text-success">@Model.Links[i].Title</h6>
@if (!string.IsNullOrEmpty(Model.Links[i].ProductPrice))
{
<p class="card-text"><strong class="text-success">@Model.Links[i].ProductPrice</strong></p>
}
@if (!string.IsNullOrEmpty(Model.Links[i].ProductDescription))
{
<p class="card-text small text-muted">@Model.Links[i].ProductDescription</p>
}
<small class="text-muted d-block">
<i class="fas fa-external-link-alt me-1"></i>
@(Model.Links[i].Url.Length > 50 ? $"{Model.Links[i].Url.Substring(0, 50)}..." : Model.Links[i].Url)
</small>
</div>
</div>
</div>
</div>
<!-- Hidden fields for form submission -->
<input type="hidden" name="Links[@i].Id" value="@Model.Links[i].Id">
<input type="hidden" name="Links[@i].Title" value="@Model.Links[i].Title">
<input type="hidden" name="Links[@i].Url" value="@Model.Links[i].Url">
<input type="hidden" name="Links[@i].Description" value="@Model.Links[i].Description">
<input type="hidden" name="Links[@i].Type" value="Product">
<input type="hidden" name="Links[@i].ProductTitle" value="@Model.Links[i].Title">
<input type="hidden" name="Links[@i].ProductDescription" value="@Model.Links[i].ProductDescription">
<input type="hidden" name="Links[@i].ProductPrice" value="@Model.Links[i].ProductPrice">
<input type="hidden" name="Links[@i].ProductImage" value="@Model.Links[i].ProductImage">
<input type="hidden" name="Links[@i].Icon" value="fas fa-shopping-bag">
<input type="hidden" name="Links[@i].Order" value="@i">
<input type="hidden" name="Links[@i].IsActive" value="true">
</div>
}
}
}
</div>
<button type="button" class="btn btn-outline-primary mb-4" id="addLinkBtn" data-bs-toggle="modal" data-bs-target="#addLinkModal">
<i class="fas fa-plus"></i> Adicionar Link
</button>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(3)">
<i class="fas fa-arrow-left me-1"></i> Anterior
</button>
<button type="button" class="btn btn-primary" onclick="nextStep(5)">
Próximo <i class="fas fa-arrow-right ms-1"></i>
</button>
</div>
</div>
</div>
</div>
@{
var facebook = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("facebook")).FirstOrDefault();
var twitter = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("twitter")).FirstOrDefault();
var whatsapp = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("whatsapp")).FirstOrDefault();
var instagram = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("instagram")).FirstOrDefault();
var tiktok = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("tiktok")).FirstOrDefault();
var pinterest = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("pinterest")).FirstOrDefault();
var discord = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("discord")).FirstOrDefault();
var kawai = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("kawai")).FirstOrDefault();
var facebookUrl = facebook !=null ? facebook.Url.Replace("https://facebook.com/","").Replace("https://www.facebook.com/","").Replace("https://fb.com/","") : "";
var twitterUrl = twitter !=null ? twitter.Url.Replace("https://x.com/","").Replace("https://twitter.com/","").Replace("https://www.twitter.com/","") : "";
var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","").Replace("whatsapp://","") : "";
var instagramUrl = instagram !=null ? instagram.Url.Replace("https://instagram.com/","").Replace("https://www.instagram.com/","") : "";
var tiktokUrl = tiktok !=null ? tiktok.Url.Replace("https://tiktok.com/@","").Replace("https://www.tiktok.com/@","").Replace("https://vm.tiktok.com/","") : "";
var pinterestUrl = pinterest !=null ? pinterest.Url.Replace("https://pinterest.com/","").Replace("https://www.pinterest.com/","").Replace("https://pin.it/","") : "";
var discordUrl = discord !=null ? discord.Url.Replace("https://discord.gg/","").Replace("https://discord.com/invite/","") : "";
var kawaiUrl = kawai !=null ? kawai.Url.Replace("https://kawai.com/","").Replace("https://www.kawai.com/","") : "";
}
<!-- Passo 5: Redes Sociais (Opcional) -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingSocial">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSocial" aria-expanded="false" aria-controls="collapseSocial">
<i class="fab fa-twitter me-2"></i>
Passo 5: Redes Sociais (Opcional)
<span class="badge bg-success ms-auto me-3" id="step5Status" style="display: none;">✓</span>
</button>
</h2>
<div id="collapseSocial" class="accordion-collapse collapse" aria-labelledby="headingSocial" data-bs-parent="#pageWizard">
<div class="accordion-body">
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle me-2"></i>
<strong>Redes Sociais Opcionais</strong>
<p class="mb-0 mt-1">Marque apenas as redes sociais que você quer conectar. Todas são opcionais e você pode pular esta etapa.</p>
</div>
<div class="row">
<div class="col-lg-6">
<!-- WhatsApp -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableWhatsApp">
<label class="form-check-label" for="enableWhatsApp">
<i class="fab fa-whatsapp text-success me-2"></i>
<strong>Conectar WhatsApp</strong>
</label>
</div>
<div class="input-group social-input-group" id="whatsappGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-whatsapp text-success me-2"></i>
https://wa.me/
</span>
<input type="text" class="form-control" id="whatsappNumber" placeholder="11987654321">
</div>
<small class="form-text text-muted">
<strong>Brasil:</strong> Digite apenas DDD + número (ex: 11987654321). O código 55 será adicionado automaticamente.<br>
<strong>Outros países:</strong> Digite o código do país + número completo.
</small>
<input asp-for="WhatsAppNumber" type="hidden" value="@(whatsappUrl ?? "")">
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
</div>
<!-- Facebook -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableFacebook">
<label class="form-check-label" for="enableFacebook">
<i class="fab fa-facebook text-primary me-2"></i>
<strong>Conectar Facebook</strong>
</label>
</div>
<div class="input-group social-input-group" id="facebookGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-facebook text-primary me-2"></i>
https://facebook.com/
</span>
<input type="text" class="form-control" id="facebookUser" placeholder="seu-usuario">
</div>
<input asp-for="FacebookUrl" type="hidden" value="@(facebookUrl ?? "")">
<span asp-validation-for="FacebookUrl" class="text-danger"></span>
</div>
</div>
<div class="col-lg-6">
<!-- Instagram -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableInstagram">
<label class="form-check-label" for="enableInstagram">
<i class="fab fa-instagram text-danger me-2"></i>
<strong>Conectar Instagram</strong>
</label>
</div>
<div class="input-group social-input-group" id="instagramGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-instagram text-danger me-2"></i>
https://instagram.com/
</span>
<input type="text" class="form-control" id="instagramUser" placeholder="seu-usuario">
</div>
<input asp-for="InstagramUrl" type="hidden" value="@(instagramUrl ?? "")">
<span asp-validation-for="InstagramUrl" class="text-danger"></span>
</div>
<!-- X / Twitter -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableTwitter">
<label class="form-check-label" for="enableTwitter">
<i class="fab fa-x-twitter me-2"></i>
<strong>Conectar X / Twitter</strong>
</label>
</div>
<div class="input-group social-input-group" id="twitterGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-x-twitter me-2"></i>
https://x.com/
</span>
<input type="text" class="form-control" id="twitterUser" placeholder="seu-usuario">
</div>
<input asp-for="TwitterUrl" type="hidden" value="@(twitterUrl ?? "")">
<span asp-validation-for="TwitterUrl" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<!-- TikTok -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableTiktok">
<label class="form-check-label" for="enableTiktok">
<i class="fab fa-tiktok me-2"></i>
<strong>Conectar TikTok</strong>
</label>
</div>
<div class="input-group social-input-group" id="tiktokGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-tiktok me-2"></i>
https://tiktok.com/@@
</span>
<input type="text" class="form-control" id="tiktokUser" placeholder="seu-usuario">
</div>
<input asp-for="TiktokUrl" type="hidden" value="@(tiktokUrl ?? "")">
<span asp-validation-for="TiktokUrl" class="text-danger"></span>
</div>
<!-- Pinterest -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enablePinterest">
<label class="form-check-label" for="enablePinterest">
<i class="fab fa-pinterest me-2"></i>
<strong>Conectar Pinterest</strong>
</label>
</div>
<div class="input-group social-input-group" id="pinterestGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-pinterest me-2"></i>
https://pinterest.com/
</span>
<input type="text" class="form-control" id="pinterestUser" placeholder="seu-usuario">
</div>
<input asp-for="PinterestUrl" type="hidden" value="@(pinterestUrl ?? "")">
<span asp-validation-for="PinterestUrl" class="text-danger"></span>
</div>
</div>
<div class="col-lg-6">
<!-- Discord -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableDiscord">
<label class="form-check-label" for="enableDiscord">
<i class="fab fa-discord me-2"></i>
<strong>Conectar Discord</strong>
</label>
</div>
<div class="input-group social-input-group" id="discordGroup" style="display: none;">
<span class="input-group-text">
<i class="fab fa-discord me-2"></i>
https://discord.gg/
</span>
<input type="text" class="form-control" id="discordUser" placeholder="codigo-convite">
</div>
<input asp-for="DiscordUrl" type="hidden" value="@(discordUrl ?? "")">
<span asp-validation-for="DiscordUrl" class="text-danger"></span>
</div>
<!-- Kawai -->
<div class="mb-4">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enableKawai">
<label class="form-check-label" for="enableKawai">
<i class="fas fa-heart me-2"></i>
<strong>Conectar Kawai</strong>
</label>
</div>
<div class="input-group social-input-group" id="kawaiGroup" style="display: none;">
<span class="input-group-text">
<i class="fas fa-heart me-2"></i>
https://kawai.com/
</span>
<input type="text" class="form-control" id="kawaiUser" placeholder="seu-usuario">
</div>
<input asp-for="KawaiUrl" type="hidden" value="@(kawaiUrl ?? "")">
<span asp-validation-for="KawaiUrl" class="text-danger"></span>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(4)">
<i class="fas fa-arrow-left me-1"></i> Anterior
</button>
<button type="submit" class="btn btn-success">
<i class="fas fa-@(Model.IsNewPage ? "rocket" : "save") me-2"></i>
@(Model.IsNewPage ? "Criar Página" : "Salvar Alterações")
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Modal para Adicionar Link -->
<div class="modal fade" id="addLinkModal" tabindex="-1" aria-labelledby="addLinkModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addLinkModalLabel">
<i class="fas fa-link me-2"></i>
Adicionar Novo Link
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addLinkForm">
<!-- Tipo de Link -->
<div class="mb-3">
<label class="form-label">Tipo de Link</label>
<div class="d-flex gap-2 flex-wrap">
<div class="form-check @(Model.AllowProductLinks ? "flex-fill" : "w-100")">
<input class="form-check-input" type="radio" name="linkType" id="linkTypeNormal" value="Normal" checked>
<label class="form-check-label w-100 p-2 border rounded" for="linkTypeNormal">
<i class="fas fa-link me-2"></i>
<strong>Link Normal</strong>
<div class="small text-muted">Link simples para sites, redes sociais, etc.</div>
</label>
</div>
<div class="form-check @(Model.AllowProductLinks ? "flex-fill" : "w-100 mt-2")" id="productLinkOption">
<input class="form-check-input" type="radio" name="linkType" id="linkTypeProduct" value="Product" @(!Model.AllowProductLinks ? "disabled" : "")>
<label class="form-check-label w-100 p-2 border rounded @(!Model.AllowProductLinks ? "position-relative" : "")" for="linkTypeProduct">
@if (!Model.AllowProductLinks)
{
<div class="position-absolute top-0 end-0 m-1" style="z-index: 2;">
<div class="bg-warning text-dark px-2 py-1 rounded shadow-sm d-flex align-items-center">
<i class="fas fa-crown me-1"></i>
<small class="fw-bold">Premium + Afiliados</small>
</div>
</div>
<div class="position-absolute top-0 start-0 w-100 h-100 bg-warning bg-opacity-5 rounded" style="z-index: 1;"></div>
}
<div class="@(!Model.AllowProductLinks ? "opacity-50" : "")">
<i class="fas fa-shopping-bag me-2"></i>
<strong>Link de Afiliado</strong>
<div class="small text-muted">Para produtos de e-commerce com preview</div>
</div>
</label>
</div>
@if (!Model.AllowProductLinks)
{
<div class="col-12 mt-2">
<div class="alert alert-warning small mb-0 d-flex align-items-center">
<i class="fas fa-crown text-warning me-2"></i>
<span class="flex-grow-1">
<strong>Links de afiliado</strong> disponíveis apenas no plano <strong>Premium + Afiliados</strong>.
</span>
<a asp-controller="Home" asp-action="Pricing" class="btn btn-warning btn-sm ms-2">
<i class="fas fa-arrow-up me-1"></i>Fazer Upgrade
</a>
</div>
</div>
}
</div>
</div>
<!-- Seção para Link Normal -->
<div id="normalLinkSection">
<div class="mb-3">
<label for="linkTitle" class="form-label">Título do Link <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="linkTitle" placeholder="Ex: Meu Site, Portfólio, Instagram..." required>
<div class="form-text">Nome que aparecerá no botão</div>
</div>
<div class="mb-3">
<label for="linkIcon" class="form-label">Tipo de Link <span class="text-danger">*</span></label>
<select class="form-select" id="linkIcon" required>
<option value="">Selecione o tipo de link</option>
<option value="fas fa-globe">🌐 Site Geral</option>
<option value="fas fa-shopping-cart">🛒 Loja/E-commerce</option>
<option value="fas fa-briefcase">💼 Portfólio</option>
<option value="fas fa-envelope">✉️ Email</option>
<option value="fas fa-phone">📞 Telefone</option>
<option value="fas fa-map-marker-alt">📍 Localização</option>
<option value="fab fa-youtube">📺 YouTube</option>
<option value="fab fa-linkedin">💼 LinkedIn</option>
<option value="fab fa-github">💻 GitHub</option>
<option value="fas fa-download">⬇️ Download</option>
<option value="fas fa-calendar">📅 Agenda</option>
<option value="fas fa-heart">❤️ Favorito</option>
</select>
<div class="form-text">Escolha o tipo para obter instruções específicas</div>
</div>
<div class="mb-3">
<label for="linkUrl" class="form-label">URL/Contato <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text bg-primary text-white fw-bold" id="urlPrefix">https://</span>
<input type="text" class="form-control" id="linkUrlInput" placeholder="exemplo.com" required>
</div>
<input type="hidden" id="linkUrl">
<div class="form-text" id="urlInstructions">Selecione primeiro o tipo de link acima</div>
</div>
<div class="mb-3">
<label for="linkDescription" class="form-label">Descrição (opcional)</label>
<input type="text" class="form-control" id="linkDescription" placeholder="Breve descrição do link">
<div class="form-text">Texto adicional que aparece abaixo do título</div>
</div>
</div>
<!-- Seção para Link de Afiliado -->
<div id="productLinkSection" style="display: none;">
<div class="mb-3">
<label for="productUrl" class="form-label">URL do Produto <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text bg-success text-white fw-bold">https://</span>
<input type="text" class="form-control" id="productUrlInput" placeholder="mercadolivre.com.br/produto..." required>
<button type="button" class="btn btn-outline-primary" id="extractProductBtn">
<i class="fas fa-magic"></i> Extrair Dados
</button>
</div>
<input type="hidden" id="productUrl">
<div class="form-text">
<small>
<strong>Suportamos:</strong> Mercado Livre, Amazon, Magazine Luiza, Americanas, Shopee, e outros e-commerces conhecidos.
</small>
</div>
</div>
<div id="extractLoading" style="display: none;" class="text-center my-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Carregando...</span>
</div>
<p class="mt-2 text-muted">Extraindo informações do produto...</p>
</div>
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="productTitle" class="form-label">Título do Produto <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="productTitle" maxlength="100" placeholder="Nome do produto" required>
</div>
<div class="mb-3">
<label for="productDescription" class="form-label">Descrição (Opcional)</label>
<textarea class="form-control" id="productDescription" rows="2" maxlength="200" placeholder="Breve descrição do produto"></textarea>
</div>
<div class="mb-3">
<label for="productPrice" class="form-label">Preço (Opcional)</label>
<input type="text" class="form-control" id="productPrice" placeholder="R$ 99,90">
</div>
</div>
<div class="col-md-4">
<label class="form-label">Imagem do Produto</label>
<div class="border rounded p-3 text-center">
<img id="productImagePreview" class="img-fluid rounded" style="display: none; max-height: 120px;">
<div id="productImagePlaceholder" class="text-muted">
<i class="fas fa-image fa-2x mb-2"></i>
<p class="small mb-0">A imagem será extraída automaticamente</p>
</div>
</div>
<input type="hidden" id="productImage">
</div>
</div>
<div class="alert alert-info small">
<i class="fas fa-info-circle me-1"></i>
<strong>Dica:</strong> Os dados serão extraídos automaticamente da página do produto.
Você pode editar manualmente se necessário.
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
<button type="button" class="btn btn-primary" id="saveLinkBtn">
<i class="fas fa-plus"></i> Adicionar Link
</button>
</div>
</div>
</div>
</div>
<style>
.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);
transform: translateY(-2px);
}
.theme-preview {
height: 100px;
position: relative;
padding: 0.75rem;
}
.theme-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
margin-bottom: 0.75rem;
}
.theme-avatar {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
}
.theme-header h6 {
margin: 0;
font-size: 0.7rem;
color: white;
}
.theme-links {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.theme-link {
height: 6px;
border-radius: 3px;
opacity: 0.8;
}
.theme-name {
padding: 0.5rem;
text-align: center;
font-weight: 500;
background-color: #f8f9fa;
font-size: 0.85rem;
}
.link-input-group {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.link-input-group:hover {
border-color: #007bff;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
}
.accordion-button:not(.collapsed) {
background-color: rgba(13, 110, 253, 0.1);
border-color: rgba(13, 110, 253, 0.25);
}
.progress-bar {
transition: width 0.6s ease;
}
.btn {
border: none !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Social Media Input Groups */
.social-input-group .input-group-text {
min-width: 180px;
justify-content: flex-start;
background-color: #f8f9fa;
border-right: none;
font-size: 0.9rem;
font-weight: 500;
}
.social-input-group .form-control {
border-left: none;
}
.social-input-group .form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.form-check-label {
cursor: pointer;
font-weight: 500;
transition: color 0.2s ease;
}
.form-check-label:hover {
color: #0056b3;
}
.form-check-input:checked + .form-check-label {
color: #198754;
}
.social-input-group {
transition: all 0.3s ease;
}
/* Estados de validação */
.form-control.is-valid {
border-color: #198754;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='m2.3 6.73.99-.99 1.99-1.99L6.98 2.99l-.99-.99L4.49 3.5 3.5 2.51 2.51 3.5z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-control.is-invalid {
border-color: #dc3545;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath d='m5.8 4.6 1.4 1.4m0 0 1.4 1.4m-1.4-1.4L5.8 8.4m1.4-1.4L8.6 5.6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
/* Product Link Preview Styles */
.product-link-preview {
background: rgba(25, 135, 84, 0.05);
border-color: rgba(25, 135, 84, 0.2);
}
.product-link-preview .card {
box-shadow: none;
background: white;
}
.product-link-preview .card-body {
padding: 1rem;
}
.product-link-preview .card-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.product-link-preview img {
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 4px;
}
/* Profile Image Upload Styles */
.profile-image-preview {
border: 2px dashed #dee2e6;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
background: rgba(13, 110, 253, 0.02);
}
.profile-image-preview:hover {
border-color: #007bff;
background: rgba(13, 110, 253, 0.05);
}
.profile-preview-img {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 50% !important;
border: 4px solid #fff !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: all 0.3s ease;
}
.profile-preview-img:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
}
#profileImageInput {
transition: border-color 0.3s ease;
}
#profileImageInput:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
#imageError {
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* URL Input Styling */
.input-group .input-group-text.bg-primary,
.input-group .input-group-text.bg-success {
border-right: 1px solid rgba(255,255,255,0.2);
font-weight: 600;
min-width: 85px;
justify-content: center;
}
.input-group .form-control {
border-left: none;
padding-left: 0.75rem;
}
.input-group .form-control:focus {
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
border-color: #86b7fe;
}
.input-group:focus-within .input-group-text {
border-color: #86b7fe;
}
</style>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script>
let linkCount = @Model.Links.Count;
let documentCount = @(Model.Documents?.Count ?? 0);
let currentStep = 1;
const totalSteps = 5;
$(document).ready(function() {
// Initialize social media fields
initializeSocialMedia();
// Initialize image upload
initializeImageUpload();
// Initialize URL input handlers
initializeUrlInputs();
// Initialize document handlers
initializeDocumentHandlers();
// Check for validation errors and show toast + open accordion
checkValidationErrors();
// Check for server-side image errors
checkServerImageErrors();
// Garantir que campos não marcados sejam string vazia ao submeter
$('form').on('submit', function(e) {
ensureUncheckedFieldsAreEmpty();
// Validar URLs de redes sociais antes de submeter
const validationResult = validateSocialMediaUrls();
if (!validationResult.valid) {
e.preventDefault();
alert('Erro de validação:\n\n' + validationResult.errors.join('\n'));
return false;
}
// Debug: Verificar quais campos de links estão sendo enviados
console.log('=== DEBUG FORM SUBMISSION ===');
const formData = new FormData(this);
for (let [key, value] of formData.entries()) {
if (key.includes('Links[') || key.includes('Url') || key.includes('Number')) {
console.log(`${key}: ${value}`);
}
}
console.log('=== FIM DEBUG ===');
});
// Generate slug when name or category changes
$('#DisplayName, #Category').on('input change', function() {
generateSlug();
updateProgress();
});
// Validar slug inicial (para páginas em edição)
if ($('#Slug').val() && $('#Category').val()) {
validateSlugAvailability($('#Category').val(), $('#Slug').val());
}
// Theme selection
$('.theme-card').on('click', function() {
$('.theme-card').removeClass('selected');
$(this).addClass('selected');
const themeName = $(this).data('theme');
$('#SelectedTheme').val(themeName);
markStepComplete(2);
});
// Add link functionality via modal
$('#addLinkBtn').on('click', function() {
const maxlinks = @Model.MaxLinksAllowed;
// Links do modal são limitados pelo MaxLinksAllowed (redes sociais são separadas e opcionais)
if (linkCount >= maxlinks) {
alert('Você atingiu o limite de ' + maxlinks + ' links para seu plano atual. As redes sociais do Passo 5 não contam neste limite.');
return false;
}
});
// Dynamic link type functionality
$('#linkIcon').on('change', function() {
updateLinkUrlField($(this).val());
});
// Toggle between link types
$('input[name="linkType"]').on('change', function() {
const linkType = $(this).val();
if (linkType === 'Product') {
$('#normalLinkSection').hide();
$('#productLinkSection').show();
} else {
$('#normalLinkSection').show();
$('#productLinkSection').hide();
}
});
// Extract product data
$('#extractProductBtn').on('click', function() {
const url = $('#productUrl').val().trim();
if (!url) {
alert('Por favor, insira a URL do produto.');
return;
}
extractProductData(url);
});
// Save link from modal
$(document).on('click', '#saveLinkBtn', function() {
const linkType = $('input[name="linkType"]:checked').val();
if (linkType === 'Product') {
saveProductLink();
} else {
saveNormalLink();
}
});
// Remove link functionality
$(document).on('click', '.remove-link-btn', function() {
$(this).closest('.link-input-group').remove();
linkCount--;
updateLinkNumbers();
});
// Form validation
$('#managePageForm').on('submit', function(e) {
console.log('Form submitted');
// 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...');
});
});
function nextStep(step) {
if (validateCurrentStep()) {
markStepComplete(currentStep);
currentStep = step;
updateProgress();
// Close current accordion and open next
$('.accordion-collapse.show').collapse('hide');
setTimeout(() => {
$(`#collapse${getStepName(step)}`).collapse('show');
}, 300);
}
}
function previousStep(step) {
currentStep = step;
updateProgress();
// Close current accordion and open previous
$('.accordion-collapse.show').collapse('hide');
setTimeout(() => {
$(`#collapse${getStepName(step)}`).collapse('show');
}, 300);
}
function skipStep(step) {
markStepComplete(step);
// Show create button or next step
updateProgress();
}
function getStepName(step) {
const names = ['', 'Basic', 'Theme', 'Documents', 'Links', 'Social'];
return names[step];
}
function validateCurrentStep() {
if (currentStep === 1) {
const name = $('#DisplayName').val().trim();
const category = $('#Category').val().trim();
if (!name || !category) {
alert('Por favor, preencha o nome da página e selecione uma categoria.');
return false;
}
} else if (currentStep === 2) {
const theme = $('#SelectedTheme').val();
if (!theme) {
alert('Por favor, selecione um tema visual.');
return false;
}
}
return true;
}
function markStepComplete(step) {
$(`#step${step}Status`).show();
}
function updateProgress() {
const progress = (currentStep / totalSteps) * 100;
$('.progress-bar').css('width', `${progress}%`).attr('aria-valuenow', progress);
$('.progress').next().find('small').first().text(`Passo ${currentStep} de ${totalSteps}`);
}
function generateSlug() {
const name = $('#DisplayName').val();
const category = $('#Category').val();
if (name && category) {
$.post('@Url.Action("GenerateSlug", "Admin")', { category: category, name: name })
.done(function(data) {
$('#Slug').val(data.slug);
$('#slugPreview').val(data.slug);
$('#categorySlug').text(data.category);
// Validar disponibilidade do slug gerado
validateSlugAvailability(category, data.slug);
});
}
}
function validateSlugAvailability(category, slug) {
if (!category || !slug) return;
const excludeId = '@Model.Id' !== '' ? '@Model.Id' : null;
// Mostrar indicador de carregamento
showSlugValidationStatus('loading', 'Verificando disponibilidade...');
$.post('@Url.Action("CheckSlugAvailability", "Admin")', {
category: category,
slug: slug,
excludeId: excludeId
})
.done(function(response) {
if (response.available) {
showSlugValidationStatus('success', 'URL disponível!');
} else {
showSlugValidationStatus('error', response.message || 'Esta URL já está em uso.');
}
})
.fail(function() {
showSlugValidationStatus('error', 'Erro ao verificar disponibilidade.');
});
}
function showSlugValidationStatus(type, message) {
const $icon = $('#slugIcon');
const $message = $('#slugValidationMessage');
const $iconContainer = $('#slugValidationIcon');
$iconContainer.show();
// Remover classes anteriores
$icon.removeClass('fas fa-check-circle fas fa-times-circle fas fa-spinner fa-spin text-success text-danger text-primary');
$message.removeClass('text-success text-danger text-primary text-muted');
switch(type) {
case 'success':
$icon.addClass('fas fa-check-circle text-success');
$message.addClass('text-success');
break;
case 'error':
$icon.addClass('fas fa-times-circle text-danger');
$message.addClass('text-danger');
break;
case 'loading':
$icon.addClass('fas fa-spinner fa-spin text-primary');
$message.addClass('text-primary');
break;
default:
$iconContainer.hide();
$message.addClass('text-muted');
}
$message.text(message);
}
function addLinkInput(title = '', url = '', description = '', icon = '', linkType = 'Normal', id='new') {
// Encontrar o próximo índice disponível baseado em todos os campos Links[] existentes
const existingIndexes = [];
$('input[name^="Links["]').each(function() {
const name = $(this).attr('name');
const match = name.match(/Links\[(\d+)\]/);
if (match) {
existingIndexes.push(parseInt(match[1]));
}
});
// Encontrar o próximo índice disponível
const nextIndex = existingIndexes.length > 0 ? Math.max(...existingIndexes) + 1 : 0;
const iconHtml = icon ? `<i class="${icon} me-2"></i>` : '';
const displayCount = $('.link-input-group').length + 1;
const linkHtml = `
<div class="link-input-group" data-link="${nextIndex}">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">
${iconHtml}Link ${displayCount}: ${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[${nextIndex}].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[${nextIndex}].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[${nextIndex}].Description" class="form-control link-description" value="${description}" placeholder="Breve descrição do link" readonly>
</div>
<input type="hidden" name="Links[${nextIndex}].Id" value="${id}">
<input type="hidden" name="Links[${nextIndex}].Type" value="${linkType}">
<input type="hidden" name="Links[${nextIndex}].Icon" value="${icon}">
<input type="hidden" name="Links[${nextIndex}].Order" value="${nextIndex}">
<input type="hidden" name="Links[${nextIndex}].IsActive" value="true">
</div>
`;
$('#linksContainer').append(linkHtml);
linkCount++;
}
function updateLinkNumbers() {
$('.link-input-group').each(function(index) {
$(this).find('h6').text('Link ' + (index + 1));
$(this).attr('data-link', index);
});
}
function initializeDocumentHandlers() {
const $documentsContainer = $('#documentsContainer');
const $addDocumentBtn = $('#addDocumentBtn');
if (!$documentsContainer.length) {
return;
}
toggleDocumentEmptyState();
toggleDocumentAddButton();
if ($addDocumentBtn.length) {
$addDocumentBtn.on('click', function() {
addDocumentInput();
});
}
$(document).on('click', '.remove-document-btn', function() {
const $group = $(this).closest('.document-input-group');
const $markRemoval = $group.find('input[name$=".MarkForRemoval"]');
const hasExisting = $group.find('input[name$=".DocumentId"]').val();
if (hasExisting) {
$group.addClass('document-removed d-none');
if ($markRemoval.length) {
$markRemoval.val('true');
}
$group.find('input[type="file"]').val('');
} else {
$group.remove();
documentCount = Math.max(documentCount - 1, 0);
}
updateDocumentNumbers();
toggleDocumentEmptyState();
toggleDocumentAddButton();
});
$(document).on('change', '.document-input-group input[type="file"]', function(e) {
const file = e.target.files[0];
if (!file) {
return;
}
if (file.type !== 'application/pdf') {
alert('Envie um arquivo em PDF.');
$(this).val('');
return;
}
if (file.size > 10 * 1024 * 1024) {
alert('Arquivo muito grande. Tamanho máximo: 10MB.');
$(this).val('');
}
});
}
function toggleDocumentEmptyState() {
const $container = $('#documentsContainer');
const $emptyState = $('#documentsEmptyState');
if (!$container.length || !$emptyState.length) {
return;
}
const visibleDocs = $container.find('.document-input-group').not('.document-removed');
if (visibleDocs.length === 0) {
$emptyState.show();
} else {
$emptyState.hide();
}
}
function toggleDocumentAddButton() {
const $button = $('#addDocumentBtn');
if (!$button.length) {
return;
}
const maxDocs = @Model.MaxDocumentsAllowed;
if (maxDocs <= 0) {
$button.prop('disabled', false).removeAttr('title');
return;
}
const visibleDocs = $('#documentsContainer .document-input-group').not('.document-removed').length;
if (visibleDocs >= maxDocs) {
$button.prop('disabled', true).attr('title', 'Você atingiu o limite de documentos do seu plano.');
} else {
$button.prop('disabled', false).removeAttr('title');
}
}
function addDocumentInput() {
const $container = $('#documentsContainer');
if (!$container.length) {
return;
}
const $button = $('#addDocumentBtn');
if ($button.length && $button.prop('disabled')) {
return;
}
const existingIndexes = [];
$('input[name^="Documents["]').each(function() {
const match = $(this).attr('name').match(/Documents\[(\d+)\]/);
if (match) {
existingIndexes.push(parseInt(match[1]));
}
});
const nextIndex = existingIndexes.length > 0 ? Math.max(...existingIndexes) + 1 : 0;
const displayCount = $container.find('.document-input-group').length + 1;
const template = `
<div class="document-input-group border rounded p-3 mb-3" data-document="${nextIndex}">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="mb-1">Documento ${displayCount}</h6>
</div>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-danger remove-document-btn">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Título <span class="text-danger">*</span></label>
<input type="text" name="Documents[${nextIndex}].Title" class="form-control" placeholder="Ex: Apresentação de Serviços">
</div>
<div class="mb-3">
<label class="form-label">Descrição (opcional)</label>
<textarea name="Documents[${nextIndex}].Description" class="form-control" rows="2" placeholder="Resumo do conteúdo"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Arquivo PDF</label>
<input type="file" name="Documents[${nextIndex}].DocumentFile" class="form-control" accept="application/pdf">
<small class="form-text text-muted">Envie um arquivo PDF (máx. 10MB).</small>
</div>
<input type="hidden" name="Documents[${nextIndex}].Id" value="">
<input type="hidden" name="Documents[${nextIndex}].DocumentId" value="">
<input type="hidden" name="Documents[${nextIndex}].FileName" value="">
<input type="hidden" name="Documents[${nextIndex}].FileSize" value="0">
<input type="hidden" name="Documents[${nextIndex}].UploadedAt" value="">
<input type="hidden" name="Documents[${nextIndex}].MarkForRemoval" value="false">
</div>`;
$container.append(template);
documentCount++;
$('#documentsEmptyState').hide();
toggleDocumentEmptyState();
toggleDocumentAddButton();
}
function updateDocumentNumbers() {
$('#documentsContainer .document-input-group').not('.document-removed').each(function(index) {
$(this).attr('data-document', index);
$(this).find('h6').text('Documento ' + (index + 1));
});
}
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;
}
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;
}
if (!title) {
alert('Por favor, preencha o título do produto.');
return;
}
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();
}
});
}
function addProductLinkInput(title, url, description, price, image, id='new') {
// Encontrar o próximo índice disponível baseado em todos os campos Links[] existentes
const existingIndexes = [];
$('input[name^="Links["]').each(function() {
const name = $(this).attr('name');
const match = name.match(/Links\[(\d+)\]/);
if (match) {
existingIndexes.push(parseInt(match[1]));
}
});
// Encontrar o próximo índice disponível
const nextIndex = existingIndexes.length > 0 ? Math.max(...existingIndexes) + 1 : 0;
const displayCount = $('.link-input-group').length + 1;
const linkHtml = `
<div class="link-input-group product-link-preview" data-link="${nextIndex}">
<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 Afiliado ${displayCount}
</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[${nextIndex}].Id" value="${id}">
<input type="hidden" name="Links[${nextIndex}].Title" value="${title}">
<input type="hidden" name="Links[${nextIndex}].Url" value="${url}">
<input type="hidden" name="Links[${nextIndex}].Description" value="${description}">
<input type="hidden" name="Links[${nextIndex}].Type" value="Product">
<input type="hidden" name="Links[${nextIndex}].ProductTitle" value="${title}">
<input type="hidden" name="Links[${nextIndex}].ProductDescription" value="${description}">
<input type="hidden" name="Links[${nextIndex}].ProductPrice" value="${price}">
<input type="hidden" name="Links[${nextIndex}].ProductImage" value="${image}">
<input type="hidden" name="Links[${nextIndex}].Icon" value="fas fa-shopping-bag">
<input type="hidden" name="Links[${nextIndex}].Order" value="${nextIndex}">
<input type="hidden" name="Links[${nextIndex}].IsActive" value="true">
</div>
`;
$('#linksContainer').append(linkHtml);
linkCount++;
markStepComplete(3);
}
function closeModalAndReset() {
// Clear modal form
$('#addLinkForm')[0].reset();
// Limpar campos de URL específicos
$('#linkUrlInput').val('');
$('#linkUrl').val('');
$('#productUrlInput').val('');
$('#productUrl').val('');
$('#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') {
// Simple toast notification
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="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
if (!$('#toastContainer').length) {
$('body').append('<div id="toastContainer" class="toast-container position-fixed top-0 end-0 p-3"></div>');
}
const $toast = $(toastHtml);
$('#toastContainer').append($toast);
const toast = new bootstrap.Toast($toast[0]);
toast.show();
setTimeout(() => {
$toast.remove();
}, 5000);
}
// Nova função para mostrar toast de erro com múltiplas mensagens
function showErrorToast(errors) {
if (!Array.isArray(errors)) {
errors = [errors];
}
const errorList = errors.map(error => `<li>${error}</li>`).join('');
const toastHtml = `
<div class="toast align-items-center text-white bg-danger border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<strong><i class="fas fa-exclamation-triangle me-2"></i>Erro de Validação</strong>
<ul class="mb-0 mt-2 small">
${errorList}
</ul>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
if (!$('#toastContainer').length) {
$('body').append('<div id="toastContainer" class="toast-container position-fixed top-0 end-0 p-3"></div>');
}
const $toast = $(toastHtml);
$('#toastContainer').append($toast);
const toast = new bootstrap.Toast($toast[0], { delay: 6000 }); // 6 segundos para erro
toast.show();
setTimeout(() => {
$toast.remove();
}, 7000);
}
// Validation Error Handling
function checkValidationErrors() {
// Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado)
// Verificamos se existe algum input com validation-error class ou summary de erros
const hasValidationSummary = $('.validation-summary-errors').length > 0;
const hasFieldErrors = $('.input-validation-error').length > 0;
if (!hasValidationSummary && !hasFieldErrors) {
return; // Não há erros reais de validação, sair
}
const errorElements = $('.text-danger:not(:empty)').filter(function() {
// Filtrar apenas spans que realmente têm mensagens de erro
const text = $(this).text().trim();
return text.length > 0;
});
if (errorElements.length > 0) {
// Find which accordion steps have errors
const stepsWithErrors = [];
errorElements.each(function() {
const $error = $(this);
const $accordion = $error.closest('.accordion-item');
if ($accordion.length > 0) {
const stepNumber = getStepNumber($accordion);
if (stepNumber && !stepsWithErrors.includes(stepNumber)) {
stepsWithErrors.push(stepNumber);
}
}
});
if (stepsWithErrors.length > 0) {
// Show validation error toast
showValidationErrorToast(stepsWithErrors);
// Open first accordion with error
const firstErrorStep = Math.min(...stepsWithErrors);
openAccordionStep(firstErrorStep);
}
}
}
function getStepNumber($accordion) {
const id = $accordion.find('.accordion-collapse').attr('id');
const stepMap = {
'collapseBasic': 1,
'collapseLinks': 3,
'collapseSocial': 4
};
return stepMap[id] || null;
}
function openAccordionStep(stepNumber) {
const stepMap = {
1: '#collapseBasic',
3: '#collapseLinks',
4: '#collapseSocial'
};
const targetId = stepMap[stepNumber];
if (targetId) {
$(targetId).collapse('show');
}
}
function showValidationErrorToast(stepsWithErrors) {
const stepNames = {
1: 'Informações Básicas',
3: 'Links',
4: 'Redes Sociais'
};
const errorStepNames = stepsWithErrors.map(step => stepNames[step]).join(', ');
const toastHtml = `
<div class="toast align-items-center text-bg-warning border-0" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999;">
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Erro de Validação</strong><br>
Verifique os campos em: ${errorStepNames}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
const $toast = $(toastHtml);
$('body').append($toast);
const toast = new bootstrap.Toast($toast[0], { delay: 4000 });
toast.show();
// Remove toast after it's hidden
$toast.on('hidden.bs.toast', function() {
$(this).remove();
});
}
// Garantir que campos não selecionados sejam string vazia
function ensureUncheckedFieldsAreEmpty() {
const socialFields = [
{ checkbox: '#enableWhatsApp', hidden: 'input[name="WhatsAppNumber"]' },
{ checkbox: '#enableFacebook', hidden: 'input[name="FacebookUrl"]' },
{ checkbox: '#enableInstagram', hidden: 'input[name="InstagramUrl"]' },
{ checkbox: '#enableTwitter', hidden: 'input[name="TwitterUrl"]' },
{ checkbox: '#enableTiktok', hidden: 'input[name="TiktokUrl"]' },
{ checkbox: '#enablePinterest', hidden: 'input[name="PinterestUrl"]' },
{ checkbox: '#enableDiscord', hidden: 'input[name="DiscordUrl"]' },
{ checkbox: '#enableKawai', hidden: 'input[name="KawaiUrl"]' }
];
socialFields.forEach(field => {
if (!$(field.checkbox).is(':checked')) {
$(field.hidden).val(' '); // Forçar espaço para campos não marcados
}
});
}
// Validar URLs de redes sociais antes de submeter o formulário
function validateSocialMediaUrls() {
const errors = [];
const userLang = navigator.language || navigator.userLanguage;
const isBrazil = userLang.startsWith('pt-BR') || userLang.startsWith('pt');
// Validar WhatsApp
if ($('#enableWhatsApp').is(':checked')) {
const whatsappValue = $('input[name="WhatsAppNumber"]').val().trim();
if (whatsappValue && whatsappValue !== ' ') {
const cleanNumber = whatsappValue.replace('https://wa.me/', '').replace(/\D/g, '');
if (isBrazil) {
// Brasil: deve ter 13 dígitos (55 + DDD + número)
if (cleanNumber.length !== 13 || !cleanNumber.startsWith('55')) {
errors.push('⚠️ WhatsApp: Número brasileiro deve ter 13 dígitos (55 + DDD + número).\nExemplo: 5511987654321');
}
} else {
// Outros países: validar entre 10 e 15 dígitos
if (cleanNumber.length < 10 || cleanNumber.length > 15) {
errors.push('⚠️ WhatsApp: Número deve ter entre 10 e 15 dígitos (incluindo código do país).');
}
}
}
}
// Validar Facebook
if ($('#enableFacebook').is(':checked')) {
const facebookValue = $('input[name="FacebookUrl"]').val().trim();
if (facebookValue && facebookValue !== ' ') {
const cleanUrl = facebookValue.replace('https://facebook.com/', '').replace('https://www.facebook.com/', '');
if (cleanUrl.length < 3) {
errors.push('⚠️ Facebook: Nome de usuário deve ter pelo menos 3 caracteres.');
}
}
}
// Validar Instagram
if ($('#enableInstagram').is(':checked')) {
const instagramValue = $('input[name="InstagramUrl"]').val().trim();
if (instagramValue && instagramValue !== ' ') {
const cleanUrl = instagramValue.replace('https://instagram.com/', '').replace('https://www.instagram.com/', '');
if (cleanUrl.length < 3) {
errors.push('⚠️ Instagram: Nome de usuário deve ter pelo menos 3 caracteres.');
}
}
}
// Validar Twitter
if ($('#enableTwitter').is(':checked')) {
const twitterValue = $('input[name="TwitterUrl"]').val().trim();
if (twitterValue && twitterValue !== ' ') {
const cleanUrl = twitterValue.replace('https://x.com/', '').replace('https://twitter.com/', '');
if (cleanUrl.length < 3) {
errors.push('⚠️ Twitter/X: Nome de usuário deve ter pelo menos 3 caracteres.');
}
}
}
// Validar TikTok
if ($('#enableTiktok').is(':checked')) {
const tiktokValue = $('input[name="TiktokUrl"]').val().trim();
if (tiktokValue && tiktokValue !== ' ') {
const cleanUrl = tiktokValue.replace('https://tiktok.com/@@', '').replace('https://www.tiktok.com/@@', '');
if (cleanUrl.length < 3) {
errors.push('⚠️ TikTok: Nome de usuário deve ter pelo menos 3 caracteres.');
}
}
}
return {
valid: errors.length === 0,
errors: errors
};
}
// Social Media Functions
function cleanSocialPrefix(value, socialType) {
const prefixes = {
'Facebook': ['https://facebook.com/', 'https://www.facebook.com/', 'https://fb.com/', 'https://www.fb.com/'],
'Instagram': ['https://instagram.com/', 'https://www.instagram.com/', 'https://instagr.am/'],
'Twitter': ['https://x.com/', 'https://twitter.com/', 'https://www.twitter.com/', 'https://www.x.com/'],
'WhatsApp': ['https://wa.me/', 'whatsapp://', 'https://api.whatsapp.com/', 'wa.me/'],
'Tiktok': ['https://tiktok.com/@@', 'https://www.tiktok.com/@@', 'https://vm.tiktok.com/'],
'Pinterest': ['https://pinterest.com/', 'https://www.pinterest.com/', 'https://pin.it/'],
'Discord': ['https://discord.gg/', 'https://discord.com/invite/'],
'Kawai': ['https://kawai.com/', 'https://www.kawai.com/']
};
const typePrefixes = prefixes[socialType] || [];
for (let prefix of typePrefixes) {
if (value.startsWith(prefix)) {
value = value.replace(prefix, '');
break;
}
}
// TikTok: remover @@ do início (o prefixo já tem @@)
if (socialType === 'Tiktok' && value.startsWith('@@')) {
value = value.substring(1);
}
return value;
}
function initializeSocialMedia() {
// WhatsApp
setupSocialField('WhatsApp', 'WhatsAppNumber', 'https://wa.me/', true);
// Facebook
setupSocialField('Facebook', 'FacebookUrl', 'https://facebook.com/', false);
// Instagram
setupSocialField('Instagram', 'InstagramUrl', 'https://instagram.com/', false);
// Twitter
setupSocialField('Twitter', 'TwitterUrl', 'https://x.com/', false);
// TikTok
setupSocialField('Tiktok', 'TiktokUrl', 'https://tiktok.com/@@', false);
// Pinterest
setupSocialField('Pinterest', 'PinterestUrl', 'https://pinterest.com/', false);
// Discord
setupSocialField('Discord', 'DiscordUrl', 'https://discord.gg/', false);
// Kawai
setupSocialField('Kawai', 'KawaiUrl', 'https://kawai.com/', false);
}
function setupSocialField(name, hiddenFieldName, prefix, isWhatsApp) {
const checkbox = $(`#enable${name}`);
const groupName = name.toLowerCase();
const group = $(`#${groupName}Group`);
const userInput = isWhatsApp ? $(`#${groupName}Number`) : $(`#${groupName}User`);
const hiddenField = $(`input[name="${hiddenFieldName}"]`);
// SEMPRE garantir que hidden field tenha um valor (espaço se vazio)
if (!hiddenField.val() || hiddenField.val().trim() === '') {
hiddenField.val(' '); // Espaço em branco para evitar null
}
// Verificar se já tem valor e configurar estado inicial
const currentValue = hiddenField.val();
if (currentValue && currentValue.trim() !== '') {
checkbox.prop('checked', true);
group.show();
// Limpar qualquer prefixo conhecido do valor atual
const cleanValue = cleanSocialPrefix(currentValue, name);
userInput.val(cleanValue);
} else {
// Se não tem valor, garantir que o hidden field seja espaço
hiddenField.val(' ');
}
// Toggle visibility
checkbox.on('change', function() {
if ($(this).is(':checked')) {
group.slideDown(200);
userInput.focus();
} else {
group.slideUp(200);
userInput.val('');
hiddenField.val(' '); // Espaço em branco para evitar null - sobrescreve valor existente
}
});
// Atualizar campo hidden em tempo real
userInput.on('input', function() {
let value = $(this).val().trim();
// Limpar qualquer prefixo conhecido
const cleanValue = cleanSocialPrefix(value, name);
if (cleanValue !== value) {
$(this).val(cleanValue);
value = cleanValue;
}
if (isWhatsApp) {
// WhatsApp: apenas números
value = value.replace(/\D/g, '');
// Auto-adicionar código +55 para números brasileiros (11 dígitos sem código país)
// Detecta cultura pt-BR para aplicar validação BR
const userLang = navigator.language || navigator.userLanguage;
const isBrazil = userLang.startsWith('pt-BR') || userLang.startsWith('pt');
if (isBrazil && value.length === 11 && !value.startsWith('55')) {
value = '55' + value;
}
$(this).val(value);
}
// Atualizar campo hidden - SEMPRE string, nunca null
if (value) {
hiddenField.val(prefix + value);
} else {
hiddenField.val(' '); // Espaço em branco para evitar null
}
// Feedback visual
updateSocialFieldFeedback(userInput, value, isWhatsApp);
});
}
function updateSocialFieldFeedback(input, value, isWhatsApp) {
// Remover classes anteriores
input.removeClass('is-valid is-invalid');
if (!value) return;
if (isWhatsApp) {
// Validar WhatsApp com detecção de idioma
const userLang = navigator.language || navigator.userLanguage;
const isBrazil = userLang.startsWith('pt-BR') || userLang.startsWith('pt');
if (isBrazil) {
// Brasil: validar 13 dígitos (55 + DDD + número)
if (value.length === 13 && value.startsWith('55')) {
input.addClass('is-valid');
} else if (value.length >= 10) {
// Ainda está digitando ou formato incorreto
input.addClass('is-invalid');
}
} else {
// Outros países: validar mínimo 10 dígitos (genérico)
if (value.length >= 10 && value.length <= 15) {
input.addClass('is-valid');
} else if (value.length > 0) {
input.addClass('is-invalid');
}
}
} else {
// Validar username (mínimo 3 caracteres, sem espaços)
if (value.length >= 3 && !/\s/.test(value)) {
input.addClass('is-valid');
} else {
input.addClass('is-invalid');
}
}
}
// Dynamic Link Type Functions
function updateLinkUrlField(iconValue) {
const $prefix = $('#urlPrefix');
const $input = $('#linkUrlInput');
const $instructions = $('#urlInstructions');
// Configurações por tipo de ícone
const linkTypes = {
'fas fa-envelope': {
prefix: 'mailto:',
placeholder: 'seuemail@@exemplo.com',
instructions: 'Digite apenas o email (sem mailto:)',
color: 'bg-success'
},
'fas fa-phone': {
prefix: 'tel:',
placeholder: '5511999999999',
instructions: 'Digite o telefone com código do país e DDD (apenas números)',
color: 'bg-success'
},
'fab fa-youtube': {
prefix: 'https://youtube.com/',
placeholder: 'watch?v=VIDEO_ID ou @@usuario ou c/CANAL',
instructions: 'Digite o ID do vídeo, @@usuário ou c/canal',
color: 'bg-danger'
},
'fab fa-linkedin': {
prefix: 'https://linkedin.com/in/',
placeholder: 'seu-perfil-linkedin',
instructions: 'Digite apenas seu nome de usuário do LinkedIn',
color: 'bg-primary'
},
'fab fa-github': {
prefix: 'https://github.com/',
placeholder: 'usuario ou usuario/repositorio',
instructions: 'Digite seu usuário ou usuário/repositório',
color: 'bg-dark'
},
'fas fa-map-marker-alt': {
prefix: 'https://maps.google.com/?q=',
visualPrefix: '📍 Maps:',
placeholder: 'Rua das Flores, 123 - São Paulo, SP',
instructions: 'Digite o endereço completo (acentos e espaços serão codificados automaticamente)',
color: 'bg-warning'
},
'fas fa-globe': {
prefix: 'https://',
placeholder: 'exemplo.com',
instructions: 'Digite apenas o domínio e caminho (sem https://)',
color: 'bg-primary'
},
'fas fa-shopping-cart': {
prefix: 'https://',
placeholder: 'minhaloja.com/produto',
instructions: 'Digite apenas o domínio e caminho da sua loja',
color: 'bg-success'
},
'fas fa-briefcase': {
prefix: 'https://',
placeholder: 'meuportifolio.com',
instructions: 'Digite apenas o domínio do seu portfólio',
color: 'bg-info'
},
'fas fa-download': {
prefix: 'https://',
placeholder: 'exemplo.com/arquivo.pdf',
instructions: 'Digite o link direto para download',
color: 'bg-secondary'
},
'fas fa-calendar': {
prefix: 'https://',
placeholder: 'calendly.com/seunome',
instructions: 'Digite o link do seu calendário (Calendly, etc.)',
color: 'bg-info'
},
'fas fa-heart': {
prefix: 'https://',
placeholder: 'exemplo.com',
instructions: 'Digite qualquer link especial',
color: 'bg-danger'
}
};
if (iconValue && linkTypes[iconValue]) {
const config = linkTypes[iconValue];
// Usar visualPrefix se existir, senão usar prefix normal
const displayPrefix = config.visualPrefix || config.prefix;
$prefix.text(displayPrefix)
.removeClass('bg-primary bg-success bg-danger bg-warning bg-info bg-secondary bg-dark')
.addClass(config.color);
$input.attr('placeholder', config.placeholder);
$instructions.text(config.instructions);
} else {
// Default
$prefix.text('https://')
.removeClass('bg-success bg-danger bg-warning bg-info bg-secondary bg-dark')
.addClass('bg-primary');
$input.attr('placeholder', 'exemplo.com');
$instructions.text('Selecione primeiro o tipo de link acima');
}
// Limpar o campo de input quando trocar tipo
$input.val('');
$('#linkUrl').val('');
}
function cleanAnyUrlPrefix(value) {
const prefixes = [
'https://', 'http://', 'mailto:', 'tel:', 'whatsapp://',
'https://wa.me/', 'https://youtube.com/', 'https://linkedin.com/in/',
'https://github.com/', 'https://instagram.com/', 'https://facebook.com/',
'https://x.com/', 'https://twitter.com/', 'https://maps.google.com/?q=',
'https://www.youtube.com/', 'https://www.linkedin.com/in/',
'https://www.github.com/', 'https://www.instagram.com/',
'https://www.facebook.com/', 'https://www.twitter.com/'
];
for (let prefix of prefixes) {
if (value.startsWith(prefix)) {
return value.replace(prefix, '');
}
}
return value;
}
// URL Input Functions
function initializeUrlInputs() {
// Setup para link normal
setupUrlField('#linkUrlInput', '#linkUrl');
// Setup para link de produto (se existir)
if ($('#productUrlInput').length) {
setupUrlField('#productUrlInput', '#productUrl');
}
}
function setupUrlField(inputSelector, hiddenSelector) {
const $input = $(inputSelector);
const $hidden = $(hiddenSelector);
// Eventos para tratar entrada do usuário
$input.on('input paste keyup', function() {
let value = $(this).val();
// Obter o prefixo real baseado no ícone selecionado
const selectedIcon = $('#linkIcon').val();
let realPrefix = 'https://';
const linkTypes = {
'fas fa-envelope': { prefix: 'mailto:' },
'fas fa-phone': { prefix: 'tel:' },
'fas fa-map-marker-alt': { prefix: 'https://maps.google.com/?q=' },
'fab fa-youtube': { prefix: 'https://youtube.com/' },
'fab fa-linkedin': { prefix: 'https://linkedin.com/in/' },
'fab fa-github': { prefix: 'https://github.com/' }
};
if (selectedIcon && linkTypes[selectedIcon]) {
realPrefix = linkTypes[selectedIcon].prefix;
}
// Apenas processar/limpar a URL para tipos que não sejam de mapa
if (realPrefix !== 'https://maps.google.com/?q=') {
let processedValue = value.trim();
processedValue = cleanAnyUrlPrefix(processedValue);
// Apenas atualiza o DOM se o valor foi alterado para não atrapalhar o cursor
if (processedValue !== value) {
$(this).val(processedValue);
}
value = processedValue;
}
// Atualizar campo hidden com URL completa
if (value) {
// Tratar casos especiais
if (realPrefix === 'https://maps.google.com/?q=') {
// Para mapas, o usuário digita normalmente, encoding só na URL final
const encodedValue = encodeURIComponent(value);
$hidden.val(realPrefix + encodedValue);
} else if (realPrefix === 'tel:') {
// Para telefone, apenas números
const telValue = value.replace(/\D/g, '');
$(this).val(telValue);
$hidden.val(realPrefix + telValue);
} else {
// Para outros tipos, a URL é concatenada diretamente
$hidden.val(realPrefix + value);
}
} else {
$hidden.val('');
}
});
// Para modal de edição - detectar se já tem URL e separar
if ($hidden.val()) {
const existingUrl = $hidden.val();
const cleanValue = cleanAnyUrlPrefix(existingUrl);
$input.val(cleanValue);
}
}
// Image Upload Functions
function initializeImageUpload() {
const fileInput = $('#profileImageInput');
const preview = $('#imagePreview');
const removeBtn = $('#removeImageBtn');
const errorSpan = $('#imageError');
const hiddenField = $('#profileImageId');
// Preview da imagem selecionada
fileInput.on('change', function(e) {
const file = e.target.files[0];
const feedbackDiv = $('#imageErrorFeedback');
// Limpar estados de erro anteriores
clearImageError();
if (!file) return;
// Validações client-side com feedback visual melhorado
if (!file.type.match(/^image\/(jpeg|jpg|png|gif)$/i)) {
setImageError('Formato inválido. Use apenas JPG, PNG ou GIF.');
showErrorToast(['Formato de imagem inválido. Use apenas JPG, PNG ou GIF.']);
fileInput.val('');
return;
}
if (file.size > 2 * 1024 * 1024) { // 2MB
setImageError('Arquivo muito grande. Máximo 2MB.');
showErrorToast(['Arquivo muito grande. O tamanho máximo é de 2MB.']);
fileInput.val('');
return;
}
// Preview
const reader = new FileReader();
reader.onload = function(e) {
preview.attr('src', e.target.result);
removeBtn.show();
markStepComplete(1); // Marcar step 1 como completo
};
reader.readAsDataURL(file);
});
// Remover imagem
removeBtn.on('click', function() {
if (confirm('Tem certeza que deseja remover a imagem de perfil?')) {
fileInput.val('');
hiddenField.val('REMOVE_IMAGE'); // Valor específico para indicar remoção
preview.attr('src', '/images/default-avatar.svg');
removeBtn.hide();
errorSpan.text('');
}
});
// Mostrar botão remover se já tem imagem
if (hiddenField.val()) {
removeBtn.show();
}
}
// Funções auxiliares para gerenciar estados de erro da imagem
function setImageError(message) {
const fileInput = $('#profileImageInput');
const errorSpan = $('#imageError');
const feedbackDiv = $('#imageErrorFeedback');
// Adicionar classes de erro
fileInput.addClass('is-invalid');
// Mostrar mensagens de erro
errorSpan.text(message);
feedbackDiv.text(message).show();
// Adicionar borda vermelha na área de preview
$('.profile-image-preview').addClass('border-danger');
}
function clearImageError() {
const fileInput = $('#profileImageInput');
const errorSpan = $('#imageError');
const feedbackDiv = $('#imageErrorFeedback');
// Remover classes de erro
fileInput.removeClass('is-invalid is-valid');
// Limpar mensagens de erro
errorSpan.text('');
feedbackDiv.hide();
// Remover borda vermelha da área de preview
$('.profile-image-preview').removeClass('border-danger');
}
// Função para verificar erros de imagem do servidor
function checkServerImageErrors() {
@if (TempData["ImageError"] != null)
{
@:const imageError = '@Html.Raw(TempData["ImageError"])';
@:setImageError(imageError);
@:showErrorToast([imageError]);
}
// Verificar ModelState errors para ProfileImageFile
@if (ViewData.ModelState.ContainsKey("ProfileImageFile"))
{
@:const modelStateError = '@Html.Raw(ViewData.ModelState["ProfileImageFile"].Errors.FirstOrDefault()?.ErrorMessage)';
@:if (modelStateError) {
@:setImageError(modelStateError);
@:showErrorToast([modelStateError]);
@:}
}
}
// ========================================
// Sistema de Confirmação ao Sair da Página
// ========================================
let formChanged = false;
let isSubmitting = false;
$(document).ready(function() {
// Marcar formulário como alterado quando qualquer campo mudar
$('form :input').on('change input', function() {
if (!isSubmitting) {
formChanged = true;
console.log('Formulário alterado detectado');
}
});
// Quando submeter o formulário, desabilitar aviso
$('form').on('submit', function() {
console.log('Formulário submetido - desabilitando avisos');
isSubmitting = true;
formChanged = false;
});
// Aviso ao tentar fechar/recarregar a página
window.addEventListener('beforeunload', function(e) {
if (formChanged && !isSubmitting) {
console.log('beforeunload: Tentativa de sair com alterações não salvas');
e.preventDefault();
e.returnValue = ''; // Chrome requer isso
return 'Você tem alterações não salvas. Deseja realmente sair?';
}
});
// Interceptar cliques em links de navegação (incluindo Dashboard)
$(document).on('click', 'a:not(.no-confirm)', function(e) {
if (formChanged && !isSubmitting) {
const href = $(this).attr('href');
// Não avisar para links externos, âncoras ou JavaScript
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
console.log('Link clicado com alterações não salvas:', href);
if (!confirm('Você tem alterações não salvas. Deseja realmente sair desta página?')) {
e.preventDefault();
console.log('Usuário cancelou navegação');
return false;
}
// Usuário confirmou, permitir navegação
console.log('Usuário confirmou saída');
formChanged = false;
}
}
});
console.log('Sistema de confirmação ao sair inicializado');
});
</script>
}
@section Styles {
<style>
/* Estilo customizado para o scroll dos temas */
.themes-container {
scrollbar-width: thin;
scrollbar-color: #007bff #f8f9fa;
}
.themes-container::-webkit-scrollbar {
width: 8px;
}
.themes-container::-webkit-scrollbar-track {
background: #f8f9fa;
border-radius: 4px;
}
.themes-container::-webkit-scrollbar-thumb {
background: #007bff;
border-radius: 4px;
border: 1px solid #f8f9fa;
}
.themes-container::-webkit-scrollbar-thumb:hover {
background: #0056b3;
}
/* Fade gradient no topo e bottom para indicar scroll */
.themes-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 20px;
background: linear-gradient(to bottom, rgba(248, 249, 250, 1), rgba(248, 249, 250, 0));
pointer-events: none;
z-index: 1;
}
.themes-container::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20px;
background: linear-gradient(to top, rgba(248, 249, 250, 1), rgba(248, 249, 250, 0));
pointer-events: none;
z-index: 1;
}
/* Smooth scroll */
.themes-container {
scroll-behavior: smooth;
}
/* Container relativo para os gradients */
.accordion-body {
position: relative;
}
</style>
}
@if (TempData["Error"] != null)
{
<div class="toast-container position-fixed top-0 end-0 p-3">
<div class="toast show" role="alert">
<div class="toast-header">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
<strong class="me-auto">Atenção</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
@TempData["Error"]
</div>
</div>
</div>
}