diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-03 10:56:21 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-03 10:56:21 +0300 |
| commit | 456cf011b36de91c9936994b1fa45703adcd309b (patch) | |
| tree | 8e60daf998f731ac50d100fa490eaecae1168042 /src/components/blog | |
Initial fork of chrismwilliams/astro-theme-cactus theme
Diffstat (limited to 'src/components/blog')
| -rw-r--r-- | src/components/blog/Masthead.astro | 84 | ||||
| -rw-r--r-- | src/components/blog/PostPreview.astro | 24 | ||||
| -rw-r--r-- | src/components/blog/TOC.astro | 22 | ||||
| -rw-r--r-- | src/components/blog/TOCHeading.astro | 27 | ||||
| -rw-r--r-- | src/components/blog/webmentions/Comments.astro | 87 | ||||
| -rw-r--r-- | src/components/blog/webmentions/Likes.astro | 52 | ||||
| -rw-r--r-- | src/components/blog/webmentions/index.astro | 23 |
7 files changed, 319 insertions, 0 deletions
diff --git a/src/components/blog/Masthead.astro b/src/components/blog/Masthead.astro new file mode 100644 index 0000000..1f52383 --- /dev/null +++ b/src/components/blog/Masthead.astro @@ -0,0 +1,84 @@ +--- +import { Image } from "astro:assets"; +import type { CollectionEntry } from "astro:content"; +import FormattedDate from "@/components/FormattedDate.astro"; + +interface Props { + content: CollectionEntry<"post">; + readingTime: string; +} + +const { + content: { data }, + readingTime, +} = Astro.props; + +const dateTimeOptions: Intl.DateTimeFormatOptions = { + month: "long", +}; +--- + +{ + data.coverImage && ( + <div class="mb-6 aspect-video"> + <Image + alt={data.coverImage.alt} + layout="constrained" + width={748} + height={420} + priority + src={data.coverImage.src} + /> + </div> + ) +} +{data.draft ? <span class="text-base text-red-500">(Draft)</span> : null} +<h1 class="title"> + {data.title} +</h1> +<div class="flex flex-wrap items-center gap-x-3 gap-y-2"> + <p class="font-semibold"> + <FormattedDate date={data.publishDate} dateTimeOptions={dateTimeOptions} /> /{" "} + {readingTime} + </p> + { + data.updatedDate && ( + <span class="bg-quote/5 text-quote rounded-lg px-2 py-1"> + Updated: + <FormattedDate class="ms-1" date={data.updatedDate} dateTimeOptions={dateTimeOptions} /> + </span> + ) + } +</div> +{ + !!data.tags?.length && ( + <div class="mt-2"> + <svg + aria-hidden="true" + class="inline-block h-6 w-6" + fill="none" + focusable="false" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="1.5" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path d="M0 0h24v24H0z" fill="none" stroke="none" /> + <path d="M7.859 6h-2.834a2.025 2.025 0 0 0 -2.025 2.025v2.834c0 .537 .213 1.052 .593 1.432l6.116 6.116a2.025 2.025 0 0 0 2.864 0l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-6.117 -6.116a2.025 2.025 0 0 0 -1.431 -.593z" /> + <path d="M17.573 18.407l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-7.117 -7.116" /> + <path d="M6 9h-.01" /> + </svg> + {data.tags.map((tag, i) => ( + <> + {/* prettier-ignore */} + <span class="contents"> + <a class="cactus-link inline-block before:content-['#']" data-pagefind-filter={`tag:${tag}`} href={`/tags/${tag}/`}><span class="sr-only">View more blogs with the tag </span>{tag} + </a>{i < data.tags.length - 1 && ", "} + </span> + </> + ))} + </div> + ) +} diff --git a/src/components/blog/PostPreview.astro b/src/components/blog/PostPreview.astro new file mode 100644 index 0000000..fc1a9a3 --- /dev/null +++ b/src/components/blog/PostPreview.astro @@ -0,0 +1,24 @@ +--- +import type { CollectionEntry } from "astro:content"; +import FormattedDate from "@/components/FormattedDate.astro"; +import type { HTMLTag, Polymorphic } from "astro/types"; + +type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }> & { + post: CollectionEntry<"post">; + withDesc?: boolean; +}; + +const { as: Tag = "div", post, withDesc = false } = Astro.props; +--- + +<FormattedDate + class="min-w-30 font-semibold text-gray-600 dark:text-gray-400" + date={post.data.publishDate} +/> +<Tag> + {post.data.draft && <span class="text-red-500">(Draft) </span>} + <a class="cactus-link" data-astro-prefetch href={`/posts/${post.id}/`}> + {post.data.title} + </a> +</Tag> +{withDesc && <q class="line-clamp-3 italic">{post.data.description}</q>} diff --git a/src/components/blog/TOC.astro b/src/components/blog/TOC.astro new file mode 100644 index 0000000..6649546 --- /dev/null +++ b/src/components/blog/TOC.astro @@ -0,0 +1,22 @@ +--- +import { generateToc } from "@/utils/generateToc"; +import type { MarkdownHeading } from "astro"; +import TOCHeading from "./TOCHeading.astro"; + +interface Props { + headings: MarkdownHeading[]; +} + +const { headings } = Astro.props; + +const toc = generateToc(headings); +--- + +<details open class="lg:sticky lg:top-12 lg:order-2 lg:-me-32 lg:basis-64"> + <summary class="title hover:marker:text-accent cursor-pointer text-lg">Table of Contents</summary> + <nav class="ms-4 lg:w-full"> + <ol class="mt-4"> + {toc.map((heading) => <TOCHeading heading={heading} />)} + </ol> + </nav> +</details> diff --git a/src/components/blog/TOCHeading.astro b/src/components/blog/TOCHeading.astro new file mode 100644 index 0000000..b9dd486 --- /dev/null +++ b/src/components/blog/TOCHeading.astro @@ -0,0 +1,27 @@ +--- +import type { TocItem } from "@/utils/generateToc"; + +interface Props { + heading: TocItem; +} + +const { + heading: { children, depth, slug, text }, +} = Astro.props; +--- + +<li class={`${depth > 2 ? "ms-2" : ""}`}> + <a + class={`line-clamp-2 hover:text-accent ${depth <= 2 ? "mt-3" : "mt-2 text-xs"}`} + href={`#${slug}`}><span aria-hidden="true" class="me-0.5">#</span>{text}</a + > + { + !!children.length && ( + <ol> + {children.map((subheading) => ( + <Astro.self heading={subheading} /> + ))} + </ol> + ) + } +</li> diff --git a/src/components/blog/webmentions/Comments.astro b/src/components/blog/webmentions/Comments.astro new file mode 100644 index 0000000..5177d57 --- /dev/null +++ b/src/components/blog/webmentions/Comments.astro @@ -0,0 +1,87 @@ +--- +import { Image } from "astro:assets"; +import type { WebmentionsChildren } from "@/types"; +import { Icon } from "astro-icon/components"; + +interface Props { + mentions: WebmentionsChildren[]; +} + +const { mentions } = Astro.props; + +const validComments = ["mention-of", "in-reply-to"]; + +const comments = mentions.filter( + (mention) => validComments.includes(mention["wm-property"]) && mention.content?.text, +); +--- + +{ + !!comments.length && ( + <div> + <p class="text-accent-2 mb-0"> + <strong>{comments.length}</strong> Mention{comments.length > 1 ? "s" : ""} + </p> + <ul class="divide-global-text/20 mt-0 divide-y ps-0" role="list"> + {comments.map((mention) => ( + <li class="p-comment h-cite my-0 flex items-start gap-x-5 py-5"> + {mention.author?.photo && mention.author.photo !== "" ? ( + mention.author.url && mention.author.url !== "" ? ( + <a + class="u-author not-prose ring-global-text hover:ring-link focus-visible:ring-link shrink-0 overflow-hidden rounded-full ring-2 hover:ring-4 focus-visible:ring-4" + href={mention.author.url} + rel="noreferrer" + target="_blank" + title={mention.author.name} + > + <Image + alt={mention.author?.name} + class="u-photo my-0 h-12 w-12" + height={48} + src={mention.author?.photo} + width={48} + /> + </a> + ) : ( + <Image + alt={mention.author?.name} + class="u-photo my-0 h-12 w-12 rounded-full" + height={48} + src={mention.author?.photo} + width={48} + /> + ) + ) : null} + <div class="flex-auto"> + <div class="p-author h-card flex items-center justify-between gap-x-2"> + <p class="p-name text-accent-2 my-0 line-clamp-1 font-semibold"> + {mention.author?.name} + </p> + <a + aria-labelledby="cmt-source" + class="u-url not-prose hover:text-link" + href={mention.url} + rel="noreferrer" + target="_blank" + > + <span class="hidden" id="cmt-source"> + Visit the source of this webmention + </span> + <Icon + aria-hidden="true" + class="h-5 w-5" + focusable="false" + name="mdi:open-in-new" + /> + </a> + </div> + <p class="comment-content mt-1 mb-0 break-words [word-break:break-word]"> + {mention.content?.text} + </p> + </div> + </li> + ))} + </ul> + </div> + ) +} diff --git a/src/components/blog/webmentions/Likes.astro b/src/components/blog/webmentions/Likes.astro new file mode 100644 index 0000000..7862c43 --- /dev/null +++ b/src/components/blog/webmentions/Likes.astro @@ -0,0 +1,52 @@ +--- +import { Image } from "astro:assets"; +import type { WebmentionsChildren } from "@/types"; + +interface Props { + mentions: WebmentionsChildren[]; +} + +const { mentions } = Astro.props; +const MAX_LIKES = 10; + +const likes = mentions.filter((mention) => mention["wm-property"] === "like-of"); +const likesToShow = likes + .filter((like) => like.author?.photo && like.author.photo !== "") + .slice(0, MAX_LIKES); +--- + +{ + !!likes.length && ( + <div> + <p class="text-accent-2 mb-0"> + <strong>{likes.length}</strong> + {likes.length > 1 ? " People" : " Person"} liked this + </p> + {!!likesToShow.length && ( + <ul class="flex list-none flex-wrap overflow-hidden ps-2" role="list"> + {likesToShow.map((like) => ( + <li class="p-like h-cite -ms-2"> + <a + class="u-url not-prose ring-global-text hover:ring-link focus-visible:ring-link relative inline-block overflow-hidden rounded-full ring-2 hover:z-10 hover:ring-4 focus-visible:z-10 focus-visible:ring-4" + href={like.author?.url} + rel="noreferrer" + target="_blank" + title={like.author?.name} + > + <span class="p-author h-card"> + <Image + alt={like.author!.name} + class="u-photo my-0 inline-block h-12 w-12" + height={48} + src={like.author!.photo} + width={48} + /> + </span> + </a> + </li> + ))} + </ul> + )} + </div> + ) +} diff --git a/src/components/blog/webmentions/index.astro b/src/components/blog/webmentions/index.astro new file mode 100644 index 0000000..232b4f3 --- /dev/null +++ b/src/components/blog/webmentions/index.astro @@ -0,0 +1,23 @@ +--- +import { getWebmentionsForUrl } from "@/utils/webmentions"; +import Comments from "./Comments.astro"; +import Likes from "./Likes.astro"; + +const url = new URL(Astro.url.pathname, Astro.site); + +const webMentions = await getWebmentionsForUrl(`${url}`); + +// Return if no webmentions +if (!webMentions.length) return; +--- + +<hr class="border-solid" /> +<h2 class="mb-8 before:hidden">Webmentions for this post</h2> +<div class="space-y-10"> + <Likes mentions={webMentions} /> + <Comments mentions={webMentions} /> +</div> +<p class="mt-8"> + Responses powered by{" "} + <a href="https://webmention.io" rel="noreferrer" target="_blank">Webmentions</a> +</p> |
