All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 5s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Run Tests (pull_request) Successful in 3s
BCards Deployment Pipeline / Build and Push Image (pull_request) Has been skipped
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (pull_request) Has been skipped
BCards Deployment Pipeline / Deploy to Release Swarm (ARM) (pull_request) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (pull_request) Has been skipped
BCards Deployment Pipeline / PR Validation (pull_request) Successful in 0s
BCards Deployment Pipeline / Deployment Summary (pull_request) Successful in 0s
BCards Deployment Pipeline / Build and Push Image (push) Successful in 19m44s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Has been skipped
BCards Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Successful in 22s
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
2255 lines
113 KiB
Plaintext
2255 lines
113 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 4</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: 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 3: Links Principais
|
|
<span class="badge bg-success ms-auto me-3" id="step3Status" 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(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>
|
|
@{
|
|
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 4: 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 4: Redes Sociais (Opcional)
|
|
<span class="badge bg-success ms-auto me-3" id="step4Status" 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="5511999999999">
|
|
</div>
|
|
<small class="form-text text-muted">Exemplo: 5511999999999 (código do país + DDD + número)</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(3)">
|
|
<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 currentStep = 1;
|
|
|
|
$(document).ready(function() {
|
|
// Initialize social media fields
|
|
initializeSocialMedia();
|
|
|
|
// Initialize image upload
|
|
initializeImageUpload();
|
|
|
|
// Initialize URL input handlers
|
|
initializeUrlInputs();
|
|
|
|
// 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() {
|
|
ensureUncheckedFieldsAreEmpty();
|
|
|
|
// 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[')) {
|
|
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 4 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', '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 / 4) * 100;
|
|
$('.progress-bar').css('width', `${progress}%`).attr('aria-valuenow', progress);
|
|
$('.progress').next().find('small').first().text(`Passo ${currentStep} de 4`);
|
|
}
|
|
|
|
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 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
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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)) {
|
|
return value.replace(prefix, '');
|
|
}
|
|
}
|
|
|
|
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, '');
|
|
$(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 (mínimo 10 dígitos)
|
|
if (value.length >= 10) {
|
|
input.addClass('is-valid');
|
|
} else {
|
|
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]);
|
|
@:}
|
|
}
|
|
}
|
|
</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>
|
|
}
|