blog-post.tsx

1'use client'
2
3import { Content } from '@/lib/content'
4import { formatDate } from './blog-list'
5import Layout from './Layout'
6import markdownStyles from '@/app/styles/markdown.module.css'
7// Import highlight.js
8import 'highlight.js/styles/github-dark.css'
9import hljs from 'highlight.js'
10import { useEffect } from 'react'
11import Link from 'next/link'
12import { languages } from '@/configs/hljs'
13
14interface Props {
15	post: Content
16	locale?: string
17	allPosts?: Content[]
18}
19
20// Tag component
21function Tag({ tag }: { tag: string }) {
22	return (
23		<span className="rounded-full bg-purple-500/10 px-3 py-1 text-sm text-purple-400">
24			{tag}
25		</span>
26	)
27}
28
29function getSimilarPosts(
30	currentPost: Content,
31	allPosts: Content[],
32	limit = 3,
33): Content[] {
34	const currentTags = new Set(currentPost.metadata.tags)
35
36	return allPosts
37		.filter((post) => post.slug !== currentPost.slug)
38		.map((post) => ({
39			post,
40			commonTags: post.metadata.tags.filter((tag: string) =>
41				currentTags.has(tag),
42			).length,
43		}))
44		.filter(({ commonTags }) => commonTags > 0)
45		.sort((a, b) => b.commonTags - a.commonTags)
46		.slice(0, limit)
47		.map(({ post }) => post)
48}
49
50export function BlogPost({ post, locale = 'en-US', allPosts = [] }: Props) {
51	useEffect(() => {
52		try {
53			// Initialize syntax highlighting with specific languages
54			hljs.configure({
55				ignoreUnescapedHTML: true,
56				languages: languages,
57			})
58
59			// Use a more specific selector and add error handling
60			document.querySelectorAll('pre code').forEach((block) => {
61				try {
62					hljs.highlightElement(block as HTMLElement)
63				} catch (e) {
64					console.warn('Failed to highlight code block:', e)
65				}
66			})
67
68			// Add click handlers for copy buttons
69			document.querySelectorAll('.copy-button').forEach((button) => {
70				button.addEventListener('click', () => {
71					const codeBlock = button
72						.closest('.code-wrapper')
73						?.querySelector('code')
74					if (codeBlock) {
75						navigator.clipboard.writeText(codeBlock.textContent || '')
76						const copyIcon = button.querySelector('.copy-icon')
77						const checkIcon = button.querySelector('.check-icon')
78						if (copyIcon && checkIcon) {
79							copyIcon.classList.add('hidden')
80							checkIcon.classList.remove('hidden')
81							setTimeout(() => {
82								copyIcon.classList.remove('hidden')
83								checkIcon.classList.add('hidden')
84							}, 2000)
85						}
86					}
87				})
88			})
89		} catch (e) {
90			console.error('Error initializing syntax highlighting:', e)
91		}
92	}, [post])
93
94	const similarPosts = getSimilarPosts(post, allPosts)
95
96	return (
97		<Layout>
98			<article className="prose prose-invert mx-auto max-w-3xl px-4 py-20">
99				<header className="mb-12">
100					<h1 className="mb-4 text-4xl font-bold">{post.metadata.title}</h1>
101					<div className="flex items-center gap-4 text-gray-400">
102						<time>{formatDate(post.metadata.date, true, locale)}</time>
103						<div className="flex gap-2">
104							{post.metadata.tags?.map((tag: string) => (
105								<Tag key={tag} tag={tag} />
106							))}
107						</div>
108					</div>
109				</header>
110
111				<div
112					className={markdownStyles.markdown}
113					dangerouslySetInnerHTML={{ __html: post.content }}
114				/>
115
116				{similarPosts.length > 0 && (
117					<section className="mt-16 border-t border-gray-800 pt-8">
118						<h2 className="mb-6 text-2xl font-bold">Similar Posts</h2>
119						<div className="space-y-6">
120							{similarPosts.map((similarPost) => {
121								const date = new Date(similarPost.metadata.date)
122								const url = `/blog/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${similarPost.slug}`
123
124								return (
125									<div
126										key={similarPost.slug}
127										className="group rounded-xl border border-gray-800 bg-gray-900/50 p-4 transition-all hover:border-purple-500/50"
128									>
129										<Link href={url}>
130											<h3 className="mb-2 text-lg font-semibold transition-colors group-hover:text-purple-400">
131												{similarPost.metadata.title}
132											</h3>
133										</Link>
134										<time className="text-sm text-gray-400">
135											{formatDate(similarPost.metadata.date, false, locale)}
136										</time>
137										<div className="mt-2 flex flex-wrap gap-2">
138											{similarPost.metadata.tags.map((tag: string) => (
139												<Tag key={tag} tag={tag} />
140											))}
141										</div>
142									</div>
143								)
144							})}
145						</div>
146					</section>
147				)}
148			</article>
149		</Layout>
150	)
151}
152