diff --git a/.claude/skills/nextjs-coding-standards/SKILL.md b/.claude/skills/nextjs-coding-standards/SKILL.md index d5dc7b3..06d9820 100644 --- a/.claude/skills/nextjs-coding-standards/SKILL.md +++ b/.claude/skills/nextjs-coding-standards/SKILL.md @@ -15,6 +15,7 @@ Reference this skill when writing or reviewing code to ensure consistency with p ### Files and Directories **Use kebab-case for all file names:** + ``` ✅ user-profile.tsx ✅ blog-post-card.tsx @@ -26,12 +27,14 @@ Reference this skill when writing or reviewing code to ensure consistency with p ``` **Why kebab-case?** + - Cross-platform compatibility (Windows vs Unix) - URL-friendly (file names often map to routes) - Easier to parse and read - Industry standard for Next.js projects **Special Next.js Files:** + ``` page.tsx # Route pages layout.tsx # Layout components @@ -44,6 +47,7 @@ route.ts # API route handlers ### Component Names (Inside Files) **Use PascalCase for component names:** + ```typescript // File: user-profile.tsx export function UserProfile() { @@ -59,6 +63,7 @@ export default function BlogPostCard() { ### Variables, Functions, Props **Use camelCase:** + ```typescript // Variables const userSettings = {} @@ -83,10 +88,11 @@ function useMarkdown() {} ### Constants **Use SCREAMING_SNAKE_CASE:** + ```typescript -const API_BASE_URL = "https://api.example.com" +const API_BASE_URL = 'https://api.example.com' const MAX_RETRIES = 3 -const DEFAULT_LOCALE = "ro-RO" +const DEFAULT_LOCALE = 'ro-RO' ``` --- @@ -133,10 +139,7 @@ export async function POST(request: NextRequest) { const parsed = bodySchema.safeParse(json) if (!parsed.success) { - return NextResponse.json( - { error: 'Validation failed', details: parsed.error }, - { status: 400 } - ) + return NextResponse.json({ error: 'Validation failed', details: parsed.error }, { status: 400 }) } // parsed.data is fully typed @@ -146,6 +149,7 @@ export async function POST(request: NextRequest) { ``` **Key Points:** + - Use `safeParse()` instead of `parse()` to avoid try/catch - Return structured error responses - Use appropriate HTTP status codes @@ -154,6 +158,7 @@ export async function POST(request: NextRequest) { ### Error Handling **Return meaningful status codes:** + ```typescript 200 // Success 201 // Created @@ -165,12 +170,13 @@ export async function POST(request: NextRequest) { ``` **Structured error responses:** + ```typescript return NextResponse.json( { error: 'Resource not found', code: 'NOT_FOUND', - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }, { status: 404 } ) @@ -277,12 +283,12 @@ export function ThemeToggle() { ```javascript // tailwind.config.js module.exports = { - darkMode: 'class', // Required for next-themes + darkMode: 'class', // Required for next-themes theme: { extend: { colors: { 'dark-primary': '#18181b', - 'accent': { + accent: { DEFAULT: '#164e63', hover: '#155e75', }, @@ -380,6 +386,7 @@ export function Card({ children, className = "" }) { ``` **Standard breakpoints:** + ``` sm: 640px // Small tablets md: 768px // Tablets @@ -459,18 +466,21 @@ content/ # Content files (outside app/) ### lib/ Organization **Modules in lib/:** + - Substantial business logic (markdown.ts, seo.ts) - API clients and data fetching - Database connections - Authentication logic **Utils in lib/utils.ts:** + - Pure helper functions - Formatters (formatDate, formatCurrency) - Validators (isEmail, isValidUrl) - String manipulations **Types in lib/types/:** + - Shared TypeScript interfaces - API response types - Domain models @@ -479,6 +489,7 @@ content/ # Content files (outside app/) ### Component Organization **By domain/feature:** + ``` components/ ├── blog/ # Blog-specific @@ -495,6 +506,7 @@ components/ ``` **Not by type:** + ``` ❌ Don't organize like this: components/ @@ -507,6 +519,7 @@ components/ ### Public Assets **Organize by feature:** + ``` public/ ├── blog/ @@ -519,6 +532,7 @@ public/ ``` **Naming conventions:** + - Use descriptive names: `hero-background.jpg` not `img1.jpg` - Use kebab-case: `user-avatar.png` - Include dimensions for images: `logo-512x512.png` @@ -530,9 +544,10 @@ public/ ### Type Safety **Avoid `any`:** + ```typescript // ❌ Bad -function processData(data: any) { } +function processData(data: any) {} // ✅ Good function processData(data: unknown) { @@ -546,7 +561,7 @@ interface PostData { title: string content: string } -function processData(data: PostData) { } +function processData(data: PostData) {} ``` ### Infer Types from Zod @@ -665,6 +680,7 @@ export function InteractiveCard({ title }) { ``` **When to use 'use client':** + - Using React hooks (useState, useEffect, etc.) - Using event handlers (onClick, onChange, etc.) - Using browser APIs (window, localStorage, etc.) @@ -743,7 +759,7 @@ export async function generateStaticParams() { ```typescript // In frontmatter -date: "2025-01-15" +date: '2025-01-15' // For display formatDate(post.frontmatter.date) // "15 ianuarie 2025" (Romanian) diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8eb48a4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +node_modules +.next +out +build +dist +.cache +package-lock.json +public +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..db041af --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 100, + "arrowParens": "avoid", + "trailingComma": "es5" +} diff --git a/CLAUDE.md b/CLAUDE.md index 9fbb92f..9905ea2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,15 +74,16 @@ lib/ ``` **Frontmatter Schema:** + ```yaml --- -title: string # Required -description: string # Required -date: "YYYY-MM-DD" # Required, ISO format -author: string # Required -tags: [string, string?, string?] # Max 3 tags -image?: string # Optional hero image -draft?: boolean # Exclude from listings if true +title: string # Required +description: string # Required +date: 'YYYY-MM-DD' # Required, ISO format +author: string # Required +tags: [string, string?, string?] # Max 3 tags +image?: string # Optional hero image +draft?: boolean # Exclude from listings if true --- ``` @@ -107,20 +108,25 @@ components/ ### File Naming Conventions **Files and Directories:** + - Use **kebab-case** for all file names: `user-profile.tsx`, `blog-post.tsx` - Special Next.js files: `page.tsx`, `layout.tsx`, `not-found.tsx`, `loading.tsx` **Component Names (inside files):** + - Use **PascalCase**: `export function UserProfile()`, `export default BlogPost` **Variables, Functions, Props:** + - Use **camelCase**: `const userSettings = {}`, `function handleSubmit() {}` - Hooks: `useTheme`, `useMarkdown` **Constants:** + - Use **SCREAMING_SNAKE_CASE**: `const API_BASE_URL = "..."` **Why kebab-case for files?** + - Cross-platform compatibility (Windows vs Unix) - URL-friendly (file names often map to routes) - Easier to parse and read @@ -152,6 +158,7 @@ export default function RootLayout({ children }) { ``` **Client Component for Toggle:** + ```typescript // components/theme-toggle.tsx 'use client' @@ -173,23 +180,25 @@ export function ThemeToggle() { ``` **Tailwind Configuration:** + ```javascript // tailwind.config.js module.exports = { - darkMode: 'class', // Use 'class' strategy for next-themes + darkMode: 'class', // Use 'class' strategy for next-themes theme: { extend: { colors: { // Define custom colors for consistency 'dark-primary': '#18181b', - 'accent': { DEFAULT: '#164e63', hover: '#155e75' } - } - } - } + accent: { DEFAULT: '#164e63', hover: '#155e75' }, + }, + }, + }, } ``` **CSS Variables Pattern:** + ```css /* globals.css */ :root { @@ -219,6 +228,7 @@ module.exports = { ### Next.js 16 Specific Patterns **Async Server Components:** + ```typescript // app/blog/page.tsx export default async function BlogPage() { @@ -228,6 +238,7 @@ export default async function BlogPage() { ``` **Static Generation with Dynamic Routes:** + ```typescript // app/blog/[...slug]/page.tsx export async function generateStaticParams() { @@ -242,6 +253,7 @@ export async function generateMetadata({ params }) { ``` **Parallel Routes for Layout Composition:** + ```typescript // app/layout.tsx export default function RootLayout({ @@ -278,12 +290,14 @@ export default function RootLayout({ ### Styling Guidelines **Color Palette:** + - Backgrounds: `zinc-900`, `slate-900`, `slate-800` - Accents: `cyan-900`, `emerald-900`, `teal-900` - Text: `slate-100`, `slate-300`, `slate-500` - Borders: `border-2`, `border-4` (thick, sharp) **Design Tokens:** + - **NO rounded corners:** Use `rounded-none` or omit (default is sharp) - **Monospace fonts:** Apply `font-mono` for terminal aesthetic - **Uppercase labels:** Use `uppercase tracking-wider` for headers @@ -291,6 +305,7 @@ export default function RootLayout({ - **Classification labels:** Add metadata like "FILE#001", "DOCUMENT LEVEL-1" **Typography:** + - Primary font: `JetBrains Mono` (monospace) - Headings: `font-mono font-bold uppercase` - Body: `font-mono text-sm` diff --git a/README.md b/README.md index 4a9a9ee..b92c4a6 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -### This is the repo for the actual profile page \ No newline at end of file +### This is the repo for the actual profile page diff --git a/app/@breadcrumbs/about/page.tsx b/app/@breadcrumbs/about/page.tsx index c04bb9f..7584620 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 ( @@ -11,5 +11,5 @@ export default function AboutBreadcrumb() { }, ]} /> - ); + ) } diff --git a/app/@breadcrumbs/blog/[...slug]/page.tsx b/app/@breadcrumbs/blog/[...slug]/page.tsx index 21c9398..3b700e9 100644 --- a/app/@breadcrumbs/blog/[...slug]/page.tsx +++ b/app/@breadcrumbs/blog/[...slug]/page.tsx @@ -1,10 +1,10 @@ -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; -import { getPostBySlug } from '@/lib/markdown'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs' +import { getPostBySlug } from '@/lib/markdown' interface BreadcrumbItem { - label: string; - href: string; - current?: boolean; + label: string + href: string + current?: boolean } function formatDirectoryName(name: string): string { @@ -12,34 +12,34 @@ function formatDirectoryName(name: string): string { tech: 'Tehnologie', design: 'Design', tutorial: 'Tutoriale', - }; + } - return directoryNames[name] || name.charAt(0).toUpperCase() + name.slice(1); + return directoryNames[name] || name.charAt(0).toUpperCase() + name.slice(1) } export default async function BlogPostBreadcrumb({ params, }: { - params: Promise<{ slug: string[] }>; + params: Promise<{ slug: string[] }> }) { - const { slug } = await params; - const slugPath = slug.join('/'); - const post = getPostBySlug(slugPath); + const { slug } = await params + const slugPath = slug.join('/') + const post = getPostBySlug(slugPath) const items: BreadcrumbItem[] = [ { label: 'Blog', href: '/blog', }, - ]; + ] if (slug.length > 1) { for (let i = 0; i < slug.length - 1; i++) { - const segmentPath = slug.slice(0, i + 1).join('/'); + const segmentPath = slug.slice(0, i + 1).join('/') items.push({ label: formatDirectoryName(slug[i]), href: `/blog/${segmentPath}`, - }); + }) } } @@ -47,7 +47,7 @@ export default async function BlogPostBreadcrumb({ label: post ? post.frontmatter.title : slug[slug.length - 1], href: `/blog/${slugPath}`, current: true, - }); + }) - return ; + return } diff --git a/app/@breadcrumbs/blog/page.tsx b/app/@breadcrumbs/blog/page.tsx index e3897f5..44e92ed 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 ( @@ -11,5 +11,5 @@ export default function BlogBreadcrumb() { }, ]} /> - ); + ) } diff --git a/app/@breadcrumbs/default.tsx b/app/@breadcrumbs/default.tsx index f934837..ef9aa2e 100644 --- a/app/@breadcrumbs/default.tsx +++ b/app/@breadcrumbs/default.tsx @@ -1,7 +1,7 @@ -'use client'; +'use client' -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs' export default function DefaultBreadcrumb() { - return ; + return } diff --git a/app/@breadcrumbs/tags/[tag]/page.tsx b/app/@breadcrumbs/tags/[tag]/page.tsx index 7f3c00a..5348acd 100644 --- a/app/@breadcrumbs/tags/[tag]/page.tsx +++ b/app/@breadcrumbs/tags/[tag]/page.tsx @@ -1,15 +1,11 @@ -import { Breadcrumbs } from '@/components/layout/breadcrumbs'; +import { Breadcrumbs } from '@/components/layout/Breadcrumbs' -export default async function TagBreadcrumb({ - params, -}: { - params: Promise<{ tag: string }>; -}) { - const { tag } = await params; +export default async function TagBreadcrumb({ params }: { params: Promise<{ tag: string }> }) { + const { tag } = await params const tagName = tag .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); + .join(' ') return ( - ); + ) } diff --git a/app/@breadcrumbs/tags/page.tsx b/app/@breadcrumbs/tags/page.tsx index 01aa702..c499354 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 ( @@ -11,5 +11,5 @@ export default function TagsBreadcrumb() { }, ]} /> - ); + ) } diff --git a/app/about/page.tsx b/app/about/page.tsx index d8f383c..9870ec7 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -12,8 +12,8 @@ export default function AboutPage() {

