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