Compare commits

..

No commits in common. "7cc8f46a1a912c01f3f17c51a09a8542c1ca9e6f" and "ce705c51ec83c2d7cd7dd746bd47426dbe7b2c7f" have entirely different histories.

10 changed files with 399 additions and 292 deletions

View File

@ -8,7 +8,7 @@ public class SeoService : ISeoService
public SeoService(IConfiguration configuration)
{
_baseUrl = configuration["BaseUrl"] ?? "https://bcards.site";
_baseUrl = configuration["BaseUrl"] ?? "https://vcart.me";
}
public SeoSettings GenerateSeoSettings(UserPage userPage, Category category)

View File

@ -2,7 +2,6 @@
@{
ViewData["Title"] = "Dashboard - BCards";
Layout = "_Layout";
var pageInCreation = Model.UserPages.FirstOrDefault(p => (p.LastModerationStatus ?? p.Status) == BCards.Web.ViewModels.PageStatus.Creating);
}
<div class="container py-4">
@ -17,32 +16,11 @@
@if (!string.IsNullOrEmpty(Model.CurrentUser.ProfileImage))
{
<img src="@Model.CurrentUser.ProfileImage" alt="@Model.CurrentUser.Name"
class="rounded-circle" style="width: 60px; height: 60px; object-fit: cover;">
class="rounded-circle" style="width: 60px; height: 60px; object-fit: cover;">
}
</div>
</div>
<!-- Acordeão de Instruções -->
@if (pageInCreation != null)
{
<div class="accordion mb-4" id="instructionsAccordion">
<div class="accordion-item border-0" style="background-color: #e9ecef;">
<h2 class="accordion-header" id="instructions-heading">
<button class="accordion-button collapsed fw-bold" type="button" data-bs-toggle="collapse" data-bs-target="#instructions-collapse" aria-expanded="false" aria-controls="instructions-collapse" style="background-color: #e0e5e9;">
<i class="fas fa-info-circle me-2"></i>Página em criação! Veja como continuar
</button>
</h2>
<div id="instructions-collapse" class="accordion-collapse collapse" aria-labelledby="instructions-heading">
<div class="accordion-body">
Você pode editar e testar sua página <b>"@pageInCreation.DisplayName"</b> quantas vezes quiser.
Ao terminar, use os botões no card da página para enviá-la para moderação.
<br><small class="text-muted">Dica: Ao segurar o dedo sobre um botão, uma dica será exibida.</small>
</div>
</div>
</div>
</div>
}
<!-- Lista de Páginas -->
<div class="row">
@foreach (var pageItem in Model.UserPages)
@ -55,34 +33,52 @@
<form method="post" action="/Admin/DeletePage/@(pageItem.Id)" style="display: inline;" onsubmit="return confirm('Tem certeza que deseja excluir esta página?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-link text-danger p-0" title="Excluir página"
style="font-size: 12px; text-decoration: none;">
style="font-size: 12px; text-decoration: none;">
<i class="fas fa-trash"></i>
</button>
</form>
<input type="hidden" id="displayName_@pageItem.Id" value="@(pageItem.DisplayName)" />
</h6>
<p class="text-muted small mb-2">@(pageItem.Category)/@(pageItem.Slug)</p>
<div class="mb-2">
@switch (pageItem.LastModerationStatus ?? pageItem.Status)
@{
var pageStatus = pageItem.Status;
if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Inactive)
{
if (pageItem.LastModerationStatus.HasValue)
{
pageStatus = pageItem.LastModerationStatus.Value;
}
}
}
@switch (pageStatus)
{
case BCards.Web.ViewModels.PageStatus.Active:
<span class="badge bg-success">Ativa</span>
break;
case BCards.Web.ViewModels.PageStatus.Expired:
<span class="badge bg-danger">Expirada</span>
break;
case BCards.Web.ViewModels.PageStatus.PendingPayment:
<span class="badge bg-warning">Pagamento Pendente</span>
break;
case BCards.Web.ViewModels.PageStatus.Inactive:
<span class="badge bg-secondary">Inativa</span>
break;
case BCards.Web.ViewModels.PageStatus.PendingModeration:
<span class="badge bg-warning">Aguardando Moderação</span>
<span class="badge bg-warning">Aguardando</span>
break;
case BCards.Web.ViewModels.PageStatus.Rejected:
<span class="badge bg-danger">Rejeitada</span>
break;
case BCards.Web.ViewModels.PageStatus.Creating:
<span class="badge bg-info"><i class="fas fa-edit me-1"></i>Em Criação</span>
break;
default:
<span class="badge bg-secondary">@pageItem.Status</span>
<span class="badge bg-info">
<i class="fas fa-edit me-1"></i>Criando
</span>
break;
}
</div>
@if (Model.CurrentPlan.AllowsAnalytics)
{
<div class="row text-center small mb-3">
@ -96,49 +92,149 @@
</div>
</div>
}
</div>
<!-- Cards com Hover Effect -->
<div class="card-footer" data-page-id="@pageItem.Id" data-status="@pageItem.Status">
<div class="d-grid gap-2">
<div class="d-flex gap-2">
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Active)
{
<a href="/page/@pageItem.Category/@pageItem.Slug" target="_blank" class="btn btn-success">
<i class="fas fa-eye me-1"></i>Ver Página Publicada
</a>
<a href="@Url.Action("ManagePage", new { id = pageItem.Id })" class="btn btn-primary" data-bs-toggle="tooltip" title="Ir para o editor da página">
<i class="fas fa-edit me-1"></i>Editar
<a href="/page/@pageItem.Category/@pageItem.Slug" target="_blank"
class="btn btn-success flex-fill">
<i class="fas fa-eye me-1"></i>Ver Página
</a>
}
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating || pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected)
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating ||
pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected ||
pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{
<button type="button" class="btn btn-outline-info" onclick="openPreview('@pageItem.Id')" data-page-category="@pageItem.Category" data-page-slug="@pageItem.Slug" data-bs-toggle="tooltip" title="Pré-visualizar a página antes da publicação">
<button type="button"
class="btn btn-outline-info flex-fill"
onclick="openPreview('@pageItem.Id')"
data-page-category="@pageItem.Category"
data-page-slug="@pageItem.Slug">
<i class="fas fa-vial me-1"></i>Testar Página
</button>
<a href="@Url.Action("ManagePage", new { id = pageItem.Id })" class="btn btn-primary" data-bs-toggle="tooltip" title="Ir para o editor da página">
<i class="fas fa-edit me-1"></i>Editar
</a>
<button type="button" class="btn btn-outline-info" onclick="submitForModeration('@pageItem.Id')" data-page-name="@pageItem.DisplayName" data-bs-toggle="tooltip" title="Enviar a página para revisão e aprovação">
<i class="fas fa-paper-plane"></i> Enviar para Moderação
</button>
}
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{
<button type="button" class="btn btn-outline-info" onclick="openPreview('@pageItem.Id')" data-page-category="@pageItem.Category" data-page-slug="@pageItem.Slug" data-bs-toggle="tooltip" title="Pré-visualizar a página antes da publicação">
<i class="fas fa-vial me-1"></i>Testar Página
</button>
<button class="btn btn-secondary" disabled data-bs-toggle="tooltip" title="A página está em revisão e não pode ser editada ou enviada novamente.">
<i class="fas fa-hourglass-half me-1"></i>Aguardando Moderação
</button>
}
else
{
<button class="btn btn-secondary" disabled>
<button class="btn btn-secondary flex-fill" disabled>
<i class="fas fa-ban me-1"></i>Indisponível
</button>
}
<div class="btn-group">
<button class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton@(pageItem.Id)"
data-bs-toggle="dropdown"
aria-expanded="false"
title="Mais opções">
<i class="fas fa-cog"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuButton@(pageItem.Id)">
<!-- Editar - sempre presente -->
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{
<li>
<span class="dropdown-item disabled">
<i class="fas fa-edit me-2"></i>Editar
</span>
</li>
}
else
{
<li>
<a href="@Url.Action("ManagePage", new { id = pageItem.Id })"
class="dropdown-item">
<i class="fas fa-edit me-2"></i>Editar
</a>
</li>
}
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating ||
pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected)
{
<li><hr class="dropdown-divider"></li>
<li>
<button type="button"
class="dropdown-item"
onclick="submitForModeration('@pageItem.Id')"
data-page-name="@pageItem.DisplayName">
<i class="fas fa-paper-plane me-2"></i>Enviar para Moderação
</button>
</li>
}
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{
<li><hr class="dropdown-divider"></li>
<li>
<span class="dropdown-item disabled">
<i class="fas fa-hourglass-half me-2"></i>Aguardando Moderação
</span>
</li>
}
</ul>
</div>
</div>
<!-- Informações da página movidas para baixo dos botões -->
<div class="px-3 pt-2 pb-1">
<small class="text-muted">Criada em @(pageItem.CreatedAt.ToString("dd/MM/yyyy"))</small>
@if ((pageItem.LastModerationStatus ?? pageItem.Status) == BCards.Web.ViewModels.PageStatus.Rejected && !string.IsNullOrEmpty(pageItem.Motive))
{
<div class="alert alert-danger alert-dismissible fade show mt-2 mb-0" role="alert">
<div class="d-flex align-items-start">
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
<div class="flex-grow-1">
<strong>Motivo da rejeição:</strong><br>
<small>@(pageItem.Motive)</small>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
else if (pageItem.LastModerationStatus == BCards.Web.ViewModels.PageStatus.Active && !string.IsNullOrEmpty(pageItem.Motive))
{
<div class="alert alert-info alert-dismissible fade show mt-2 mb-0" role="alert">
<div class="d-flex align-items-start">
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
<div class="flex-grow-1">
<strong>Motivo:</strong><br>
<small>@(pageItem.Motive)</small>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
</div>
</div>
</div>
</div>
@if ((pageItem.LastModerationStatus ?? pageItem.Status) == BCards.Web.ViewModels.PageStatus.Creating)
{
<div class="col-12">
<div class="alert alert-secondary d-flex align-items-center alert-dismissible alert-permanent fade show">
<i class="fas fa-exclamation-triangle me-3"></i>
<div>
<strong>Página em criação!</strong>
Você pode editar e testar quantas vezes quiser. <br />
Ao terminar, clique em <i class="fas fa-ellipsis-v"></i> para enviar a página <b><span id="pageNameDisplay"></span></b> para moderação!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<script>
var pageNameDisplay = document.getElementById('pageNameDisplay');
var displayName = document.getElementById('displayName_@pageItem.Id');
pageNameDisplay.innerHTML = displayName.value;
</script>
}
}
<!-- Card para Criar Nova Página -->
@ -150,7 +246,8 @@
<div>
<i class="fas fa-plus fa-2x text-muted mb-3"></i>
<h6 class="text-muted">Criar Nova Página</h6>
<a href="@Url.Action("ManagePage", new { id = "new" })" class="btn btn-primary">Começar</a>
<a href="@Url.Action("ManagePage", new { id = "new" })"
class="btn btn-primary">Começar</a>
</div>
</div>
</div>
@ -158,89 +255,121 @@
}
else if (!Model.UserPages.Any())
{
<!-- Primeira Página -->
<div class="col-12">
<div class="card border-primary">
<div class="card-body text-center p-5">
<div class="mb-4"><i class="display-1 text-primary">🚀</i></div>
<div class="mb-4">
<i class="display-1 text-primary">🚀</i>
</div>
<h3>Crie sua primeira página!</h3>
<p class="text-muted mb-4">Comece criando sua página profissional personalizada com seus links organizados.</p>
<a href="@Url.Action("ManagePage", new { id = "new" })" class="btn btn-primary btn-lg">Criar Minha Página</a>
<p class="text-muted mb-4">
Comece criando sua página profissional personalizada com seus links organizados.
</p>
<a href="@Url.Action("ManagePage", new { id = "new" })" class="btn btn-primary btn-lg">
Criar Minha Página
</a>
</div>
</div>
</div>
}
else
{
<!-- Limite atingido -->
<div class="col-12">
<div class="alert alert-warning d-flex align-items-center alert-permanent">
<i class="fas fa-exclamation-triangle me-3"></i>
<div>
<strong>Limite atingido!</strong> Você já criou o máximo de @Model.CurrentPlan.MaxPages página(s) para seu plano atual.
<strong>Limite atingido!</strong>
Você já criou o máximo de @Model.CurrentPlan.MaxPages página(s) para seu plano atual.
<a href="@Url.Action("Pricing", "Home")" class="alert-link ms-2">Fazer upgrade</a>
</div>
</div>
</div>
}
</div>
</div>
<div class="col-md-4">
<!-- Painel de Plano Atual como Acordeão -->
<div class="accordion" id="planAccordion">
<div class="card mb-4 @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "border-warning" : "")">
<div class="card-header @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "bg-warning" : "bg-primary") text-white">
<h2 class="mb-0">
<button class="btn btn-link text-white text-decoration-none w-100 d-flex justify-content-between align-items-center p-0" type="button" data-bs-toggle="collapse" data-bs-target="#plan-collapse" aria-expanded="true" aria-controls="plan-collapse">
<span><i class="fas fa-crown me-2"></i>Plano Atual</span>
<i id="plan-toggle-icon" class="fas fa-eye-slash"></i>
</button>
</h2>
</div>
<div id="plan-collapse" class="accordion-collapse collapse">
<div class="card-body">
<h5 class="text-capitalize mb-1">@Model.CurrentPlan.Name</h5>
@if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial)
{
<p class="text-warning mb-2"><i class="fas fa-clock me-1"></i>@Model.DaysRemaining dia(s) restante(s)</p>
}
else
{
<p class="text-muted small mb-2">R$ @Model.CurrentPlan.Price.ToString("F2")/mês</p>
}
<div class="mb-3">
<div class="d-flex justify-content-between small mb-1">
<span>Páginas</span>
<span>@Model.UserPages.Count/@Model.CurrentPlan.MaxPages</span>
</div>
<div class="progress" style="height: 6px;">
@{
var pagesPercentage = Model.CurrentPlan.MaxPages > 0 ? (double)Model.UserPages.Count / Model.CurrentPlan.MaxPages * 100 : 0;
}
<div class="progress-bar @(pagesPercentage >= 80 ? "bg-warning" : "bg-primary")" style="width: @pagesPercentage%"></div>
</div>
</div>
<div class="small mb-2"><i class="fas fa-link me-2"></i>Links por página: @(Model.CurrentPlan.MaxLinksPerPage == int.MaxValue ? "Ilimitado" : Model.CurrentPlan.MaxLinksPerPage.ToString())
</div>
<div class="small mb-2"><i class="fas fa-chart-bar me-2"></i>Analytics: @(Model.CurrentPlan.AllowsAnalytics ? "✅" : "❌")</div>
<div class="small mb-3"><i class="fas fa-palette me-2"></i>Temas premium: @(Model.CurrentPlan.AllowsCustomThemes ? "✅" : "❌")</div>
@if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial)
{
<a href="@Url.Action("Pricing", "Home")" class="btn btn-warning w-100"><i class="fas fa-rocket me-2"></i>Fazer Upgrade</a>
}
else
{
<a href="@Url.Action("ManageSubscription", "Payment")" class="btn btn-outline-secondary w-100"><i class="fas fa-cog me-2"></i>Gerenciar Assinatura</a>
<!-- Plano Atual -->
<div class="card mb-4 @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "border-warning" : "")">
<div class="card-header @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "bg-warning" : "bg-primary") text-white">
<h6 class="mb-0">
<i class="fas fa-crown me-2"></i>
Plano Atual
</h6>
</div>
<div class="card-body">
<h5 class="text-capitalize mb-1">@Model.CurrentPlan.Name</h5>
@if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial)
{
<p class="text-warning mb-2">
<i class="fas fa-clock me-1"></i>
@Model.DaysRemaining dia(s) restante(s)
</p>
}
else
{
<p class="text-muted small mb-2">R$ @Model.CurrentPlan.Price.ToString("F2")/mês</p>
}
<div class="mb-3">
<div class="d-flex justify-content-between small mb-1">
<span>Páginas</span>
<span>@Model.UserPages.Count/@Model.CurrentPlan.MaxPages</span>
</div>
<div class="progress" style="height: 6px;">
@{
var pagesPercentage = Model.CurrentPlan.MaxPages > 0 ?
(double)Model.UserPages.Count / Model.CurrentPlan.MaxPages * 100 : 0;
}
<div class="progress-bar @(pagesPercentage >= 80 ? "bg-warning" : "bg-primary")"
style="width: @pagesPercentage%"></div>
</div>
</div>
<div class="small mb-2">
<i class="fas fa-link me-2"></i>
Links por página: @(Model.CurrentPlan.MaxLinksPerPage == int.MaxValue ? "Ilimitado" : Model.CurrentPlan.MaxLinksPerPage.ToString())
</div>
<div class="small mb-2">
<i class="fas fa-chart-bar me-2"></i>
Analytics: @(Model.CurrentPlan.AllowsAnalytics ? "✅" : "❌")
</div>
<div class="small mb-3">
<i class="fas fa-palette me-2"></i>
Temas premium: @(Model.CurrentPlan.AllowsCustomThemes ? "✅" : "❌")
</div>
@if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial)
{
<a href="@Url.Action("Pricing", "Home")" class="btn btn-warning w-100">
<i class="fas fa-rocket me-2"></i>
Fazer Upgrade
</a>
}
else
{
<a href="@Url.Action("ManageSubscription", "Payment")" class="btn btn-outline-secondary w-100">
<i class="fas fa-cog me-2"></i>
Gerenciar Assinatura
</a>
}
</div>
</div>
<!-- Estatísticas e Dicas (mantidos como antes) -->
<!-- Estatísticas Rápidas -->
@if (Model.CurrentPlan.AllowsAnalytics && Model.UserPages.Any())
{
<div class="card mb-4">
<div class="card-header"><h6 class="mb-0"><i class="fas fa-chart-line me-2"></i>Estatísticas Gerais</h6></div>
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-chart-line me-2"></i>
Estatísticas Gerais
</h6>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
@ -256,22 +385,46 @@
{
<hr class="my-3">
<div class="text-center">
<div class="h5 text-info mb-0">@((Model.UserPages.Sum(p => p.TotalClicks) * 100.0 / Model.UserPages.Sum(p => p.TotalViews)).ToString("F1"))%</div>
<div class="h5 text-info mb-0">
@((Model.UserPages.Sum(p => p.TotalClicks) * 100.0 / Model.UserPages.Sum(p => p.TotalViews)).ToString("F1"))%
</div>
<small class="text-muted">Taxa de Cliques</small>
</div>
}
</div>
</div>
}
<!-- Dicas -->
<div class="card">
<div class="card-header"><h6 class="mb-0"><i class="fas fa-lightbulb me-2"></i>💡 Dicas</h6></div>
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb me-2"></i>
💡 Dicas
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled small mb-0">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Use uma bio clara e objetiva</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Organize seus links por importância</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Escolha URLs fáceis de lembrar</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Atualize regularmente seus links</li>
<li class="mb-0"><i class="fas fa-check text-success me-2"></i>Monitore suas estatísticas</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Use uma bio clara e objetiva
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Organize seus links por importância
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Escolha URLs fáceis de lembrar
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Atualize regularmente seus links
</li>
<li class="mb-0">
<i class="fas fa-check text-success me-2"></i>
Monitore suas estatísticas
</li>
</ul>
</div>
</div>
@ -281,156 +434,135 @@
@section Scripts {
<script>
// Funções auxiliares para cookies
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Função para abrir preview com token fresh
async function openPreview(pageId) {
const button = event.target.closest('button');
const category = button.dataset.pageCategory;
const slug = button.dataset.pageSlug;
document.addEventListener('DOMContentLoaded', function () {
// Inicializar tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
// Desabilitar botão temporariamente
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Carregando...';
try {
// Gerar novo token
const response = await fetch(`/Admin/GeneratePreviewToken/${pageId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
}
});
// Lógica para o acordeão de instruções
const instructionsAccordion = document.getElementById('instructions-collapse');
if (instructionsAccordion) {
if (getCookie('instructions_collapsed') === 'true') {
// Já começa fechado pelo `collapsed` no botão, então não fazemos nada
} else {
// Se não tem cookie ou está aberto, abre
new bootstrap.Collapse(instructionsAccordion, { toggle: true });
}
instructionsAccordion.addEventListener('show.bs.collapse', function () {
setCookie('instructions_collapsed', 'false', 365);
});
instructionsAccordion.addEventListener('hide.bs.collapse', function () {
setCookie('instructions_collapsed', 'true', 365);
});
}
const result = await response.json();
// Lógica para o acordeão do plano
const planAccordion = document.getElementById('plan-collapse');
const planToggleIcon = document.getElementById('plan-toggle-icon');
if (planAccordion && planToggleIcon) {
if (getCookie('plan_collapsed') === 'true') {
planToggleIcon.classList.replace('fa-eye-slash', 'fa-eye');
} else {
new bootstrap.Collapse(planAccordion, { toggle: true });
planToggleIcon.classList.replace('fa-eye', 'fa-eye-slash');
}
planAccordion.addEventListener('show.bs.collapse', function () {
setCookie('plan_collapsed', 'false', 365);
planToggleIcon.classList.replace('fa-eye', 'fa-eye-slash');
});
planAccordion.addEventListener('hide.bs.collapse', function () {
setCookie('plan_collapsed', 'true', 365);
planToggleIcon.classList.replace('fa-eye-slash', 'fa-eye');
});
if (result.success) {
// Abrir preview em nova aba com token novo
const previewUrl = `${window.location.origin}/page/${category}/${slug}?preview=${result.previewToken}`;
window.open(previewUrl, '_blank');
} else {
showToast(result.message || 'Erro ao gerar preview', 'error');
}
} catch (error) {
console.error('Erro ao gerar preview:', error);
showToast('Erro ao gerar preview. Tente novamente.', 'error');
} finally {
// Reabilitar botão
button.disabled = false;
button.innerHTML = originalText;
}
}
async function submitForModeration(pageId) {
const pageName = event.target.dataset.pageName || 'esta página';
if (!confirm(`Enviar "${pageName}" para moderação?\n\nApós enviar, você não poderá mais editá-la até receber o resultado da análise.`)) {
return;
}
// Desabilitar botão durante envio
const button = event.target;
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enviando...';
try {
const response = await fetch(`/Admin/SubmitForModeration/${pageId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
}
});
const result = await response.json();
if (result.success) {
// Mostrar toast de sucesso
showToast(result.message, 'success');
// Recarregar página após 2 segundos
setTimeout(() => {
location.reload();
}, 2000);
} else {
showToast(result.message || 'Erro ao enviar página', 'error');
// Reabilitar botão
button.disabled = false;
button.innerHTML = originalText;
}
} catch (error) {
console.error('Erro:', error);
showToast('Erro ao enviar página para moderação', 'error');
// Reabilitar botão
button.disabled = false;
button.innerHTML = originalText;
}
}
function showToast(message, type) {
const toastContainer = getOrCreateToastContainer();
const bgClass = type === 'success' ? 'bg-success' : type === 'error' ? 'bg-danger' : 'bg-info';
const icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-triangle' : 'fa-info-circle';
const toastHtml = `
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="5000">
<div class="toast-header ${bgClass} text-white">
<i class="fas ${icon} me-2"></i>
<strong class="me-auto">${type === 'success' ? 'Sucesso' : type === 'error' ? 'Erro' : 'Informação'}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">${message}</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const newToast = toastContainer.lastElementChild;
const toast = new bootstrap.Toast(newToast);
toast.show();
// Remover toast após ser fechado
newToast.addEventListener('hidden.bs.toast', function() {
newToast.remove();
});
}
// Funções existentes (submitForModeration, openPreview, etc.)
async function openPreview(pageId) {
const button = event.target.closest('button');
const category = button.dataset.pageCategory;
const slug = button.dataset.pageSlug;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Carregando...';
try {
const response = await fetch(`/Admin/GeneratePreviewToken/${pageId}`, {
method: 'POST',
headers: { 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value }
});
const result = await response.json();
if (result.success) {
window.open(`${window.location.origin}/page/${category}/${slug}?preview=${result.previewToken}`, '_blank');
} else {
showToast(result.message || 'Erro ao gerar preview', 'error');
}
} catch (error) {
showToast('Erro ao gerar preview. Tente novamente.', 'error');
} finally {
button.disabled = false;
button.innerHTML = '<i class="fas fa-vial me-1"></i>Testar Página';
}
function getOrCreateToastContainer() {
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '1055';
document.body.appendChild(container);
}
return container;
}
async function submitForModeration(pageId) {
const pageName = event.target.closest('button').dataset.pageName || 'esta página';
if (!confirm(`Enviar "${pageName}" para moderação?\n\nApós enviar, você não poderá mais editá-la até receber o resultado da análise.`)) return;
const button = event.target.closest('button');
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enviando...';
try {
const response = await fetch(`/Admin/SubmitForModeration/${pageId}`, {
method: 'POST',
headers: { 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value }
});
const result = await response.json();
if (result.success) {
showToast(result.message, 'success');
setTimeout(() => location.reload(), 2000);
} else {
button.disabled = false;
button.innerHTML = '<i class="fas fa-paper-plane"></i><small>Enviar para Moderação</small>';
showToast(result.message || 'Erro ao enviar página', 'error');
}
} catch (error) {
button.disabled = false;
button.innerHTML = '<i class="fas fa-paper-plane"></i><small>Enviar para Moderação</small>';
showToast('Erro ao enviar página para moderação', 'error');
}
}
function showToast(message, type) {
const toastContainer = getOrCreateToastContainer();
const bgClass = type === 'success' ? 'bg-success' : type === 'error' ? 'bg-danger' : 'bg-info';
const icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-triangle' : 'fa-info-circle';
const toastHtml =
`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="5000">
<div class="toast-header ${bgClass} text-white">
<i class="fas ${icon} me-2"></i>
<strong class="me-auto">${type === 'success' ? 'Sucesso' : type === 'error' ? 'Erro' : 'Informação'}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">${message}</div>
</div>`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const newToast = toastContainer.lastElementChild;
const toast = new bootstrap.Toast(newToast);
toast.show();
newToast.addEventListener('hidden.bs.toast', () => newToast.remove());
}
function getOrCreateToastContainer() {
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '1055';
document.body.appendChild(container);
}
return container;
}
</script>
}

View File

@ -97,7 +97,7 @@
<i class="fs-2 text-primary">🔗</i>
</div>
<h5>URLs Organizadas</h5>
<p class="text-muted">Suas URLs são organizadas por categoria: bcards.site/corretor/seu-nome</p>
<p class="text-muted">Suas URLs são organizadas por categoria: vcart.me/corretor/seu-nome</p>
</div>
</div>
</div>

View File

@ -55,7 +55,7 @@
<li><strong>Informação sobre Compartilhamento:</strong> O direito de saber com quais entidades públicas e privadas compartilhamos seus dados.</li>
<li><strong>Revogação do Consentimento:</strong> O direito de revogar seu consentimento a qualquer momento.</li>
</ul>
<p>Para exercer seus direitos, entre em contato com nosso Encarregado de Proteção de Dados (DPO) através do e-mail <a class="email-obfuscated" data-user="dpo" data-domain="bcards.site">[carregando e-mail...]</a>. O prazo para resposta é de até 15 dias, conforme a legislação.</p>
<p>Para exercer seus direitos, entre em contato com nosso Encarregado de Proteção de Dados (DPO) através do e-mail <a href="mailto:dpo@vcart.me">dpo@vcart.me</a>. O prazo para resposta é de até 15 dias, conforme a legislação.</p>
<h4 class="mt-5 fw-bold">5. Cookies e Tecnologias de Rastreamento</h4>
<p>Utilizamos cookies para melhorar sua experiência. Cookies são pequenos arquivos de texto armazenados em seu dispositivo. Você pode gerenciar suas preferências de cookies através do nosso banner de consentimento ou nas configurações do seu navegador.</p>

View File

@ -47,7 +47,7 @@
<li><strong>Oposición:</strong> El derecho a oponerse al tratamiento de sus datos para ciertos fines.</li>
<li><strong>Portabilidad:</strong> El derecho a recibir sus datos en un formato estructurado.</li>
</ul>
<p>Para ejercer sus derechos (conocidos como derechos ARCO en México y Chile), por favor contacte a nuestro Oficial de Protección de Datos (DPO) a través del correo electrónico <a class="email-obfuscated" data-user="dpo" data-domain="bcards.site">[cargando email...]</a>.</p>
<p>Para ejercer sus derechos (conocidos como derechos ARCO en México y Chile), por favor contacte a nuestro Oficial de Protección de Datos (DPO) a través del correo electrónico <a href="mailto:dpo@vcart.me">dpo@vcart.me</a>.</p>
<h4 class="mt-5 fw-bold">5. Retención de Datos</h4>
<p>Mantendremos sus datos personales mientras su cuenta esté activa. Si su cuenta es desactivada o permanece inactiva por más de 12 meses, sus datos serán anonimizados o eliminados, excepto aquellos que necesitemos retener para cumplir con obligaciones legales.</p>

View File

@ -21,7 +21,7 @@
<div class="alert alert-secondary text-center">
<h5 class="alert-heading">Canal de Atendimento ao Titular</h5>
<p class="mb-0">Envie um e-mail para:</p>
<a class="email-obfuscated fs-5 fw-bold" data-user="dpo" data-domain="bcards.site">[carregando e-mail...]</a>
<a href="mailto:dpo@vcart.me" class="fs-5 fw-bold">dpo@vcart.me</a>
</div>
<p>No seu e-mail, por favor, inclua:</p>

View File

@ -64,7 +64,7 @@
<ul>
<li><strong>Legislação Aplicável:</strong> Estes Termos serão regidos e interpretados de acordo com as leis da República Federativa do Brasil, sem consideração com o conflito de disposições legais.</li>
<li><strong>Alterações nos Termos:</strong> Podemos modificar estes Termos a qualquer momento. Se fizermos alterações materiais, forneceremos um aviso com antecedência razoável. Ao continuar a usar o BCards após as alterações entrarem em vigor, você concorda em ficar vinculado aos termos revisados.</li>
<li><strong>Contato:</strong> Para qualquer dúvida sobre estes Termos, entre em contato conosco pelo e-mail <a class="email-obfuscated" data-user="suporte" data-domain="bcards.site">[carregando e-mail...]</a>.</li>
<li><strong>Contato:</strong> Para qualquer dúvida sobre estes Termos, entre em contato conosco pelo e-mail <a href="mailto:suporte@vcart.me">suporte@vcart.me</a>.</li>
</ul>
</div>
</div>

View File

@ -57,7 +57,7 @@
<ul>
<li><strong>Legislación Aplicable:</strong> Estos Términos se regirán por las leyes de Brasil para todos los usuarios. Para disputas específicas en Colombia, Chile o México, se pueden considerar las leyes locales.</li>
<li><strong>Cambios en los Términos:</strong> Podemos modificar estos Términos en cualquier momento. Le notificaremos con antelación.</li>
<li><strong>Contacto:</strong> Para cualquier pregunta sobre estos Términos, contáctenos en <a class="email-obfuscated" data-user="suporte" data-domain="bcards.site">[cargando email...]</a>.</li>
<li><strong>Contacto:</strong> Para cualquier pregunta sobre estos Términos, contáctenos en <a href="mailto:suporte@vcart.me">suporte@vcart.me</a>.</li>
</ul>
</div>
</div>

View File

@ -242,8 +242,6 @@
<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/js/cookie-consent.js" asp-append-version="true"></script>
<script src="~/js/email-handler.js" asp-append-version="true"></script>
<!-- Scripts para menu ativo e barra de carregamento -->
<script>
@ -348,3 +346,4 @@
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
</html>

View File

@ -1,24 +0,0 @@
/*!
* Script para Ofuscação de E-mail
* BCards - 2025
*/
document.addEventListener('DOMContentLoaded', function () {
const emailElements = document.querySelectorAll('.email-obfuscated');
emailElements.forEach(el => {
try {
const user = el.getAttribute('data-user');
const domain = el.getAttribute('data-domain');
if (user && domain) {
const email = user + '@' + domain;
el.href = 'mailto:' + email;
el.textContent = email;
}
} catch (e) {
console.error('Falha ao ofuscar e-mail:', e);
el.textContent = 'E-mail indisponível';
}
});
});