- Bun venit pe blogul meu! Sunt un dezvoltator pasionat de tehnologie, - specializat în dezvoltarea web modernă cu Next.js, React și TypeScript. + Bun venit pe blogul meu! Sunt un dezvoltator pasionat de tehnologie, specializat în + dezvoltarea web modernă cu Next.js, React și TypeScript.

Ce vei găsi aici

@@ -27,10 +27,18 @@ export default function AboutPage() {

Tehnologii folosite

Acest blog este construit cu:

    -
  • Next.js 15 - Framework React pentru producție
  • -
  • TypeScript - Pentru type safety
  • -
  • Tailwind CSS - Pentru stilizare rapidă
  • -
  • Markdown - Pentru conținut
  • +
  • + Next.js 15 - Framework React pentru producție +
  • +
  • + TypeScript - Pentru type safety +
  • +
  • + Tailwind CSS - Pentru stilizare rapidă +
  • +
  • + Markdown - Pentru conținut +

Contact

diff --git a/app/blog/[...slug]/not-found.tsx b/app/blog/[...slug]/not-found.tsx index 973ee8c..7aa1f31 100644 --- a/app/blog/[...slug]/not-found.tsx +++ b/app/blog/[...slug]/not-found.tsx @@ -10,10 +10,16 @@ export default function NotFound() { Ne pare rău, dar articolul pe care îl cauți nu există sau a fost mutat.

- + Vezi toate articolele - + Pagina principală
diff --git a/app/blog/[...slug]/page.tsx b/app/blog/[...slug]/page.tsx index e694a90..1e3b1d8 100644 --- a/app/blog/[...slug]/page.tsx +++ b/app/blog/[...slug]/page.tsx @@ -3,13 +3,21 @@ 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() - return posts.map((post) => ({ slug: post.slug.split('/') })) + return posts.map(post => ({ slug: post.slug.split('/') })) } -export async function generateMetadata({ params }: { params: Promise<{ slug: string[] }> }): Promise { +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string[] }> +}): Promise { const { slug } = await params const slugPath = slug.join('/') const post = getPostBySlug(slugPath) @@ -39,41 +47,22 @@ 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 +75,136 @@ 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..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,41 +57,34 @@ 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 */} -
-

