BCards/src/BCards.Web/Views/UserPage/Display.cshtml

601 lines
26 KiB
Plaintext

@model BCards.Web.Models.IPageDisplay
@{
var seo = ViewBag.SeoSettings as BCards.Web.Models.SeoSettings;
var category = ViewBag.Category as BCards.Web.Models.Category;
var isPreview = ViewBag.IsPreview as bool? ?? false;
var isLivePage = ViewBag.IsLivePage as bool? ?? false;
ViewData["Title"] = seo?.Title ?? $"{Model.DisplayName} - {category?.Name}";
// Lógica de layout corrigida para usar o _PreviewLayout
Layout = isPreview ? "_PreviewLayout" : "_UserPageLayout";
}
@section Head {
@if (isLivePage)
{
<meta name="robots" content="index, follow">
@if (!string.IsNullOrEmpty(ViewBag.PageUrl as string))
{
<link rel="canonical" href="@ViewBag.PageUrl">
}
}
else
{
// A meta tag de noindex/nofollow para previews já está no _PreviewLayout
<meta name="robots" content="noindex, nofollow">
}
}
@section Styles {
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@{
// Renderiza os estilos do tema dinâmico
var partialOutput = await Html.PartialAsync("_ThemeStyles", Model.Theme);
using (var writer = new System.IO.StringWriter())
{
partialOutput.WriteTo(writer, HtmlEncoder);
@Html.Raw(writer.ToString())
}
}
/* QR Code Section Styles */
.qrcode-section {
margin-top: 2rem;
}
.documents-section .list-group-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: inherit;
transition: all 0.2s ease;
border-radius: 12px;
padding: 1rem;
}
.documents-section .list-group-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.documents-section .list-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.qrcode-toggle {
width: 100%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.3s ease;
color: inherit;
font-size: 1rem;
font-weight: 500;
}
.qrcode-toggle:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.qrcode-toggle i {
transition: transform 0.3s ease;
}
.qrcode-container {
margin-top: 1rem;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
text-align: center;
animation: slideDown 0.3s ease;
}
@@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.qrcode-canvas {
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto 1rem;
padding: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 250px;
}
.qrcode-canvas > div {
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
.qrcode-canvas img,
.qrcode-canvas canvas {
display: block !important;
margin: 0 auto !important;
max-width: 200px !important;
max-height: 200px !important;
}
/* Hide duplicate canvas/img if library generates both */
.qrcode-canvas img + canvas,
.qrcode-canvas canvas + img {
display: none !important;
}
.qrcode-hint {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.btn-download-qr {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-download-qr:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
}
.btn-download-qr:active {
transform: translateY(0);
}
/* Mobile Responsive */
@@media (max-width: 576px) {
.qrcode-toggle {
font-size: 0.9rem;
padding: 0.875rem;
}
.qrcode-container {
padding: 1rem;
}
.qrcode-canvas {
padding: 0.75rem;
}
.btn-download-qr {
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
}
}
</style>
}
<div class="user-page min-vh-100 d-flex align-items-center py-4">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6 col-md-8">
<div class="profile-card mx-auto">
<!-- Profile Image & Info -->
<div class="profile-header text-center mb-4">
@if (!string.IsNullOrEmpty(Model.ProfileImageId))
{
<img src="@Model.ProfileImageUrl" alt="@Model.DisplayName" class="profile-image-large rounded-circle mb-3">
}
else
{
<div class="profile-icon-placeholder mb-3">
<i class="fas fa-id-card"></i>
</div>
}
<h1 class="profile-name mb-0">@Model.DisplayName</h1>
</div>
<div class="text-center">
@if (!string.IsNullOrEmpty(Model.Bio))
{
<p class="profile-bio">@Model.Bio</p>
}
<!-- Links Container -->
<div class="links-container">
@if (Model.Links?.Any(l => l.IsActive) == true)
{
@for (int i = 0; i < Model.Links.Count; i++)
{
var link = Model.Links[i];
if (link.IsActive)
{
var hasExpandableContent = (!string.IsNullOrEmpty(link.Description) ||
(link.Type == BCards.Web.Models.LinkType.Product && !string.IsNullOrEmpty(link.ProductDescription)));
<div class="universal-link" data-link-id="@i">
<a href="@link.Url"
class="universal-link-header"
onclick="recordClick('@Model.Id', @i)"
target="_blank"
rel="noopener noreferrer">
<div class="universal-link-content">
@if (link.Type == BCards.Web.Models.LinkType.Product && !string.IsNullOrEmpty(link.ProductImage))
{
<img src="@link.ProductImage"
alt="@(link.ProductTitle ?? link.Title)"
class="link-thumbnail"
loading="lazy"
onerror="this.style.display='none'">
}
else if (!string.IsNullOrEmpty(link.Icon))
{
<div class="link-icon"><i class="@link.Icon"></i></div>
}
else
{
<div class="link-icon"><i class="fas fa-link"></i></div>
}
<div class="link-text-container">
<div class="link-title">
@if (link.Type == BCards.Web.Models.LinkType.Product && !string.IsNullOrEmpty(link.ProductTitle))
{
@link.ProductTitle
}
else
{
@link.Title
}
</div>
@if (link.Type == BCards.Web.Models.LinkType.Product && !string.IsNullOrEmpty(link.ProductPrice))
{
<div class="link-subtitle">@link.ProductPrice</div>
}
else if (!string.IsNullOrEmpty(link.Description) && link.Description.Length > 50)
{
<div class="link-subtitle">@(link.Description.Substring(0, 50))...</div>
}
</div>
</div>
@if (hasExpandableContent)
{
<button class="expand-arrow"
type="button"
onclick="event.preventDefault(); event.stopPropagation(); toggleLinkDetails(@i)">
<i class="fas fa-chevron-down"></i>
</button>
}
</a>
@if (hasExpandableContent)
{
<div class="universal-link-details" id="details-@i">
@if (link.Type == BCards.Web.Models.LinkType.Product)
{
@if (!string.IsNullOrEmpty(link.ProductImage))
{
<img src="@link.ProductImage"
alt="@(link.ProductTitle ?? link.Title)"
class="expanded-image"
loading="lazy">
}
@if (!string.IsNullOrEmpty(link.ProductPrice))
{
<div class="expanded-price">@link.ProductPrice</div>
}
@if (!string.IsNullOrEmpty(link.ProductDescription))
{
<div class="expanded-description">@link.ProductDescription</div>
}
}
else
{
@if (!string.IsNullOrEmpty(link.Description))
{
<div class="expanded-description">@link.Description</div>
}
}
<div class="expanded-action">
<i class="fas fa-external-link-alt"></i>
Clique no título acima para abrir
</div>
</div>
}
</div>
}
}
}
@* Documentos integrados no mesmo container de links *@
@if (Model.Documents?.Any() == true)
{
@for (int docIndex = 0; docIndex < Model.Documents.Count; docIndex++)
{
var document = Model.Documents[docIndex];
var hasDescription = !string.IsNullOrEmpty(document.Description);
var uniqueId = $"doc-{docIndex}";
<div class="universal-link" data-document-id="@uniqueId">
<a href="/api/document/@document.FileId"
class="universal-link-header"
target="_blank"
rel="noopener noreferrer">
<div class="universal-link-content">
<div class="link-icon">
<i class="fas fa-file-pdf"></i>
</div>
<div class="link-text-container">
<div class="link-title">@document.Title</div>
@if (hasDescription && document.Description.Length > 50)
{
<div class="link-subtitle">@(document.Description.Substring(0, 50))...</div>
}
</div>
</div>
@if (hasDescription)
{
<button class="expand-arrow"
type="button"
onclick="event.preventDefault(); event.stopPropagation(); toggleLinkDetails('@uniqueId')">
<i class="fas fa-chevron-down"></i>
</button>
}
</a>
@if (hasDescription)
{
<div class="universal-link-details" id="details-@uniqueId">
<div class="expanded-description">@document.Description</div>
<div class="expanded-action">
<i class="fas fa-external-link-alt"></i>
Clique no título acima para abrir o PDF
</div>
</div>
}
</div>
}
}
@if ((Model.Links?.Any(l => l.IsActive) != true) && (Model.Documents?.Any() != true))
{
<div class="text-muted">
<p>Nenhum link disponível no momento.</p>
</div>
}
</div>
<!-- QR Code Section -->
<div class="qrcode-section mt-4">
<button class="qrcode-toggle" onclick="toggleQRCode()" type="button">
<i class="fas fa-qrcode me-2"></i>
<span id="qrToggleText">Ocultar QR Code</span>
<i class="fas fa-chevron-up ms-auto" id="qrToggleIcon"></i>
</button>
<div class="qrcode-container" id="qrcodeContainer" style="display: block;">
<div class="qrcode-canvas" id="qrcode"></div>
<p class="qrcode-hint">Escaneie para compartilhar esta página</p>
<button class="btn-download-qr" onclick="downloadQR()" type="button">
<i class="fas fa-download me-1"></i> Baixar QR Code
</button>
</div>
</div>
<!-- Footer -->
<div class="profile-footer">
<div class="footer-promo" onclick="togglePromo(this)">
<div class="footer-promo-header">
<span>💡 Gostou desta página?</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="footer-promo-content">
Crie a sua própria página personalizada com <strong>BCards</strong>!
É rápido, fácil e profissional. Compartilhe todos os seus links em um só lugar.
<div class="mt-2">
<a href="@Url.Action("Index", "Home")" class="footer-promo-button">
<i class="fas fa-rocket"></i>
Criar Minha Página
</a>
</div>
</div>
</div>
<div class="footer-credits">
Criado com <a href="@Url.Action("Index", "Home")">BCards</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<!-- QRCode.js Library - Load FIRST -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
function recordClick(pageId, linkIndex) {
// Detectar se é LivePage ou UserPage
var isLivePage = @Json.Serialize(isLivePage);
if (isLivePage) {
// LivePages não registram cliques via fetch - usam redirecionamento direto
return;
} else {
// UserPages registram cliques via API
fetch('/page/click/' + pageId, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ linkIndex: linkIndex })
}).catch(function(error) {
console.log('Error recording click:', error);
});
}
}
function toggleLinkDetails(linkIndex) {
const currentDetails = document.getElementById('details-' + linkIndex);
// Suporta tanto data-link-id (links) quanto data-document-id (documentos)
let currentArrow = document.querySelector(`[data-link-id="${linkIndex}"] .expand-arrow`);
if (!currentArrow) {
currentArrow = document.querySelector(`[data-document-id="${linkIndex}"] .expand-arrow`);
}
if (!currentDetails || !currentArrow) return;
const isCurrentlyExpanded = currentDetails.classList.contains('show');
document.querySelectorAll('.universal-link-details').forEach(details => details.classList.remove('show'));
document.querySelectorAll('.expand-arrow').forEach(arrow => {
arrow.classList.remove('expanded');
arrow.querySelector('i').style.transform = 'rotate(0deg)';
});
if (!isCurrentlyExpanded) {
currentDetails.classList.add('show');
currentArrow.classList.add('expanded');
currentArrow.querySelector('i').style.transform = 'rotate(180deg)';
}
}
function togglePromo(element) {
const content = element.querySelector('.footer-promo-content');
const arrow = element.querySelector('.footer-promo-header i');
const isExpanded = content.classList.toggle('show');
arrow.style.transform = isExpanded ? 'rotate(180deg)' : 'rotate(0deg)';
element.querySelector('.footer-promo-header').classList.toggle('expanded', isExpanded);
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.universal-link-details, .footer-promo-content').forEach(d => d.classList.remove('show'));
document.querySelectorAll('.expand-arrow i, .footer-promo-header i').forEach(arrow => arrow.style.transform = 'rotate(0deg)');
document.querySelectorAll('.expand-arrow').forEach(button => {
button.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
button.click();
}
});
});
// Generate QR Code on page load
generateQRCode();
});
// QR Code Functions
let qrCodeGenerated = false;
function toggleQRCode() {
const container = document.getElementById('qrcodeContainer');
const icon = document.getElementById('qrToggleIcon');
const text = document.getElementById('qrToggleText');
if (container.style.display === 'block') {
// Close
container.style.display = 'none';
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
text.textContent = 'Mostrar QR Code';
} else {
// Open
container.style.display = 'block';
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
text.textContent = 'Ocultar QR Code';
}
}
function generateQRCode() {
if (qrCodeGenerated) {
console.log('QR Code already generated, skipping...');
return;
}
const qrcodeElement = document.getElementById("qrcode");
if (!qrcodeElement) {
console.error('QR Code container not found');
return;
}
// Check if already has content (double-call prevention)
if (qrcodeElement.querySelector('canvas')) {
console.log('Canvas already exists, skipping generation');
qrCodeGenerated = true;
return;
}
// Mark as generated BEFORE creating to prevent race conditions
qrCodeGenerated = true;
// Clear any existing content
qrcodeElement.innerHTML = '';
const pageUrl = window.location.href.split('?')[0]; // Remove query params
try {
new QRCode(qrcodeElement, {
text: pageUrl,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
console.log('QR Code generated successfully for:', pageUrl);
} catch (error) {
console.error('Error generating QR Code:', error);
qrcodeElement.innerHTML = '<p class="text-danger small">Erro ao gerar QR Code</p>';
qrCodeGenerated = false; // Reset on error so it can retry
}
}
function downloadQR() {
try {
const canvas = document.querySelector('#qrcode canvas');
if (!canvas) {
alert('QR Code não está disponível');
return;
}
const url = canvas.toDataURL("image/png");
const link = document.createElement('a');
link.download = 'qrcode-@(Model.Slug ?? "page").png';
link.href = url;
link.click();
} catch (error) {
console.error('Error downloading QR Code:', error);
alert('Erro ao baixar QR Code');
}
}
</script>
}