|
|
--- |
|
|
import HtmlEmbed from "./HtmlEmbed.astro"; |
|
|
|
|
|
interface Props { |
|
|
title: string; |
|
|
titleRaw?: string; |
|
|
description?: string; |
|
|
authors?: Array< |
|
|
string | { name: string; url?: string; affiliationIndices?: number[] } |
|
|
>; |
|
|
affiliations?: Array<{ id: number; name: string; url?: string }>; |
|
|
affiliation?: string; |
|
|
published?: string; |
|
|
doi?: string; |
|
|
pdfProOnly?: boolean; |
|
|
showPdf?: boolean; |
|
|
} |
|
|
|
|
|
const { |
|
|
title, |
|
|
titleRaw, |
|
|
description, |
|
|
authors = [], |
|
|
affiliations = [], |
|
|
affiliation, |
|
|
published, |
|
|
doi, |
|
|
pdfProOnly = false, |
|
|
showPdf = true, |
|
|
} = Astro.props as Props; |
|
|
|
|
|
type Author = { name: string; url?: string; affiliationIndices?: number[] }; |
|
|
|
|
|
function normalizeAuthors( |
|
|
input: Array< |
|
|
| string |
|
|
| { |
|
|
name?: string; |
|
|
url?: string; |
|
|
link?: string; |
|
|
affiliationIndices?: number[]; |
|
|
} |
|
|
>, |
|
|
): Author[] { |
|
|
return (Array.isArray(input) ? input : []) |
|
|
.map((a) => { |
|
|
if (typeof a === "string") { |
|
|
return { name: a } as Author; |
|
|
} |
|
|
const name = (a?.name ?? "").toString(); |
|
|
const url = (a?.url ?? a?.link) as string | undefined; |
|
|
const affiliationIndices = Array.isArray((a as any)?.affiliationIndices) |
|
|
? (a as any).affiliationIndices |
|
|
: undefined; |
|
|
return { name, url, affiliationIndices } as Author; |
|
|
}) |
|
|
.filter((a) => a.name && a.name.trim().length > 0); |
|
|
} |
|
|
|
|
|
const normalizedAuthors: Author[] = normalizeAuthors(authors as any); |
|
|
|
|
|
|
|
|
const authorAffiliationIndexSet = new Set<number>(); |
|
|
for (const author of normalizedAuthors) { |
|
|
const indices = Array.isArray(author.affiliationIndices) |
|
|
? author.affiliationIndices |
|
|
: []; |
|
|
for (const idx of indices) { |
|
|
if (typeof idx === "number") { |
|
|
authorAffiliationIndexSet.add(idx); |
|
|
} |
|
|
} |
|
|
} |
|
|
const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1; |
|
|
const hasMultipleAffiliations = |
|
|
Array.isArray(affiliations) && affiliations.length > 1; |
|
|
|
|
|
function stripHtml(text: string): string { |
|
|
return String(text || "").replace(/<[^>]*>/g, ""); |
|
|
} |
|
|
|
|
|
function slugify(text: string): string { |
|
|
return ( |
|
|
String(text || "") |
|
|
.normalize("NFKD") |
|
|
.replace(/\p{Diacritic}+/gu, "") |
|
|
.toLowerCase() |
|
|
.replace(/[^a-z0-9]+/g, "-") |
|
|
.replace(/^-+|-+$/g, "") |
|
|
.slice(0, 120) || "article" |
|
|
); |
|
|
} |
|
|
|
|
|
const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title); |
|
|
const pdfFilename = `${slugify(pdfBase)}.pdf`; |
|
|
--- |
|
|
|
|
|
<section class="hero"> |
|
|
<h1 class="hero-title" set:html={title} /> |
|
|
<div class="hero-banner"> |
|
|
<HtmlEmbed src="banner.html" frameless /> |
|
|
{description && <p class="hero-desc">{description}</p>} |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
<header class="meta" aria-label="Article meta information"> |
|
|
<div class="meta-container"> |
|
|
{ |
|
|
normalizedAuthors.length > 0 && ( |
|
|
<div class="meta-container-cell"> |
|
|
<h3>Author{normalizedAuthors.length > 1 ? "s" : ""}</h3> |
|
|
<ul class="authors"> |
|
|
{normalizedAuthors.map((a, i) => { |
|
|
const supers = |
|
|
shouldShowAffiliationSupers && |
|
|
Array.isArray(a.affiliationIndices) && |
|
|
a.affiliationIndices.length ? ( |
|
|
<sup>{a.affiliationIndices.join(", ")}</sup> |
|
|
) : null; |
|
|
return ( |
|
|
<li> |
|
|
{a.url ? <a href={a.url}>{a.name}</a> : a.name}{supers}{i < normalizedAuthors.length - 1 && <span set:html=", " />} |
|
|
</li> |
|
|
); |
|
|
})} |
|
|
</ul> |
|
|
</div> |
|
|
) |
|
|
} |
|
|
{ |
|
|
Array.isArray(affiliations) && affiliations.length > 0 && ( |
|
|
<div class="meta-container-cell meta-container-cell--affiliations"> |
|
|
<h3>Affiliation{affiliations.length > 1 ? "s" : ""}</h3> |
|
|
{hasMultipleAffiliations ? ( |
|
|
<ol class="affiliations"> |
|
|
{affiliations.map((af) => ( |
|
|
<li value={af.id}> |
|
|
{af.url ? ( |
|
|
<a href={af.url} target="_blank" rel="noopener noreferrer"> |
|
|
{af.name} |
|
|
</a> |
|
|
) : ( |
|
|
af.name |
|
|
)} |
|
|
</li> |
|
|
))} |
|
|
</ol> |
|
|
) : ( |
|
|
<p> |
|
|
{affiliations[0]?.url ? ( |
|
|
<a |
|
|
href={affiliations[0].url} |
|
|
target="_blank" |
|
|
rel="noopener noreferrer" |
|
|
> |
|
|
{affiliations[0].name} |
|
|
</a> |
|
|
) : ( |
|
|
affiliations[0]?.name |
|
|
)} |
|
|
</p> |
|
|
)} |
|
|
</div> |
|
|
) |
|
|
} |
|
|
{ |
|
|
(!affiliations || affiliations.length === 0) && affiliation && ( |
|
|
<div class="meta-container-cell meta-container-cell--affiliations"> |
|
|
<h3>Affiliation</h3> |
|
|
<p>{affiliation}</p> |
|
|
</div> |
|
|
) |
|
|
} |
|
|
{ |
|
|
published && ( |
|
|
<div class="meta-container-cell meta-container-cell--published"> |
|
|
<h3>Published</h3> |
|
|
<p>{published}</p> |
|
|
</div> |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{showPdf && ( |
|
|
<div class="meta-container-cell meta-container-cell--pdf"> |
|
|
<div class="pdf-header-wrapper"> |
|
|
<h3>PDF</h3> |
|
|
<span class="pro-badge-wrapper" style="display: none;"> |
|
|
<span class="pro-badge-prefix">- you are</span> |
|
|
<span class="pro-badge">PRO</span> |
|
|
</span> |
|
|
<span class="pro-only-label" style="display: none;"> |
|
|
<span class="pro-only-dash">-</span> |
|
|
<span class="pro-only-text">pro only</span> |
|
|
<svg class="pro-only-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> |
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path> |
|
|
</svg> |
|
|
</span> |
|
|
</div> |
|
|
<div id="pdf-download-container" data-pdf-pro-only={pdfProOnly.toString()}> |
|
|
<p class="pdf-loading">Checking access...</p> |
|
|
<p class="pdf-pro-only" style="display: none;"> |
|
|
<a |
|
|
class="button" |
|
|
href={`/${pdfFilename}`} |
|
|
download={pdfFilename} |
|
|
aria-label={`Download PDF ${pdfFilename}`} |
|
|
> |
|
|
Download PDF |
|
|
</a> |
|
|
</p> |
|
|
<div class="pdf-locked" style="display: none;"> |
|
|
<a |
|
|
class="button button-locked" |
|
|
href="https://huggingface.co/subscribe/pro" |
|
|
target="_blank" |
|
|
rel="noopener noreferrer" |
|
|
> |
|
|
<svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 12 12" fill="none"> |
|
|
<path d="M6.48 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86A5.3 5.3 0 0 0 7.99 7.5a3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86A5.3 5.3 0 0 0 4.01 4.5a3.95 3.95 0 0 0 1.51-3.24h.96Z" fill="currentColor"></path> |
|
|
</svg> |
|
|
<span class="locked-title">Subscribe to Pro</span> |
|
|
</a> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
{showPdf && ( |
|
|
<script is:inline> |
|
|
|
|
|
|
|
|
|
|
|
const LOCAL_IS_PRO = true; |
|
|
|
|
|
const FALLBACK_TIMEOUT_MS = 3000; |
|
|
const MAX_RETRY_ATTEMPTS = 100; |
|
|
let userPlanChecked = false; |
|
|
let initialized = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isProUser(plan) { |
|
|
if (!plan) return false; |
|
|
return plan.user === "pro"; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updatePdfAccess(isPro, pdfProOnly) { |
|
|
const loadingEl = document.querySelector(".pdf-loading"); |
|
|
const proOnlyEl = document.querySelector(".pdf-pro-only"); |
|
|
const lockedEl = document.querySelector(".pdf-locked"); |
|
|
const proOnlyLabel = document.querySelector(".pro-only-label"); |
|
|
const proBadgeWrapper = document.querySelector(".pro-badge-wrapper"); |
|
|
|
|
|
|
|
|
if (loadingEl) loadingEl.style.display = "none"; |
|
|
|
|
|
|
|
|
if (!pdfProOnly) { |
|
|
if (proOnlyEl) proOnlyEl.style.display = "block"; |
|
|
if (proOnlyLabel) proOnlyLabel.style.display = "none"; |
|
|
if (lockedEl) lockedEl.style.display = "none"; |
|
|
if (proBadgeWrapper) proBadgeWrapper.style.display = "none"; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (isPro) { |
|
|
if (proOnlyEl) proOnlyEl.style.display = "block"; |
|
|
if (proOnlyLabel) proOnlyLabel.style.display = "none"; |
|
|
if (lockedEl) lockedEl.style.display = "none"; |
|
|
if (proBadgeWrapper) proBadgeWrapper.style.display = "inline-flex"; |
|
|
} else { |
|
|
if (proOnlyEl) proOnlyEl.style.display = "none"; |
|
|
if (proOnlyLabel) proOnlyLabel.style.display = "inline-flex"; |
|
|
if (lockedEl) lockedEl.style.display = "block"; |
|
|
if (proBadgeWrapper) proBadgeWrapper.style.display = "none"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleUserPlan(plan, pdfProOnly) { |
|
|
userPlanChecked = true; |
|
|
const isPro = isProUser(plan); |
|
|
updatePdfAccess(isPro, pdfProOnly); |
|
|
|
|
|
|
|
|
console.log("[PDF Access]", { plan, isPro }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleFallback(pdfProOnly) { |
|
|
if (LOCAL_IS_PRO) { |
|
|
handleUserPlan({ user: "pro" }, pdfProOnly); |
|
|
} else { |
|
|
handleUserPlan({ user: "free" }, pdfProOnly); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initPdfAccess(retryCount = 0) { |
|
|
|
|
|
if (initialized) return; |
|
|
|
|
|
|
|
|
const pdfContainer = document.querySelector("#pdf-download-container"); |
|
|
|
|
|
if (!pdfContainer) { |
|
|
|
|
|
if (retryCount < MAX_RETRY_ATTEMPTS) { |
|
|
setTimeout(() => initPdfAccess(retryCount + 1), 10); |
|
|
} else { |
|
|
console.warn("[PDF Access] Container not found after max retries"); |
|
|
|
|
|
setTimeout(() => { |
|
|
const container = document.querySelector("#pdf-download-container"); |
|
|
if (container && !initialized) { |
|
|
initialized = true; |
|
|
const pdfProOnly = container.getAttribute("data-pdf-pro-only") === "true"; |
|
|
updatePdfAccess(!pdfProOnly, pdfProOnly); |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
initialized = true; |
|
|
const pdfProOnly = pdfContainer.getAttribute("data-pdf-pro-only") === "true"; |
|
|
|
|
|
|
|
|
if (!pdfProOnly) { |
|
|
updatePdfAccess(true, pdfProOnly); |
|
|
} else { |
|
|
|
|
|
|
|
|
const messageHandler = (event) => { |
|
|
if (event.data.type === "USER_PLAN") { |
|
|
handleUserPlan(event.data.plan, pdfProOnly); |
|
|
} |
|
|
}; |
|
|
|
|
|
window.addEventListener("message", messageHandler); |
|
|
|
|
|
|
|
|
if (window.parent && window.parent !== window) { |
|
|
|
|
|
window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*"); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (!userPlanChecked) { |
|
|
handleFallback(pdfProOnly); |
|
|
} |
|
|
}, FALLBACK_TIMEOUT_MS); |
|
|
} else { |
|
|
|
|
|
handleFallback(pdfProOnly); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
(function() { |
|
|
if (document.readyState === "loading") { |
|
|
document.addEventListener("DOMContentLoaded", () => { |
|
|
setTimeout(() => initPdfAccess(), 0); |
|
|
}); |
|
|
} else { |
|
|
|
|
|
setTimeout(() => initPdfAccess(), 0); |
|
|
} |
|
|
})(); |
|
|
</script> |
|
|
)} |
|
|
|
|
|
<style> |
|
|
|
|
|
.hero { |
|
|
width: 100%; |
|
|
padding: 64px 16px 16px; |
|
|
text-align: center; |
|
|
} |
|
|
.hero-title { |
|
|
font-size: clamp(28px, 4vw, 48px); |
|
|
font-weight: 800; |
|
|
line-height: 1.1; |
|
|
margin: 0 0 8px; |
|
|
max-width: 100%; |
|
|
} |
|
|
.hero-banner { |
|
|
max-width: 980px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
.hero-desc { |
|
|
color: var(--muted-color); |
|
|
font-style: italic; |
|
|
margin: 0 auto 16px; |
|
|
max-width: 55%; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.hero-desc { |
|
|
max-width: 90%; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.meta { |
|
|
border-top: 1px solid var(--border-color); |
|
|
border-bottom: 1px solid var(--border-color); |
|
|
padding: 1rem 0; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
.meta-container { |
|
|
max-width: 980px; |
|
|
display: flex; |
|
|
flex-direction: row; |
|
|
justify-content: space-between; |
|
|
margin: 0 auto; |
|
|
padding: 0 var(--content-padding-x); |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.meta-container a:not(.button) { |
|
|
color: var(--primary-color); |
|
|
text-decoration: underline; |
|
|
text-underline-offset: 2px; |
|
|
text-decoration-thickness: 0.06em; |
|
|
text-decoration-color: var(--link-underline); |
|
|
transition: text-decoration-color 0.15s ease-in-out; |
|
|
} |
|
|
.meta-container a:hover { |
|
|
text-decoration-color: var(--link-underline-hover); |
|
|
} |
|
|
.meta-container a.button, |
|
|
.meta-container .button { |
|
|
text-decoration: none; |
|
|
} |
|
|
.meta-container-cell { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 8px; |
|
|
max-width: 400px; |
|
|
} |
|
|
.meta-container-cell h3 { |
|
|
margin: 0; |
|
|
font-size: 12px; |
|
|
font-weight: 400; |
|
|
color: var(--muted-color); |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.02em; |
|
|
} |
|
|
.meta-container-cell p { |
|
|
margin: 0; |
|
|
} |
|
|
.authors { |
|
|
margin: 0; |
|
|
list-style-type: none; |
|
|
padding-left: 0; |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
.authors li { |
|
|
white-space: nowrap; |
|
|
padding:0; |
|
|
} |
|
|
.affiliations { |
|
|
margin: 0; |
|
|
padding-left: 1.25em; |
|
|
} |
|
|
.affiliations li { |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
header.meta .meta-container { |
|
|
flex-wrap: wrap; |
|
|
row-gap: 12px; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.meta-container-cell--affiliations, |
|
|
.meta-container-cell--pdf { |
|
|
text-align: right; |
|
|
} |
|
|
} |
|
|
|
|
|
@media print { |
|
|
.meta-container-cell--pdf { |
|
|
display: none !important; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.pdf-header-wrapper { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
line-height: 1; |
|
|
} |
|
|
|
|
|
.pdf-header-wrapper h3 { |
|
|
line-height: 1; |
|
|
} |
|
|
|
|
|
#pdf-download-container { |
|
|
min-height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px); |
|
|
} |
|
|
|
|
|
.pdf-loading { |
|
|
color: var(--muted-color); |
|
|
font-size: var(--button-font-size); |
|
|
line-height: 1; |
|
|
padding: var(--button-padding-y) var(--button-padding-x); |
|
|
margin: 0; |
|
|
display: inline-block; |
|
|
box-sizing: border-box; |
|
|
border: 1px solid transparent; |
|
|
height: calc(var(--button-padding-y) * 2 + var(--button-font-size) + 2px); |
|
|
vertical-align: top; |
|
|
} |
|
|
|
|
|
.pdf-pro-only { |
|
|
margin: 0; |
|
|
line-height: 0; |
|
|
} |
|
|
|
|
|
.pdf-pro-only .button { |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.pro-badge-wrapper { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 5px; |
|
|
font-style: normal; |
|
|
} |
|
|
|
|
|
.pro-badge-prefix { |
|
|
font-size: 0.85em; |
|
|
opacity: 0.5; |
|
|
font-weight: 400; |
|
|
font-style: normal; |
|
|
} |
|
|
|
|
|
.pro-badge { |
|
|
display: inline-block; |
|
|
border: 1px solid rgba(0, 0, 0, 0.025); |
|
|
background: linear-gradient(to bottom right, #f9a8d4, #86efac, #fde047); |
|
|
color: black; |
|
|
padding: 1px 5px; |
|
|
border-radius: 3px; |
|
|
font-size: 0.5rem; |
|
|
font-weight: 700; |
|
|
font-style: normal; |
|
|
letter-spacing: 0.025em; |
|
|
text-transform: uppercase; |
|
|
} |
|
|
|
|
|
|
|
|
:global(.dark) .pro-badge, |
|
|
:global([data-theme="dark"]) .pro-badge { |
|
|
background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308); |
|
|
border-color: rgba(255, 255, 255, 0.15); |
|
|
} |
|
|
|
|
|
.pro-only-label { |
|
|
display: inline-flex; |
|
|
flex-direction: row; |
|
|
align-items: center; |
|
|
gap: 5px; |
|
|
font-size: 0.85em; |
|
|
opacity: 0.5; |
|
|
font-weight: 400; |
|
|
line-height: 1; |
|
|
} |
|
|
|
|
|
.pro-only-dash { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
line-height: 1; |
|
|
} |
|
|
|
|
|
.pro-only-icon { |
|
|
width: 11px; |
|
|
height: 11px; |
|
|
flex-shrink: 0; |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.pro-only-text { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
line-height: 1; |
|
|
} |
|
|
|
|
|
.pdf-locked { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.button-locked { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
background: linear-gradient(135deg, |
|
|
var(--primary-color) 0%, |
|
|
oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%); |
|
|
border-radius: var(--button-radius); |
|
|
padding: var(--button-padding-y) var(--button-padding-x); |
|
|
font-size: var(--button-font-size); |
|
|
line-height: 1; |
|
|
color: var(--on-primary); |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
font-weight: normal; |
|
|
border-color: rgba(0, 0, 0, 0.15); |
|
|
} |
|
|
|
|
|
.button-locked:active { |
|
|
transform: translateY(0); |
|
|
} |
|
|
|
|
|
.lock-icon { |
|
|
font-size: 1em; |
|
|
flex-shrink: 0; |
|
|
position: relative; |
|
|
z-index: 1; |
|
|
} |
|
|
|
|
|
.locked-title { |
|
|
position: relative; |
|
|
z-index: 1; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.meta-container-cell--pdf { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: flex-end; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
|