support table of contents for blog
This commit is contained in:
parent
bcc894e0bc
commit
51bcc2c793
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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 }) => (
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue