feat: unify code-like components with shared CodeWindow, fix blog re-render loop, and stabilize layouts
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
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
This commit is contained in:
@@ -1,77 +1,113 @@
|
||||
'use client';
|
||||
"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 * 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('');
|
||||
const [filteredPosts, setFilteredPosts] = useState(blogPosts);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
// Sort posts by date
|
||||
const allPosts = [...blogPosts].sort((a, b) =>
|
||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
// 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(),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Get unique tags
|
||||
const allTags = Array.from(new Set(allPosts.flatMap(post => post.tags || [])));
|
||||
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('#')) {
|
||||
if (query.startsWith("#")) {
|
||||
const tag = query.slice(1);
|
||||
setFilteredPosts(allPosts.filter(post =>
|
||||
post.tags?.some(t => t.toLowerCase() === tag.toLowerCase())
|
||||
));
|
||||
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);
|
||||
}));
|
||||
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]);
|
||||
}, [searchQuery, allPosts]);
|
||||
|
||||
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."
|
||||
<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 className="narrow-container">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-16">
|
||||
<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="md:col-span-4">
|
||||
<div className="lg:col-span-4 lg:order-2">
|
||||
<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>
|
||||
<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-6">
|
||||
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Themen</h3>
|
||||
<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"
|
||||
className="text-left group"
|
||||
>
|
||||
<Tag tag={tag} index={index} />
|
||||
</button>
|
||||
@@ -84,11 +120,13 @@ export default function BlogPage() {
|
||||
</div>
|
||||
|
||||
{/* Posts area */}
|
||||
<div className="md:col-span-8">
|
||||
<div id="posts-container" className="flex flex-col gap-8">
|
||||
<div className="lg:col-span-8 lg:order-1">
|
||||
<div id="posts-container" className="flex flex-col gap-12">
|
||||
{filteredPosts.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>No posts found matching your criteria.</p>
|
||||
<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) => (
|
||||
@@ -100,7 +138,7 @@ export default function BlogPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user