Compare commits

..

2 Commits

Author SHA1 Message Date
Ricardo Carneiro
7cc8f46a1a fix: ajustes no dashboard
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 2s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m6s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m15s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 1s
2025-08-31 02:34:39 -03:00
Ricardo Carneiro
6598dbdcdd fix: ajustar email para bcards.site 2025-08-31 01:50:19 -03:00
10 changed files with 292 additions and 399 deletions

View File

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

View File

@ -2,6 +2,7 @@
@{ @{
ViewData["Title"] = "Dashboard - BCards"; ViewData["Title"] = "Dashboard - BCards";
Layout = "_Layout"; Layout = "_Layout";
var pageInCreation = Model.UserPages.FirstOrDefault(p => (p.LastModerationStatus ?? p.Status) == BCards.Web.ViewModels.PageStatus.Creating);
} }
<div class="container py-4"> <div class="container py-4">
@ -15,12 +16,33 @@
<div> <div>
@if (!string.IsNullOrEmpty(Model.CurrentUser.ProfileImage)) @if (!string.IsNullOrEmpty(Model.CurrentUser.ProfileImage))
{ {
<img src="@Model.CurrentUser.ProfileImage" alt="@Model.CurrentUser.Name" <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>
</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 --> <!-- Lista de Páginas -->
<div class="row"> <div class="row">
@foreach (var pageItem in Model.UserPages) @foreach (var pageItem in Model.UserPages)
@ -32,53 +54,35 @@
@(pageItem.DisplayName) @(pageItem.DisplayName)
<form method="post" action="/Admin/DeletePage/@(pageItem.Id)" style="display: inline;" onsubmit="return confirm('Tem certeza que deseja excluir esta página?')"> <form method="post" action="/Admin/DeletePage/@(pageItem.Id)" style="display: inline;" onsubmit="return confirm('Tem certeza que deseja excluir esta página?')">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<button type="submit" class="btn btn-link text-danger p-0" title="Excluir página" <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> <i class="fas fa-trash"></i>
</button> </button>
</form> </form>
<input type="hidden" id="displayName_@pageItem.Id" value="@(pageItem.DisplayName)" />
</h6> </h6>
<p class="text-muted small mb-2">@(pageItem.Category)/@(pageItem.Slug)</p> <p class="text-muted small mb-2">@(pageItem.Category)/@(pageItem.Slug)</p>
<div class="mb-2"> <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: case BCards.Web.ViewModels.PageStatus.Active:
<span class="badge bg-success">Ativa</span> <span class="badge bg-success">Ativa</span>
break; 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: case BCards.Web.ViewModels.PageStatus.PendingModeration:
<span class="badge bg-warning">Aguardando</span> <span class="badge bg-warning">Aguardando Moderação</span>
break; break;
case BCards.Web.ViewModels.PageStatus.Rejected: case BCards.Web.ViewModels.PageStatus.Rejected:
<span class="badge bg-danger">Rejeitada</span> <span class="badge bg-danger">Rejeitada</span>
break; break;
case BCards.Web.ViewModels.PageStatus.Creating: case BCards.Web.ViewModels.PageStatus.Creating:
<span class="badge bg-info"> <span class="badge bg-info"><i class="fas fa-edit me-1"></i>Em Criação</span>
<i class="fas fa-edit me-1"></i>Criando break;
</span> default:
<span class="badge bg-secondary">@pageItem.Status</span>
break; break;
} }
</div> </div>
@if (Model.CurrentPlan.AllowsAnalytics) @if (Model.CurrentPlan.AllowsAnalytics)
{ {
<div class="row text-center small mb-3"> <div class="row text-center small mb-3">
@ -92,151 +96,51 @@
</div> </div>
</div> </div>
} }
</div> </div>
<!-- Cards com Hover Effect -->
<div class="card-footer" data-page-id="@pageItem.Id" data-status="@pageItem.Status"> <div class="card-footer" data-page-id="@pageItem.Id" data-status="@pageItem.Status">
<div class="d-flex gap-2"> <div class="d-grid gap-2">
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Active) @if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Active)
{ {
<a href="/page/@pageItem.Category/@pageItem.Slug" target="_blank" <a href="/page/@pageItem.Category/@pageItem.Slug" target="_blank" class="btn btn-success">
class="btn btn-success flex-fill"> <i class="fas fa-eye me-1"></i>Ver Página Publicada
<i class="fas fa-eye me-1"></i>Ver Página </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> </a>
} }
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating || else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating || pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected)
pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected ||
pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{ {
<button type="button" <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">
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 <i class="fas fa-vial me-1"></i>Testar Página
</button> </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 else
{ {
<button class="btn btn-secondary flex-fill" disabled> <button class="btn btn-secondary" disabled>
<i class="fas fa-ban me-1"></i>Indisponível <i class="fas fa-ban me-1"></i>Indisponível
</button> </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>
</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 --> <!-- Card para Criar Nova Página -->
@if (Model.CanCreateNewPage) @if (Model.CanCreateNewPage)
{ {
@ -246,8 +150,7 @@
<div> <div>
<i class="fas fa-plus fa-2x text-muted mb-3"></i> <i class="fas fa-plus fa-2x text-muted mb-3"></i>
<h6 class="text-muted">Criar Nova Página</h6> <h6 class="text-muted">Criar Nova Página</h6>
<a href="@Url.Action("ManagePage", new { id = "new" })" <a href="@Url.Action("ManagePage", new { id = "new" })" class="btn btn-primary">Começar</a>
class="btn btn-primary">Começar</a>
</div> </div>
</div> </div>
</div> </div>
@ -255,121 +158,89 @@
} }
else if (!Model.UserPages.Any()) else if (!Model.UserPages.Any())
{ {
<!-- Primeira Página -->
<div class="col-12"> <div class="col-12">
<div class="card border-primary"> <div class="card border-primary">
<div class="card-body text-center p-5"> <div class="card-body text-center p-5">
<div class="mb-4"> <div class="mb-4"><i class="display-1 text-primary">🚀</i></div>
<i class="display-1 text-primary">🚀</i>
</div>
<h3>Crie sua primeira página!</h3> <h3>Crie sua primeira página!</h3>
<p class="text-muted mb-4"> <p class="text-muted mb-4">Comece criando sua página profissional personalizada com seus links organizados.</p>
Comece criando sua página profissional personalizada com seus links organizados. <a href="@Url.Action("ManagePage", new { id = "new" })" class="btn btn-primary btn-lg">Criar Minha Página</a>
</p>
<a href="@Url.Action("ManagePage", new { id = "new" })" class="btn btn-primary btn-lg">
Criar Minha Página
</a>
</div> </div>
</div> </div>
</div> </div>
} }
else else
{ {
<!-- Limite atingido -->
<div class="col-12"> <div class="col-12">
<div class="alert alert-warning d-flex align-items-center alert-permanent"> <div class="alert alert-warning d-flex align-items-center alert-permanent">
<i class="fas fa-exclamation-triangle me-3"></i> <i class="fas fa-exclamation-triangle me-3"></i>
<div> <div>
<strong>Limite atingido!</strong> <strong>Limite atingido!</strong> Você já criou o máximo de @Model.CurrentPlan.MaxPages página(s) para seu plano atual.
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> <a href="@Url.Action("Pricing", "Home")" class="alert-link ms-2">Fazer upgrade</a>
</div> </div>
</div> </div>
</div> </div>
} }
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<!-- Plano Atual --> <!-- Painel de Plano Atual como Acordeão -->
<div class="card mb-4 @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "border-warning" : "")"> <div class="accordion" id="planAccordion">
<div class="card-header @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "bg-warning" : "bg-primary") text-white"> <div class="card mb-4 @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "border-warning" : "")">
<h6 class="mb-0"> <div class="card-header @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "bg-warning" : "bg-primary") text-white">
<i class="fas fa-crown me-2"></i> <h2 class="mb-0">
Plano Atual <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">
</h6> <span><i class="fas fa-crown me-2"></i>Plano Atual</span>
</div> <i id="plan-toggle-icon" class="fas fa-eye-slash"></i>
<div class="card-body"> </button>
<h5 class="text-capitalize mb-1">@Model.CurrentPlan.Name</h5> </h2>
</div>
@if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial) <div id="plan-collapse" class="accordion-collapse collapse">
{ <div class="card-body">
<p class="text-warning mb-2"> <h5 class="text-capitalize mb-1">@Model.CurrentPlan.Name</h5>
<i class="fas fa-clock me-1"></i> @if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial)
@Model.DaysRemaining dia(s) restante(s) {
</p> <p class="text-warning mb-2"><i class="fas fa-clock me-1"></i>@Model.DaysRemaining dia(s) restante(s)</p>
} }
else else
{ {
<p class="text-muted small mb-2">R$ @Model.CurrentPlan.Price.ToString("F2")/mês</p> <p class="text-muted small mb-2">R$ @Model.CurrentPlan.Price.ToString("F2")/mês</p>
} }
<div class="mb-3">
<div class="mb-3"> <div class="d-flex justify-content-between small mb-1">
<div class="d-flex justify-content-between small mb-1"> <span>Páginas</span>
<span>Páginas</span> <span>@Model.UserPages.Count/@Model.CurrentPlan.MaxPages</span>
<span>@Model.UserPages.Count/@Model.CurrentPlan.MaxPages</span> </div>
</div> <div class="progress" style="height: 6px;">
<div class="progress" style="height: 6px;"> @{
@{ var pagesPercentage = Model.CurrentPlan.MaxPages > 0 ? (double)Model.UserPages.Count / Model.CurrentPlan.MaxPages * 100 : 0;
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 class="progress-bar @(pagesPercentage >= 80 ? "bg-warning" : "bg-primary")"
style="width: @pagesPercentage%"></div>
</div> </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>
</div> </div>
<!-- Estatísticas Rápidas --> <!-- Estatísticas e Dicas (mantidos como antes) -->
@if (Model.CurrentPlan.AllowsAnalytics && Model.UserPages.Any()) @if (Model.CurrentPlan.AllowsAnalytics && Model.UserPages.Any())
{ {
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header"><h6 class="mb-0"><i class="fas fa-chart-line me-2"></i>Estatísticas Gerais</h6></div>
<h6 class="mb-0">
<i class="fas fa-chart-line me-2"></i>
Estatísticas Gerais
</h6>
</div>
<div class="card-body"> <div class="card-body">
<div class="row text-center"> <div class="row text-center">
<div class="col-6"> <div class="col-6">
@ -385,46 +256,22 @@
{ {
<hr class="my-3"> <hr class="my-3">
<div class="text-center"> <div class="text-center">
<div class="h5 text-info mb-0"> <div class="h5 text-info mb-0">@((Model.UserPages.Sum(p => p.TotalClicks) * 100.0 / Model.UserPages.Sum(p => p.TotalViews)).ToString("F1"))%</div>
@((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> <small class="text-muted">Taxa de Cliques</small>
</div> </div>
} }
</div> </div>
</div> </div>
} }
<!-- Dicas -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header"><h6 class="mb-0"><i class="fas fa-lightbulb me-2"></i>💡 Dicas</h6></div>
<h6 class="mb-0">
<i class="fas fa-lightbulb me-2"></i>
💡 Dicas
</h6>
</div>
<div class="card-body"> <div class="card-body">
<ul class="list-unstyled small mb-0"> <ul class="list-unstyled small mb-0">
<li class="mb-2"> <li class="mb-2"><i class="fas fa-check text-success me-2"></i>Use uma bio clara e objetiva</li>
<i class="fas fa-check text-success me-2"></i> <li class="mb-2"><i class="fas fa-check text-success me-2"></i>Organize seus links por importância</li>
Use uma bio clara e objetiva <li class="mb-2"><i class="fas fa-check text-success me-2"></i>Escolha URLs fáceis de lembrar</li>
</li> <li class="mb-2"><i class="fas fa-check text-success me-2"></i>Atualize regularmente seus links</li>
<li class="mb-2"> <li class="mb-0"><i class="fas fa-check text-success me-2"></i>Monitore suas estatísticas</li>
<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> </ul>
</div> </div>
</div> </div>
@ -434,135 +281,156 @@
@section Scripts { @section Scripts {
<script> <script>
// Função para abrir preview com token fresh // Funções auxiliares para cookies
async function openPreview(pageId) { function setCookie(name, value, days) {
const button = event.target.closest('button'); var expires = "";
const category = button.dataset.pageCategory; if (days) {
const slug = button.dataset.pageSlug; var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
// Desabilitar botão temporariamente expires = "; expires=" + date.toUTCString();
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
}
});
const result = await response.json();
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) { document.cookie = name + "=" + (value || "") + expires + "; path=/";
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;
} }
} function getCookie(name) {
var nameEQ = name + "=";
async function submitForModeration(pageId) { var ca = document.cookie.split(';');
const pageName = event.target.dataset.pageName || 'esta página'; for (var i = 0; i < ca.length; i++) {
var c = ca[i];
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.`)) { while (c.charAt(0) == ' ') c = c.substring(1, c.length);
return; if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
// 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) { return null;
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) { document.addEventListener('DOMContentLoaded', function () {
const toastContainer = getOrCreateToastContainer(); // Inicializar tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
const bgClass = type === 'success' ? 'bg-success' : type === 'error' ? 'bg-danger' : 'bg-info'; var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
const icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-triangle' : 'fa-info-circle'; return new bootstrap.Tooltip(tooltipTriggerEl);
});
const toastHtml = `
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="5000"> // Lógica para o acordeão de instruções
<div class="toast-header ${bgClass} text-white"> const instructionsAccordion = document.getElementById('instructions-collapse');
<i class="fas ${icon} me-2"></i> if (instructionsAccordion) {
<strong class="me-auto">${type === 'success' ? 'Sucesso' : type === 'error' ? 'Erro' : 'Informação'}</strong> if (getCookie('instructions_collapsed') === 'true') {
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button> // Já começa fechado pelo `collapsed` no botão, então não fazemos nada
</div> } else {
<div class="toast-body">${message}</div> // Se não tem cookie ou está aberto, abre
</div> new bootstrap.Collapse(instructionsAccordion, { toggle: true });
`; }
instructionsAccordion.addEventListener('show.bs.collapse', function () {
toastContainer.insertAdjacentHTML('beforeend', toastHtml); setCookie('instructions_collapsed', 'false', 365);
});
const newToast = toastContainer.lastElementChild; instructionsAccordion.addEventListener('hide.bs.collapse', function () {
const toast = new bootstrap.Toast(newToast); setCookie('instructions_collapsed', 'true', 365);
toast.show(); });
}
// Remover toast após ser fechado
newToast.addEventListener('hidden.bs.toast', function() { // Lógica para o acordeão do plano
newToast.remove(); 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');
});
}
}); });
}
function getOrCreateToastContainer() { // Funções existentes (submitForModeration, openPreview, etc.)
let container = document.querySelector('.toast-container'); async function openPreview(pageId) {
if (!container) { const button = event.target.closest('button');
container = document.createElement('div'); const category = button.dataset.pageCategory;
container.className = 'toast-container position-fixed top-0 end-0 p-3'; const slug = button.dataset.pageSlug;
container.style.zIndex = '1055'; button.disabled = true;
document.body.appendChild(container); 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';
}
} }
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> </script>
} }
@ -596,4 +464,4 @@ function getOrCreateToastContainer() {
</div> </div>
</div> </div>
</div> </div>
} }

View File

@ -97,7 +97,7 @@
<i class="fs-2 text-primary">🔗</i> <i class="fs-2 text-primary">🔗</i>
</div> </div>
<h5>URLs Organizadas</h5> <h5>URLs Organizadas</h5>
<p class="text-muted">Suas URLs são organizadas por categoria: vcart.me/corretor/seu-nome</p> <p class="text-muted">Suas URLs são organizadas por categoria: bcards.site/corretor/seu-nome</p>
</div> </div>
</div> </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>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> <li><strong>Revogação do Consentimento:</strong> O direito de revogar seu consentimento a qualquer momento.</li>
</ul> </ul>
<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> <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>
<h4 class="mt-5 fw-bold">5. Cookies e Tecnologias de Rastreamento</h4> <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> <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>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> <li><strong>Portabilidad:</strong> El derecho a recibir sus datos en un formato estructurado.</li>
</ul> </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 href="mailto:dpo@vcart.me">dpo@vcart.me</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 class="email-obfuscated" data-user="dpo" data-domain="bcards.site">[cargando email...]</a>.</p>
<h4 class="mt-5 fw-bold">5. Retención de Datos</h4> <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> <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"> <div class="alert alert-secondary text-center">
<h5 class="alert-heading">Canal de Atendimento ao Titular</h5> <h5 class="alert-heading">Canal de Atendimento ao Titular</h5>
<p class="mb-0">Envie um e-mail para:</p> <p class="mb-0">Envie um e-mail para:</p>
<a href="mailto:dpo@vcart.me" class="fs-5 fw-bold">dpo@vcart.me</a> <a class="email-obfuscated fs-5 fw-bold" data-user="dpo" data-domain="bcards.site">[carregando e-mail...]</a>
</div> </div>
<p>No seu e-mail, por favor, inclua:</p> <p>No seu e-mail, por favor, inclua:</p>

View File

@ -64,7 +64,7 @@
<ul> <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>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>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 href="mailto:suporte@vcart.me">suporte@vcart.me</a>.</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>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -57,7 +57,7 @@
<ul> <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>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>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 href="mailto:suporte@vcart.me">suporte@vcart.me</a>.</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>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -242,6 +242,8 @@
<script src="~/lib/jquery/jquery.min.js"></script> <script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.bundle.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/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 --> <!-- Scripts para menu ativo e barra de carregamento -->
<script> <script>
@ -346,4 +348,3 @@
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</body> </body>
</html> </html>
</html>

View File

@ -0,0 +1,24 @@
/*!
* 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';
}
});
});