aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.tsx4
-rw-r--r--src/blog/3dprint.md66
-rw-r--r--src/components/Navbar.tsx2
-rw-r--r--src/pages/Blog.tsx50
-rw-r--r--src/pages/Post.tsx75
-rw-r--r--src/vite-env.d.ts4
6 files changed, 200 insertions, 1 deletions
diff --git a/src/App.tsx b/src/App.tsx
index c6e88bd..60de46b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,8 @@ import HomelabPage from './pages/Homelab';
import Footer from './components/Footer';
import PrintingPage from './pages/Printing';
import NotFoundPage from './pages/404Page';
+import Blog from './pages/Blog';
+import Post from './pages/Post';
function App() {
@@ -20,6 +22,8 @@ function App() {
<Route path='/projects' element={<ProjectsPage />} />
<Route path='/homelab' element={<HomelabPage />} />
<Route path='/printing' element={<PrintingPage />} />
+ <Route path='/blog' element={<Blog />} />
+ <Route path='/blog/:slug' element={<Post />} />
<Route path='*' element={<NotFoundPage />} />
</Routes>
</section>
diff --git a/src/blog/3dprint.md b/src/blog/3dprint.md
new file mode 100644
index 0000000..ab3cf28
--- /dev/null
+++ b/src/blog/3dprint.md
@@ -0,0 +1,66 @@
+---
+title: "3D Printing"
+date: "2025-06-25"
+excerpt: "My 3D-printing projects: from a robotic arm to a DIY drone – including the ESP32 controller code."
+cover: "/src/assets/a1.webp"
+---
+
+# Projects
+
+## Robotic Arm
+
+[3D model on MakerWorld](https://makerworld.com/en/models/528885-robotic-arm#profileId-445995) – modified to work with my servo motors.
+
+```cpp
+#include <Bluepad32.h>
+#include <ESP32Servo.h>
+
+#define DEADZONE 30
+#define BASE_PIN 15
+#define SHOULDER_PIN 2
+#define ELBOW_PIN 4
+#define WRIST_PIN 16
+#define HAND_PIN 17
+
+Servo base, shoulder, elbow, wrist, hand;
+ControllerPtr pad;
+
+void onConnectedGamepad(ControllerPtr ctl) {
+ Serial.printf("Gamepad #%d connected\n", ctl->index());
+ pad = ctl;
+}
+
+void onDisconnectedGamepad(ControllerPtr ctl) {
+ Serial.printf("Gamepad disconnected\n");
+}
+
+int16_t mapAxis(int16_t v) {
+ if (abs(v) < DEADZONE) v = 0;
+ return map(v, -512, 512, 0, 180);
+}
+
+void setup() {
+ BP32.setup(&onConnectedGamepad, &onDisconnectedGamepad);
+ BP32.enableNewBluetoothConnections(true);
+
+ base.attach(BASE_PIN);
+ shoulder.attach(SHOULDER_PIN);
+ elbow.attach(ELBOW_PIN);
+ wrist.attach(WRIST_PIN);
+ hand.attach(HAND_PIN);
+}
+
+void loop() {
+ BP32.update();
+ if (pad && pad->isConnected()) {
+ base.write(mapAxis(pad->axisX()));
+ shoulder.write(mapAxis(-pad->axisY()));
+ elbow.write(mapAxis(-pad->axisRY()));
+ hand.write(mapAxis(pad->throttle() - pad->brake()));
+
+ if (pad->l1()) wrist.write(0);
+ else if (pad->r1()) wrist.write(180);
+ else wrist.write(90);
+ }
+ delay(15);
+} \ No newline at end of file
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 4246508..fd1b415 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -7,7 +7,7 @@ export default function Navbar() {
{ label: 'Gear', href: '/gear' },
{ label: 'Projects', href: '/projects' },
{ label: 'Homelab', href: '/homelab' },
- { label: '3D Printing', href: '/printing' }
+ { label: 'Blog', href: '/blog' }
];
return (
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
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 11f02fe..913581e 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1,5 @@
/// <reference types="vite/client" />
+declare module '*.md?raw' {
+ const content: string;
+ export default content;
+}