support table of contents for blog

This commit is contained in:
saml33 2024-03-09 23:04:47 +11:00
parent bcc894e0bc
commit 51bcc2c793
9 changed files with 141 additions and 12 deletions

View File

@ -6,6 +6,7 @@ import RichText from '../../../components/rich-text/RichText'
import PostDetails from '../../../components/blog/PostDetails'
import PageHeader from '../../../components/explore/PageHeader'
import AppCallToAction from '../../../components/shared/AppCallToAction'
import TableOfContents from '../../../components/shared/TableOfContents'
interface BlogPostPageParams {
slug: string
@ -82,6 +83,7 @@ async function BlogPostPage({ params }: BlogPostPageProps) {
ctaDescription,
ctaUrl,
slug,
showTableOfContents,
} = blogPost
const ctaData = { ctaTitle, ctaDescription, ctaUrl }
@ -96,14 +98,27 @@ async function BlogPostPage({ params }: BlogPostPageProps) {
backgroundImageUrl={headerImageUrl}
showBack
/>
<div className="px-6 lg:px-20 pb-10 md:pb-16 max-w-3xl mx-auto">
<PostDetails post={blogPost} />
<RichText document={postContent} />
{ctaData?.ctaUrl ? (
<div className="pt-6">
<AppCallToAction data={ctaData} />
<div
className={`px-6 lg:px-20 pb-10 md:pb-16 ${
showTableOfContents ? '' : 'max-w-3xl'
} mx-auto`}
>
<div className="flex flex-col md:flex-row md:justify-center">
{showTableOfContents ? (
<div className="relative">
<TableOfContents content={postContent} />
</div>
) : null}
<div className={showTableOfContents ? 'md:max-w-xl md:ml-10' : ''}>
<PostDetails post={blogPost} />
<RichText document={postContent} />
{ctaData?.ctaUrl ? (
<div className="pt-6">
<AppCallToAction data={ctaData} />
</div>
) : null}
</div>
) : null}
</div>
</div>
</>
)

View File

@ -29,6 +29,10 @@ function RichText({ document, currentPrice }: RichTextProps) {
return null
}
const headingTwos = document.content
.filter((node) => node.nodeType === 'heading-2')
.map((h: any) => h.content[0].value)
const options = {
renderMark: {
[MARKS.BOLD]: (text) => <Bold>{text}</Bold>,
@ -72,7 +76,10 @@ function RichText({ document, currentPrice }: RichTextProps) {
}
},
[BLOCKS.EMBEDDED_ASSET]: (node) => renderImage(node),
[BLOCKS.HEADING_2]: (node, children) => <H2>{children}</H2>,
[BLOCKS.HEADING_2]: (node, children) => {
const headingIndex = headingTwos.findIndex((v) => v === children[0])
return <H2 id={`anchor-link-${headingIndex}`}>{children}</H2>
},
[BLOCKS.HEADING_3]: (node, children) => <H3>{children}</H3>,
[BLOCKS.HEADING_4]: (node, children) => <H4>{children}</H4>,
[BLOCKS.UL_LIST]: (node, children) => <Ul>{children}</Ul>,
@ -140,7 +147,11 @@ const Text = ({ children }) => (
</p>
)
const H2 = ({ children }) => <h2 className="text-2xl">{children}</h2>
const H2 = ({ children, id }) => (
<h2 className="text-2xl" id={id}>
{children}
</h2>
)
const H3 = ({ children }) => <h3 className="mb-2 text-xl">{children}</h3>
const H4 = ({ children }) => <h3 className="mb-1.5 text-lg">{children}</h3>
const Ul = ({ children }) => (

View File

@ -0,0 +1,65 @@
'use client'
import { Document as RichTextDocument } from '@contentful/rich-text-types'
import { useEffect, useState } from 'react'
const TableOfContents = ({
content,
}: {
content: RichTextDocument | undefined
}) => {
if (!content) return null
const headingTwos = content.content
.filter((node) => node.nodeType === 'heading-2')
.map((h: any) => h.content[0].value)
const [activeSection, setActiveSection] = useState<number | null>(null)
useEffect(() => {
const handleScroll = () => {
const sectionOffsets = headingTwos.map((heading, index) => {
const id = `anchor-link-${index}`
const section = document.getElementById(id)
if (section) {
return {
id: index + 1,
offsetTop: section?.offsetTop,
offsetBottom: section?.offsetTop + section?.offsetHeight,
}
}
})
const currentScroll = window.pageYOffset + window.innerHeight / 2
const active = sectionOffsets.find((section) => {
return section
? currentScroll >= section.offsetTop &&
currentScroll <= section.offsetBottom
: undefined
})
if (active?.id) {
setActiveSection(active.id)
}
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [headingTwos])
return (
<div className="mt-8 p-6 rounded-lg bg-th-bkg-2 space-y-2 h-max md:sticky md:top-8 md:w-64">
{headingTwos.map((heading, index) => (
<a
className={`block ${
activeSection === index + 1 ? 'text-th-active' : 'text-th-fgd-2'
}`}
href={`#anchor-link-${index}`}
key={heading}
onClick={() => setActiveSection(index + 1)}
>
{heading}
</a>
))}
</div>
)
}
export default TableOfContents

View File

@ -25,6 +25,7 @@ export interface BlogPost {
ctaDescription: string | undefined
ctaUrl: string | undefined
lastModified: string
showTableOfContents: boolean | undefined
}
// A function to transform a Contentful blog post
@ -57,6 +58,7 @@ export function parseContentfulBlogPost(
ctaDescription: blogPostEntry.fields.ctaDescription,
ctaUrl: blogPostEntry.fields.ctaUrl,
lastModified: blogPostEntry.sys.updatedAt,
showTableOfContents: blogPostEntry.fields.showTableOfContents,
}
}

View File

@ -0,0 +1,23 @@
import type {
ChainModifiers,
Entry,
EntryFieldTypes,
EntrySkeletonType,
LocaleCode,
} from 'contentful'
export interface TypeAppAnnouncementFields {
title: EntryFieldTypes.Symbol
description?: EntryFieldTypes.Symbol
linkPath: EntryFieldTypes.Symbol
image?: EntryFieldTypes.AssetLink
}
export type TypeAppAnnouncementSkeleton = EntrySkeletonType<
TypeAppAnnouncementFields,
'appAnnouncement'
>
export type TypeAppAnnouncement<
Modifiers extends ChainModifiers,
Locales extends LocaleCode,
> = Entry<TypeAppAnnouncementSkeleton, Modifiers, Locales>

View File

@ -10,9 +10,17 @@ export interface TypeBlogPostFields {
postTitle: EntryFieldTypes.Symbol
slug: EntryFieldTypes.Symbol
author?: EntryFieldTypes.Symbol
category: EntryFieldTypes.Symbol<'Markets' | 'Meme Coins' | 'Repost'>
category: EntryFieldTypes.Symbol<
| 'Announcements'
| 'DePIN'
| 'Interviews'
| 'Markets'
| 'Meme Coins'
| 'Repost'
>
authorProfileImage?: EntryFieldTypes.AssetLink
postDescription: EntryFieldTypes.Text
showTableOfContents?: EntryFieldTypes.Boolean
postContent: EntryFieldTypes.RichText
postHeroImage?: EntryFieldTypes.AssetLink
seoTitle?: EntryFieldTypes.Symbol

View File

@ -11,7 +11,7 @@ export interface TypeLearnPostFields {
slug: EntryFieldTypes.Symbol
author?: EntryFieldTypes.Symbol
category: EntryFieldTypes.Symbol<
'Listing on Mango' | 'Repost' | 'Spot Trading'
'Learn to Trade' | 'Listing on Mango' | 'Repost' | 'Spot Trading'
>
authorProfileImage?: EntryFieldTypes.AssetLink
postDescription: EntryFieldTypes.Text

View File

@ -1,3 +1,8 @@
export type {
TypeAppAnnouncement,
TypeAppAnnouncementFields,
TypeAppAnnouncementSkeleton,
} from './TypeAppAnnouncement'
export type {
TypeBlogPost,
TypeBlogPostFields,

File diff suppressed because one or more lines are too long