diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.tsx | 4 | ||||
| -rw-r--r-- | src/blog/3dprint.md | 66 | ||||
| -rw-r--r-- | src/components/Navbar.tsx | 2 | ||||
| -rw-r--r-- | src/pages/Blog.tsx | 50 | ||||
| -rw-r--r-- | src/pages/Post.tsx | 75 | ||||
| -rw-r--r-- | src/vite-env.d.ts | 4 |
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; +} |
