💂‍♂️ Added lint and prettier #4

Merged
raresj merged 3 commits from feat/lintprittier into master 2025-11-14 13:34:24 +00:00
43 changed files with 5717 additions and 423 deletions
Showing only changes of commit 820a2b88d5 - Show all commits

View File

@@ -15,6 +15,7 @@ Reference this skill when writing or reviewing code to ensure consistency with p
### Files and Directories ### Files and Directories
**Use kebab-case for all file names:** **Use kebab-case for all file names:**
``` ```
✅ user-profile.tsx ✅ user-profile.tsx
✅ blog-post-card.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?** **Why kebab-case?**
- Cross-platform compatibility (Windows vs Unix) - Cross-platform compatibility (Windows vs Unix)
- URL-friendly (file names often map to routes) - URL-friendly (file names often map to routes)
- Easier to parse and read - Easier to parse and read
- Industry standard for Next.js projects - Industry standard for Next.js projects
**Special Next.js Files:** **Special Next.js Files:**
``` ```
page.tsx # Route pages page.tsx # Route pages
layout.tsx # Layout components layout.tsx # Layout components
@@ -44,6 +47,7 @@ route.ts # API route handlers
### Component Names (Inside Files) ### Component Names (Inside Files)
**Use PascalCase for component names:** **Use PascalCase for component names:**
```typescript ```typescript
// File: user-profile.tsx // File: user-profile.tsx
export function UserProfile() { export function UserProfile() {
@@ -59,6 +63,7 @@ export default function BlogPostCard() {
### Variables, Functions, Props ### Variables, Functions, Props
**Use camelCase:** **Use camelCase:**
```typescript ```typescript
// Variables // Variables
const userSettings = {} const userSettings = {}
@@ -83,10 +88,11 @@ function useMarkdown() {}
### Constants ### Constants
**Use SCREAMING_SNAKE_CASE:** **Use SCREAMING_SNAKE_CASE:**
```typescript ```typescript
const API_BASE_URL = "https://api.example.com" const API_BASE_URL = 'https://api.example.com'
const MAX_RETRIES = 3 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) const parsed = bodySchema.safeParse(json)
if (!parsed.success) { if (!parsed.success) {
return NextResponse.json( return NextResponse.json({ error: 'Validation failed', details: parsed.error }, { status: 400 })
{ error: 'Validation failed', details: parsed.error },
{ status: 400 }
)
} }
// parsed.data is fully typed // parsed.data is fully typed
@@ -146,6 +149,7 @@ export async function POST(request: NextRequest) {
``` ```
**Key Points:** **Key Points:**
- Use `safeParse()` instead of `parse()` to avoid try/catch - Use `safeParse()` instead of `parse()` to avoid try/catch
- Return structured error responses - Return structured error responses
- Use appropriate HTTP status codes - Use appropriate HTTP status codes
@@ -154,6 +158,7 @@ export async function POST(request: NextRequest) {
### Error Handling ### Error Handling
**Return meaningful status codes:** **Return meaningful status codes:**
```typescript ```typescript
200 // Success 200 // Success
201 // Created 201 // Created
@@ -165,12 +170,13 @@ export async function POST(request: NextRequest) {
``` ```
**Structured error responses:** **Structured error responses:**
```typescript ```typescript
return NextResponse.json( return NextResponse.json(
{ {
error: 'Resource not found', error: 'Resource not found',
code: 'NOT_FOUND', code: 'NOT_FOUND',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}, },
{ status: 404 } { status: 404 }
) )
@@ -277,12 +283,12 @@ export function ThemeToggle() {
```javascript ```javascript
// tailwind.config.js // tailwind.config.js
module.exports = { module.exports = {
darkMode: 'class', // Required for next-themes darkMode: 'class', // Required for next-themes
theme: { theme: {
extend: { extend: {
colors: { colors: {
'dark-primary': '#18181b', 'dark-primary': '#18181b',
'accent': { accent: {
DEFAULT: '#164e63', DEFAULT: '#164e63',
hover: '#155e75', hover: '#155e75',
}, },
@@ -380,6 +386,7 @@ export function Card({ children, className = "" }) {
``` ```
**Standard breakpoints:** **Standard breakpoints:**
``` ```
sm: 640px // Small tablets sm: 640px // Small tablets
md: 768px // Tablets md: 768px // Tablets
@@ -459,18 +466,21 @@ content/ # Content files (outside app/)
### lib/ Organization ### lib/ Organization
**Modules in lib/:** **Modules in lib/:**
- Substantial business logic (markdown.ts, seo.ts) - Substantial business logic (markdown.ts, seo.ts)
- API clients and data fetching - API clients and data fetching
- Database connections - Database connections
- Authentication logic - Authentication logic
**Utils in lib/utils.ts:** **Utils in lib/utils.ts:**
- Pure helper functions - Pure helper functions
- Formatters (formatDate, formatCurrency) - Formatters (formatDate, formatCurrency)
- Validators (isEmail, isValidUrl) - Validators (isEmail, isValidUrl)
- String manipulations - String manipulations
**Types in lib/types/:** **Types in lib/types/:**
- Shared TypeScript interfaces - Shared TypeScript interfaces
- API response types - API response types
- Domain models - Domain models
@@ -479,6 +489,7 @@ content/ # Content files (outside app/)
### Component Organization ### Component Organization
**By domain/feature:** **By domain/feature:**
``` ```
components/ components/
├── blog/ # Blog-specific ├── blog/ # Blog-specific
@@ -495,6 +506,7 @@ components/
``` ```
**Not by type:** **Not by type:**
``` ```
❌ Don't organize like this: ❌ Don't organize like this:
components/ components/
@@ -507,6 +519,7 @@ components/
### Public Assets ### Public Assets
**Organize by feature:** **Organize by feature:**
``` ```
public/ public/
├── blog/ ├── blog/
@@ -519,6 +532,7 @@ public/
``` ```
**Naming conventions:** **Naming conventions:**
- Use descriptive names: `hero-background.jpg` not `img1.jpg` - Use descriptive names: `hero-background.jpg` not `img1.jpg`
- Use kebab-case: `user-avatar.png` - Use kebab-case: `user-avatar.png`
- Include dimensions for images: `logo-512x512.png` - Include dimensions for images: `logo-512x512.png`
@@ -530,9 +544,10 @@ public/
### Type Safety ### Type Safety
**Avoid `any`:** **Avoid `any`:**
```typescript ```typescript
// ❌ Bad // ❌ Bad
function processData(data: any) { } function processData(data: any) {}
// ✅ Good // ✅ Good
function processData(data: unknown) { function processData(data: unknown) {
@@ -546,7 +561,7 @@ interface PostData {
title: string title: string
content: string content: string
} }
function processData(data: PostData) { } function processData(data: PostData) {}
``` ```
### Infer Types from Zod ### Infer Types from Zod
@@ -665,6 +680,7 @@ export function InteractiveCard({ title }) {
``` ```
**When to use 'use client':** **When to use 'use client':**
- Using React hooks (useState, useEffect, etc.) - Using React hooks (useState, useEffect, etc.)
- Using event handlers (onClick, onChange, etc.) - Using event handlers (onClick, onChange, etc.)
- Using browser APIs (window, localStorage, etc.) - Using browser APIs (window, localStorage, etc.)
@@ -743,7 +759,7 @@ export async function generateStaticParams() {
```typescript ```typescript
// In frontmatter // In frontmatter
date: "2025-01-15" date: '2025-01-15'
// For display // For display
formatDate(post.frontmatter.date) // "15 ianuarie 2025" (Romanian) formatDate(post.frontmatter.date) // "15 ianuarie 2025" (Romanian)

9
.prettierignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules
.next
out
build
dist
.cache
package-lock.json
public
coverage

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"arrowParens": "avoid",
"trailingComma": "es5"
}

View File

@@ -74,15 +74,16 @@ lib/
``` ```
**Frontmatter Schema:** **Frontmatter Schema:**
```yaml ```yaml
--- ---
title: string # Required title: string # Required
description: string # Required description: string # Required
date: "YYYY-MM-DD" # Required, ISO format date: 'YYYY-MM-DD' # Required, ISO format
author: string # Required author: string # Required
tags: [string, string?, string?] # Max 3 tags tags: [string, string?, string?] # Max 3 tags
image?: string # Optional hero image image?: string # Optional hero image
draft?: boolean # Exclude from listings if true draft?: boolean # Exclude from listings if true
--- ---
``` ```
@@ -107,20 +108,25 @@ components/
### File Naming Conventions ### File Naming Conventions
**Files and Directories:** **Files and Directories:**
- Use **kebab-case** for all file names: `user-profile.tsx`, `blog-post.tsx` - 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` - Special Next.js files: `page.tsx`, `layout.tsx`, `not-found.tsx`, `loading.tsx`
**Component Names (inside files):** **Component Names (inside files):**
- Use **PascalCase**: `export function UserProfile()`, `export default BlogPost` - Use **PascalCase**: `export function UserProfile()`, `export default BlogPost`
**Variables, Functions, Props:** **Variables, Functions, Props:**
- Use **camelCase**: `const userSettings = {}`, `function handleSubmit() {}` - Use **camelCase**: `const userSettings = {}`, `function handleSubmit() {}`
- Hooks: `useTheme`, `useMarkdown` - Hooks: `useTheme`, `useMarkdown`
**Constants:** **Constants:**
- Use **SCREAMING_SNAKE_CASE**: `const API_BASE_URL = "..."` - Use **SCREAMING_SNAKE_CASE**: `const API_BASE_URL = "..."`
**Why kebab-case for files?** **Why kebab-case for files?**
- Cross-platform compatibility (Windows vs Unix) - Cross-platform compatibility (Windows vs Unix)
- URL-friendly (file names often map to routes) - URL-friendly (file names often map to routes)
- Easier to parse and read - Easier to parse and read
@@ -152,6 +158,7 @@ export default function RootLayout({ children }) {
``` ```
**Client Component for Toggle:** **Client Component for Toggle:**
```typescript ```typescript
// components/theme-toggle.tsx // components/theme-toggle.tsx
'use client' 'use client'
@@ -173,23 +180,25 @@ export function ThemeToggle() {
``` ```
**Tailwind Configuration:** **Tailwind Configuration:**
```javascript ```javascript
// tailwind.config.js // tailwind.config.js
module.exports = { module.exports = {
darkMode: 'class', // Use 'class' strategy for next-themes darkMode: 'class', // Use 'class' strategy for next-themes
theme: { theme: {
extend: { extend: {
colors: { colors: {
// Define custom colors for consistency // Define custom colors for consistency
'dark-primary': '#18181b', 'dark-primary': '#18181b',
'accent': { DEFAULT: '#164e63', hover: '#155e75' } accent: { DEFAULT: '#164e63', hover: '#155e75' },
} },
} },
} },
} }
``` ```
**CSS Variables Pattern:** **CSS Variables Pattern:**
```css ```css
/* globals.css */ /* globals.css */
:root { :root {
@@ -219,6 +228,7 @@ module.exports = {
### Next.js 16 Specific Patterns ### Next.js 16 Specific Patterns
**Async Server Components:** **Async Server Components:**
```typescript ```typescript
// app/blog/page.tsx // app/blog/page.tsx
export default async function BlogPage() { export default async function BlogPage() {
@@ -228,6 +238,7 @@ export default async function BlogPage() {
``` ```
**Static Generation with Dynamic Routes:** **Static Generation with Dynamic Routes:**
```typescript ```typescript
// app/blog/[...slug]/page.tsx // app/blog/[...slug]/page.tsx
export async function generateStaticParams() { export async function generateStaticParams() {
@@ -242,6 +253,7 @@ export async function generateMetadata({ params }) {
``` ```
**Parallel Routes for Layout Composition:** **Parallel Routes for Layout Composition:**
```typescript ```typescript
// app/layout.tsx // app/layout.tsx
export default function RootLayout({ export default function RootLayout({
@@ -278,12 +290,14 @@ export default function RootLayout({
### Styling Guidelines ### Styling Guidelines
**Color Palette:** **Color Palette:**
- Backgrounds: `zinc-900`, `slate-900`, `slate-800` - Backgrounds: `zinc-900`, `slate-900`, `slate-800`
- Accents: `cyan-900`, `emerald-900`, `teal-900` - Accents: `cyan-900`, `emerald-900`, `teal-900`
- Text: `slate-100`, `slate-300`, `slate-500` - Text: `slate-100`, `slate-300`, `slate-500`
- Borders: `border-2`, `border-4` (thick, sharp) - Borders: `border-2`, `border-4` (thick, sharp)
**Design Tokens:** **Design Tokens:**
- **NO rounded corners:** Use `rounded-none` or omit (default is sharp) - **NO rounded corners:** Use `rounded-none` or omit (default is sharp)
- **Monospace fonts:** Apply `font-mono` for terminal aesthetic - **Monospace fonts:** Apply `font-mono` for terminal aesthetic
- **Uppercase labels:** Use `uppercase tracking-wider` for headers - **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" - **Classification labels:** Add metadata like "FILE#001", "DOCUMENT LEVEL-1"
**Typography:** **Typography:**
- Primary font: `JetBrains Mono` (monospace) - Primary font: `JetBrains Mono` (monospace)
- Headings: `font-mono font-bold uppercase` - Headings: `font-mono font-bold uppercase`
- Body: `font-mono text-sm` - Body: `font-mono text-sm`

View File

@@ -1,4 +1,4 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
export default function AboutBreadcrumb() { export default function AboutBreadcrumb() {
return ( return (
@@ -11,5 +11,5 @@ export default function AboutBreadcrumb() {
}, },
]} ]}
/> />
); )
} }

View File

@@ -1,10 +1,10 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
import { getPostBySlug } from '@/lib/markdown'; import { getPostBySlug } from '@/lib/markdown'
interface BreadcrumbItem { interface BreadcrumbItem {
label: string; label: string
href: string; href: string
current?: boolean; current?: boolean
} }
function formatDirectoryName(name: string): string { function formatDirectoryName(name: string): string {
@@ -12,34 +12,34 @@ function formatDirectoryName(name: string): string {
tech: 'Tehnologie', tech: 'Tehnologie',
design: 'Design', design: 'Design',
tutorial: 'Tutoriale', 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({ export default async function BlogPostBreadcrumb({
params, params,
}: { }: {
params: Promise<{ slug: string[] }>; params: Promise<{ slug: string[] }>
}) { }) {
const { slug } = await params; const { slug } = await params
const slugPath = slug.join('/'); const slugPath = slug.join('/')
const post = getPostBySlug(slugPath); const post = getPostBySlug(slugPath)
const items: BreadcrumbItem[] = [ const items: BreadcrumbItem[] = [
{ {
label: 'Blog', label: 'Blog',
href: '/blog', href: '/blog',
}, },
]; ]
if (slug.length > 1) { if (slug.length > 1) {
for (let i = 0; i < slug.length - 1; i++) { 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({ items.push({
label: formatDirectoryName(slug[i]), label: formatDirectoryName(slug[i]),
href: `/blog/${segmentPath}`, href: `/blog/${segmentPath}`,
}); })
} }
} }
@@ -47,7 +47,7 @@ export default async function BlogPostBreadcrumb({
label: post ? post.frontmatter.title : slug[slug.length - 1], label: post ? post.frontmatter.title : slug[slug.length - 1],
href: `/blog/${slugPath}`, href: `/blog/${slugPath}`,
current: true, current: true,
}); })
return <Breadcrumbs items={items} />; return <Breadcrumbs items={items} />
} }

View File

@@ -1,4 +1,4 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
export default function BlogBreadcrumb() { export default function BlogBreadcrumb() {
return ( return (
@@ -11,5 +11,5 @@ export default function BlogBreadcrumb() {
}, },
]} ]}
/> />
); )
} }

View File

@@ -1,7 +1,7 @@
'use client'; 'use client'
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
export default function DefaultBreadcrumb() { export default function DefaultBreadcrumb() {
return <Breadcrumbs />; return <Breadcrumbs />
} }

View File

@@ -1,15 +1,11 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
export default async function TagBreadcrumb({ export default async function TagBreadcrumb({ params }: { params: Promise<{ tag: string }> }) {
params, const { tag } = await params
}: {
params: Promise<{ tag: string }>;
}) {
const { tag } = await params;
const tagName = tag const tagName = tag
.split('-') .split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) .map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '); .join(' ')
return ( return (
<Breadcrumbs <Breadcrumbs
@@ -25,5 +21,5 @@ export default async function TagBreadcrumb({
}, },
]} ]}
/> />
); )
} }

View File

@@ -1,4 +1,4 @@
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'; import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
export default function TagsBreadcrumb() { export default function TagsBreadcrumb() {
return ( return (
@@ -11,5 +11,5 @@ export default function TagsBreadcrumb() {
}, },
]} ]}
/> />
); )
} }

View File

@@ -12,8 +12,8 @@ export default function AboutPage() {
<div className="prose dark:prose-invert max-w-none"> <div className="prose dark:prose-invert max-w-none">
<p className="text-lg leading-relaxed mb-6"> <p className="text-lg leading-relaxed mb-6">
Bun venit pe blogul meu! Sunt un dezvoltator pasionat de tehnologie, Bun venit pe blogul meu! Sunt un dezvoltator pasionat de tehnologie, specializat în
specializat în dezvoltarea web modernă cu Next.js, React și TypeScript. dezvoltarea web modernă cu Next.js, React și TypeScript.
</p> </p>
<h2 className="text-2xl font-semibold mt-8 mb-4">Ce vei găsi aici</h2> <h2 className="text-2xl font-semibold mt-8 mb-4">Ce vei găsi aici</h2>
@@ -27,10 +27,18 @@ export default function AboutPage() {
<h2 className="text-2xl font-semibold mt-8 mb-4">Tehnologii folosite</h2> <h2 className="text-2xl font-semibold mt-8 mb-4">Tehnologii folosite</h2>
<p>Acest blog este construit cu:</p> <p>Acest blog este construit cu:</p>
<ul className="space-y-2"> <ul className="space-y-2">
<li><strong>Next.js 15</strong> - Framework React pentru producție</li> <li>
<li><strong>TypeScript</strong> - Pentru type safety</li> <strong>Next.js 15</strong> - Framework React pentru producție
<li><strong>Tailwind CSS</strong> - Pentru stilizare rapidă</li> </li>
<li><strong>Markdown</strong> - Pentru conținut</li> <li>
<strong>TypeScript</strong> - Pentru type safety
</li>
<li>
<strong>Tailwind CSS</strong> - Pentru stilizare rapidă
</li>
<li>
<strong>Markdown</strong> - Pentru conținut
</li>
</ul> </ul>
<h2 className="text-2xl font-semibold mt-8 mb-4">Contact</h2> <h2 className="text-2xl font-semibold mt-8 mb-4">Contact</h2>

View File

@@ -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. Ne pare rău, dar articolul pe care îl cauți nu există sau a fost mutat.
</p> </p>
<div className="space-x-4"> <div className="space-x-4">
<Link href="/blog" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition"> <Link
href="/blog"
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition"
>
Vezi toate articolele Vezi toate articolele
</Link> </Link>
<Link href="/" className="inline-block px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition"> <Link
href="/"
className="inline-block px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition"
>
Pagina principală Pagina principală
</Link> </Link>
</div> </div>

View File

@@ -10,10 +10,14 @@ import MarkdownRenderer from '@/components/blog/markdown-renderer'
export async function generateStaticParams() { export async function generateStaticParams() {
const posts = await getAllPosts() 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<Metadata> { export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string[] }>
}): Promise<Metadata> {
const { slug } = await params const { slug } = await params
const slugPath = slug.join('/') const slugPath = slug.join('/')
const post = getPostBySlug(slugPath) const post = getPostBySlug(slugPath)
@@ -51,7 +55,10 @@ function extractHeadings(content: string) {
while ((match = headingRegex.exec(content)) !== null) { while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length const level = match[1].length
const text = match[2] const text = match[2]
const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') const id = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
headings.push({ id, text, level }) headings.push({ id, text, level })
} }
@@ -110,7 +117,8 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
</h1> </h1>
<p className="text-lg text-[rgb(var(--text-secondary))] leading-relaxed mb-6 font-mono"> <p className="text-lg text-[rgb(var(--text-secondary))] leading-relaxed mb-6 font-mono">
<span className="text-[var(--neon-pink)]">&gt;&gt;</span> {post.frontmatter.description} <span className="text-[var(--neon-pink)]">&gt;&gt;</span>{' '}
{post.frontmatter.description}
</p> </p>
</div> </div>
@@ -121,9 +129,13 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
</span> </span>
</div> </div>
<div> <div>
<p className="font-mono font-bold text-[var(--neon-cyan)] uppercase text-sm">{post.frontmatter.author}</p> <p className="font-mono font-bold text-[var(--neon-cyan)] uppercase text-sm">
{post.frontmatter.author}
</p>
<div className="flex items-center gap-2 text-xs text-[rgb(var(--text-muted))] font-mono"> <div className="flex items-center gap-2 text-xs text-[rgb(var(--text-muted))] font-mono">
<time className="text-[var(--neon-magenta)]">{formatDate(post.frontmatter.date)}</time> <time className="text-[var(--neon-magenta)]">
{formatDate(post.frontmatter.date)}
</time>
<span className="text-[var(--neon-pink)]">//</span> <span className="text-[var(--neon-pink)]">//</span>
<span className="text-[var(--neon-cyan)]">{post.readingTime}min READ</span> <span className="text-[var(--neon-cyan)]">{post.readingTime}min READ</span>
</div> </div>
@@ -147,17 +159,25 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
{relatedPosts.length > 0 && ( {relatedPosts.length > 0 && (
<section className="mt-12 pt-8 border-t border-zinc-800"> <section className="mt-12 pt-8 border-t border-zinc-800">
<h2 className="text-2xl font-mono font-bold uppercase text-[var(--neon-cyan)] mb-6">// Articole similare</h2> <h2 className="text-2xl font-mono font-bold uppercase text-[var(--neon-cyan)] mb-6">
// Articole similare
</h2>
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-3">
{relatedPosts.map((relatedPost) => ( {relatedPosts.map(relatedPost => (
<Link <Link
key={relatedPost.slug} key={relatedPost.slug}
href={`/blog/${relatedPost.slug}`} href={`/blog/${relatedPost.slug}`}
className="block p-4 border border-zinc-800 bg-zinc-950 hover:border-[var(--neon-cyan)] transition-all hover:shadow-[0_0_8px_rgba(90,139,149,0.2)]" className="block p-4 border border-zinc-800 bg-zinc-950 hover:border-[var(--neon-cyan)] transition-all hover:shadow-[0_0_8px_rgba(90,139,149,0.2)]"
> >
<h3 className="font-mono font-semibold text-cyan-400 mb-2 line-clamp-2">{relatedPost.frontmatter.title}</h3> <h3 className="font-mono font-semibold text-cyan-400 mb-2 line-clamp-2">
<p className="text-sm text-zinc-400 line-clamp-2">{relatedPost.frontmatter.description}</p> {relatedPost.frontmatter.title}
<p className="text-xs text-zinc-600 mt-2 font-mono">{formatDate(relatedPost.frontmatter.date)}</p> </h3>
<p className="text-sm text-zinc-400 line-clamp-2">
{relatedPost.frontmatter.description}
</p>
<p className="text-xs text-zinc-600 mt-2 font-mono">
{formatDate(relatedPost.frontmatter.date)}
</p>
</Link> </Link>
))} ))}
</div> </div>
@@ -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)]" 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)]"
> >
<svg className="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg> </svg>
[BACK TO BLOG] [BACK TO BLOG]
</Link> </Link>

View File

@@ -23,15 +23,14 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
const postsPerPage = 9 const postsPerPage = 9
const filteredAndSortedPosts = useMemo(() => { const filteredAndSortedPosts = useMemo(() => {
let result = posts.filter((post) => { const result = posts.filter(post => {
const matchesSearch = const matchesSearch =
searchQuery === '' || searchQuery === '' ||
post.frontmatter.title.toLowerCase().includes(searchQuery.toLowerCase()) || post.frontmatter.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.frontmatter.description.toLowerCase().includes(searchQuery.toLowerCase()) post.frontmatter.description.toLowerCase().includes(searchQuery.toLowerCase())
const matchesTags = const matchesTags =
selectedTags.length === 0 || selectedTags.length === 0 || selectedTags.every(tag => post.frontmatter.tags.includes(tag))
selectedTags.every((tag) => post.frontmatter.tags.includes(tag))
return matchesSearch && matchesTags return matchesSearch && matchesTags
}) })
@@ -58,15 +57,12 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
) )
const toggleTag = (tag: string) => { const toggleTag = (tag: string) => {
setSelectedTags((prev) => setSelectedTags(prev => (prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]))
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
)
setCurrentPage(1) setCurrentPage(1)
} }
return ( return (
<div className="min-h-screen bg-[rgb(var(--bg-primary))]"> <div className="min-h-screen bg-[rgb(var(--bg-primary))]">
<div className="max-w-7xl mx-auto px-6 py-12"> <div className="max-w-7xl mx-auto px-6 py-12">
{/* Header */} {/* Header */}
<div className="border-l border-[var(--neon-cyan)] pl-6 mb-12"> <div className="border-l border-[var(--neon-cyan)] pl-6 mb-12">
@@ -83,15 +79,12 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
<div className="flex flex-col lg:flex-row gap-4"> <div className="flex flex-col lg:flex-row gap-4">
<SearchBar <SearchBar
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={(value) => { onSearchChange={value => {
setSearchQuery(value) setSearchQuery(value)
setCurrentPage(1) setCurrentPage(1)
}} }}
/> />
<SortDropdown <SortDropdown sortBy={sortBy} onSortChange={setSortBy} />
sortBy={sortBy}
onSortChange={setSortBy}
/>
</div> </div>
</div> </div>
@@ -109,7 +102,8 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
{/* Results Count */} {/* Results Count */}
<div className="mb-6"> <div className="mb-6">
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase"> <p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase">
FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'} FOUND {filteredAndSortedPosts.length}{' '}
{filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'}
</p> </p>
</div> </div>
@@ -142,14 +136,14 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
<div className="border border-[rgb(var(--border-primary))] bg-[rgb(var(--bg-secondary))] p-6"> <div className="border border-[rgb(var(--border-primary))] bg-[rgb(var(--bg-secondary))] p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<button <button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1} disabled={currentPage === 1}
className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer" className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer"
> >
&lt; PREV &lt; PREV
</button> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button <button
key={page} key={page}
onClick={() => setCurrentPage(page)} onClick={() => setCurrentPage(page)}
@@ -164,7 +158,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
))} ))}
</div> </div>
<button <button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer" className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer"
> >

View File

@@ -3,7 +3,7 @@ import BlogPageClient from './blog-client'
export default async function BlogPage() { export default async function BlogPage() {
const posts = await getAllPosts() 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 <BlogPageClient posts={posts} allTags={allTags} /> return <BlogPageClient posts={posts} allTags={allTags} />
} }

View File

@@ -1,4 +1,4 @@
@import "tailwindcss"; @import 'tailwindcss';
@theme { @theme {
--color-*: initial; --color-*: initial;
@@ -70,12 +70,7 @@
/* Scanline effect */ /* Scanline effect */
.scanline { .scanline {
background: linear-gradient( background: linear-gradient(0deg, transparent 0%, rgba(6, 182, 212, 0.1) 50%, transparent 100%);
0deg,
transparent 0%,
rgba(6, 182, 212, 0.1) 50%,
transparent 100%
);
background-size: 100% 3px; background-size: 100% 3px;
pointer-events: none; pointer-events: none;
} }
@@ -135,19 +130,43 @@
} }
@keyframes glitch-1 { @keyframes glitch-1 {
0%, 100% { clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%); transform: translate(0); } 0%,
20% { clip-path: polygon(0 15%, 100% 15%, 100% 65%, 0 65%); } 100% {
40% { clip-path: polygon(0 30%, 100% 30%, 100% 70%, 0 70%); } clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%);
60% { clip-path: polygon(0 5%, 100% 5%, 100% 60%, 0 60%); } transform: translate(0);
80% { clip-path: polygon(0 25%, 100% 25%, 100% 40%, 0 40%); } }
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 { @keyframes glitch-2 {
0%, 100% { clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%); transform: translate(0); } 0%,
20% { clip-path: polygon(0 70%, 100% 70%, 100% 95%, 0 95%); } 100% {
40% { clip-path: polygon(0 40%, 100% 40%, 100% 85%, 0 85%); } clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%);
60% { clip-path: polygon(0 60%, 100% 60%, 100% 100%, 0 100%); } transform: translate(0);
80% { clip-path: polygon(0 50%, 100% 50%, 100% 90%, 0 90%); } }
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 */ /* Grayscale filter with instant toggle */
@@ -162,7 +181,7 @@
/* Cyberpunk Glitch Effect for Button */ /* Cyberpunk Glitch Effect for Button */
.glitch-btn { .glitch-btn {
position: relative; 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 { .glitch-layer {
@@ -179,21 +198,22 @@
} }
.glitch-layer:first-of-type { .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 */ color: rgb(6 182 212); /* cyan-500 */
transform: translate(-2px, 0); transform: translate(-2px, 0);
clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%); clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%);
} }
.glitch-layer:last-of-type { .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 */ color: rgb(16 185 129); /* emerald-500 */
transform: translate(2px, 0); transform: translate(2px, 0);
clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%); clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%);
} }
@keyframes glitch-1 { @keyframes glitch-1 {
0%, 100% { 0%,
100% {
transform: translate(0, 0); transform: translate(0, 0);
clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%); clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%);
} }
@@ -212,7 +232,8 @@
} }
@keyframes glitch-2 { @keyframes glitch-2 {
0%, 100% { 0%,
100% {
transform: translate(0, 0); transform: translate(0, 0);
clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%); clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%);
} }
@@ -262,7 +283,8 @@
/* SCP-style subtle flicker hover */ /* SCP-style subtle flicker hover */
@keyframes scp-flicker { @keyframes scp-flicker {
0%, 100% { 0%,
100% {
border-color: rgb(71 85 105); border-color: rgb(71 85 105);
box-shadow: 0 0 0 rgba(90, 139, 149, 0); box-shadow: 0 0 0 rgba(90, 139, 149, 0);
transform: translate(0, 0); transform: translate(0, 0);
@@ -292,7 +314,9 @@
.cyber-glitch-hover:hover { .cyber-glitch-hover:hover {
animation: scp-flicker 150ms ease-in-out 3; animation: scp-flicker 150ms ease-in-out 3;
border-color: var(--neon-cyan) !important; 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 */ /* Navbar hide on scroll */
@@ -405,7 +429,9 @@
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
position: relative; position: relative;
box-shadow: -4px 0 15px rgba(155,90,142,0.3), inset 0 0 20px rgba(155,90,142,0.05); 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 { .cyberpunk-prose blockquote::before {
@@ -426,8 +452,8 @@
font-size: 0.875rem; font-size: 0.875rem;
font-family: monospace; font-family: monospace;
border: 2px solid var(--neon-cyan); border: 2px solid var(--neon-cyan);
box-shadow: 0 0 8px rgba(90,139,149,0.3); box-shadow: 0 0 8px rgba(90, 139, 149, 0.3);
text-shadow: 0 0 6px rgba(90,139,149,0.6); text-shadow: 0 0 6px rgba(90, 139, 149, 0.6);
} }
.cyberpunk-prose pre { .cyberpunk-prose pre {
@@ -437,7 +463,9 @@
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
overflow-x: auto; overflow-x: auto;
box-shadow: 0 0 25px rgba(123,101,147,0.6), inset 0 0 25px rgba(123,101,147,0.1); 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 { .cyberpunk-prose pre code {
@@ -450,7 +478,7 @@
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
border: 4px solid var(--neon-pink); border: 4px solid var(--neon-pink);
box-shadow: 0 0 20px rgba(155,90,110,0.5); box-shadow: 0 0 20px rgba(155, 90, 110, 0.5);
} }
.cyberpunk-prose hr { .cyberpunk-prose hr {
@@ -460,4 +488,3 @@
margin-bottom: 3rem; margin-bottom: 3rem;
} }
} }

View File

@@ -28,11 +28,7 @@ export const metadata: Metadata = {
}, },
} }
export default function RootLayout({ export default function RootLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode
}) {
return ( return (
<html lang="ro" suppressHydrationWarning className={jetbrainsMono.variable}> <html lang="ro" suppressHydrationWarning className={jetbrainsMono.variable}>
<body className="font-mono bg-zinc-50 text-slate-900 dark:bg-zinc-900 dark:text-slate-100 transition-colors duration-300"> <body className="font-mono bg-zinc-50 text-slate-900 dark:bg-zinc-900 dark:text-slate-100 transition-colors duration-300">
@@ -51,7 +47,9 @@ export default function RootLayout({
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<div className="border-2 border-slate-300 dark:border-slate-800 p-6"> <div className="border-2 border-slate-300 dark:border-slate-800 p-6">
<p className="text-center text-slate-500 dark:text-slate-500 font-mono text-xs uppercase tracking-wider"> <p className="text-center text-slate-500 dark:text-slate-500 font-mono text-xs uppercase tracking-wider">
© 2025 <span style={{ color: 'var(--neon-cyan)' }}>//</span> BLOG & <span style={{ color: 'var(--neon-pink)' }}>PORTOFOLIU</span> <span style={{ color: 'var(--neon-cyan)' }}>//</span> ALL RIGHTS RESERVED © 2025 <span style={{ color: 'var(--neon-cyan)' }}>//</span> BLOG &{' '}
<span style={{ color: 'var(--neon-pink)' }}>PORTOFOLIU</span>{' '}
<span style={{ color: 'var(--neon-cyan)' }}>//</span> ALL RIGHTS RESERVED
</p> </p>
</div> </div>
</div> </div>

View File

@@ -22,19 +22,35 @@ export default async function HomePage() {
<div className="mb-8 flex items-center justify-between border-b-2 border-slate-300 dark:border-slate-800 pb-4"> <div className="mb-8 flex items-center justify-between border-b-2 border-slate-300 dark:border-slate-800 pb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" /> <Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" />
<span className="font-mono text-xs text-slate-500 uppercase tracking-widest">TERMINAL:// V2.0</span> <span className="font-mono text-xs text-slate-500 uppercase tracking-widest">
TERMINAL:// V2.0
</span>
</div> </div>
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<Link href="/blog" className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400">[BLOG]</Link> <Link
<Link href="/about" className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400">[ABOUT]</Link> href="/blog"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[BLOG]
</Link>
<Link
href="/about"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[ABOUT]
</Link>
<ThemeToggle /> <ThemeToggle />
</div> </div>
</div> </div>
<div className="border-l-4 border-cyan-700 dark:border-cyan-900 pl-6 mb-8"> <div className="border-l-4 border-cyan-700 dark:border-cyan-900 pl-6 mb-8">
<p className="font-mono text-xs text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-2">DOCUMENT LEVEL-1 // CLASSIFIED</p> <p className="font-mono text-xs text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-2">
DOCUMENT LEVEL-1 // CLASSIFIED
</p>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-mono font-bold text-slate-900 dark:text-slate-100 uppercase tracking-tight mb-6"> <h1 className="text-4xl md:text-6xl lg:text-7xl font-mono font-bold text-slate-900 dark:text-slate-100 uppercase tracking-tight mb-6">
BUILD. WRITE.<br/>SHARE. BUILD. WRITE.
<br />
SHARE.
</h1> </h1>
<p className="text-base md:text-lg lg:text-xl text-slate-700 dark:text-slate-400 font-mono leading-relaxed max-w-2xl"> <p className="text-base md:text-lg lg:text-xl text-slate-700 dark:text-slate-400 font-mono leading-relaxed max-w-2xl">
&gt; Explorează idei despre dezvoltare, design și tehnologie_ &gt; Explorează idei despre dezvoltare, design și tehnologie_
@@ -42,10 +58,16 @@ export default async function HomePage() {
</div> </div>
<div className="flex gap-4 flex-wrap mt-12"> <div className="flex gap-4 flex-wrap mt-12">
<Link href="/blog" className="px-6 md:px-8 py-3 md:py-4 bg-cyan-700 dark:bg-cyan-900 text-white dark:text-slate-100 border-2 border-cyan-600 dark:border-cyan-700 font-mono font-bold uppercase text-xs md:text-sm tracking-wider hover:bg-cyan-600 dark:hover:bg-cyan-800 hover:border-cyan-500 dark:hover:border-cyan-600 rounded-none transition-colors duration-200"> <Link
href="/blog"
className="px-6 md:px-8 py-3 md:py-4 bg-cyan-700 dark:bg-cyan-900 text-white dark:text-slate-100 border-2 border-cyan-600 dark:border-cyan-700 font-mono font-bold uppercase text-xs md:text-sm tracking-wider hover:bg-cyan-600 dark:hover:bg-cyan-800 hover:border-cyan-500 dark:hover:border-cyan-600 rounded-none transition-colors duration-200"
>
[EXPLOREAZĂ BLOG] [EXPLOREAZĂ BLOG]
</Link> </Link>
<Link href="/about" className="px-6 md:px-8 py-3 md:py-4 bg-transparent text-slate-700 dark:text-slate-300 border-2 border-slate-400 dark:border-slate-700 font-mono font-bold uppercase text-xs md:text-sm tracking-wider hover:bg-slate-200 dark:hover:bg-slate-800 hover:border-slate-500 dark:hover:border-slate-600 rounded-none transition-colors duration-200"> <Link
href="/about"
className="px-6 md:px-8 py-3 md:py-4 bg-transparent text-slate-700 dark:text-slate-300 border-2 border-slate-400 dark:border-slate-700 font-mono font-bold uppercase text-xs md:text-sm tracking-wider hover:bg-slate-200 dark:hover:bg-slate-800 hover:border-slate-500 dark:hover:border-slate-600 rounded-none transition-colors duration-200"
>
[DESPRE MINE] [DESPRE MINE]
</Link> </Link>
</div> </div>
@@ -81,7 +103,9 @@ export default async function HomePage() {
/> />
) : ( ) : (
<div className="w-full h-full bg-zinc-300 dark:bg-zinc-800 flex items-center justify-center"> <div className="w-full h-full bg-zinc-300 dark:bg-zinc-800 flex items-center justify-center">
<span className="font-mono text-6xl text-slate-400 dark:text-slate-700">#{String(index + 1).padStart(2, '0')}</span> <span className="font-mono text-6xl text-slate-400 dark:text-slate-700">
#{String(index + 1).padStart(2, '0')}
</span>
</div> </div>
)} )}
<div className="absolute inset-0 bg-zinc-100/60 dark:bg-zinc-900/60"></div> <div className="absolute inset-0 bg-zinc-100/60 dark:bg-zinc-900/60"></div>

View File

@@ -17,7 +17,8 @@ export function BlogCard({ post, variant }: BlogCardProps) {
<article className="border border-slate-700 bg-slate-900 p-6 h-full cyber-glitch-hover"> <article className="border border-slate-700 bg-slate-900 p-6 h-full cyber-glitch-hover">
<div className="border-l-2 pl-4 mb-4" style={{ borderColor: 'var(--neon-pink)' }}> <div className="border-l-2 pl-4 mb-4" style={{ borderColor: 'var(--neon-pink)' }}>
<span className="font-mono text-xs text-zinc-100 uppercase tracking-wider"> <span className="font-mono text-xs text-zinc-100 uppercase tracking-wider">
{post.frontmatter.category} <span style={{ color: 'var(--neon-cyan)' }}>//</span> {formatDate(post.frontmatter.date)} {post.frontmatter.category} <span style={{ color: 'var(--neon-cyan)' }}>//</span>{' '}
{formatDate(post.frontmatter.date)}
</span> </span>
</div> </div>
<h3 className="font-mono text-xl font-bold text-zinc-100 uppercase mb-3"> <h3 className="font-mono text-xl font-bold text-zinc-100 uppercase mb-3">
@@ -27,8 +28,11 @@ export function BlogCard({ post, variant }: BlogCardProps) {
{post.frontmatter.description} {post.frontmatter.description}
</p> </p>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{post.frontmatter.tags.map((tag) => ( {post.frontmatter.tags.map(tag => (
<span key={tag} className="px-3 py-1 bg-zinc-800 border border-slate-700 text-cyan-400 font-mono text-xs uppercase"> <span
key={tag}
className="px-3 py-1 bg-zinc-800 border border-slate-700 text-cyan-400 font-mono text-xs uppercase"
>
#{tag} #{tag}
</span> </span>
))} ))}
@@ -68,8 +72,11 @@ export function BlogCard({ post, variant }: BlogCardProps) {
{post.frontmatter.description} {post.frontmatter.description}
</p> </p>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{post.frontmatter.tags.map((tag) => ( {post.frontmatter.tags.map(tag => (
<span key={tag} className="px-3 py-1 bg-zinc-800 border border-slate-700 text-cyan-400 font-mono text-xs uppercase"> <span
key={tag}
className="px-3 py-1 bg-zinc-800 border border-slate-700 text-cyan-400 font-mono text-xs uppercase"
>
#{tag} #{tag}
</span> </span>
))} ))}
@@ -99,7 +106,8 @@ export function BlogCard({ post, variant }: BlogCardProps) {
<div className="p-6"> <div className="p-6">
<div className="border-l-2 pl-4 mb-4" style={{ borderColor: 'var(--neon-pink)' }}> <div className="border-l-2 pl-4 mb-4" style={{ borderColor: 'var(--neon-pink)' }}>
<span className="font-mono text-xs text-zinc-100 uppercase tracking-wider"> <span className="font-mono text-xs text-zinc-100 uppercase tracking-wider">
{post.frontmatter.category} <span style={{ color: 'var(--neon-cyan)' }}>//</span> {formatDate(post.frontmatter.date)} {post.frontmatter.category} <span style={{ color: 'var(--neon-cyan)' }}>//</span>{' '}
{formatDate(post.frontmatter.date)}
</span> </span>
</div> </div>
<h3 className="font-mono text-xl font-bold text-zinc-100 uppercase mb-3"> <h3 className="font-mono text-xl font-bold text-zinc-100 uppercase mb-3">
@@ -109,8 +117,11 @@ export function BlogCard({ post, variant }: BlogCardProps) {
{post.frontmatter.description} {post.frontmatter.description}
</p> </p>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{post.frontmatter.tags.map((tag) => ( {post.frontmatter.tags.map(tag => (
<span key={tag} className="px-3 py-1 bg-zinc-800 border border-slate-700 text-cyan-400 font-mono text-xs uppercase"> <span
key={tag}
className="px-3 py-1 bg-zinc-800 border border-slate-700 text-cyan-400 font-mono text-xs uppercase"
>
#{tag} #{tag}
</span> </span>
))} ))}

View File

@@ -23,7 +23,9 @@ export function CodeBlock({ code, language, filename, showLineNumbers = true }:
<div className="flex items-center justify-between px-4 py-2 bg-[rgb(var(--bg-secondary))] dark:bg-zinc-950 border-b-2 border-[var(--neon-purple)] relative"> <div className="flex items-center justify-between px-4 py-2 bg-[rgb(var(--bg-secondary))] dark:bg-zinc-950 border-b-2 border-[var(--neon-purple)] relative">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{filename && ( {filename && (
<span className="text-[var(--neon-cyan)] font-mono text-sm uppercase">&gt;&gt; {filename}</span> <span className="text-[var(--neon-cyan)] font-mono text-sm uppercase">
&gt;&gt; {filename}
</span>
)} )}
<span className="px-2 py-1 border border-[var(--neon-purple)] text-[var(--neon-purple)] text-xs font-mono uppercase"> <span className="px-2 py-1 border border-[var(--neon-purple)] text-[var(--neon-purple)] text-xs font-mono uppercase">
[{language}] [{language}]

View File

@@ -1,13 +1,13 @@
'use client'; 'use client'
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm'
import Image from 'next/image'; import Image from 'next/image'
import Link from 'next/link'; import Link from 'next/link'
import { CodeBlock } from './code-block'; import { CodeBlock } from './code-block'
interface MarkdownRendererProps { interface MarkdownRendererProps {
content: string; content: string
} }
export default function MarkdownRenderer({ content }: MarkdownRendererProps) { export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
@@ -16,48 +16,42 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{
h1: ({ children }) => { h1: ({ children }) => {
const text = String(children); const text = String(children)
const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); const id = text
return <h1 id={id}>{children}</h1>; .toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
return <h1 id={id}>{children}</h1>
}, },
h2: ({ children }) => { h2: ({ children }) => {
const text = String(children); const text = String(children)
const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); const id = text
return <h2 id={id}>{children}</h2>; .toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
return <h2 id={id}>{children}</h2>
}, },
h3: ({ children }) => { h3: ({ children }) => {
const text = String(children); const text = String(children)
const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); const id = text
return <h3 id={id}>{children}</h3>; .toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
return <h3 id={id}>{children}</h3>
}, },
code: ({ inline, className, children, ...props }: any) => { code: ({ inline, className, children, ...props }: any) => {
const match = /language-(\w+)/.exec(className || ''); const match = /language-(\w+)/.exec(className || '')
if (!inline && match) { if (!inline && match) {
return ( return <CodeBlock code={String(children).replace(/\n$/, '')} language={match[1]} />
<CodeBlock
code={String(children).replace(/\n$/, '')}
language={match[1]}
/>
);
} }
return ( return <code {...props}>{children}</code>
<code {...props}>
{children}
</code>
);
}, },
img: ({ src, alt }) => { img: ({ src, alt }) => {
if (!src || typeof src !== 'string') return null; if (!src || typeof src !== 'string') return null
const isExternal = src.startsWith('http://') || src.startsWith('https://'); const isExternal = src.startsWith('http://') || src.startsWith('https://')
if (isExternal) { if (isExternal) {
return ( return <img src={src} alt={alt || ''} className="w-full h-auto" />
<img
src={src}
alt={alt || ''}
className="w-full h-auto"
/>
);
} }
return ( return (
@@ -70,33 +64,25 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
style={{ width: '100%', height: 'auto' }} style={{ width: '100%', height: 'auto' }}
/> />
</div> </div>
); )
}, },
a: ({ href, children }) => { a: ({ href, children }) => {
if (!href) return <>{children}</>; if (!href) return <>{children}</>
const isExternal = href.startsWith('http://') || href.startsWith('https://'); const isExternal = href.startsWith('http://') || href.startsWith('https://')
if (isExternal) { if (isExternal) {
return ( return (
<a <a href={href} target="_blank" rel="noopener noreferrer">
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children} {children}
</a> </a>
); )
} }
return ( return <Link href={href}>{children}</Link>
<Link href={href}>
{children}
</Link>
);
}, },
}} }}
> >
{content} {content}
</ReactMarkdown> </ReactMarkdown>
); )
} }

View File

@@ -28,11 +28,17 @@ export function Navbar() {
}, [lastScrollY]) }, [lastScrollY])
return ( return (
<nav className={`border-b-4 border-slate-700 bg-slate-900 dark:bg-zinc-950 sticky top-0 z-50 ${isVisible ? 'navbar-visible' : 'navbar-hidden'}`}> <nav
className={`border-b-4 border-slate-700 bg-slate-900 dark:bg-zinc-950 sticky top-0 z-50 ${isVisible ? 'navbar-visible' : 'navbar-hidden'}`}
>
<div className="max-w-7xl mx-auto px-6 py-4"> <div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
<Link href="/" className="font-mono text-sm uppercase tracking-wider transition-colors cursor-pointer" style={{ color: 'var(--neon-cyan)' }}> <Link
href="/"
className="font-mono text-sm uppercase tracking-wider transition-colors cursor-pointer"
style={{ color: 'var(--neon-cyan)' }}
>
&lt; HOME &lt; HOME
</Link> </Link>
<span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider"> <span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider">
@@ -40,7 +46,10 @@ export function Navbar() {
</span> </span>
</div> </div>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<Link href="/about" className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"> <Link
href="/about"
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
>
[ABOUT] [ABOUT]
</Link> </Link>
<ThemeToggle /> <ThemeToggle />

View File

@@ -25,15 +25,13 @@ export function ReadingProgress() {
className="h-full bg-gradient-to-r from-[var(--neon-cyan)] via-[var(--neon-magenta)] to-[var(--neon-pink)] transition-all duration-150" className="h-full bg-gradient-to-r from-[var(--neon-cyan)] via-[var(--neon-magenta)] to-[var(--neon-pink)] transition-all duration-150"
style={{ style={{
width: `${progress}%`, width: `${progress}%`,
boxShadow: progress > 0 ? '0 0 8px var(--neon-cyan)' : 'none' boxShadow: progress > 0 ? '0 0 8px var(--neon-cyan)' : 'none',
}} }}
/> />
</div> </div>
<div className="fixed top-4 right-4 z-50 px-3 py-1.5 bg-[rgb(var(--bg-primary))] border-2 border-[var(--neon-cyan)] text-xs font-mono font-bold text-[var(--neon-cyan)] relative"> <div className="fixed top-4 right-4 z-50 px-3 py-1.5 bg-[rgb(var(--bg-primary))] border-2 border-[var(--neon-cyan)] text-xs font-mono font-bold text-[var(--neon-cyan)] relative">
<span className="relative z-10"> <span className="relative z-10">[{Math.round(progress)}%]</span>
[{Math.round(progress)}%]
</span>
</div> </div>
</> </>
) )

View File

@@ -6,12 +6,14 @@ interface SearchBarProps {
export function SearchBar({ searchQuery, onSearchChange }: SearchBarProps) { export function SearchBar({ searchQuery, onSearchChange }: SearchBarProps) {
return ( return (
<div className="flex-1 flex items-center border border-slate-700 bg-zinc-900 transition-all focus-within:border-[var(--neon-cyan)] focus-within:shadow-[0_0_6px_rgba(0,255,255,0.4),inset_0_0_6px_rgba(0,255,255,0.05)]"> <div className="flex-1 flex items-center border border-slate-700 bg-zinc-900 transition-all focus-within:border-[var(--neon-cyan)] focus-within:shadow-[0_0_6px_rgba(0,255,255,0.4),inset_0_0_6px_rgba(0,255,255,0.05)]">
<span className="pl-4 pr-2 font-mono text-lg" style={{ color: 'var(--neon-cyan)' }}>&gt;</span> <span className="pl-4 pr-2 font-mono text-lg" style={{ color: 'var(--neon-cyan)' }}>
&gt;
</span>
<input <input
type="text" type="text"
placeholder="SEARCH POSTS..." placeholder="SEARCH POSTS..."
value={searchQuery} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} onChange={e => onSearchChange(e.target.value)}
className="flex-1 bg-transparent font-mono text-zinc-100 dark:text-zinc-100 px-2 py-3 focus:outline-none placeholder:text-zinc-600 uppercase text-sm" className="flex-1 bg-transparent font-mono text-zinc-100 dark:text-zinc-100 px-2 py-3 focus:outline-none placeholder:text-zinc-600 uppercase text-sm"
/> />
</div> </div>

View File

@@ -9,12 +9,30 @@ export function SortDropdown({ sortBy, onSortChange }: SortDropdownProps) {
return ( return (
<select <select
value={sortBy} value={sortBy}
onChange={(e) => onSortChange(e.target.value as SortOption)} onChange={e => onSortChange(e.target.value as SortOption)}
className="border-2 border-slate-700 bg-zinc-900 dark:bg-zinc-900 text-zinc-100 dark:text-zinc-100 font-mono px-6 py-3 uppercase text-sm cyber-focus-pink transition-all hover:border-cyan-400 cursor-pointer" className="border-2 border-slate-700 bg-zinc-900 dark:bg-zinc-900 text-zinc-100 dark:text-zinc-100 font-mono px-6 py-3 uppercase text-sm cyber-focus-pink transition-all hover:border-cyan-400 cursor-pointer"
> >
<option value="newest" className="bg-zinc-900 text-zinc-100" style={{ backgroundColor: '#18181b', color: '#f4f4f5' }}>NEWEST FIRST</option> <option
<option value="oldest" className="bg-zinc-900 text-zinc-100" style={{ backgroundColor: '#18181b', color: '#f4f4f5' }}>OLDEST FIRST</option> value="newest"
<option value="title" className="bg-zinc-900 text-zinc-100" style={{ backgroundColor: '#18181b', color: '#f4f4f5' }}>BY TITLE</option> className="bg-zinc-900 text-zinc-100"
style={{ backgroundColor: '#18181b', color: '#f4f4f5' }}
>
NEWEST FIRST
</option>
<option
value="oldest"
className="bg-zinc-900 text-zinc-100"
style={{ backgroundColor: '#18181b', color: '#f4f4f5' }}
>
OLDEST FIRST
</option>
<option
value="title"
className="bg-zinc-900 text-zinc-100"
style={{ backgroundColor: '#18181b', color: '#f4f4f5' }}
>
BY TITLE
</option>
</select> </select>
) )
} }

View File

@@ -48,7 +48,9 @@ export function StickyFooter({ url, title }: StickyFooterProps) {
${isVisible ? 'translate-y-0' : 'translate-y-full'} ${isVisible ? 'translate-y-0' : 'translate-y-full'}
`} `}
style={{ style={{
boxShadow: isVisible ? '0 -8px 30px rgba(155,90,142,0.5), inset 0 4px 20px rgba(155,90,142,0.1)' : 'none' boxShadow: isVisible
? '0 -8px 30px rgba(155,90,142,0.5), inset 0 4px 20px rgba(155,90,142,0.1)'
: 'none',
}} }}
> >
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[var(--neon-magenta)] to-transparent opacity-70" /> <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[var(--neon-magenta)] to-transparent opacity-70" />
@@ -60,7 +62,10 @@ export function StickyFooter({ url, title }: StickyFooterProps) {
<div className="w-2 h-2 bg-[var(--neon-cyan)] shadow-[0_0_6px_rgba(90,139,149,1)]" /> <div className="w-2 h-2 bg-[var(--neon-cyan)] shadow-[0_0_6px_rgba(90,139,149,1)]" />
<div className="w-2 h-2 bg-[var(--neon-pink)] shadow-[0_0_6px_rgba(155,90,110,1)]" /> <div className="w-2 h-2 bg-[var(--neon-pink)] shadow-[0_0_6px_rgba(155,90,110,1)]" />
</div> </div>
<span className="text-[var(--neon-cyan)] font-mono text-xs uppercase tracking-wider" style={{ textShadow: '0 0 8px rgba(90,139,149,0.6)' }}> <span
className="text-[var(--neon-cyan)] font-mono text-xs uppercase tracking-wider"
style={{ textShadow: '0 0 8px rgba(90,139,149,0.6)' }}
>
&gt;&gt; SHARE: &gt;&gt; SHARE:
</span> </span>
</div> </div>
@@ -89,7 +94,9 @@ export function StickyFooter({ url, title }: StickyFooterProps) {
<button <button
onClick={handleCopyLink} onClick={handleCopyLink}
className="flex items-center gap-2 px-4 py-2 bg-black border-4 border-[var(--neon-pink)] text-[var(--neon-pink)] font-mono text-xs uppercase tracking-wider transition-all hover:shadow-[0_0_25px_rgba(155,90,110,0.8)] hover:bg-pink-900/20" className="flex items-center gap-2 px-4 py-2 bg-black border-4 border-[var(--neon-pink)] text-[var(--neon-pink)] font-mono text-xs uppercase tracking-wider transition-all hover:shadow-[0_0_25px_rgba(155,90,110,0.8)] hover:bg-pink-900/20"
style={{ textShadow: copied ? '0 0 10px rgba(155,90,110,1)' : '0 0 8px rgba(155,90,110,0.6)' }} style={{
textShadow: copied ? '0 0 10px rgba(155,90,110,1)' : '0 0 8px rgba(155,90,110,0.6)',
}}
> >
{copied ? '[✓ COPIED]' : '[COPY]'} {copied ? '[✓ COPIED]' : '[COPY]'}
</button> </button>

View File

@@ -17,8 +17,8 @@ export function TableOfContents({ headings }: TOCProps) {
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { entries => {
entries.forEach((entry) => { entries.forEach(entry => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
setActiveId(entry.target.id) setActiveId(entry.target.id)
} }
@@ -43,31 +43,45 @@ export function TableOfContents({ headings }: TOCProps) {
<div className="border-b border-[var(--neon-magenta)] pb-3 mb-4 relative"> <div className="border-b border-[var(--neon-magenta)] pb-3 mb-4 relative">
<div className="flex gap-1.5 mb-2 justify-end"> <div className="flex gap-1.5 mb-2 justify-end">
<div className="w-3 h-3 border border-[var(--neon-cyan)]/40 hover:bg-[var(--neon-cyan)]/10 transition-colors cursor-pointer" title="Minimize" /> <div
<div className="w-3 h-3 border border-[var(--neon-cyan)]/40 hover:bg-[var(--neon-cyan)]/10 transition-colors cursor-pointer" title="Maximize" /> className="w-3 h-3 border border-[var(--neon-cyan)]/40 hover:bg-[var(--neon-cyan)]/10 transition-colors cursor-pointer"
<div className="w-3 h-3 border border-[var(--neon-pink)]/40 hover:bg-[var(--neon-pink)]/10 transition-colors cursor-pointer" title="Close" /> title="Minimize"
/>
<div
className="w-3 h-3 border border-[var(--neon-cyan)]/40 hover:bg-[var(--neon-cyan)]/10 transition-colors cursor-pointer"
title="Maximize"
/>
<div
className="w-3 h-3 border border-[var(--neon-pink)]/40 hover:bg-[var(--neon-pink)]/10 transition-colors cursor-pointer"
title="Close"
/>
</div> </div>
<h3 className="text-xs font-mono font-bold text-[var(--neon-cyan)] uppercase tracking-wider" style={{ textShadow: '0 0 6px rgba(90,139,149,0.5)' }}> <h3
className="text-xs font-mono font-bold text-[var(--neon-cyan)] uppercase tracking-wider"
style={{ textShadow: '0 0 6px rgba(90,139,149,0.5)' }}
>
&gt;&gt; NAVIGATION &gt;&gt; NAVIGATION
</h3> </h3>
</div> </div>
<nav className="space-y-1 relative"> <nav className="space-y-1 relative">
{headings.map((heading) => ( {headings.map(heading => (
<a <a
key={heading.id} key={heading.id}
href={`#${heading.id}`} href={`#${heading.id}`}
className={` className={`
block text-sm font-mono py-2 border-l-2 transition-all duration-150 block text-sm font-mono py-2 border-l-2 transition-all duration-150
${heading.level === 2 ? 'pl-3' : 'pl-6'} ${heading.level === 2 ? 'pl-3' : 'pl-6'}
${activeId === heading.id ${
? 'text-[var(--neon-cyan)] border-[var(--neon-cyan)] bg-cyan-500/5 shadow-[0_0_8px_rgba(90,139,149,0.3)]' activeId === heading.id
: 'text-zinc-500 border-zinc-900 hover:border-[var(--neon-magenta)] hover:text-[var(--neon-magenta)] hover:bg-magenta-500/3 hover:shadow-[0_0_4px_rgba(155,90,142,0.2)]' ? 'text-[var(--neon-cyan)] border-[var(--neon-cyan)] bg-cyan-500/5 shadow-[0_0_8px_rgba(90,139,149,0.3)]'
: 'text-zinc-500 border-zinc-900 hover:border-[var(--neon-magenta)] hover:text-[var(--neon-magenta)] hover:bg-magenta-500/3 hover:shadow-[0_0_4px_rgba(155,90,142,0.2)]'
} }
`} `}
style={activeId === heading.id ? { textShadow: '0 0 4px rgba(90,139,149,0.5)' } : {}} style={activeId === heading.id ? { textShadow: '0 0 4px rgba(90,139,149,0.5)' } : {}}
> >
<span className="inline-block">{activeId === heading.id ? '▶ ' : '◆ '}</span>{heading.text} <span className="inline-block">{activeId === heading.id ? '▶ ' : '◆ '}</span>
{heading.text}
</a> </a>
))} ))}
</nav> </nav>

View File

@@ -14,7 +14,7 @@ export function TagFilter({ allTags, selectedTags, onToggleTag, onClearTags }: T
FILTER BY TAG FILTER BY TAG
</p> </p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{allTags.map((tag) => ( {allTags.map(tag => (
<button <button
key={tag} key={tag}
onClick={() => onToggleTag(tag)} onClick={() => onToggleTag(tag)}

View File

@@ -1,14 +1,14 @@
'use client'; 'use client'
import Link from 'next/link'; import Link from 'next/link'
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation'
import { Fragment } from 'react'; import { Fragment } from 'react'
import { BreadcrumbsSchema } from './breadcrumbs-schema'; import { BreadcrumbsSchema } from './breadcrumbs-schema'
interface BreadcrumbItem { interface BreadcrumbItem {
label: string; label: string
href: string; href: string
current?: boolean; current?: boolean
} }
function HomeIcon({ className }: { className?: string }) { function HomeIcon({ className }: { className?: string }) {
@@ -27,25 +27,15 @@ function HomeIcon({ className }: { className?: string }) {
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/> />
</svg> </svg>
); )
} }
function ChevronIcon({ className }: { className?: string }) { function ChevronIcon({ className }: { className?: string }) {
return ( return (
<svg <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
className={className} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg> </svg>
); )
} }
function formatSegmentLabel(segment: string): string { function formatSegmentLabel(segment: string): string {
@@ -53,36 +43,36 @@ function formatSegmentLabel(segment: string): string {
blog: 'Blog', blog: 'Blog',
tags: 'Tag-uri', tags: 'Tag-uri',
about: 'Despre', about: 'Despre',
}; }
if (specialCases[segment]) { if (specialCases[segment]) {
return specialCases[segment]; return specialCases[segment]
} }
return segment return segment
.split('-') .split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) .map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '); .join(' ')
} }
export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) { export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
const pathname = usePathname(); const pathname = usePathname()
let breadcrumbs: BreadcrumbItem[] = items || []; let breadcrumbs: BreadcrumbItem[] = items || []
if (!items) { if (!items) {
const segments = pathname.split('/').filter(Boolean); const segments = pathname.split('/').filter(Boolean)
breadcrumbs = segments.map((segment, index) => { breadcrumbs = segments.map((segment, index) => {
const href = '/' + segments.slice(0, index + 1).join('/'); const href = '/' + segments.slice(0, index + 1).join('/')
const label = formatSegmentLabel(segment); const label = formatSegmentLabel(segment)
const current = index === segments.length - 1; const current = index === segments.length - 1
return { label, href, current }; return { label, href, current }
}); })
} }
if (pathname === '/') { if (pathname === '/') {
return null; return null
} }
const schemaItems = [ const schemaItems = [
@@ -92,7 +82,7 @@ export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
name: item.label, name: item.label,
item: item.href, item: item.href,
})), })),
]; ]
return ( return (
<> <>
@@ -112,7 +102,7 @@ export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
</Link> </Link>
</li> </li>
{breadcrumbs.map((item) => ( {breadcrumbs.map(item => (
<Fragment key={item.href}> <Fragment key={item.href}>
<li className="text-gray-400 flex-shrink-0"> <li className="text-gray-400 flex-shrink-0">
<ChevronIcon className="w-4 h-4" /> <ChevronIcon className="w-4 h-4" />
@@ -141,5 +131,5 @@ export function Breadcrumbs({ items }: { items?: BreadcrumbItem[] }) {
</nav> </nav>
<BreadcrumbsSchema items={schemaItems} /> <BreadcrumbsSchema items={schemaItems} />
</> </>
); )
} }

View File

@@ -1,25 +1,25 @@
interface BreadcrumbSchemaItem { interface BreadcrumbSchemaItem {
position: number; position: number
name: string; name: string
item: string; item: string
} }
export function BreadcrumbsSchema({ items }: { items: BreadcrumbSchemaItem[] }) { export function BreadcrumbsSchema({ items }: { items: BreadcrumbSchemaItem[] }) {
const structuredData = { const structuredData = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'BreadcrumbList', '@type': 'BreadcrumbList',
itemListElement: items.map((item) => ({ itemListElement: items.map(item => ({
'@type': 'ListItem', '@type': 'ListItem',
position: item.position, position: item.position,
name: item.name, name: item.name,
item: `http://localhost:3000${item.item}`, item: `http://localhost:3000${item.item}`,
})), })),
}; }
return ( return (
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/> />
); )
} }

View File

@@ -43,18 +43,17 @@ export function ThemeToggle() {
className={` className={`
relative font-mono text-xs uppercase tracking-wider relative font-mono text-xs uppercase tracking-wider
px-3 py-1 border-2 transition-all duration-300 px-3 py-1 border-2 transition-all duration-300
${theme === 'dark' ${
? 'text-cyan-400 border-cyan-900 hover:border-cyan-700 bg-cyan-950/20' theme === 'dark'
: 'text-emerald-600 border-emerald-700 hover:border-emerald-500 bg-emerald-50/50' ? 'text-cyan-400 border-cyan-900 hover:border-cyan-700 bg-cyan-950/20'
: 'text-emerald-600 border-emerald-700 hover:border-emerald-500 bg-emerald-50/50'
} }
${isGlitching ? 'glitch-btn' : ''} ${isGlitching ? 'glitch-btn' : ''}
border-pulse overflow-hidden border-pulse overflow-hidden
`} `}
aria-label="Toggle theme" aria-label="Toggle theme"
> >
<span className="relative z-10"> <span className="relative z-10">{theme === 'dark' ? '[DARK MODE]' : '[LIGHT MODE]'}</span>
{theme === 'dark' ? '[DARK MODE]' : '[LIGHT MODE]'}
</span>
{isGlitching && ( {isGlitching && (
<> <>
<span className="glitch-layer" aria-hidden="true"> <span className="glitch-layer" aria-hidden="true">

View File

@@ -1,10 +1,10 @@
--- ---
title: "Getting Started with Next.js 15" title: 'Getting Started with Next.js 15'
description: "Learn how to build modern web applications with Next.js 15 and TypeScript." description: 'Learn how to build modern web applications with Next.js 15 and TypeScript.'
date: "2025-01-07" date: '2025-01-07'
author: "John Doe" author: 'John Doe'
category: "Tutorial" category: 'Tutorial'
tags: ["nextjs", "typescript", "tutorial"] tags: ['nextjs', 'typescript', 'tutorial']
--- ---
# Getting Started with Next.js 15 # Getting Started with Next.js 15

View File

@@ -1,10 +1,10 @@
--- ---
title: "Articol Tehnic din Subdirector" title: 'Articol Tehnic din Subdirector'
description: "Test pentru subdirectoare și organizare ierarhică" description: 'Test pentru subdirectoare și organizare ierarhică'
date: "2025-01-10" date: '2025-01-10'
author: "Tech Writer" author: 'Tech Writer'
category: "Tehnologie" category: 'Tehnologie'
tags: ["nextjs", "react", "typescript"] tags: ['nextjs', 'react', 'typescript']
draft: false draft: false
--- ---
@@ -25,14 +25,14 @@ Next.js este un framework React puternic care oferă:
```typescript ```typescript
interface User { interface User {
id: number; id: number
name: string; name: string
email: string; email: string
} }
async function fetchUser(id: number): Promise<User> { async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`); const response = await fetch(`/api/users/${id}`)
return response.json(); return response.json()
} }
``` ```

View File

@@ -1,17 +1,17 @@
--- ---
title: "Test Complet Markdown" title: 'Test Complet Markdown'
description: "Un articol de test care demonstrează toate elementele markdown suportate" description: 'Un articol de test care demonstrează toate elementele markdown suportate'
date: "2025-01-15" date: '2025-01-15'
author: "Test Author" author: 'Test Author'
category: "Tutorial" category: 'Tutorial'
tags: ["markdown", "test", "demo"] tags: ['markdown', 'test', 'demo']
image: "/38636.jpg" image: '/38636.jpg'
draft: false draft: false
--- ---
# Heading 1 # Heading 1
Acesta este un paragraf normal cu **text bold** și *text italic*. Putem combina ***bold și italic***. Acesta este un paragraf normal cu **text bold** și _text italic_. Putem combina **_bold și italic_**.
## Heading 2 ## Heading 2
@@ -47,11 +47,11 @@ Bloc de cod JavaScript:
```javascript ```javascript
function greet(name) { function greet(name) {
console.log(`Hello, ${name}!`); console.log(`Hello, ${name}!`)
return true; return true
} }
greet("World"); greet('World')
``` ```
Bloc de cod Python: Bloc de cod Python:
@@ -85,7 +85,7 @@ print(f"Result: {result}")
## Tabele ## Tabele
| Coloana 1 | Coloana 2 | Coloana 3 | | Coloana 1 | Coloana 2 | Coloana 3 |
|-----------|-----------|-----------| | --------- | --------- | --------- |
| Celula 1 | Celula 2 | Celula 3 | | Celula 1 | Celula 2 | Celula 3 |
| Date 1 | Date 2 | Date 3 | | Date 1 | Date 2 | Date 3 |
| Info 1 | Info 2 | Info 3 | | Info 1 | Info 2 | Info 3 |

33
eslint.config.mjs Normal file
View File

@@ -0,0 +1,33 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettier from 'eslint-config-prettier'
export default [
js.configs.recommended,
...tseslint.configs.recommended,
prettier,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
{
ignores: [
'node_modules/',
'.next/',
'out/',
'build/',
'dist/',
'.cache/',
'*.config.js',
'public/',
'coverage/',
],
},
]

View File

@@ -1,43 +1,43 @@
import fs from 'fs'; import fs from 'fs'
import path from 'path'; import path from 'path'
import matter from 'gray-matter'; import matter from 'gray-matter'
import { FrontMatter, Post } from './types/frontmatter'; import { FrontMatter, Post } from './types/frontmatter'
import { generateExcerpt } from './utils'; import { generateExcerpt } from './utils'
const POSTS_PATH = path.join(process.cwd(), 'content', 'blog'); const POSTS_PATH = path.join(process.cwd(), 'content', 'blog')
export function sanitizePath(inputPath: string): string { export function sanitizePath(inputPath: string): string {
const normalized = path.normalize(inputPath).replace(/^(\.\.[\/\\])+/, ''); const normalized = path.normalize(inputPath).replace(/^(\.\.[/\\])+/, '')
if (normalized.includes('..') || path.isAbsolute(normalized)) { if (normalized.includes('..') || path.isAbsolute(normalized)) {
throw new Error('Invalid path'); throw new Error('Invalid path')
} }
return normalized; return normalized
} }
export function calculateReadingTime(content: string): number { export function calculateReadingTime(content: string): number {
const wordsPerMinute = 200; const wordsPerMinute = 200
const words = content.trim().split(/\s+/).length; const words = content.trim().split(/\s+/).length
return Math.ceil(words / wordsPerMinute); return Math.ceil(words / wordsPerMinute)
} }
export function validateFrontmatter(data: any): FrontMatter { export function validateFrontmatter(data: any): FrontMatter {
if (!data.title || typeof data.title !== 'string') { if (!data.title || typeof data.title !== 'string') {
throw new Error('Invalid title'); throw new Error('Invalid title')
} }
if (!data.description || typeof data.description !== 'string') { if (!data.description || typeof data.description !== 'string') {
throw new Error('Invalid description'); throw new Error('Invalid description')
} }
if (!data.date || typeof data.date !== 'string') { if (!data.date || typeof data.date !== 'string') {
throw new Error('Invalid date'); throw new Error('Invalid date')
} }
if (!data.author || typeof data.author !== 'string') { if (!data.author || typeof data.author !== 'string') {
throw new Error('Invalid author'); throw new Error('Invalid author')
} }
if (!data.category || typeof data.category !== 'string') { if (!data.category || typeof data.category !== 'string') {
throw new Error('Invalid category'); throw new Error('Invalid category')
} }
if (!Array.isArray(data.tags) || data.tags.length === 0 || data.tags.length > 3) { if (!Array.isArray(data.tags) || data.tags.length === 0 || data.tags.length > 3) {
throw new Error('Tags must be array with 1-3 items'); throw new Error('Tags must be array with 1-3 items')
} }
return { return {
@@ -49,21 +49,21 @@ export function validateFrontmatter(data: any): FrontMatter {
tags: data.tags, tags: data.tags,
image: data.image, image: data.image,
draft: data.draft || false, draft: data.draft || false,
}; }
} }
export function getPostBySlug(slug: string | string[]): Post | null { export function getPostBySlug(slug: string | string[]): Post | null {
const slugArray = Array.isArray(slug) ? slug : slug.split('/'); const slugArray = Array.isArray(slug) ? slug : slug.split('/')
const sanitized = slugArray.map(s => sanitizePath(s)); const sanitized = slugArray.map(s => sanitizePath(s))
const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md'; const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md'
if (!fs.existsSync(fullPath)) { if (!fs.existsSync(fullPath)) {
return null; return null
} }
const fileContents = fs.readFileSync(fullPath, 'utf8'); const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents); const { data, content } = matter(fileContents)
const frontmatter = validateFrontmatter(data); const frontmatter = validateFrontmatter(data)
return { return {
slug: sanitized.join('/'), slug: sanitized.join('/'),
@@ -71,84 +71,86 @@ export function getPostBySlug(slug: string | string[]): Post | null {
content, content,
readingTime: calculateReadingTime(content), readingTime: calculateReadingTime(content),
excerpt: generateExcerpt(content), excerpt: generateExcerpt(content),
}; }
} }
export function getAllPosts(includeContent = false): Post[] { export function getAllPosts(includeContent = false): Post[] {
const posts: Post[] = []; const posts: Post[] = []
function walkDir(dir: string, prefix = ''): void { function walkDir(dir: string, prefix = ''): void {
const files = fs.readdirSync(dir); const files = fs.readdirSync(dir)
for (const file of files) { for (const file of files) {
const filePath = path.join(dir, file); const filePath = path.join(dir, file)
const stat = fs.statSync(filePath); const stat = fs.statSync(filePath)
if (stat.isDirectory()) { if (stat.isDirectory()) {
walkDir(filePath, prefix ? `${prefix}/${file}` : file); walkDir(filePath, prefix ? `${prefix}/${file}` : file)
} else if (file.endsWith('.md')) { } else if (file.endsWith('.md')) {
const slug = prefix ? `${prefix}/${file.replace(/\.md$/, '')}` : file.replace(/\.md$/, ''); const slug = prefix ? `${prefix}/${file.replace(/\.md$/, '')}` : file.replace(/\.md$/, '')
try { try {
const post = getPostBySlug(slug.split('/')); const post = getPostBySlug(slug.split('/'))
if (post && !post.frontmatter.draft) { if (post && !post.frontmatter.draft) {
posts.push(includeContent ? post : { ...post, content: '' }); posts.push(includeContent ? post : { ...post, content: '' })
} }
} catch (error) { } catch (error) {
console.error(`Error loading post ${slug}:`, error); console.error(`Error loading post ${slug}:`, error)
} }
} }
} }
} }
if (fs.existsSync(POSTS_PATH)) { if (fs.existsSync(POSTS_PATH)) {
walkDir(POSTS_PATH); walkDir(POSTS_PATH)
} }
return posts.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()); return posts.sort(
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
)
} }
export async function getRelatedPosts(currentSlug: string, limit = 3): Promise<Post[]> { export async function getRelatedPosts(currentSlug: string, limit = 3): Promise<Post[]> {
const currentPost = getPostBySlug(currentSlug); const currentPost = getPostBySlug(currentSlug)
if (!currentPost) return []; if (!currentPost) return []
const allPosts = getAllPosts(false); const allPosts = getAllPosts(false)
const { category, tags } = currentPost.frontmatter; const { category, tags } = currentPost.frontmatter
const scored = allPosts const scored = allPosts
.filter(post => post.slug !== currentSlug) .filter(post => post.slug !== currentSlug)
.map(post => { .map(post => {
let score = 0; let score = 0
if (post.frontmatter.category === category) score += 3; if (post.frontmatter.category === category) score += 3
score += post.frontmatter.tags.filter(tag => tags.includes(tag)).length * 2; score += post.frontmatter.tags.filter(tag => tags.includes(tag)).length * 2
return { post, score }; return { post, score }
}) })
.filter(({ score }) => score > 0) .filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score); .sort((a, b) => b.score - a.score)
return scored.slice(0, limit).map(({ post }) => post); return scored.slice(0, limit).map(({ post }) => post)
} }
export function getAllPostSlugs(): string[][] { export function getAllPostSlugs(): string[][] {
const slugs: string[][] = []; const slugs: string[][] = []
function walkDir(dir: string, prefix: string[] = []): void { function walkDir(dir: string, prefix: string[] = []): void {
const files = fs.readdirSync(dir); const files = fs.readdirSync(dir)
for (const file of files) { for (const file of files) {
const filePath = path.join(dir, file); const filePath = path.join(dir, file)
const stat = fs.statSync(filePath); const stat = fs.statSync(filePath)
if (stat.isDirectory()) { if (stat.isDirectory()) {
walkDir(filePath, [...prefix, file]); walkDir(filePath, [...prefix, file])
} else if (file.endsWith('.md')) { } else if (file.endsWith('.md')) {
slugs.push([...prefix, file.replace(/\.md$/, '')]); slugs.push([...prefix, file.replace(/\.md$/, '')])
} }
} }
} }
if (fs.existsSync(POSTS_PATH)) { if (fs.existsSync(POSTS_PATH)) {
walkDir(POSTS_PATH); walkDir(POSTS_PATH)
} }
return slugs; return slugs
} }

View File

@@ -1,22 +1,22 @@
export interface FrontMatter { export interface FrontMatter {
title: string; title: string
description: string; description: string
date: string; date: string
author: string; author: string
category: string; category: string
tags: string[]; tags: string[]
image?: string; image?: string
draft?: boolean; draft?: boolean
} }
export interface Post { export interface Post {
slug: string; slug: string
frontmatter: FrontMatter; frontmatter: FrontMatter
content: string; content: string
readingTime: number; readingTime: number
excerpt: string; excerpt: string
} }
export interface BlogParams { export interface BlogParams {
slug: string[]; slug: string[]
} }

View File

@@ -1,47 +1,65 @@
export function formatDate(dateString: string): string { export function formatDate(dateString: string): string {
const date = new Date(dateString); const date = new Date(dateString)
const months = [ const months = [
'ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'ianuarie',
'iulie', 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie' 'februarie',
]; 'martie',
'aprilie',
'mai',
'iunie',
'iulie',
'august',
'septembrie',
'octombrie',
'noiembrie',
'decembrie',
]
return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`; return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`
} }
export function formatRelativeDate(dateString: string): string { export function formatRelativeDate(dateString: string): string {
const date = new Date(dateString); const date = new Date(dateString)
const now = new Date(); const now = new Date()
const diffTime = Math.abs(now.getTime() - date.getTime()); const diffTime = Math.abs(now.getTime() - date.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 0) return 'astăzi'; if (diffDays === 0) return 'astăzi'
if (diffDays === 1) return 'ieri'; if (diffDays === 1) return 'ieri'
if (diffDays < 7) return `acum ${diffDays} zile`; if (diffDays < 7) return `acum ${diffDays} zile`
if (diffDays < 30) return `acum ${Math.floor(diffDays / 7)} săptămâni`; if (diffDays < 30) return `acum ${Math.floor(diffDays / 7)} săptămâni`
if (diffDays < 365) return `acum ${Math.floor(diffDays / 30)} luni`; if (diffDays < 365) return `acum ${Math.floor(diffDays / 30)} luni`
return `acum ${Math.floor(diffDays / 365)} ani`; return `acum ${Math.floor(diffDays / 365)} ani`
} }
export function generateExcerpt(content: string, maxLength = 160): string { export function generateExcerpt(content: string, maxLength = 160): string {
const text = content const text = content
.replace(/^---[\s\S]*?---/, '') .replace(/^---[\s\S]*?---/, '')
.replace(/!\[.*?\]\(.*?\)/g, '') .replace(/!\[.*?\]\(.*?\)/g, '')
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/[#*`]/g, '') .replace(/[#*`]/g, '')
.trim(); .trim()
if (text.length <= maxLength) return text; if (text.length <= maxLength) return text
const truncated = text.slice(0, maxLength); const truncated = text.slice(0, maxLength)
const lastSpace = truncated.lastIndexOf(' '); const lastSpace = truncated.lastIndexOf(' ')
return truncated.slice(0, lastSpace) + '...'; return truncated.slice(0, lastSpace) + '...'
} }
export function generateSlug(title: string): string { export function generateSlug(title: string): string {
const romanianMap: Record<string, string> = { const romanianMap: Record<string, string> = {
'ă': 'a', 'â': 'a', 'î': 'i', 'ș': 's', 'ț': 't', ă: 'a',
'Ă': 'a', 'Â': 'a', 'Î': 'i', 'Ș': 's', 'Ț': 't' â: 'a',
}; î: 'i',
ș: 's',
ț: 't',
Ă: 'a',
Â: 'a',
Î: 'i',
Ș: 's',
Ț: 't',
}
return title return title
.split('') .split('')
@@ -49,5 +67,5 @@ export function generateSlug(title: string): string {
.join('') .join('')
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9]+/g, '-') .replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, ''); .replace(/^-+|-+$/g, '')
} }

5075
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,10 @@
"dev": "next dev -p 3030", "dev": "next dev -p 3030",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
"validate-posts": "node scripts/validate-posts.js" "validate-posts": "node scripts/validate-posts.js"
}, },
"repository": { "repository": {
@@ -38,5 +41,17 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"typescript": "^5.9.3" "typescript": "^5.9.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.1",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"prettier": "^3.6.2",
"typescript-eslint": "^8.46.4"
} }
} }

View File

@@ -2,9 +2,9 @@
module.exports = { module.exports = {
darkMode: 'class', darkMode: 'class',
content: [ content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}", './pages/**/*.{js,ts,jsx,tsx,mdx}',
"./components/**/*.{js,ts,jsx,tsx,mdx}", './components/**/*.{js,ts,jsx,tsx,mdx}',
"./app/**/*.{js,ts,jsx,tsx,mdx}", './app/**/*.{js,ts,jsx,tsx,mdx}',
], ],
theme: { theme: {
extend: { extend: {
@@ -24,7 +24,7 @@ module.exports = {
'dark-primary': '#18181b', 'dark-primary': '#18181b',
'dark-secondary': '#0f172a', 'dark-secondary': '#0f172a',
'dark-tertiary': '#1e293b', 'dark-tertiary': '#1e293b',
'accent': { accent: {
DEFAULT: '#164e63', DEFAULT: '#164e63',
hover: '#155e75', hover: '#155e75',
light: '#0e7490', light: '#0e7490',
@@ -39,10 +39,10 @@ module.exports = {
}, },
}, },
animation: { animation: {
'glitch': 'glitch 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) both', glitch: 'glitch 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) both',
'flicker': 'flicker 0.15s infinite', flicker: 'flicker 0.15s infinite',
'scanline': 'scanline 8s linear infinite', scanline: 'scanline 8s linear infinite',
'noise': 'noise 0.2s infinite', noise: 'noise 0.2s infinite',
}, },
keyframes: { keyframes: {
glitch: { glitch: {

View File

@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -23,9 +19,7 @@
} }
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["./*"]
"./*"
]
} }
}, },
"include": [ "include": [
@@ -35,7 +29,5 @@
".next/types/**/*.ts", ".next/types/**/*.ts",
".next/dev/types/**/*.ts" ".next/dev/types/**/*.ts"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }