From 82b77be57a13520ac58e16ea5bec8a491a0c7297 Mon Sep 17 00:00:00 2001 From: RJ Date: Thu, 13 Nov 2025 16:04:17 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20update=20blog=20pos?= =?UTF-8?q?t=20styles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/@breadcrumbs/about/page.tsx | 2 +- app/@breadcrumbs/blog/[...slug]/page.tsx | 2 +- app/@breadcrumbs/blog/page.tsx | 2 +- app/@breadcrumbs/default.tsx | 2 +- app/@breadcrumbs/tags/[tag]/page.tsx | 2 +- app/@breadcrumbs/tags/page.tsx | 2 +- app/blog/[...slug]/page.tsx | 188 +++++++++++++++-------- app/blog/blog-client.tsx | 29 ++-- app/blog/layout.tsx | 8 +- app/globals.css | 169 ++++++++++++++++++-- components/blog/blog-card.tsx | 12 +- components/blog/code-block.tsx | 55 +++++++ components/blog/markdown-renderer.tsx | 107 ++++--------- components/blog/reading-progress.tsx | 40 +++++ components/blog/search-bar.tsx | 2 +- components/blog/sticky-footer.tsx | 109 +++++++++++++ components/blog/table-of-contents.tsx | 79 ++++++++++ components/blog/tag-filter.tsx | 4 +- 18 files changed, 623 insertions(+), 191 deletions(-) create mode 100644 components/blog/code-block.tsx create mode 100644 components/blog/reading-progress.tsx create mode 100644 components/blog/sticky-footer.tsx create mode 100644 components/blog/table-of-contents.tsx diff --git a/app/@breadcrumbs/about/page.tsx b/app/@breadcrumbs/about/page.tsx index c04bb9f..ed78d5e 100644 --- a/app/@breadcrumbs/about/page.tsx +++ b/app/@breadcrumbs/about/page.tsx @@ -1,4 +1,4 @@ -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; export default function AboutBreadcrumb() { return ( diff --git a/app/@breadcrumbs/blog/[...slug]/page.tsx b/app/@breadcrumbs/blog/[...slug]/page.tsx index 21c9398..a2dc242 100644 --- a/app/@breadcrumbs/blog/[...slug]/page.tsx +++ b/app/@breadcrumbs/blog/[...slug]/page.tsx @@ -1,4 +1,4 @@ -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; import { getPostBySlug } from '@/lib/markdown'; interface BreadcrumbItem { diff --git a/app/@breadcrumbs/blog/page.tsx b/app/@breadcrumbs/blog/page.tsx index e3897f5..6abd4ba 100644 --- a/app/@breadcrumbs/blog/page.tsx +++ b/app/@breadcrumbs/blog/page.tsx @@ -1,4 +1,4 @@ -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; export default function BlogBreadcrumb() { return ( diff --git a/app/@breadcrumbs/default.tsx b/app/@breadcrumbs/default.tsx index f934837..8f1d385 100644 --- a/app/@breadcrumbs/default.tsx +++ b/app/@breadcrumbs/default.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; export default function DefaultBreadcrumb() { return ; diff --git a/app/@breadcrumbs/tags/[tag]/page.tsx b/app/@breadcrumbs/tags/[tag]/page.tsx index 7f3c00a..9451e8d 100644 --- a/app/@breadcrumbs/tags/[tag]/page.tsx +++ b/app/@breadcrumbs/tags/[tag]/page.tsx @@ -1,4 +1,4 @@ -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; export default async function TagBreadcrumb({ params, diff --git a/app/@breadcrumbs/tags/page.tsx b/app/@breadcrumbs/tags/page.tsx index 01aa702..8d74cd3 100644 --- a/app/@breadcrumbs/tags/page.tsx +++ b/app/@breadcrumbs/tags/page.tsx @@ -1,4 +1,4 @@ -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; export default function TagsBreadcrumb() { return ( diff --git a/app/blog/[...slug]/page.tsx b/app/blog/[...slug]/page.tsx index e694a90..b8fe778 100644 --- a/app/blog/[...slug]/page.tsx +++ b/app/blog/[...slug]/page.tsx @@ -3,6 +3,10 @@ import { notFound } from 'next/navigation' import Link from 'next/link' import { getAllPosts, getPostBySlug, getRelatedPosts } from '@/lib/markdown' import { formatDate, formatRelativeDate } from '@/lib/utils' +import { TableOfContents } from '@/components/blog/table-of-contents' +import { ReadingProgress } from '@/components/blog/reading-progress' +import { StickyFooter } from '@/components/blog/sticky-footer' +import MarkdownRenderer from '@/components/blog/markdown-renderer' export async function generateStaticParams() { const posts = await getAllPosts() @@ -39,41 +43,19 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str } } -function AuthorInfo({ author, date }: { author: string; date: string }) { - return ( -
-
- - {author.charAt(0).toUpperCase()} - -
-
-

{author}

-

- Publicat {formatRelativeDate(date)} • {formatDate(date)} -

-
-
- ) -} +function extractHeadings(content: string) { + const headingRegex = /^(#{2,3})\s+(.+)$/gm + const headings: { id: string; text: string; level: number }[] = [] + let match -function RelatedPosts({ posts }: { posts: any[] }) { - if (posts.length === 0) return null + while ((match = headingRegex.exec(content)) !== null) { + const level = match[1].length + const text = match[2] + const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + headings.push({ id, text, level }) + } - return ( -
-

Articole similare

-
- {posts.map((post) => ( - -

{post.frontmatter.title}

-

{post.frontmatter.description}

-

{formatDate(post.frontmatter.date)}

- - ))} -
-
- ) + return headings } export default async function BlogPostPage({ params }: { params: Promise<{ slug: string[] }> }) { @@ -86,44 +68,118 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: } const relatedPosts = await getRelatedPosts(slugPath) + const headings = extractHeadings(post.content) + const fullUrl = `https://yourdomain.com/blog/${slugPath}` return ( -
-
- {post.frontmatter.image && ( - {post.frontmatter.title} - )} -

{post.frontmatter.title}

-

{post.frontmatter.description}

- {post.frontmatter.tags && post.frontmatter.tags.length > 0 && ( -
- {post.frontmatter.tags.map((tag: string) => ( - - #{tag} - - ))} -
- )} - -
+ <> + -
-
- Timp estimat de citire: {post.readingTime} minute +
+
+ + +
+
+
+
+

+ >> CLASSIFIED_DOC://PUBLIC_ACCESS +

+
+
+
+
+
+
+
+ {post.frontmatter.tags.map((tag: string) => ( + + #{tag} + + ))} +
+
+ +
+

+ {post.frontmatter.title} +

+ +

+ >> {post.frontmatter.description} +

+
+ +
+
+ + {post.frontmatter.author.charAt(0).toUpperCase()} + +
+
+

{post.frontmatter.author}

+
+ + // + {post.readingTime}min READ +
+
+
+
+ + {post.frontmatter.image && ( +
+ {post.frontmatter.title} +
+ )} + +
+ +
+ + {relatedPosts.length > 0 && ( +
+

// Articole similare

+
+ {relatedPosts.map((relatedPost) => ( + +

{relatedPost.frontmatter.title}

+

{relatedPost.frontmatter.description}

+

{formatDate(relatedPost.frontmatter.date)}

+ + ))} +
+
+ )} + + +
-
- - - -
+ + ) } diff --git a/app/blog/blog-client.tsx b/app/blog/blog-client.tsx index 3183960..c44761b 100644 --- a/app/blog/blog-client.tsx +++ b/app/blog/blog-client.tsx @@ -65,22 +65,21 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) } return ( -
- +
{/* Header */} -
-

+

+

DATABASE QUERY // SEARCH RESULTS

-

+

> BLOG ARCHIVE_

{/* Search Bar */} -
+
-

+

FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'}

@@ -131,8 +130,8 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) })}
) : ( -
-

+

+

NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS

@@ -140,12 +139,12 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {/* Pagination */} {totalPages > 1 && ( -
+
@@ -154,10 +153,10 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) diff --git a/app/blog/layout.tsx b/app/blog/layout.tsx index ed2682a..b0b5945 100644 --- a/app/blog/layout.tsx +++ b/app/blog/layout.tsx @@ -1,4 +1,5 @@ import { Metadata } from 'next' +import { Navbar } from '@/components/blog/navbar' export const metadata: Metadata = { title: 'Blog', @@ -6,5 +7,10 @@ export const metadata: Metadata = { } export default function BlogLayout({ children }: { children: React.ReactNode }) { - return <>{children} + return ( + <> + + {children} + + ) } diff --git a/app/globals.css b/app/globals.css index 2f0a761..3df505d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -9,19 +9,20 @@ @layer base { :root { /* Light mode colors */ - --bg-primary: 241 245 249; - --bg-secondary: 226 232 240; - --bg-tertiary: 203 213 225; - --text-primary: 15 23 42; - --text-secondary: 51 65 85; - --text-muted: 100 116 139; - --border-primary: 203 213 225; - --border-subtle: 226 232 240; + --bg-primary: 250 250 250; + --bg-secondary: 240 240 243; + --bg-tertiary: 228 228 231; + --text-primary: 24 24 27; + --text-secondary: 63 63 70; + --text-muted: 113 113 122; + --border-primary: 212 212 216; + --border-subtle: 228 228 231; - --neon-pink: #8b4a5e; - --neon-cyan: #4a7b85; - --neon-purple: #6b5583; - --neon-magenta: #8b4a7e; + /* Desaturated cyberpunk for light mode - darker for readability */ + --neon-pink: #7a3d52; + --neon-cyan: #2d5a63; + --neon-purple: #5a4670; + --neon-magenta: #7a3d6b; } .dark { @@ -35,10 +36,11 @@ --border-primary: 71 85 105; --border-subtle: 30 41 59; - --neon-pink: #9b5a6e; - --neon-cyan: #5a8b95; - --neon-purple: #7b6593; - --neon-magenta: #9b5a8e; + /* Desaturated cyberpunk for dark mode */ + --neon-pink: #8a5568; + --neon-cyan: #4d7580; + --neon-purple: #6a5685; + --neon-magenta: #8a5579; } } @@ -322,5 +324,140 @@ inset 0 0 10px rgba(255, 0, 128, 0.1); border-color: var(--neon-pink); } + + /* Cyberpunk Prose Styling */ + .cyberpunk-prose { + color: rgb(212 212 216); + } + + .cyberpunk-prose h1, + .cyberpunk-prose h2, + .cyberpunk-prose h3 { + color: var(--neon-cyan); + font-family: var(--font-jetbrains-mono); + font-weight: 700; + text-transform: uppercase; + letter-spacing: -0.025em; + } + + .cyberpunk-prose h1 { + font-size: 2.25rem; + margin-bottom: 2rem; + margin-top: 3rem; + padding-bottom: 1rem; + border-bottom: 2px solid var(--neon-cyan); + } + + .cyberpunk-prose h2 { + font-size: 1.875rem; + margin-bottom: 1.5rem; + margin-top: 2.5rem; + } + + .cyberpunk-prose h3 { + font-size: 1.5rem; + margin-bottom: 1rem; + margin-top: 2rem; + } + + .cyberpunk-prose p { + color: rgb(212 212 216); + line-height: 1.625; + margin-bottom: 1.5rem; + font-size: 1.125rem; + } + + .cyberpunk-prose a { + color: var(--neon-magenta); + text-decoration: none; + font-weight: 600; + transition: color 0.2s; + } + + .cyberpunk-prose a:hover { + color: var(--neon-pink); + } + + .cyberpunk-prose ul, + .cyberpunk-prose ol { + color: rgb(212 212 216); + padding-left: 1.5rem; + margin-bottom: 1.5rem; + } + + .cyberpunk-prose ul > * + *, + .cyberpunk-prose ol > * + * { + margin-top: 0.5rem; + } + + .cyberpunk-prose li { + font-size: 1.125rem; + } + + .cyberpunk-prose blockquote { + border-left: 4px solid var(--neon-magenta); + padding-left: 1.5rem; + font-style: italic; + color: rgb(161 161 170); + background-color: #000; + padding-top: 1.5rem; + padding-bottom: 1.5rem; + margin-top: 2rem; + margin-bottom: 2rem; + position: relative; + box-shadow: -4px 0 15px rgba(155,90,142,0.3), inset 0 0 20px rgba(155,90,142,0.05); + } + + .cyberpunk-prose blockquote::before { + content: '"'; + position: absolute; + top: -0.5rem; + left: 0.5rem; + font-size: 3.75rem; + color: var(--neon-magenta); + opacity: 0.3; + font-family: monospace; + } + + .cyberpunk-prose code { + color: var(--neon-cyan); + background-color: #000; + padding: 0.125rem 0.5rem; + font-size: 0.875rem; + font-family: monospace; + border: 2px solid var(--neon-cyan); + box-shadow: 0 0 8px rgba(90,139,149,0.3); + text-shadow: 0 0 6px rgba(90,139,149,0.6); + } + + .cyberpunk-prose pre { + background-color: #000; + border: 4px solid var(--neon-purple); + padding: 1.5rem; + margin-top: 2rem; + margin-bottom: 2rem; + overflow-x: auto; + box-shadow: 0 0 25px rgba(123,101,147,0.6), inset 0 0 25px rgba(123,101,147,0.1); + } + + .cyberpunk-prose pre code { + background-color: transparent; + border: 0; + padding: 0; + } + + .cyberpunk-prose img { + margin-top: 2rem; + margin-bottom: 2rem; + border: 4px solid var(--neon-pink); + box-shadow: 0 0 20px rgba(155,90,110,0.5); + } + + .cyberpunk-prose hr { + border-color: rgb(39 39 42); + border-top-width: 2px; + margin-top: 3rem; + margin-bottom: 3rem; + } } diff --git a/components/blog/blog-card.tsx b/components/blog/blog-card.tsx index 9f34e44..98bb0e5 100644 --- a/components/blog/blog-card.tsx +++ b/components/blog/blog-card.tsx @@ -14,8 +14,8 @@ export function BlogCard({ post, variant }: BlogCardProps) { if (!hasImage || variant === 'text-only') { return ( -
-
+
+
{post.frontmatter.category} // {formatDate(post.frontmatter.date)} @@ -44,7 +44,7 @@ export function BlogCard({ post, variant }: BlogCardProps) { if (variant === 'image-side') { return ( -
+
-
+
{post.frontmatter.category} // {formatDate(post.frontmatter.date)} @@ -86,7 +86,7 @@ export function BlogCard({ post, variant }: BlogCardProps) { return ( -
+
-
+
{post.frontmatter.category} // {formatDate(post.frontmatter.date)} diff --git a/components/blog/code-block.tsx b/components/blog/code-block.tsx new file mode 100644 index 0000000..a867408 --- /dev/null +++ b/components/blog/code-block.tsx @@ -0,0 +1,55 @@ +'use client' + +import { useState } from 'react' + +interface CodeBlockProps { + code: string + language: string + filename?: string + showLineNumbers?: boolean +} + +export function CodeBlock({ code, language, filename, showLineNumbers = true }: CodeBlockProps) { + const [copied, setCopied] = useState(false) + + const handleCopy = async () => { + await navigator.clipboard.writeText(code) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+
+
+ {filename && ( + >> {filename} + )} + + [{language}] + +
+ +
+ +
+
+
+
+
+
+
+ +
+
+          {code}
+        
+
+
+ ) +} diff --git a/components/blog/markdown-renderer.tsx b/components/blog/markdown-renderer.tsx index fa9b0dc..529b985 100644 --- a/components/blog/markdown-renderer.tsx +++ b/components/blog/markdown-renderer.tsx @@ -4,8 +4,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import Image from 'next/image'; import Link from 'next/link'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { CodeBlock } from './code-block'; interface MarkdownRendererProps { content: string; @@ -16,55 +15,33 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) { ( -

{children}

- ), - h2: ({ children }) => ( -

{children}

- ), - h3: ({ children }) => ( -

{children}

- ), - h4: ({ children }) => ( -

{children}

- ), - h5: ({ children }) => ( -
{children}
- ), - h6: ({ children }) => ( -
{children}
- ), - p: ({ children }) => ( -

{children}

- ), - ul: ({ children }) => ( -
    {children}
- ), - ol: ({ children }) => ( -
    {children}
- ), - li: ({ children }) => ( -
  • {children}
  • - ), - blockquote: ({ children }) => ( -
    - {children} -
    - ), + h1: ({ children }) => { + const text = String(children); + const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); + return

    {children}

    ; + }, + h2: ({ children }) => { + const text = String(children); + const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); + return

    {children}

    ; + }, + h3: ({ children }) => { + const text = String(children); + const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); + return

    {children}

    ; + }, code: ({ inline, className, children, ...props }: any) => { const match = /language-(\w+)/.exec(className || ''); - return !inline && match ? ( - - {String(children).replace(/\n$/, '')} - - ) : ( - + if (!inline && match) { + return ( + + ); + } + return ( + {children} ); @@ -78,19 +55,18 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) { {alt ); } return ( -
    +
    {alt
    @@ -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 }) => ( -
    - - {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 ( +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + >> SHARE: + +
    + +
    + + [X] + + + + [IN] + + + +
    + + +
    +
    +
    + ) +} 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
    -

    {post.frontmatter.author}

    +

    + {post.frontmatter.author} +

    - + // {post.readingTime}min READ
    @@ -147,17 +159,25 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: {relatedPosts.length > 0 && (
    -

    // Articole similare

    +

    + // Articole similare +

    - {relatedPosts.map((relatedPost) => ( + {relatedPosts.map(relatedPost => ( -

    {relatedPost.frontmatter.title}

    -

    {relatedPost.frontmatter.description}

    -

    {formatDate(relatedPost.frontmatter.date)}

    +

    + {relatedPost.frontmatter.title} +

    +

    + {relatedPost.frontmatter.description} +

    +

    + {formatDate(relatedPost.frontmatter.date)} +

    ))}
    @@ -170,7 +190,12 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: className="flex items-center text-[var(--neon-pink)] hover:text-[var(--neon-magenta)] transition-all font-mono text-sm uppercase border border-[var(--neon-pink)] px-4 py-2 hover:shadow-[0_0_6px_rgba(155,90,110,0.3)]" > - + [BACK TO BLOG] diff --git a/app/blog/blog-client.tsx b/app/blog/blog-client.tsx index c44761b..6af0ce7 100644 --- a/app/blog/blog-client.tsx +++ b/app/blog/blog-client.tsx @@ -23,15 +23,14 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) const postsPerPage = 9 const filteredAndSortedPosts = useMemo(() => { - let result = posts.filter((post) => { + const result = posts.filter(post => { const matchesSearch = searchQuery === '' || post.frontmatter.title.toLowerCase().includes(searchQuery.toLowerCase()) || post.frontmatter.description.toLowerCase().includes(searchQuery.toLowerCase()) const matchesTags = - selectedTags.length === 0 || - selectedTags.every((tag) => post.frontmatter.tags.includes(tag)) + selectedTags.length === 0 || selectedTags.every(tag => post.frontmatter.tags.includes(tag)) return matchesSearch && matchesTags }) @@ -58,15 +57,12 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) ) const toggleTag = (tag: string) => { - setSelectedTags((prev) => - prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] - ) + setSelectedTags(prev => (prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag])) setCurrentPage(1) } return (
    -
    {/* Header */}
    @@ -83,15 +79,12 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
    { + onSearchChange={value => { setSearchQuery(value) setCurrentPage(1) }} /> - +
    @@ -109,7 +102,8 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {/* Results Count */}

    - FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'} + FOUND {filteredAndSortedPosts.length}{' '} + {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'}

    @@ -142,14 +136,14 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
    - {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
    diff --git a/components/blog/table-of-contents.tsx b/components/blog/table-of-contents.tsx index a70c9f7..abb7bed 100644 --- a/components/blog/table-of-contents.tsx +++ b/components/blog/table-of-contents.tsx @@ -17,8 +17,8 @@ export function TableOfContents({ headings }: TOCProps) { useEffect(() => { const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { + entries => { + entries.forEach(entry => { if (entry.isIntersecting) { setActiveId(entry.target.id) } @@ -43,31 +43,45 @@ export function TableOfContents({ headings }: TOCProps) {
    -
    -
    -
    +
    +
    +
    -

    +

    >> NAVIGATION

    diff --git a/components/blog/tag-filter.tsx b/components/blog/tag-filter.tsx index deebbcf..91756b4 100644 --- a/components/blog/tag-filter.tsx +++ b/components/blog/tag-filter.tsx @@ -14,7 +14,7 @@ export function TagFilter({ allTags, selectedTags, onToggleTag, onClearTags }: T FILTER BY TAG

    - {allTags.map((tag) => ( + {allTags.map(tag => (