aboutsummaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/Blog.tsx50
-rw-r--r--src/pages/Post.tsx75
2 files changed, 125 insertions, 0 deletions
diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx
new file mode 100644
index 0000000..5db87c8
--- /dev/null
+++ b/src/pages/Blog.tsx
@@ -0,0 +1,50 @@
+import { useMemo } from "react";
+import matter from "gray-matter";
+import { Link } from "react-router-dom";
+import CardLink from "../components/CardLink";
+
+interface PostMeta {
+ slug: string;
+ title: string;
+ date: string;
+ excerpt: string;
+ cover?: string;
+}
+
+export default function Blog() {
+ const posts = useMemo<PostMeta[]>(() => {
+ const files = import.meta.glob("../blog/*.md", {
+ eager: true,
+ query: "?raw",
+ import: "default",
+ }) as Record<string, string>;
+
+ return Object.entries(files)
+ .map(([path, raw]) => {
+ const { data } = matter(raw);
+ const slug = path.split("/").pop()!.replace(".md", "");
+ return { slug, ...data } as PostMeta;
+ })
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+ }, []);
+
+ return (
+ <section className="container mx-auto px-4 py-10">
+ <h1 className="text-4xl font-bold mb-8">Blog</h1>
+
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
+ {posts.map((post) => (
+ /* 1) Link für internes Routing */
+ /* 2) CardLink bekommt genau deine Prop-Namen */
+ <Link key={post.slug} to={`/blog/${post.slug}`} className="block">
+ <CardLink
+ title={post.title}
+ body={post.excerpt} // <— body statt description
+ imgSrc={post.cover} // <— imgSrc statt image
+ />
+ </Link>
+ ))}
+ </div>
+ </section>
+ );
+}
diff --git a/src/pages/Post.tsx b/src/pages/Post.tsx
new file mode 100644
index 0000000..17022d2
--- /dev/null
+++ b/src/pages/Post.tsx
@@ -0,0 +1,75 @@
+import { useEffect, useState } from "react";
+import { useParams, Link } from "react-router-dom";
+import matter from "gray-matter";
+import ReactMarkdown from "react-markdown";
+import CodeSnippet from "../components/CodeSnippet";
+import LinkWithIcon from "../components/LinkWithIcon";
+import NotFoundPage from "./404Page";
+
+interface PostMeta {
+ title: string;
+ date: string;
+ cover?: string;
+}
+
+export default function Post() {
+ const { slug } = useParams<{ slug: string }>();
+ const [meta, setMeta] = useState<PostMeta | null>(null);
+ const [content, setContent] = useState("");
+ const [notFound, setNotFound] = useState(false);
+
+ useEffect(() => {
+ import(`../blog/${slug}.md?raw`)
+ .then((m) => {
+ const { data, content } = matter(m.default);
+ setMeta(data as PostMeta);
+ setContent(content);
+ })
+ .catch(() => setNotFound(true));
+ }, [slug]);
+
+ if (!meta) return null;
+ if (notFound) return <NotFoundPage />;
+
+ return (
+ <article className="prose prose-zinc dark:prose-invert mx-auto px-4 py-10">
+ <Link to="/blog" className="no-underline">
+ ← Zurück zum Blog
+ </Link>
+
+ {meta.cover && (
+ <img
+ src={meta.cover}
+ alt={meta.title}
+ className="w-full h-60 object-cover rounded-lg my-6"
+ />
+ )}
+
+ <h1>{meta.title}</h1>
+ <p className="text-sm text-zinc-500 mb-8">
+ {new Date(meta.date).toLocaleDateString("de-DE")}
+ </p>
+
+ <article className="markdown-body">
+ <ReactMarkdown
+ components={{
+ code({ children }) {
+ const text = String(children).replace(/\n$/, "");
+ return <CodeSnippet code={text} initialLines={5} />;
+ },
+
+ a({ href = "", children, ...props }) {
+ return (
+ <LinkWithIcon href={href} {...props}>
+ {children}
+ </LinkWithIcon>
+ );
+ },
+ }}
+ >
+ {content}
+ </ReactMarkdown>
+ </article>
+ </article>
+ );
+} \ No newline at end of file