+
@@ -106,7 +82,6 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
href={href}
target="_blank"
rel="noopener noreferrer"
- className="text-blue-600 hover:underline"
>
{children}
@@ -114,35 +89,11 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
}
return (
-
+
{children}
);
},
- table: ({ children }) => (
-
- ),
- thead: ({ children }) => (
-
{children}
- ),
- tbody: ({ children }) => (
-
{children}
- ),
- tr: ({ children }) => (
-
{children}
- ),
- th: ({ children }) => (
-
- {children}
- |
- ),
- td: ({ children }) => (
-
{children} |
- ),
}}
>
{content}
diff --git a/components/blog/reading-progress.tsx b/components/blog/reading-progress.tsx
new file mode 100644
index 0000000..6307268
--- /dev/null
+++ b/components/blog/reading-progress.tsx
@@ -0,0 +1,40 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+
+export function ReadingProgress() {
+ const [progress, setProgress] = useState(0)
+
+ useEffect(() => {
+ const updateProgress = () => {
+ const scrollTop = window.scrollY
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight
+ const scrollPercent = (scrollTop / docHeight) * 100
+ setProgress(Math.min(scrollPercent, 100))
+ }
+
+ window.addEventListener('scroll', updateProgress, { passive: true })
+ updateProgress()
+ return () => window.removeEventListener('scroll', updateProgress)
+ }, [])
+
+ return (
+ <>
+
+
0 ? '0 0 8px var(--neon-cyan)' : 'none'
+ }}
+ />
+
+
+
+
+ [{Math.round(progress)}%]
+
+
+ >
+ )
+}
diff --git a/components/blog/search-bar.tsx b/components/blog/search-bar.tsx
index 4a64b30..b56530c 100644
--- a/components/blog/search-bar.tsx
+++ b/components/blog/search-bar.tsx
@@ -5,7 +5,7 @@ interface SearchBarProps {
export function SearchBar({ searchQuery, onSearchChange }: SearchBarProps) {
return (
-
+
>
{
+ const handleScroll = () => {
+ const currentScrollY = window.scrollY
+ setIsVisible(currentScrollY < lastScrollY || currentScrollY < 100)
+ setLastScrollY(currentScrollY)
+ }
+
+ window.addEventListener('scroll', handleScroll, { passive: true })
+ return () => window.removeEventListener('scroll', handleScroll)
+ }, [lastScrollY])
+
+ const shareLinks = {
+ twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${url}`,
+ linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`,
+ }
+
+ const handleCopyLink = async () => {
+ await navigator.clipboard.writeText(url)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ const scrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }
+
+ return (
+
+ )
+}
diff --git a/components/blog/table-of-contents.tsx b/components/blog/table-of-contents.tsx
new file mode 100644
index 0000000..a70c9f7
--- /dev/null
+++ b/components/blog/table-of-contents.tsx
@@ -0,0 +1,79 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+
+interface Heading {
+ id: string
+ text: string
+ level: number
+}
+
+interface TOCProps {
+ headings: Heading[]
+}
+
+export function TableOfContents({ headings }: TOCProps) {
+ const [activeId, setActiveId] = useState('')
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ setActiveId(entry.target.id)
+ }
+ })
+ },
+ { rootMargin: '-100px 0px -66%' }
+ )
+
+ headings.forEach(({ id }) => {
+ const element = document.getElementById(id)
+ if (element) observer.observe(element)
+ })
+
+ return () => observer.disconnect()
+ }, [headings])
+
+ return (
+
+ )
+}
diff --git a/components/blog/tag-filter.tsx b/components/blog/tag-filter.tsx
index f7a19be..deebbcf 100644
--- a/components/blog/tag-filter.tsx
+++ b/components/blog/tag-filter.tsx
@@ -9,7 +9,7 @@ export function TagFilter({ allTags, selectedTags, onToggleTag, onClearTags }: T
if (allTags.length === 0) return null
return (
-
+
FILTER BY TAG
@@ -18,7 +18,7 @@ export function TagFilter({ allTags, selectedTags, onToggleTag, onClearTags }: T