feat: footnotes

Signed-off-by: Filipe Medeiros <hello@filipesm.eu>
This commit is contained in:
Filipe Medeiros 2023-04-17 00:49:18 +01:00
parent c527b818f4
commit 091d0538f9
Signed by: filipe
GPG key ID: 9533BD5467CC1E78
6 changed files with 89 additions and 10 deletions

View file

@ -0,0 +1,43 @@
---
import type { PortableTextBlock } from '@portabletext/types';
import { PortableText } from 'astro-portabletext';
import InlineLink from './portableText/components/InlineLink.astro';
export interface Props {
blogPostContent: PortableTextBlock[];
}
const { blogPostContent } = Astro.props;
const blocksWithFootnotes = blogPostContent.filter((block) =>
block.markDefs?.some((mark) => mark._type === 'footnote'),
);
const footnotes: PortableTextBlock[][] = [];
for (const block of blocksWithFootnotes) {
for (const mark of block.markDefs!) {
footnotes.push(mark.content as PortableTextBlock[]);
}
}
---
<ul>
{
footnotes.map((footnote, index) => (
<li id={`footnote-${index + 1}`} class="flex gap-1">
<span>{index + 1}.</span>
<div>
<PortableText
value={footnote}
components={{
mark: {
// @ts-expect-error
link: InlineLink,
},
}}
/>
</div>
</li>
))
}
</ul>

View file

@ -16,6 +16,7 @@ import Heading4 from './components/Heading4.astro';
import Heading5 from './components/Heading5.astro'; import Heading5 from './components/Heading5.astro';
import Heading6 from './components/Heading6.astro'; import Heading6 from './components/Heading6.astro';
import Paragraph from './components/Paragraph.astro'; import Paragraph from './components/Paragraph.astro';
import FootnoteMark from './components/FootnoteMark.astro';
export interface Props { export interface Props {
content: PortableTextBlock[]; content: PortableTextBlock[];
@ -37,6 +38,7 @@ const components = {
mark: { mark: {
code: InlineCode, code: InlineCode,
link: InlineLink, link: InlineLink,
footnote: FootnoteMark,
}, },
list: { list: {
bullet: BulletList, bullet: BulletList,

View file

@ -0,0 +1,33 @@
---
import type { PortableTextListItemBlock } from '@portabletext/types';
import InlineLink from '../../InlineLink.astro';
export interface Props {
node: PortableTextListItemBlock;
}
// This is an incredible hack!! Ahah
// Not sure if safe, but seems to be
declare global {
var footnoteCounts: Record<string, number> | undefined;
}
if (!globalThis.footnoteCounts) {
globalThis.footnoteCounts = {};
}
if (globalThis.footnoteCounts[Astro.params.slug!] === undefined) {
globalThis.footnoteCounts[Astro.params.slug!] = 1;
} else {
globalThis.footnoteCounts[Astro.params.slug!]++;
}
const footnoteCount = globalThis.footnoteCounts[Astro.params.slug!];
---
<slot /><InlineLink
class="align-super ml-1 text-sm"
href={`#footnote-${footnoteCount}`}
>
{footnoteCount}
</InlineLink>

View file

@ -1,10 +0,0 @@
export default function langToLocale(lang: string) {
switch (lang) {
case 'pt':
return 'pt_PT';
case 'en':
return 'en_EN';
default:
return 'pt_PT';
}
}

View file

@ -8,6 +8,7 @@ import PageTitle from '../../../components/PageTitle.astro';
import Layout from '../../../layouts/Layout.astro'; import Layout from '../../../layouts/Layout.astro';
import BlogPostMetadata from '../../../components/BlogPostMetadata.astro'; import BlogPostMetadata from '../../../components/BlogPostMetadata.astro';
import type { ImageObject } from '../../../lib/cms/types'; import type { ImageObject } from '../../../lib/cms/types';
import BlogPostFootnotes from '../../../components/BlogPostFootnotes.astro';
export interface Props { export interface Props {
title: string; title: string;
@ -69,6 +70,8 @@ const { slug } = Astro.params;
<time datetime={publishDate} class="text-xl">{publishDate}</time> <time datetime={publishDate} class="text-xl">{publishDate}</time>
</header> </header>
<BlogPostContent content={content} /> <BlogPostContent content={content} />
<hr class="my-5 border-t border-primary-700" />
<BlogPostFootnotes blogPostContent={content} />
</article> </article>
<Fragment slot="footer"> <Fragment slot="footer">
<nav class="flex gap-4"> <nav class="flex gap-4">

View file

@ -61,6 +61,14 @@ const blogPost = defineType({
description: description:
'This image will be used as the header image in the article page, on the article list/blog page and also eventually on social previews and other websites.', 'This image will be used as the header image in the article page, on the article list/blog page and also eventually on social previews and other websites.',
validation: (r) => r.required(), validation: (r) => r.required(),
fields: [
{
title: 'Alt text',
name: 'altText',
type: 'string',
validation: (r) => r.required(),
},
],
options: { options: {
hotspot: true, hotspot: true,
}, },