+

+

DATABASE QUERY // SEARCH RESULTS

-

+

> BLOG ARCHIVE_

{/* Search Bar */} -
+
{ + onSearchChange={value => { setSearchQuery(value) setCurrentPage(1) }} /> - +
@@ -109,8 +101,9 @@ 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'}

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

+

+

NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS

@@ -140,24 +133,24 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {/* Pagination */} {totalPages > 1 && ( -
+
- {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
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/blog/page.tsx b/app/blog/page.tsx index 37399c4..4d2b83e 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -3,7 +3,7 @@ import BlogPageClient from './blog-client' export default async function BlogPage() { const posts = await getAllPosts() - const allTags = Array.from(new Set(posts.flatMap((post) => post.frontmatter.tags))).sort() + const allTags = Array.from(new Set(posts.flatMap(post => post.frontmatter.tags))).sort() return } diff --git a/app/globals.css b/app/globals.css index 2f0a761..3d22f01 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @theme { --color-*: initial; @@ -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; } } @@ -68,12 +70,7 @@ /* Scanline effect */ .scanline { - background: linear-gradient( - 0deg, - transparent 0%, - rgba(6, 182, 212, 0.1) 50%, - transparent 100% - ); + background: linear-gradient(0deg, transparent 0%, rgba(6, 182, 212, 0.1) 50%, transparent 100%); background-size: 100% 3px; pointer-events: none; } @@ -133,19 +130,43 @@ } @keyframes glitch-1 { - 0%, 100% { clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%); transform: translate(0); } - 20% { clip-path: polygon(0 15%, 100% 15%, 100% 65%, 0 65%); } - 40% { clip-path: polygon(0 30%, 100% 30%, 100% 70%, 0 70%); } - 60% { clip-path: polygon(0 5%, 100% 5%, 100% 60%, 0 60%); } - 80% { clip-path: polygon(0 25%, 100% 25%, 100% 40%, 0 40%); } + 0%, + 100% { + clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%); + transform: translate(0); + } + 20% { + clip-path: polygon(0 15%, 100% 15%, 100% 65%, 0 65%); + } + 40% { + clip-path: polygon(0 30%, 100% 30%, 100% 70%, 0 70%); + } + 60% { + clip-path: polygon(0 5%, 100% 5%, 100% 60%, 0 60%); + } + 80% { + clip-path: polygon(0 25%, 100% 25%, 100% 40%, 0 40%); + } } @keyframes glitch-2 { - 0%, 100% { clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%); transform: translate(0); } - 20% { clip-path: polygon(0 70%, 100% 70%, 100% 95%, 0 95%); } - 40% { clip-path: polygon(0 40%, 100% 40%, 100% 85%, 0 85%); } - 60% { clip-path: polygon(0 60%, 100% 60%, 100% 100%, 0 100%); } - 80% { clip-path: polygon(0 50%, 100% 50%, 100% 90%, 0 90%); } + 0%, + 100% { + clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%); + transform: translate(0); + } + 20% { + clip-path: polygon(0 70%, 100% 70%, 100% 95%, 0 95%); + } + 40% { + clip-path: polygon(0 40%, 100% 40%, 100% 85%, 0 85%); + } + 60% { + clip-path: polygon(0 60%, 100% 60%, 100% 100%, 0 100%); + } + 80% { + clip-path: polygon(0 50%, 100% 50%, 100% 90%, 0 90%); + } } /* Grayscale filter with instant toggle */ @@ -160,7 +181,7 @@ /* Cyberpunk Glitch Effect for Button */ .glitch-btn { position: relative; - animation: glitch 300ms cubic-bezier(.25, .46, .45, .94); + animation: glitch 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } .glitch-layer { @@ -177,21 +198,22 @@ } .glitch-layer:first-of-type { - animation: glitch-1 300ms cubic-bezier(.25, .46, .45, .94); + animation: glitch-1 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94); color: rgb(6 182 212); /* cyan-500 */ transform: translate(-2px, 0); clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%); } .glitch-layer:last-of-type { - animation: glitch-2 300ms cubic-bezier(.25, .46, .45, .94); + animation: glitch-2 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94); color: rgb(16 185 129); /* emerald-500 */ transform: translate(2px, 0); clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%); } @keyframes glitch-1 { - 0%, 100% { + 0%, + 100% { transform: translate(0, 0); clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%); } @@ -210,7 +232,8 @@ } @keyframes glitch-2 { - 0%, 100% { + 0%, + 100% { transform: translate(0, 0); clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%); } @@ -260,7 +283,8 @@ /* SCP-style subtle flicker hover */ @keyframes scp-flicker { - 0%, 100% { + 0%, + 100% { border-color: rgb(71 85 105); box-shadow: 0 0 0 rgba(90, 139, 149, 0); transform: translate(0, 0); @@ -290,7 +314,9 @@ .cyber-glitch-hover:hover { animation: scp-flicker 150ms ease-in-out 3; border-color: var(--neon-cyan) !important; - box-shadow: 0 1px 4px rgba(90, 139, 149, 0.15), inset 0 0 8px rgba(90, 139, 149, 0.05); + box-shadow: + 0 1px 4px rgba(90, 139, 149, 0.15), + inset 0 0 8px rgba(90, 139, 149, 0.05); } /* Navbar hide on scroll */ @@ -322,5 +348,143 @@ 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/app/layout.tsx b/app/layout.tsx index bb1c3f8..fdef6eb 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -28,11 +28,7 @@ export const metadata: Metadata = { }, } -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( @@ -51,7 +47,9 @@ export default function RootLayout({

- © 2025 // BLOG & PORTOFOLIU // ALL RIGHTS RESERVED + © 2025 // BLOG &{' '} + PORTOFOLIU{' '} + // ALL RIGHTS RESERVED

diff --git a/app/page.tsx b/app/page.tsx index c28083c..46bfce0 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -22,19 +22,35 @@ export default async function HomePage() {
Logo - TERMINAL:// V2.0 + + TERMINAL:// V2.0 +
- [BLOG] - [ABOUT] + + [BLOG] + + + [ABOUT] +
-

DOCUMENT LEVEL-1 // CLASSIFIED

+

+ DOCUMENT LEVEL-1 // CLASSIFIED +

- BUILD. WRITE.
SHARE. + BUILD. WRITE. +
+ SHARE.

> Explorează idei despre dezvoltare, design și tehnologie_ @@ -42,10 +58,16 @@ export default async function HomePage() {

- + [EXPLOREAZĂ BLOG] - + [DESPRE MINE]
@@ -81,7 +103,9 @@ export default async function HomePage() { /> ) : (
- #{String(index + 1).padStart(2, '0')} + + #{String(index + 1).padStart(2, '0')} +
)}
diff --git a/components/blog/blog-card.tsx b/components/blog/blog-card.tsx index 9f34e44..aebba54 100644 --- a/components/blog/blog-card.tsx +++ b/components/blog/blog-card.tsx @@ -14,10 +14,11 @@ export function BlogCard({ post, variant }: BlogCardProps) { if (!hasImage || variant === 'text-only') { return ( -
-
+
+
- {post.frontmatter.category} // {formatDate(post.frontmatter.date)} + {post.frontmatter.category} //{' '} + {formatDate(post.frontmatter.date)}

@@ -27,8 +28,11 @@ export function BlogCard({ post, variant }: BlogCardProps) { {post.frontmatter.description}

- {post.frontmatter.tags.map((tag) => ( - + {post.frontmatter.tags.map(tag => ( + #{tag} ))} @@ -44,7 +48,7 @@ export function BlogCard({ post, variant }: BlogCardProps) { if (variant === 'image-side') { return ( -
+
-
+
{post.frontmatter.category} // {formatDate(post.frontmatter.date)} @@ -68,8 +72,11 @@ export function BlogCard({ post, variant }: BlogCardProps) { {post.frontmatter.description}

- {post.frontmatter.tags.map((tag) => ( - + {post.frontmatter.tags.map(tag => ( + #{tag} ))} @@ -86,7 +93,7 @@ export function BlogCard({ post, variant }: BlogCardProps) { return ( -
+
-
+
- {post.frontmatter.category} // {formatDate(post.frontmatter.date)} + {post.frontmatter.category} //{' '} + {formatDate(post.frontmatter.date)}

@@ -109,8 +117,11 @@ export function BlogCard({ post, variant }: BlogCardProps) { {post.frontmatter.description}

- {post.frontmatter.tags.map((tag) => ( - + {post.frontmatter.tags.map(tag => ( + #{tag} ))} diff --git a/components/blog/code-block.tsx b/components/blog/code-block.tsx new file mode 100644 index 0000000..dcc2a94 --- /dev/null +++ b/components/blog/code-block.tsx @@ -0,0 +1,57 @@ +'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..0dc1d40 100644 --- a/components/blog/markdown-renderer.tsx +++ b/components/blog/markdown-renderer.tsx @@ -1,14 +1,13 @@ -'use client'; +'use client' -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 ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import Image from 'next/image' +import Link from 'next/link' +import { CodeBlock } from './code-block' interface MarkdownRendererProps { - content: string; + content: string } export default function MarkdownRenderer({ content }: MarkdownRendererProps) { @@ -16,136 +15,74 @@ 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$/, '')} - - ) : ( - - {children} - - ); + const match = /language-(\w+)/.exec(className || '') + if (!inline && match) { + return + } + return {children} }, img: ({ src, alt }) => { - if (!src || typeof src !== 'string') return null; - const isExternal = src.startsWith('http://') || src.startsWith('https://'); + if (!src || typeof src !== 'string') return null + const isExternal = src.startsWith('http://') || src.startsWith('https://') if (isExternal) { - return ( - {alt - ); + return {alt } return ( -
    +
    {alt
    - ); + ) }, a: ({ href, children }) => { - if (!href) return <>{children}; - const isExternal = href.startsWith('http://') || href.startsWith('https://'); + if (!href) return <>{children} + const isExternal = href.startsWith('http://') || href.startsWith('https://') if (isExternal) { return ( - + {children} - ); + ) } - return ( - - {children} - - ); + 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/navbar.tsx b/components/blog/navbar.tsx index 13ed66f..9f77ee4 100644 --- a/components/blog/navbar.tsx +++ b/components/blog/navbar.tsx @@ -28,11 +28,17 @@ export function Navbar() { }, [lastScrollY]) return ( -