| --- |
| 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> |
|
|