Skip to content

Commit f43da75

Browse files
Merge pull request #32 from ShipFriend0516/feature/toc
[Feature] Table of Contents
2 parents cf9e742 + a25171d commit f43da75

File tree

4 files changed

+83
-18
lines changed

4 files changed

+83
-18
lines changed

app/entities/post/detail/PostBody.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22
import LoadingIndicator from '@/app/entities/common/Loading/LoadingIndicator';
33
import MDEditor from '@uiw/react-md-editor';
4+
import PostTOC from '@/app/entities/post/detail/PostTOC';
45

56
interface Props {
67
content: string;
@@ -9,23 +10,28 @@ interface Props {
910

1011
const PostBody = ({ content, loading }: Props) => {
1112
return (
12-
<div className={'w-full post-body px-4 py-16 min-h-[500px]'}>
13+
<div
14+
className={'max-w-3xl post-body px-4 py-16 min-h-[500px] relative '}
15+
>
1316
{loading ? (
1417
<div className={'w-1/3 mx-auto'}>
1518
<LoadingIndicator />
1619
</div>
1720
) : (
18-
<MDEditor.Markdown
19-
style={{
20-
backgroundColor: 'var(--background)',
21-
color: 'var(--text-primary)',
22-
}}
23-
className={''}
24-
source={content}
25-
wrapperElement={{
26-
'data-color-mode': 'dark',
27-
}}
28-
/>
21+
<>
22+
<MDEditor.Markdown
23+
style={{
24+
backgroundColor: 'var(--background)',
25+
color: 'var(--text-primary)',
26+
}}
27+
className={''}
28+
source={content}
29+
wrapperElement={{
30+
'data-color-mode': 'dark',
31+
}}
32+
/>
33+
<PostTOC postContent={content || ''} />
34+
</>
2935
)}
3036
</div>
3137
);

app/entities/post/detail/PostTOC.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Link from 'next/link';
2+
3+
const PostTOC = ({ postContent }: { postContent: string }) => {
4+
const parseHeadings = (content: string) => {
5+
const headings = content.match(/#{1,6} .+/g);
6+
7+
return (headings ?? []).map((heading: string) => ({
8+
id: heading.replace(/#/g, '').trim(),
9+
type: heading.lastIndexOf('#') + 1,
10+
title: heading.replace(/#/g, '').trim(),
11+
}));
12+
};
13+
14+
return (
15+
<div className="fixed post-toc hidden lg:block w-[280px] top-1/2 -translate-y-1/2 left-[calc(50%+524px)] transition-all text-sm bg-gray-100/80 rounded-md p-4 text-black">
16+
<h4 className={'text-xl font-bold mb-2'}>📌 Table of Contents</h4>
17+
<ul className={'list-none'}>
18+
{parseHeadings(postContent).map((heading) => {
19+
const href =
20+
`#${heading.id
21+
.toLowerCase()
22+
.replaceAll('.', '')
23+
.replaceAll(/[^a-zA-Z0-9-]/g, '-')
24+
.replaceAll(/-+/g, '-')
25+
.replaceAll(/^-|-$/g, '')}` || '';
26+
return (
27+
<li
28+
key={heading.id}
29+
style={{ marginLeft: `${(heading.type - 1) * 16}px` }}
30+
className={`${heading.type === 1 ? 'font-bold' : ''} `}
31+
>
32+
<Link
33+
scroll={true}
34+
className={
35+
'p-1 transition-all hover:bg-green-50 rounded-md text-nowrap overflow-x-hidden scroll-smooth '
36+
}
37+
onClick={(e) => {
38+
e.preventDefault();
39+
document.querySelector(href)?.scrollIntoView({
40+
behavior: 'smooth',
41+
});
42+
}}
43+
href={`#${heading.id
44+
.toLowerCase()
45+
.replaceAll('.', '')
46+
.replaceAll(/[^a-zA-Z0-9-]/g, '-')
47+
.replaceAll(/-+/g, '-')
48+
.replaceAll(/^-|-$/g, '')}`}
49+
>
50+
{'∟ ' + heading.title}
51+
</Link>
52+
</li>
53+
);
54+
})}
55+
</ul>
56+
</div>
57+
);
58+
};
59+
60+
export default PostTOC;

app/globals.css

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ article.post li {
4949
list-style: circle;
5050
}
5151

52+
.post-toc ul > li {
53+
list-style: none;
54+
}
55+
5256
article.post h1,
5357
article.post h2,
5458
article.post h3,
@@ -73,12 +77,6 @@ article.post h3 {
7377
margin-bottom: 1rem;
7478
}
7579

76-
article.post h4 {
77-
font-size: 1rem;
78-
font-weight: bold;
79-
margin-bottom: 1rem;
80-
}
81-
8280
/* Footer Style */
8381

8482
footer {

app/posts/[slug]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Metadata } from 'next';
66
import dbConnect from '@/app/lib/dbConnect';
77
import Post from '@/app/models/Post';
88
import PostJSONLd from '@/app/entities/post/detail/PostJSONLd';
9+
import PostTOC from '@/app/entities/post/detail/PostTOC';
910

1011
async function getPostDetail(slug: string) {
1112
await dbConnect();

0 commit comments

Comments
 (0)