Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
- Restructure to pnpm monorepo (site moved to apps/web) - Integrate @mintel/tsconfig, @mintel/eslint-config, @mintel/husky-config - Implement Docker service architecture (Varnish, Directus, Gatekeeper) - Setup environment-aware Gitea Actions deployment
107 lines
3.8 KiB
TypeScript
107 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { useState, useEffect } from 'react';
|
|
import { MediumCard } from '../../src/components/MediumCard';
|
|
import { SearchBar } from '../../src/components/SearchBar';
|
|
import { Tag } from '../../src/components/Tag';
|
|
import { blogPosts } from '../../src/data/blogPosts';
|
|
import { PageHeader } from '../../src/components/PageHeader';
|
|
import { Reveal } from '../../src/components/Reveal';
|
|
|
|
export default function BlogPage() {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [filteredPosts, setFilteredPosts] = useState(blogPosts);
|
|
|
|
// Sort posts by date
|
|
const allPosts = [...blogPosts].sort((a, b) =>
|
|
new Date(b.date).getTime() - new Date(a.date).getTime()
|
|
);
|
|
|
|
// Get unique tags
|
|
const allTags = Array.from(new Set(allPosts.flatMap(post => post.tags || [])));
|
|
|
|
useEffect(() => {
|
|
const query = searchQuery.toLowerCase().trim();
|
|
if (query.startsWith('#')) {
|
|
const tag = query.slice(1);
|
|
setFilteredPosts(allPosts.filter(post =>
|
|
post.tags?.some(t => t.toLowerCase() === tag.toLowerCase())
|
|
));
|
|
} else {
|
|
setFilteredPosts(allPosts.filter(post => {
|
|
const title = post.title.toLowerCase();
|
|
const description = post.description.toLowerCase();
|
|
const tags = (post.tags || []).join(' ').toLowerCase();
|
|
return title.includes(query) || description.includes(query) || tags.includes(query);
|
|
}));
|
|
}
|
|
}, [searchQuery]);
|
|
|
|
const filterByTag = (tag: string) => {
|
|
setSearchQuery(`#${tag}`);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-24 py-12 md:py-24 overflow-hidden">
|
|
<PageHeader
|
|
title={<>Blog <br /><span className="text-slate-200">& Notes.</span></>}
|
|
description="A public notebook of things I figured out, mistakes I made, and tools I tested."
|
|
backgroundSymbol="B"
|
|
/>
|
|
|
|
<section className="narrow-container">
|
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-16">
|
|
{/* Sidebar / Filter area */}
|
|
<div className="md:col-span-4">
|
|
<div className="sticky top-32 space-y-16">
|
|
<Reveal>
|
|
<div className="space-y-4">
|
|
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Suchen</h3>
|
|
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
|
</div>
|
|
</Reveal>
|
|
|
|
{allTags.length > 0 && (
|
|
<Reveal delay={0.2}>
|
|
<div className="space-y-6">
|
|
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Themen</h3>
|
|
<div className="flex flex-wrap gap-2">
|
|
{allTags.map((tag, index) => (
|
|
<button
|
|
key={tag}
|
|
onClick={() => filterByTag(tag)}
|
|
className="text-left"
|
|
>
|
|
<Tag tag={tag} index={index} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Reveal>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Posts area */}
|
|
<div className="md:col-span-8">
|
|
<div id="posts-container" className="flex flex-col gap-8">
|
|
{filteredPosts.length === 0 ? (
|
|
<div className="empty-state">
|
|
<p>No posts found matching your criteria.</p>
|
|
</div>
|
|
) : (
|
|
filteredPosts.map((post, i) => (
|
|
<Reveal key={post.slug} delay={0.1 * i} width="100%">
|
|
<MediumCard post={post} />
|
|
</Reveal>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|