summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Citations.astro15
-rw-r--r--src/components/Translations.astro133
-rw-r--r--src/components/licenses/WTFPL.astro53
-rw-r--r--src/components/organisms/Date.astro4
-rw-r--r--src/components/signature/Authors.astro275
-rw-r--r--src/components/templates/Authors.astro355
-rw-r--r--src/components/templates/CopyrightNotice.astro (renamed from src/components/CopyrightNotice.astro)21
-rw-r--r--src/components/templates/licenses/CC.astro (renamed from src/components/licenses/CC.astro)101
-rw-r--r--src/components/templates/licenses/WTFPL.astro70
-rw-r--r--src/components/templates/signature/Commit.astro (renamed from src/components/signature/Commit.astro)0
-rw-r--r--src/components/templates/signature/Downloads.astro (renamed from src/components/signature/Downloads.astro)0
-rw-r--r--src/components/templates/signature/Signature.astro (renamed from src/components/signature/Signature.astro)0
-rw-r--r--src/components/templates/signature/Summary.astro (renamed from src/components/signature/Summary.astro)4
13 files changed, 587 insertions, 444 deletions
diff --git a/src/components/Citations.astro b/src/components/Citations.astro
index ff42ad4..7ae6b26 100644
--- a/src/components/Citations.astro
+++ b/src/components/Citations.astro
@@ -1,9 +1,8 @@
---
-import type { CollectionEntry } from "astro:content";
-import { getEntries } from "astro:content";
+import type { BlogPosting } from "@lib/collection/types";
-type Props = { citations: CollectionEntry<"blog">["data"]["relatedPosts"] };
-const citations = await getEntries(Astro.props.citations ?? []);
+type Props = { citations: BlogPosting[] };
+const { citations } = Astro.props;
---
{
citations.length > 0 &&
@@ -12,15 +11,15 @@ const citations = await getEntries(Astro.props.citations ?? []);
<p>O autor recomenda ler também:</p>
<ul>
{
- citations.map(({ collection, id, data }) => (
+ citations.map(({ "@id": id, headline }) => (
<li
itemprop="citation"
itemscope
itemtype="http://schema.org/BlogPosting"
- itemid={Astro.url.href.replace(/[^\/]*\/?$/, id)}
+ itemid={id}
>
- <a href={`/${collection}/read/${id}`}>
- <cite itemprop="headline">{data.title}</cite>
+ <a href={`/blog/read/${id}`}>
+ <cite itemprop="headline">{headline}</cite>
</a>
</li>
))
diff --git a/src/components/Translations.astro b/src/components/Translations.astro
index 6c2bf42..b78e5af 100644
--- a/src/components/Translations.astro
+++ b/src/components/Translations.astro
@@ -1,102 +1,115 @@
---
-import type { CollectionEntry } from "astro:content";
import {
getFlagEmojiFromLocale,
getLanguageNameFromLocale,
} from "../utils/lang";
-import { getEntries } from "astro:content";
+import type { BlogPosting } from "@lib/collection/types";
+import { get } from "@utils/anonymous";
interface Props {
- lang: string;
- translations?: CollectionEntry<"blog">["data"]["translations"];
+ id: string;
+ lang: Intl.LocalesArgument;
+ workTranslations: BlogPosting[];
+ translationOfWork?: BlogPosting;
}
-const { lang } = Astro.props;
+const { id, lang, workTranslations, translationOfWork } = Astro.props;
-const translations = await getEntries(Astro.props.translations ?? []).then(
- (translations) =>
- translations.sort((x, y) => x.data.lang.localeCompare(y.data.lang)),
+const entries = [
+ { "@type": "BlogPosting", "@id": id, inLanguage: lang },
+ ...workTranslations,
+];
+if (translationOfWork !== undefined) {
+ entries.push(translationOfWork);
+}
+const translations = entries.sort((x, y) =>
+ x.inLanguage.localeCompare(y.inLanguage, lang)
);
+
+const list = new Intl.ListFormat(lang, {
+ type: "unit",
+ style: "narrow",
+});
+const parts = list.formatToParts(translations.map(get("inLanguage")));
+let i = 0;
---
-{
- /* TODO: What about <https://schema.org/translationOfWork> and <https://schema.org/translator>? */
-}
+{/* TODO: <https://schema.org/translator> */}
{
translations.length > 0 && (
<aside>
- <nav>
- <p>Traduções:</p>
- <ul class="translations">
+ <nav class="mute small">
+ <span>Traduções:{" "}</span>
+ <span class="translations" role="list">
{
- translations.map(async (
- { data, collection, id },
- ) => {
- const active = lang.localeCompare(data.lang) === 0;
- return (
- <li
- itemprop={active ? undefined : "workTranslation"}
+ parts.map(
+ (
+ { type, value }: {
+ type: "element" | "literal";
+ value: string;
+ },
+ ) => {
+ switch (type) {
+ case "element": {
+ const {
+ "@id": identifier,
+ headline,
+ inLanguage,
+ } = translations[i++];
+ const original = id === identifier;
+ const active =
+ lang.localeCompare(inLanguage, lang) === 0;
+ return (
+ <span
+ role="listitem"
+ itemprop={active
+ ? undefined
+ : original
+ ? "translationOfWork"
+ : "workTranslation"}
itemscope={!active}
itemtype={active ? undefined : "http://schema.org/BlogPosting"}
- itemid={active
- ? undefined
- : new URL(`${collection}/read/${id}`, Astro.site).href}
+ itemid={active ? undefined : identifier}
>
<a
- href={`/${collection}/read/${id}`}
+ href={identifier}
class:list={[{ active }]}
rel={active ? undefined : "alternate"}
- hreflang={active ? undefined : data.lang}
+ hreflang={active ? undefined : inLanguage}
type="text/html"
- title={data.title}
- ><span class="emoji">{getFlagEmojiFromLocale(data.lang)}</span>
- {getLanguageNameFromLocale(data.lang)} (<span
+ title={headline}
+ ><span class="emoji">{getFlagEmojiFromLocale(inLanguage)}</span>
+ {getLanguageNameFromLocale(inLanguage)} (<span
itemprop="inLanguage"
- >{data.lang}</span>)</a>
- </li>
+ >{inLanguage}</span>)</a>
+ </span>
);
- })
+ }
+ case "literal": {
+ return <span>{value}</span>;
+ }
+ }
+ },
+ )
}
- </ul>
+ </span>
</nav>
</aside>
)
}
<style>
- .translations {
- list-style-type: none;
- padding-inline-start: 0;
- }
-
- .translations > li {
- display: inline;
+ nav {
+ padding-block: calc(var(--size-2) * 1em);
}
- .translations > li > a > .emoji {
- text-decoration: none;
- font-family: var(--ff-icons);
- }
-
- .translations > li > a.active {
+ a.active {
font-weight: bolder;
- text-decoration: underline;
- color: var(--color-active);
- }
-
- nav:has(.translations) {
- display: flex;
- gap: 1rem;
- }
-
- nav:has(.translations) > * {
- font-size: smaller;
}
- .translations > li:not(:first-child)::before {
- content: "|";
- margin-inline: 0.5em;
+ a:hover {
+ color: var(--color-active);
}
@media print {
diff --git a/src/components/licenses/WTFPL.astro b/src/components/licenses/WTFPL.astro
deleted file mode 100644
index feab7ec..0000000
--- a/src/components/licenses/WTFPL.astro
+++ /dev/null
@@ -1,53 +0,0 @@
----
-import type { Props as BaseProps } from "../CopyrightNotice.astro";
-interface Props extends BaseProps {}
-
-let { website, author, email, dateCreated } = Astro.props;
----
-
-<footer itemprop="copyrightNotice">
- <p>
- <small>
- Copyright © <span itemprop="copyrightYear">{
- dateCreated.getFullYear()
- }</span>
- <span
- itemprop="copyrightholder"
- itemscope
- itemtype="https://schema.org/Person"
- >{
- website ? (
- <a
- itemprop="url"
- rel="author external noreferrer"
- target="_blank"
- href={website}
- content={website}
- ><span itemprop="name">{author}</span></a>
- ) : author
- }
- {
- email && (
- <>&lt;<a
- itemprop="email"
- rel="author external noreferrer"
- target="_blank"
- href={`mailto:${email}`}
- >{email}</a>&gt;</>
- )
- }</span>
- </small>
- </p>
- <p>
- <small>
- This work is free. You can redistribute it and/or modify it under the
- terms of the Do What The Fuck You Want To Public License, Version 2, as
- published by Sam Hocevar. See <a
- itemprop="license"
- href="http://www.wtfpl.net/"
- rel="license noreferrer"
- target="_blank"
- >http://www.wtfpl.net/</a> for more details.
- </small>
- </p>
-</footer>
diff --git a/src/components/organisms/Date.astro b/src/components/organisms/Date.astro
index 960cfb7..3718664 100644
--- a/src/components/organisms/Date.astro
+++ b/src/components/organisms/Date.astro
@@ -3,9 +3,9 @@ import type { HTMLAttributes } from "astro/types";
interface Props {
date: Date;
- locales: Intl.LocalesArgument;
+ locales?: Intl.LocalesArgument;
options: Intl.DateTimeFormatOptions;
- itemprop: HTMLAttributes<"time">["itemprop"];
+ itemprop?: HTMLAttributes<"time">["itemprop"];
}
const { date, locales, options } = Astro.props;
diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro
deleted file mode 100644
index 4e52d4e..0000000
--- a/src/components/signature/Authors.astro
+++ /dev/null
@@ -1,275 +0,0 @@
----
-import { toPK } from "@lib/pgp";
-import { createKeyFromArmor } from "@lib/pgp/create";
-import type { Verification } from "@lib/pgp/verify";
-import { defined, get, instanciate } from "@utils/anonymous";
-import { z } from "astro:content";
-import type { EntityTypesEnum } from "src/consts";
-import qrcode from "yaqrcode";
-import type { getSigners } from "@lib/collection/helpers";
-
-interface Props {
- verifications: NonNullable<Verification["verifications"]>;
- expectedSigners: Awaited<ReturnType<typeof getSigners>>;
- commitSignerKey?: string;
-}
-
-const {
- verifications: verificationsPromise,
- expectedSigners,
- commitSignerKey,
-} = Astro.props;
-
-const fingerprintToData = new Map<
- string,
- { websites: URL[]; role: z.infer<typeof EntityTypesEnum> }
->();
-
-for (const { entity, role } of expectedSigners) {
- const key = await createKeyFromArmor(entity.data.publickey.armor);
- const fingerprint = key.getFingerprint();
- fingerprintToData.set(fingerprint, {
- websites: entity.data.websites?.map(instanciate(URL)) ?? [],
- role,
- });
-}
-
-let verifications = await Promise.all(
- verificationsPromise.map(async ({ key, keyID, userID, verified }) => {
- return {
- key: await key,
- keyID,
- userID: await userID,
- verified: await verified.catch(() => false),
- };
- }),
-);
-
-const expectedKeys = await Promise.all(
- expectedSigners.map(get("entity")).map(({ data }) =>
- createKeyFromArmor(data.publickey.armor)
- ),
-);
-
-const expectedFingerprints = new Set(
- expectedKeys.map((key) => key.getFingerprint()),
-);
-
-const verifiedFingerprints = new Set(
- verifications.map((v) => v.key).filter(defined).map(toPK).map((key) =>
- key.getFingerprint()
- ),
-);
-
-if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) {
- throw new Error(
- `Missing signature from expected signers: ${[
- ...expectedFingerprints.difference(verifiedFingerprints).values(),
- ]}`,
- );
-}
----
-
-<div>
- <table>
- <caption>
- <strong>Assinaturas</strong>
- <p>
- Para verificar uma assinatura é necessário a <a href="#message"
- >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em
- >chaves públicas</em> dos assinantes. Esta tabela mostra algumas
- informações sobre os assinantes e as suas chaves públicas.
- </p>
- </caption>
- <colgroup>
- <col />
- <col />
- </colgroup>
- <colgroup>
- <col />
- <col />
- <col />
- </colgroup>
- <thead>
- <tr>
- <th scope="col">Assinante</th>
- <th scope="col">Função</th>
- <th scope="col">Fingerprint</th>
- <th scope="col">Válido</th>
- <th scope="col">Commiter</th>
- </tr>
- </thead>
- <tbody>
- {
- verifications.map(({ userID, key, keyID, verified }) => {
- const fingerprint = key
- ? toPK(key).getFingerprint()
- : undefined;
- const info = fingerprint
- ? fingerprintToData.get(fingerprint)
- : undefined;
- const primary = userID?.[0];
- let role = "";
- switch (info?.role) {
- case "author": {
- role = "Autor";
- break;
- }
- case "co-author": {
- role = "Co-autor";
- break;
- }
- case "translator": {
- role = "Tradutor";
- break;
- }
- }
- return (
- <tr>
- <th scope="row">
- <address
- itemprop="author"
- itemscope
- itemtype="https://schema.org/Person"
- >
- {
- primary?.name
- ? info?.websites[0] ? (
- <a
- itemprop="url"
- rel="author external noreferrer"
- target="_blank"
- href={info.websites[0]}
- ><span itemprop="name">{primary.name}</span></a>
- ) : <span itemprop="name">{primary.name}</span>
- : primary?.email
- ? (
- <>&lt;<a
- itemprop="email"
- rel="author external noreferrer"
- target="_blank"
- href={primary?.email && `mailto:${primary.email}`}
- >{primary?.email}</a>&gt;</>
- )
- : ""
- }
- {
- primary && (
- <>
- <button
- popovertarget={`user-id-${fingerprint}`}
- class="emoji"
- >
- ➕
- </button>
- <section
- class="user-id"
- popover
- id={`user-id-${fingerprint}`}
- >
- {
- userID && (
- <><p><code>UserID</code>s</p><ul>
- {userID.map((x) => <li>{x.userID}</li>)}
- </ul></>
- )
- }
- {
- info?.websites && (
- <><p>Websites</p><ul>
- {
- info.websites.map((
- x,
- ) => <li><a href={x}>{x}</a></li>)
- }
- </ul></>
- )
- }
- </section>
- </>
- )
- }
- </address>
- </th>
- <td>{role}</td>
- <td>
- <><span title={fingerprint?.replace(/(....)/g, "$1 ")}>{
- key
- ? "0x" + toPK(key).getKeyID().toHex()
- : "0x" + keyID.toHex()
- }</span>
- {
- key && false && (
- <img
- src={qrcode(toPK(key).armor(), {
- typeNumber: 40,
- errorCorrectLevel: "L",
- })}
- />
- )
- }
- {
- key &&
- (
- <button popovertarget={`armor-${fingerprint}`}>
- Armor
- </button>
- <section class="armor" popover id={`armor-${fingerprint}`}>
- <pre><code>{toPK(key).armor()}</code></pre>
- </section>
- )
- }
- </>
- </td>
- <td>{verified ? "✅" : "❌"}</td>
- <td>
- {
- commitSignerKey &&
- key?.getFingerprint().toUpperCase()?.endsWith(
- commitSignerKey.toUpperCase(),
- ) && "✅"
- }
- </td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
-</div>
-
-<style>
- div {
- overflow-x: auto;
- }
-
- table {
- table-layout: fixed;
- border-collapse: collapse;
- border: 3px solid;
- margin-inline: auto;
- max-width: 90svw;
- }
-
- th,
- td {
- padding: 1rem;
- text-align: center;
- }
-
- tbody tr:nth-child(odd) {
- background-color: #e7e7e7;
- }
-
- section[popover] {
- text-align: initial;
- }
- section[popover].armor {
- max-height: calc(200dvh / 3);
- }
- @media (prefers-color-scheme: dark) {
- tbody tr:nth-child(odd) {
- background-color: #181818;
- }
- }
-</style>
diff --git a/src/components/templates/Authors.astro b/src/components/templates/Authors.astro
new file mode 100644
index 0000000..61ca026
--- /dev/null
+++ b/src/components/templates/Authors.astro
@@ -0,0 +1,355 @@
+---
+import { type RevocationReason, toPK } from "@lib/pgp";
+import type { Verification } from "@lib/pgp/verify";
+import { defined, get } from "@utils/anonymous";
+import type { getSigners } from "@lib/collection/helpers";
+import type { PublicKey, UserIDPacket } from "openpgp";
+import {
+ createVerificationSummary,
+ VerificationResult,
+} from "@lib/pgp/summary";
+import Date from "@components/organisms/Date.astro";
+
+interface Props {
+ verifications: NonNullable<Verification["verifications"]>;
+ expectedSigners: Map<
+ string,
+ {
+ signer: Awaited<ReturnType<typeof getSigners>>[number];
+ users: UserIDPacket[];
+ key: PublicKey;
+ }
+ >;
+ commitSignerKey?: string;
+}
+
+const {
+ verifications: verificationsPromise,
+ expectedSigners,
+ commitSignerKey,
+} = Astro.props;
+
+let verifications = await Promise.all(
+ verificationsPromise.map(async (verification) => {
+ const { key, keyID, userID, verified } = verification;
+ return {
+ key: await key,
+ keyID,
+ userID: await userID,
+ verified: await verified.catch(() => false),
+ summary: await createVerificationSummary(verification),
+ };
+ }),
+);
+
+const expectedKeys = Array.from(expectedSigners.values()).map(get("key"));
+
+const expectedFingerprints = new Set(
+ expectedKeys.map((key) => key.getFingerprint()),
+);
+
+const verifiedFingerprints = new Set(
+ verifications.map((v) => v.key).filter(defined).map(toPK).map((key) =>
+ key.getFingerprint()
+ ),
+);
+
+if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) {
+ throw new Error(
+ `Missing signature from expected signers: ${[
+ ...expectedFingerprints.difference(verifiedFingerprints).values(),
+ ]}`,
+ );
+}
+---
+
+<div>
+ <table>
+ <caption>
+ <strong>Assinaturas</strong>
+ <p>
+ Para verificar uma assinatura é necessário a <a href="#message"
+ >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em
+ >chaves públicas</em> dos assinantes. Esta tabela mostra algumas
+ informações sobre os assinantes, as suas chaves públicas e as suas
+ assinaturas.
+ </p>
+ </caption>
+ <colgroup>
+ <col />
+ <col />
+ </colgroup>
+ <colgroup>
+ <col />
+ <col />
+ <col />
+ </colgroup>
+ <colgroup>
+ <col />
+ </colgroup>
+ <thead>
+ <tr>
+ <th scope="col">Assinante</th>
+ <th scope="col">Função</th>
+ <th scope="col">Fingerprint</th>
+ <th scope="col">Válido</th>
+ <th scope="col">Commiter</th>
+ <th scope="col">Mais Informações</th>
+ </tr>
+ </thead>
+ <tbody>
+ {
+ verifications.map(
+ ({ userID, key, keyID, verified, summary }) => {
+ const fingerprint = key
+ ? toPK(key).getFingerprint()
+ : undefined;
+ const info = fingerprint
+ ? expectedSigners.get(fingerprint)
+ : undefined;
+ const primary = userID?.[0];
+ const signer = info?.signer;
+ let role = "";
+ switch (signer?.role) {
+ case "author": {
+ role = "Autor";
+ break;
+ }
+ case "co-author": {
+ role = "Co-autor";
+ break;
+ }
+ case "translator": {
+ role = "Tradutor";
+ break;
+ }
+ }
+ const {
+ reasons,
+ created,
+ expired,
+ revoked,
+ revocationReason,
+ trusted,
+ } = (summary[1].get(keyID.toHex()) ?? []).reduce(
+ (acc, x) => {
+ if (!("key" in x || "keyID" in x)) {
+ return acc;
+ }
+
+ switch (x.result) {
+ case VerificationResult.MISSING_KEY:
+ acc.reasons.push(x.reason);
+ acc.created = x.created ?? undefined;
+ break;
+ case VerificationResult.UNTRUSTED_KEY:
+ acc.created = x.created ?? undefined;
+ acc.trusted &&= false;
+ break;
+ case VerificationResult.TRUSTED_KEY:
+ acc.created = x.created ?? undefined;
+ acc.trusted = true;
+ break;
+ case VerificationResult
+ .EXPIRATION_AFTER_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.expired = x.expired;
+ break;
+ case VerificationResult
+ .EXPIRATION_BEFORE_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.expired = x.expired;
+ break;
+ case VerificationResult
+ .REVOCATION_AFTER_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.revoked = x.revoked;
+ acc.revocationReason = x.revocationReason;
+ break;
+ case VerificationResult
+ .REVOCATION_BEFORE_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.revoked = x.revoked;
+ acc.revocationReason = x.revocationReason;
+ break;
+ case VerificationResult.KEY_DOES_NOT_SIGN:
+ break;
+ }
+
+ return acc;
+ },
+ {
+ reasons: [],
+ created: undefined,
+ expired: undefined,
+ revoked: undefined,
+ revocationReason: undefined,
+ trusted: false,
+ } as {
+ reasons: Error[];
+ created?: Date;
+ expired?: Date;
+ revoked?: Date;
+ revocationReason?: RevocationReason;
+ trusted: boolean;
+ },
+ );
+ return (
+ <tr>
+ <th scope="row">
+ <address
+ itemprop="author"
+ itemscope
+ itemtype="https://schema.org/Person"
+ >
+ {
+ primary?.name
+ ? signer?.entity?.data?.websites[0]
+ ? (
+ <a
+ itemprop="url"
+ rel="author external noreferrer"
+ target="_blank"
+ href={signer?.entity?.data?.websites[0]}
+ ><span itemprop="name">{primary.name}</span></a>
+ )
+ : <span itemprop="name">{primary.name}</span>
+ : primary?.email
+ ? (
+ <>&lt;<a
+ itemprop="email"
+ rel="author external noreferrer"
+ target="_blank"
+ href={primary?.email && `mailto:${primary.email}`}
+ >{primary?.email}</a>&gt;</>
+ )
+ : ""
+ }
+ </address>
+ {signer !== undefined && <><hr /><a href="#">Ver perfil</a></>}
+ </th>
+ <td>{role}</td>
+ <td>
+ <samp title={fingerprint?.replace(/(....)/g, "$1 ")}>{
+ key
+ ? "0x" + toPK(key).getKeyID().toHex()
+ : "0x" + keyID.toHex()
+ }</samp>
+ </td>
+ <td>{verified ? "✅" : "❌"}</td>
+ <td>
+ {
+ commitSignerKey &&
+ key?.getFingerprint().toUpperCase()?.endsWith(
+ commitSignerKey.toUpperCase(),
+ ) && "✅"
+ }
+ </td>
+ <td>
+ {
+ key && (
+ <>
+ <button
+ type="button"
+ class="emoji"
+ popovertarget={`info-${fingerprint}`}
+ >
+ ➕
+ </button>
+ <section popover id={`info-${fingerprint}`}>
+ <p>Erros</p>
+ {
+ summary[0].map((x) => {
+ if (!("reason" in x)) {
+ return undefined;
+ }
+
+ let reason = x.reason;
+
+ return <pre>{reason.message}</pre>;
+ })
+ }
+ <p><strong>Informações</strong></p>
+ <dl class="divider">
+ {
+ reasons.map(({ message }) => (
+ <> <dt>Erro</dt> <dd>{message}</dd></>
+ ))
+ }
+ {
+ created && (
+ <>
+ <dt>Data da assinatura</dt>
+ <dd><Date date={created} options={{}} /></dd>
+ </>
+ )
+ }
+ {
+ expired && (
+ <>
+ <dt>Data de expiração</dt>
+ <dd><Date date={expired} options={{}} /></dd>
+ </>
+ )
+ }
+ {
+ revoked && (
+ <>
+ <dt>Data de revogação</dt>
+ <dd><Date date={revoked} options={{}} /></dd>
+ </>
+ )
+ }
+ {
+ revocationReason && (
+ <>
+ <dt>Razão para a revogação</dt>
+ <dd>
+ {revocationReason.flag}: {revocationReason.msg}
+ </dd>
+ </>
+ )
+ }
+ <dt>Chave confiável</dt>
+ <dd>{trusted ? "✅" : "❌"}</dd>
+ </dl>
+ </section></>
+ )
+ }
+ </td>
+ </tr>
+ );
+ },
+ )
+ }
+ </tbody>
+ </table>
+</div>
+
+<style>
+ div {
+ overflow-x: auto;
+ }
+
+ table {
+ table-layout: fixed;
+ border-collapse: collapse;
+ border: 3px solid;
+ margin-inline: auto;
+ max-width: 90svw;
+ }
+
+ th,
+ td {
+ padding: 1rem;
+ text-align: center;
+ }
+
+ tbody tr:nth-child(odd) {
+ background-color: oklch(from var(--color-light) l c h / calc(alpha * 0.25));
+ }
+
+ section[popover] {
+ text-align: initial;
+ }
+</style>
diff --git a/src/components/CopyrightNotice.astro b/src/components/templates/CopyrightNotice.astro
index 8a5ebc6..8335293 100644
--- a/src/components/CopyrightNotice.astro
+++ b/src/components/templates/CopyrightNotice.astro
@@ -5,25 +5,26 @@ import {
} from "@lib/collection/schemas";
import CC from "./licenses/CC.astro";
import WTFPL from "./licenses/WTFPL.astro";
+import type { Person } from "@lib/collection/types";
export interface Props {
title: string;
- author: string;
- email?: string;
- website?: string;
- dateCreated: Date;
+ holders: Person[];
+ years: number[];
license?: typeof LICENSES[number];
}
let { license = "public domain" } = Astro.props;
let Notice = undefined;
-if (license === "WTFPL") {
- Notice = WTFPL;
-} else if (
- CREATIVE_COMMONS_LICENSES.some((x) => license.localeCompare(x) === 0)
-) {
- Notice = CC;
+if (license !== undefined) {
+ if (license === "WTFPL") {
+ Notice = WTFPL;
+ } else if (
+ CREATIVE_COMMONS_LICENSES.some((x) => license.localeCompare(x) === 0)
+ ) {
+ Notice = CC;
+ }
}
---
diff --git a/src/components/licenses/CC.astro b/src/components/templates/licenses/CC.astro
index 61f9114..2aea423 100644
--- a/src/components/licenses/CC.astro
+++ b/src/components/templates/licenses/CC.astro
@@ -1,8 +1,13 @@
---
-import type { Props as BaseProps } from "../CopyRightNotice.astro";
-interface Props extends BaseProps {}
+import type { ComponentProps } from "astro/types";
+import type CopyrightNotice from "@components/templates/CopyrightNotice.astro";
+import { listYearsWithRanges } from "@utils/datetime";
+interface Props extends ComponentProps<typeof CopyrightNotice> {}
+
+let { license, title, holders, years } = Astro.props;
+
+if (typeof license !== "string") throw new Error();
-let { title, website, author, dateCreated, license } = Astro.props;
const publicdomain = license === "CC0";
const sa = /SA/.test(license);
const nd = /ND/.test(license);
@@ -10,6 +15,9 @@ const nc = /NC/.test(license);
const licenseURL = `https://creativecommons.org/licenses/${
license.slice(3).toLowerCase()
}/4.0/`;
+
+const firstYear = Math.min(...years);
+const lastYears = years.sort((a, b) => a - b).slice(1);
---
<footer itemprop="copyrightNotice">
@@ -17,21 +25,31 @@ const licenseURL = `https://creativecommons.org/licenses/${
publicdomain ? (
<p>
<small>
- <a href={Astro.url}>{title}</a> by <span
- itemprop="copyrightholder"
- itemscope
- itemtype="https://schema.org/Person"
- >{
- website ? (
- <a
- itemprop="url"
- rel="author external noreferrer"
- target="_blank"
- href={website}
- content={website}
- ><span itemprop="name">{author}</span></a>
- ) : author
- }</span> is marked <a
+ <a href={Astro.url}>{title}</a> by {
+ holders.map(({ name, url }) => {
+ if (name === undefined) return undefined;
+
+ const website = url?.[0];
+
+ return (
+ <span
+ itemprop="copyrightHolder"
+ itemscope
+ itemtype="https://schema.org/Person"
+ >{
+ website !== undefined ? (
+ <a
+ itemprop="url"
+ rel="author external noreferrer"
+ target="_blank"
+ href={website}
+ content={website}
+ ><span itemprop="name">{name}</span></a>
+ ) : <span itemprop="name">{name}</span>
+ }</span>
+ );
+ })
+ } is marked <a
itemprop="license"
rel="license noreferrer"
target="_blank"
@@ -54,22 +72,37 @@ const licenseURL = `https://creativecommons.org/licenses/${
<p>
<small>
<a href={Astro.url}>{title}</a> © <span itemprop="copyrightYear">{
- dateCreated.getFullYear()
- }</span> by <span
- itemprop="copyrightholder"
- itemscope
- itemtype="https://schema.org/Person"
- >{
- website ? (
- <a
- itemprop="url"
- href={website}
- target="_blank"
- rel="author external noreferrer"
- content={website}
- ><span itemprop="name">{author}</span></a>
- ) : author
- }</span> is licensed under <a
+ firstYear
+ }</span>, {
+ listYearsWithRanges(lastYears, {
+ list: { type: "unit", style: "narrow" },
+ locale: "en",
+ }).replace(/^\d+/, "")
+ } by {
+ holders.map(({ name, url }) => {
+ if (name === undefined) return undefined;
+
+ const website = url?.[0];
+
+ return (
+ <span
+ itemprop="copyrightHolder"
+ itemscope
+ itemtype="https://schema.org/Person"
+ >{
+ website !== undefined ? (
+ <a
+ itemprop="url"
+ rel="author external noreferrer"
+ target="_blank"
+ href={website}
+ content={website}
+ ><span itemprop="name">{name}</span></a>
+ ) : <span itemprop="name">{name}</span>
+ }</span>
+ );
+ })
+ } is licensed under <a
itemprop="license"
rel="license noreferrer"
target="_blank"
diff --git a/src/components/templates/licenses/WTFPL.astro b/src/components/templates/licenses/WTFPL.astro
new file mode 100644
index 0000000..d3546c7
--- /dev/null
+++ b/src/components/templates/licenses/WTFPL.astro
@@ -0,0 +1,70 @@
+---
+import { listYearsWithRanges } from "@utils/datetime";
+import type { Props as BaseProps } from "../CopyrightNotice.astro";
+interface Props extends BaseProps {}
+
+const { years, holders } = Astro.props;
+const firstYear = Math.min(...years);
+const lastYears = years.sort((a, b) => a - b).slice(1);
+---
+
+<footer itemprop="copyrightNotice">
+ <p>
+ <small>
+ Copyright © <span itemprop="copyrightYear">{firstYear}</span>, {
+ listYearsWithRanges(lastYears, {
+ list: { type: "unit", style: "narrow" },
+ locale: "en",
+ }).replace(/^\d+/, "")
+ }
+
+ {
+ holders.map(({ name, url, email }) => {
+ if (name === undefined) return undefined;
+
+ const website = url?.[0];
+
+ return (
+ <span
+ itemprop="copyrightHolder"
+ itemscope
+ itemtype="https://schema.org/Person"
+ >{
+ website !== undefined ? (
+ <a
+ itemprop="url"
+ rel="author external noreferrer"
+ target="_blank"
+ href={website}
+ content={website}
+ ><span itemprop="name">{name}</span></a>
+ ) : <span itemprop="name">{name}</span>
+ }
+ {
+ email !== undefined && (
+ <>&lt;<a
+ itemprop="email"
+ rel="author external noreferrer"
+ target="_blank"
+ href={`mailto:${email}`}
+ >{email}</a>&gt;</>
+ )
+ }</span>
+ );
+ })
+ }
+ </small>
+ </p>
+ <p>
+ <small>
+ This work is free. You can redistribute it and/or modify it under the
+ terms of the Do What The Fuck You Want To Public License, Version 2, as
+ published by Sam Hocevar. See <a
+ itemprop="license"
+ href="http://www.wtfpl.net/"
+ rel="license noreferrer"
+ target="_blank"
+ >http://www.wtfpl.net/</a> for more details.
+ </small>
+ </p>
+</footer>
diff --git a/src/components/signature/Commit.astro b/src/components/templates/signature/Commit.astro
index 328a8f9..328a8f9 100644
--- a/src/components/signature/Commit.astro
+++ b/src/components/templates/signature/Commit.astro
diff --git a/src/components/signature/Downloads.astro b/src/components/templates/signature/Downloads.astro
index 3497b37..3497b37 100644
--- a/src/components/signature/Downloads.astro
+++ b/src/components/templates/signature/Downloads.astro
diff --git a/src/components/signature/Signature.astro b/src/components/templates/signature/Signature.astro
index 57e9902..57e9902 100644
--- a/src/components/signature/Signature.astro
+++ b/src/components/templates/signature/Signature.astro
diff --git a/src/components/signature/Summary.astro b/src/components/templates/signature/Summary.astro
index f25c1d1..3469a0f 100644
--- a/src/components/signature/Summary.astro
+++ b/src/components/templates/signature/Summary.astro
@@ -1,6 +1,6 @@
---
import {
- createVerificationSummary,
+ createVerificationsSummary,
logLevel,
type Summary,
VerificationResult,
@@ -11,7 +11,7 @@ import type { NonEmptyArray } from "@utils/iterator";
interface Props extends Verification {}
-let [errors, keys] = await createVerificationSummary(Astro.props);
+let [errors, keys] = await createVerificationsSummary(Astro.props);
const failed = errors.filter((summary) => "reason" in summary);
if (failed.length > 0) {