diff options
author | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-28 18:14:22 -0300 |
---|---|---|
committer | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-28 18:14:22 -0300 |
commit | 79fd506d30eef3d113f4a8e3ab9ebd9004f1e8cc (patch) | |
tree | 96ff57c92e897c3cc3331e23043d20f1665c7d0a /src/components | |
parent | a1eac976b20e39f86d5944fbec68e2a0f8ffb746 (diff) |
feat: index page
Signed-off-by: João Augusto Costa Branco Marado Torres <torres.dev@disroot.org>
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/BaseHead.astro | 54 | ||||
-rw-r--r-- | src/components/Commit.astro | 49 | ||||
-rw-r--r-- | src/components/CopyrightNotice.astro | 5 | ||||
-rw-r--r-- | src/components/Footer.astro | 125 | ||||
-rw-r--r-- | src/components/Header.astro | 69 | ||||
-rw-r--r-- | src/components/Search.astro | 32 | ||||
-rw-r--r-- | src/components/organisms/ActiveLink.astro (renamed from src/components/HeaderLink.astro) | 0 | ||||
-rw-r--r-- | src/components/organisms/Date.astro | 14 | ||||
-rw-r--r-- | src/components/organisms/KeywordsList.astro | 31 | ||||
-rw-r--r-- | src/components/signature/Authors.astro | 8 | ||||
-rw-r--r-- | src/components/templates/MicroBlog.astro | 114 | ||||
-rw-r--r-- | src/components/templates/Search.astro | 67 | ||||
-rw-r--r-- | src/components/templates/SimplePostList.astro | 81 |
13 files changed, 476 insertions, 173 deletions
diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro index 5ac0410..b4dbb74 100644 --- a/src/components/BaseHead.astro +++ b/src/components/BaseHead.astro @@ -1,8 +1,6 @@ --- -// Import the global.css file here so that it is included on -// all pages through the use of the <BaseHead /> component. +import { env } from "@lib/env"; import "../styles/global.css"; -import { SITE_AUTHOR, SITE_DESCRIPTION, SITE_TITLE } from "../consts"; import { ClientRouter } from "astro:transitions"; export interface Props { @@ -12,11 +10,24 @@ export interface Props { keywords?: string[]; } +const { + PUBLIC_SITE_TITLE, + PUBLIC_SITE_DESCRIPTION, + PUBLIC_SITE_AUTHOR, + PUBLIC_TOR_URL, +} = env; + +const isOnion = Astro.url.origin.endsWith(".onion"); +const alternate = !isOnion ? PUBLIC_TOR_URL : Astro.site; + const canonicalURL = new URL(Astro.url.pathname, Astro.site); -const { title, description = SITE_DESCRIPTION, image, keywords = [] } = - Astro.props; -// const socialImage = image ?? Astro.site.href + 'assets/social.png' +const { + title, + description = PUBLIC_SITE_DESCRIPTION, + image = new URL("favicon.svg", Astro.site), + keywords = [], +} = Astro.props; --- <!-- Global Metadata --> @@ -28,24 +39,30 @@ const { title, description = SITE_DESCRIPTION, image, keywords = [] } = <link rel="alternate" type="application/rss+xml" - title={SITE_TITLE} + title={PUBLIC_SITE_TITLE} href={new URL("rss.xml", Astro.site)} /> <meta name="generator" content={Astro.generator} /> <!-- Canonical URL --> <link rel="canonical" href={canonicalURL} /> +<link + rel="alternate" + href={alternate} + type="text/html" + title={`${isOnion ? "Clearnet" : "Tor"} version`} +> <!-- Primary Meta Tags --> <title>{title}</title> <meta name="title" content={title} /> <meta name="description" content={description} /> -<meta name="author" content={SITE_AUTHOR} /> +<meta name="author" content={PUBLIC_SITE_AUTHOR} /> {keywords.length > 0 && <meta name="keywords" content={keywords.join(",")} />} -<meta name="theme-color" content="#a50026" /> +<meta name="theme-color" content="oklch(0.4564 0.1835 20.81)" /> <meta name="theme-color" - content="#f46d43" + content="oklch(0.6923 0.1759 37.7)" media="(prefers-color-scheme: dark)" /> @@ -54,26 +71,13 @@ const { title, description = SITE_DESCRIPTION, image, keywords = [] } = <meta property="og:url" content={Astro.url} /> <meta property="og:title" content={title} /> <meta property="og:description" content={description} /> -{image && <meta property="og:image" content={new URL(image, Astro.url)} />} +<meta property="og:image" content={image} /> <!-- Twitter --> <meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:url" content={Astro.url} /> <meta property="twitter:title" content={title} /> <meta property="twitter:description" content={description} /> -{image && <meta property="twitter:image" content={new URL(image, Astro.url)} />} +<meta property="twitter:image" content={image} /> <ClientRouter /> - -<script is:inline> - const root = document.documentElement; - const theme = localStorage.getItem("theme"); - if ( - theme === "dark" || - (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) - ) { - root.classList.add("theme-dark"); - } else { - root.classList.remove("theme-dark"); - } -</script> diff --git a/src/components/Commit.astro b/src/components/Commit.astro deleted file mode 100644 index 3ee284a..0000000 --- a/src/components/Commit.astro +++ /dev/null @@ -1,49 +0,0 @@ ---- -import type { Commit } from "@lib/git/types"; -import { gitDir } from "@lib/git"; - -type Props = Commit; - -const { hash, files, author, signature } = Astro.props; - -const git = await gitDir; ---- -<p>Git commit info:</p> -<dl> - <dt>Hash</dt> - <dd>{hash}</dd> - <dt>Files</dt> - {files.map((file) => <dd>{file.pathname.replace(git, "")}</dd>)} - <dt>Author</dt> - <dd>{author.name} <{author.email}></dd> - { - signature && ( - <dt>Commit Signature</dt> - <dd> - <dl> - <dt>Type</dt> - <dd>{signature.type}</dd> - <dt>Signer</dt> - <dd>{signature.signerName}</dd> - <dt>Key fingerprint</dt> - <dd>{signature.keyFingerPrint}</dd> - </dl> - </dd> - ) - } -</dl> - -<style> - dl { - display: grid; - grid-template-columns: 1fr 1fr; - } - - dl > dt, dd { - display: inline-block; - } - - dt::after { - content: ": "; - } -</style> diff --git a/src/components/CopyrightNotice.astro b/src/components/CopyrightNotice.astro index 2aa72ad..6b3bd48 100644 --- a/src/components/CopyrightNotice.astro +++ b/src/components/CopyrightNotice.astro @@ -1,7 +1,10 @@ --- +import { + CREATIVE_COMMONS_LICENSES, + type LICENSES, +} from "@lib/collection/schemas"; import CC from "./licenses/CC.astro"; import WTFPL from "./licenses/WTFPL.astro"; -import { CREATIVE_COMMONS_LICENSES, LICENSES } from "../consts.ts"; export interface Props { title: string; diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 11c62c4..c3dffca 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -1,62 +1,107 @@ --- +import { env } from "@lib/env"; +const { + PUBLIC_GIT_URL, + PUBLIC_TOR_URL, + PUBLIC_GIT_TOR_URL, + PUBLIC_SIMPLE_X_ADDRESS, +} = env; +const isOnion = Astro.url.origin.endsWith(".onion"); +const site = isOnion ? PUBLIC_TOR_URL : Astro.site; +const git = isOnion ? PUBLIC_GIT_TOR_URL ?? PUBLIC_GIT_URL : PUBLIC_GIT_URL; --- -<footer> +<footer class="small"> + { + !isOnion && PUBLIC_TOR_URL && ( + <p class="mute"> + Disponível também em: <a class="tor" href={PUBLIC_TOR_URL}>{ + PUBLIC_TOR_URL + }</a> + </p> + ) + } <address> - Sítio web de <a href={Astro.site} target="_blank" rel="author" - >João Augusto Costa Branco Marado Torres</a> + <p> + Sítio web de <a href={site} target="_blank" rel="author" + >João Augusto Costa Branco Marado Torres</a> + </p> + { + PUBLIC_SIMPLE_X_ADDRESS && ( + <p> + Contacte-me através do <a href={PUBLIC_SIMPLE_X_ADDRESS}>SimpleX</a>! + </p> + ) + } </address> - <section id="copying"> - <h2>Licença de <span lang="en">Software</span></h2> + <p> + Isto é <abbr title="Free Libre and Open Source Software">FLOSS</abbr>, <a + href={git} + >usa as tuas liberdades</a> + </p> + <section id="copying" class="mute"> + <h2 class="sr-only">Licença de <span lang="en">Software</span></h2> <div lang="en"> <p> - <small> - <<a href="/" hreflang="pt-PT">cravodeabril.pt</a>> Copyright - © 2025 João Augusto Costa Branco Marado Torres - </small> + <<a href="/" hreflang="pt-PT">cravodeabril.pt</a>> Copyright + © 2025 João Augusto Costa Branco Marado Torres </p> <p> - <small> - This program is free software: you can redistribute it and/or modify - it under the terms of the <a - href="https://www.gnu.org/licenses/agpl-3.0.html" - target="_blank" - rel="external license" - >GNU Affero General Public License</a> as published by the Free - Software Foundation, either version 3 of the License, or (at your - option) any later version. - </small> + This program is free software: you can redistribute it and/or modify it + under the terms of the <a + href="https://www.gnu.org/licenses/agpl-3.0.html" + target="_blank" + rel="external license" + >GNU Affero General Public License</a> as published by the Free Software + Foundation, either version 3 of the License, or (at your option) any + later version. </p> <p> - <small> - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Affero General Public License for more details. - </small> + This program is distributed in the hope that it will be useful, but + <strong>without any warranty</strong>; without even the implied warranty + of + <strong>merchantability</strong> or <strong>fitness for a particular + purpose</strong>. See the GNU Affero General Public License for more + details. </p> <p> - <small> - You should have received a copy of the GNU Affero General Public - License along with this program. If not, see <a - href="https://www.gnu.org/licenses/" - target="_blank" - rel="external" - >https://www.gnu.org/licenses</a> - </small> + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <a + href="https://www.gnu.org/licenses/" + target="_blank" + rel="external" + >https://www.gnu.org/licenses</a> </p> </div> </section> <nav> <ul> - <li><a>Código de Conduta</a></li> - <li><a>Declaração de Exoneração de Responsabilidade</a></li> - <li><a>Aviso sobre cookies</a></li> - <li><a>Declaração de acessibilidade</a></li> - <li><a>Apoio</a></li> - <li><a>Contacto</a></li> - <li><a>Código fonte</a></li> + <li><a href="/">Código de Conduta</a></li> + <li><a href="/">Declaração de Exoneração de Responsabilidade</a></li> + <li><a href="/">Aviso sobre cookies</a></li> + <li><a href="/">Declaração de acessibilidade</a></li> + <li><a href="/">Apoio</a></li> </ul> </nav> </footer> + +<style> + footer { + border-block-start: 1px solid var(--color-light); + padding-block-start: calc(var(--size-4) * 1em); + } + + .tor { + word-wrap: break-word; + } + + nav > ul { + display: flex; + flex-direction: column; + gap: calc(var(--size-1) * 1em); + & > li { + padding-block: calc(var(--size-1) * 1em); + } + } +</style> diff --git a/src/components/Header.astro b/src/components/Header.astro index 28ab542..496337f 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -1,26 +1,53 @@ --- -import HeaderLink from "./HeaderLink.astro"; -import Search from "./Search.astro"; - -export interface Props { - showSearch?: boolean; - showNav?: boolean; -} - -const { showSearch, showNav } = Astro.props; +import ActiveLink from "./organisms/ActiveLink.astro"; +import Search from "./templates/Search.astro"; --- <header> - <h1><<a href="/">cravodeabril.pt</a>></h1> - {showSearch && <Search />} - { - showNav && ( - <nav> - <ul> - <li><HeaderLink href="/blog">Publicações</HeaderLink></li> - <li><HeaderLink href="/blog/keywords">Palavras-Chave</HeaderLink></li> - </ul> - </nav> - ) - } + <h1> + <span class="bracket"><</span><a href="/">cravodeabril.pt</a><span + class="bracket" + >></span> + </h1> + <Search /> + <nav> + <ul> + <li class="small"><ActiveLink href="/blog">Publicações</ActiveLink></li> + <li class="small"> + <ActiveLink href="/blog/keywords">Palavras-Chave</ActiveLink> + </li> + <li class="small"> + <ActiveLink href="/blog/micro/1">Micro blogue</ActiveLink> + </li> + </ul> + </nav> </header> + +<style> + header { + margin-block-end: calc(var(--size-4) * 1em); + border-block-end: 1px solid var(--color-light); + } + .bracket { + color: var(--color-active); + } + nav { + display: flex; + max-width: max-content; + align-items: center; + justify-content: center; + } + ul { + display: flex; + flex: 1; + gap: calc(var(--size-0) * 1em); + list-style-type: none; + align-items: center; + justify-content: center; + padding: calc(var(--size-2) * 1em); + margin-block-start: 0; + } + li { + padding: calc(var(--size-1) * 1em); + } +</style> diff --git a/src/components/Search.astro b/src/components/Search.astro deleted file mode 100644 index 5ca4569..0000000 --- a/src/components/Search.astro +++ /dev/null @@ -1,32 +0,0 @@ ---- - ---- - -<search> - <form - action="https://www.google.com/search" - target="_blank" - rel="external noreferrer search" - role="search" - autocomplete="on" - name="search" - > - <p> - <label>Barra de pesquisa: <input - name="q" - type="search" - placeholder={`site:${Astro.site} consulta de pesquisa`} - value={`site:${Astro.site} `} - required - title={`"site:${Astro.site} " é usado para que os resultados da pesquisa fiquem restritos a este website`} - pattern={`site:${Astro.site} .+`} - size={`site:${Astro.site} .+`.length} - /></label> - </p> - <p><button type="submit">Pesquisar</button></p> - <p> - <small>Esta pesquisa é efectuada pelo Google e utiliza software - proprietário.</small> - </p> - </form> -</search> diff --git a/src/components/HeaderLink.astro b/src/components/organisms/ActiveLink.astro index 8c01f92..8c01f92 100644 --- a/src/components/HeaderLink.astro +++ b/src/components/organisms/ActiveLink.astro diff --git a/src/components/organisms/Date.astro b/src/components/organisms/Date.astro new file mode 100644 index 0000000..a8b643d --- /dev/null +++ b/src/components/organisms/Date.astro @@ -0,0 +1,14 @@ +--- +interface Props { + date: Date; + locales: Intl.LocalesArgument; + options: Intl.DateTimeFormatOptions; +} + +const { date, locales, options } = Astro.props; + +const datetime = date.toISOString(); +const format = new Intl.DateTimeFormat(locales, options).format(date); +--- + +<date {datetime}>{format}</date> diff --git a/src/components/organisms/KeywordsList.astro b/src/components/organisms/KeywordsList.astro new file mode 100644 index 0000000..4d4b140 --- /dev/null +++ b/src/components/organisms/KeywordsList.astro @@ -0,0 +1,31 @@ +--- +interface Props { + keywords: string[]; +} + +const { keywords } = Astro.props; +--- + +<p> + {keywords.map((x) => <span>#<b>{x}</b></span>)} +</p> + +<style> + p { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + gap: calc(var(--size-0) * 1em); + + & > * { + border-radius: calc(infinity * 1px); + background-color: color-mix( + in srgb, + var(--color-active) 10%, + transparent + ); + color: var(--color-active); + padding-inline: calc(var(--size-2) * 1em); + } + } +</style> diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro index 43a2b36..71a3d62 100644 --- a/src/components/signature/Authors.astro +++ b/src/components/signature/Authors.astro @@ -3,16 +3,14 @@ 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 { type CollectionEntry, z } from "astro:content"; +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: { - entity: CollectionEntry<"entity">; - role: z.infer<typeof EntityTypesEnum>; - }[]; + expectedSigners: Awaited<ReturnType<typeof getSigners>>; commitSignerKey?: string; } diff --git a/src/components/templates/MicroBlog.astro b/src/components/templates/MicroBlog.astro new file mode 100644 index 0000000..b7019c5 --- /dev/null +++ b/src/components/templates/MicroBlog.astro @@ -0,0 +1,114 @@ +--- +import Date from "@components/organisms/Date.astro"; +import KeywordsList from "@components/organisms/KeywordsList.astro"; +import { getFirstUserID, getLastUpdate } from "@lib/collection/helpers"; +import { Micro } from "@lib/collection/schemas"; +import type { CollectionEntry, z } from "astro:content"; + +interface Props extends CollectionEntry<"blog"> { + data: z.infer<typeof Micro>; +} + +const micro = Astro.props; +const { id, data, rendered } = micro; +const { title, lang, keywords } = data; +const date = getLastUpdate(micro); +const user = await getFirstUserID(micro); +const display = user?.name ?? user?.email ?? user?.entity ?? ""; +const [first, ...names] = display.split(/\s/); +const last = names.length > 0 ? names[names.length - 1] : ""; +const little = ((first?.[0] ?? "") + (last?.[0] ?? "")).slice(0, 2); +--- +<article> + <header> + <h3 class="title"> + <a href={`/blog/read/${id}`}>{title}</a> + </h3> + <span class="profile_picture">{ + user?.website ? <a href={user.website}>{little}</a> : ( + <span>{little}</span> + ) + }</span> + <div> + {first} {last} <small>· <Date + {date} + locales={lang} + options={{ month: "short", day: "numeric" }} + /></small> + </div> + </header> + <div class="content"> + <small {lang}> + <Fragment set:html={rendered?.html} /> + </small> + <footer> + <div class="keywords small"><KeywordsList {keywords} /></div> + </footer> + </div> + <aside> + <small><a href="/blog/micro/1">Ver todos os microposts</a></small> + </aside> +</article> + +<style is:inline> + .content > [lang] > *:first-child { + margin-block-start: 0; + } + .content > [lang] > *:last-child { + margin-block-end: 0; + } +</style> +<style> + article { + border-radius: calc(var(--size-1) * 1em); + box-shadow: 0 0 calc(var(--size-1) * 1em) var(--color-light); + padding: calc(var(--size-4) * 1em); + display: grid; + grid-template-rows: repeat(3, auto); + grid-template-columns: calc(var(--size-9) * 1em) auto; + gap: calc(var(--size-1) * 1em); + + & > header { + display: contents; + } + + & > aside { + grid-row: 3 / 4; + grid-column: 1 / 3; + border-block-start: 1px solid #e7e7e7; + padding-block-start: calc(var(--size-1) * 1em); + } + } + + .profile_picture { + grid-row: 1 / 3; + grid-column: 1 / 2; + + & > * { + display: inline-grid; + place-content: center; + width: calc(var(--size-9) * 1em); + height: calc(var(--size-9) * 1em); + aspect-ratio: 1 / 1; + background-color: var(--color-active); + color: #fff; + font-weight: 950; + font-size: smaller; + border-radius: calc(infinity * 1px); + text-align: center; + text-transform: uppercase; + } + } + + .title { + display: none; + } + .content { + grid-row: 2/3; + grid-column: 2 / 3; + } + + .keywords { + margin-block-end: 0; + } +</style> diff --git a/src/components/templates/Search.astro b/src/components/templates/Search.astro new file mode 100644 index 0000000..5245643 --- /dev/null +++ b/src/components/templates/Search.astro @@ -0,0 +1,67 @@ +--- +const { site } = Astro; +--- + +<search> + <link rel="dns-prefetch" href="https://www.google.com/search"> + <form + action="https://www.google.com/search" + target="_blank" + rel="external noreferrer search" + role="search" + autocomplete="on" + name="search" + > + <details> + <summary>Pesquisar no website</summary> + <div class="details"> + <p> + <label>Barra de pesquisa <input + name="q" + type="search" + placeholder={`site:${site} consulta de pesquisa`} + value={`site:${site} `} + required + title={`"site:${site} " é usado para que os resultados da pesquisa fiquem restritos a este website`} + pattern={`site:${site} .+`} + size={`site:${site} .+`.length} + /></label> + </p> + <p class="mute"> + <small>Esta pesquisa é efectuada pelo Google e <strong>utiliza + software proprietário.</strong></small> + </p> + <p><button type="submit">🔍 Pesquisar</button></p> + </div> + </details> + </form> +</search> + +<style> + search { + padding-block-end: calc(var(--size-4) * 1em); + } + + summary { + font-size: calc(var(--size-3) * 1rem); + font-weight: bolder; + } + + .details { + border-radius: calc(var(--size-1) * 1em); + border: 1px solid var(--color-light); + margin-block-start: calc(var(--size-1) * 1em); + font-size: calc(var(--size-3) * 1rem); + padding-inline: calc(var(--size-4) * 1em); + padding-block: calc(var(--size-2) * 1em); + + & > p { + margin-block: calc(var(--size-2) * 1em); + line-height: calc(var(--size-8) * 1rem); + } + + & input[type="search"] { + width: 100%; + } + } +</style> diff --git a/src/components/templates/SimplePostList.astro b/src/components/templates/SimplePostList.astro new file mode 100644 index 0000000..0ec33e3 --- /dev/null +++ b/src/components/templates/SimplePostList.astro @@ -0,0 +1,81 @@ +--- +import Date from "@components/organisms/Date.astro"; +import KeywordsList from "@components/organisms/KeywordsList.astro"; +import { getFirstUserID, getLastUpdate } from "@lib/collection/helpers"; +import type { Original } from "@lib/collection/schemas"; +import type { z } from "astro:content"; +import type { CollectionEntry } from "astro:content"; + +interface Props { + posts: (CollectionEntry<"blog"> & { data: z.infer<typeof Original> })[]; +} + +const { posts } = Astro.props; +--- +<ol> + { + await Promise.all(posts.map(async (post) => { + const { id, data } = post; + const { title, description, lang, keywords } = data; + const { name, email, entity } = await getFirstUserID(post); + const display = name ?? email ?? entity; + return ( + <li> + <article> + <h3><a href={`/blog/read/${id}`}>{title}</a></h3> + { + description && + description.split("\n\n").map((paragraph) => ( + <p class="small">{paragraph}</p> + )) + } + + <footer class="small"> + <Date + date={getLastUpdate(post)} + locales={lang} + options={{ + year: "numeric", + month: "long", + day: "numeric", + }} + />{display} + <KeywordsList {keywords} /> + </footer> + </article> + </li> + ); + })) + } +</ol> + +<style> + ol { + margin-inline-start: calc(var(--size-7) * 1em); + margin-block: calc(var(--size-7) * 1em); + & > li { + margin-block-start: calc(var(--size-2) * 1em); + & > article { + padding-inline-end: calc(var(--size-9) * 1em); + + & > p:not(:first-of-type) { + margin-block-start: 1.5em; + } + + & > footer { + display: flex; + flex-direction: column; + gap: calc(var(--size-1) * 1em); + } + } + } + } + + @media (width >= 40rem) { + ol > li > article > footer { + flex-direction: row; + align-items: center; + justify-content: space-between; + } + } +</style> |