feat: footnotes
Signed-off-by: Filipe Medeiros <hello@filipesm.eu>
This commit is contained in:
parent
c527b818f4
commit
091d0538f9
43
frontend/src/components/BlogPostFootnotes.astro
Normal file
43
frontend/src/components/BlogPostFootnotes.astro
Normal 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>
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue