|
|
--- |
|
|
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; |
|
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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> |
|
|
<div class="authors"> |
|
|
{normalizedAuthors.map((a, i) => { |
|
|
const supers = |
|
|
shouldShowAffiliationSupers && |
|
|
Array.isArray(a.affiliationIndices) && |
|
|
a.affiliationIndices.length ? ( |
|
|
<sup>{a.affiliationIndices.join(",")}</sup> |
|
|
) : null; |
|
|
return ( |
|
|
<> |
|
|
<span> |
|
|
{a.url ? <a href={a.url}>{a.name}</a> : a.name} |
|
|
{supers} |
|
|
</span> |
|
|
{i < normalizedAuthors.length - 1 && <span>, </span>} |
|
|
</> |
|
|
); |
|
|
})} |
|
|
</div> |
|
|
</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> |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<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 { |
|
|
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 { |
|
|
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; |
|
|
} |
|
|
|
|
|
.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: 250px; |
|
|
} |
|
|
.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; |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
.authors span { |
|
|
white-space: nowrap; |
|
|
} |
|
|
.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; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
|