Spaces:
Running
Running
--- | |
import HtmlEmbed from "./HtmlEmbed.astro"; | |
interface Props { | |
title: string; // may contain HTML (e.g., <br/>) | |
titleRaw?: string; // plain title for slug/PDF (optional) | |
description?: string; | |
authors?: Array<string | { name: string; url?: string; affiliationIndices?: number[] }>; | |
affiliations?: Array<{ id: number; name: string; url?: string }>; | |
affiliation?: string; // legacy single affiliation | |
published?: string; | |
doi?: string; | |
} | |
const { title, titleRaw, description, authors = [], affiliations = [], affiliation, published, doi } = 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); | |
// Determine if affiliation superscripts should be shown (only when there are multiple distinct affiliations referenced by authors) | |
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}></h1> | |
<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} | |
</li> | |
); | |
})} | |
</ul> | |
</div> | |
)} | |
{(Array.isArray(affiliations) && affiliations.length > 0) && ( | |
<div class="meta-container-cell"> | |
<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"> | |
<h3>Affiliation</h3> | |
<p>{affiliation}</p> | |
</div> | |
)} | |
{published && ( | |
<div class="meta-container-cell meta-container-cell--published"> | |
<h3>Published</h3> | |
<p>{published}</p> | |
</div> | |
)} | |
<!-- {doi && ( | |
<div class="meta-container-cell"> | |
<h3>DOI</h3> | |
<p><a href={`https://doi.org/${doi}`} target="_blank" rel="noopener noreferrer">{doi}</a></p> | |
</div> | |
)} --> | |
<div class="meta-container-cell meta-container-cell--pdf"> | |
<h3>PDF</h3> | |
<p> | |
<a class="button" href={`/${pdfFilename}`} download={pdfFilename} aria-label={`Download PDF ${pdfFilename}`}> | |
Download PDF | |
</a> | |
</p> | |
</div> | |
</div> | |
</header> | |
<style> | |
/* Hero (full-width) */ | |
.hero { | |
width: 100%; | |
padding: 48px 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%; | |
margin: auto; | |
} | |
.hero-banner { | |
max-width: 980px; | |
margin: 0 auto; | |
} | |
.hero-desc { | |
color: var(--muted-color); | |
font-style: italic; | |
margin: 0 0 16px 0; | |
} | |
/* Meta (byline-like header) */ | |
.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: 760px; | |
display: flex; | |
flex-direction: row; | |
justify-content: space-between; | |
margin: 0 auto; | |
padding: 0 var(--content-padding-x); | |
gap: 8px; | |
} | |
/* Subtle underline for links in meta; keep buttons without underline */ | |
.meta-container a { | |
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 .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; | |
} | |
.meta-container-cell h3 { | |
margin: 0; | |
font-size: 12px; | |
font-weight: 400; | |
color: var(--muted-color); | |
text-transform: uppercase; | |
letter-spacing: .02em; | |
} | |
.meta-container-cell p { | |
margin: 0; | |
} | |
.authors { | |
margin: 0; | |
list-style-type: none; | |
padding-left: 0; | |
} | |
.affiliations { | |
margin: 0; | |
padding-left: 1.25em; | |
} | |
.affiliations li { | |
margin: 0; | |
} | |
header.meta .meta-container { | |
flex-wrap: wrap; | |
row-gap: 12px; | |
} | |
@media print { | |
.meta-container-cell--pdf { | |
display: none ; | |
} | |
} | |
</style> | |