Files
mintel.me/apps/web/app/blog/page.tsx
Marc Mintel c1295546a6
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
feat: unify code-like components with shared CodeWindow, fix blog re-render loop, and stabilize layouts
2026-02-15 17:34:07 +01:00

145 lines
4.7 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";
import { Section } from "../../src/components/Section";
import { AbstractCircuit, GradientMesh } from "../../src/components/Effects";
import { Label } from "../../src/components/Typography";
export default function BlogPage() {
const [searchQuery, setSearchQuery] = useState("");
// Memoize allPosts to prevent infinite re-render loop
const allPosts = React.useMemo(
() =>
[...blogPosts].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
),
[],
);
const [filteredPosts, setFilteredPosts] = useState(allPosts);
// Memoize allTags
const allTags = React.useMemo(
() => Array.from(new Set(allPosts.flatMap((post) => post.tags || []))),
[allPosts],
);
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, allPosts]);
const filterByTag = (tag: string) => {
setSearchQuery(`#${tag}`);
};
return (
<div className="flex flex-col bg-white overflow-hidden relative min-h-screen">
<AbstractCircuit />
<PageHeader
title={
<>
Blog <br />
<span className="text-slate-400">& Notes.</span>
</>
}
description="Ein technisches Notizbuch über Lösungen, Fehler und Werkzeuge."
backLink={{ href: "/", label: "Zurück" }}
backgroundSymbol="B"
/>
<Section
number="01"
title="Journal"
borderTop
effects={<GradientMesh variant="metallic" className="opacity-50" />}
>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
{/* Sidebar / Filter area */}
<div className="lg:col-span-4 lg:order-2">
<div className="sticky top-32 space-y-16">
<Reveal>
<div className="space-y-6">
<Label className="text-slate-400 uppercase tracking-[0.3em]">
Suchen
</Label>
<SearchBar value={searchQuery} onChange={setSearchQuery} />
</div>
</Reveal>
{allTags.length > 0 && (
<Reveal delay={0.2}>
<div className="space-y-8">
<Label className="text-slate-400 uppercase tracking-[0.3em]">
Themen
</Label>
<div className="flex flex-wrap gap-2">
{allTags.map((tag, index) => (
<button
key={tag}
onClick={() => filterByTag(tag)}
className="text-left group"
>
<Tag tag={tag} index={index} />
</button>
))}
</div>
</div>
</Reveal>
)}
</div>
</div>
{/* Posts area */}
<div className="lg:col-span-8 lg:order-1">
<div id="posts-container" className="flex flex-col gap-12">
{filteredPosts.length === 0 ? (
<div className="py-24 text-center border border-dashed border-slate-200 rounded-3xl">
<p className="text-slate-400 font-mono text-sm uppercase tracking-widest">
Keine Beiträge gefunden.
</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>
);
}