feat: molzi3d.de v1.0.0 — Komplettes Redesign mit Next.js 16
- WordPress durch Next.js 16 + Tailwind CSS v4 + Framer Motion ersetzt - 44 Guides + 15 Seiten aus WordPress migriert (HTML -> Markdown) - Emerald Design-System mit Light/Dark Mode Toggle - Sidebar-First Navigation (Dokumentations-Stil) - Difficulty-Badges, Lesezeit, verwandte Guides - Statischer Export fuer Plesk-Hosting - WordPress-DB Backup gesichert (6.2 MB) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
app/src/components/guides/GuideCard.tsx
Normal file
24
app/src/components/guides/GuideCard.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Link from "next/link";
|
||||
import { DifficultyBadge } from "@/components/ui/DifficultyBadge";
|
||||
import type { GuideMetadata } from "@/lib/content";
|
||||
|
||||
export const GuideCard = ({ guide }: { guide: GuideMetadata }) => (
|
||||
<Link
|
||||
href={`/guides/${guide.slug}`}
|
||||
className="group block rounded-lg border border-border bg-card p-5 transition-all hover:border-accent/50 hover:shadow-md hover:shadow-accent/5"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<DifficultyBadge level={guide.difficulty} />
|
||||
<span className="text-xs text-muted-foreground">{guide.readingTime}</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-foreground group-hover:text-accent transition-colors mb-1">
|
||||
{guide.title}
|
||||
</h3>
|
||||
{guide.excerpt && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">{guide.excerpt}</p>
|
||||
)}
|
||||
<div className="mt-3 text-xs text-muted-foreground">
|
||||
{guide.category}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
113
app/src/components/layout/Header.tsx
Normal file
113
app/src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import { ThemeToggle } from "@/components/ui/ThemeToggle";
|
||||
import { SearchButton } from "@/components/ui/SearchButton";
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
export const Header = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
|
||||
<div className="flex h-16 items-center justify-between px-4 lg:px-6">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-2 font-bold text-lg">
|
||||
<span className="text-accent text-2xl">△</span>
|
||||
<span>
|
||||
molzi<span className="text-accent">3d</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav className="hidden md:flex items-center gap-6 text-sm">
|
||||
<Link
|
||||
href="/guides"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Guides
|
||||
</Link>
|
||||
<Link
|
||||
href="/rechner"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Rechner
|
||||
</Link>
|
||||
<Link
|
||||
href="/faq"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
FAQ
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchButton />
|
||||
<ThemeToggle theme={theme} onToggle={toggleTheme} />
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<button
|
||||
className="md:hidden p-2 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<path d="M4 4l12 12M16 4L4 16" />
|
||||
) : (
|
||||
<path d="M3 5h14M3 10h14M3 15h14" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && (
|
||||
<motion.nav
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="md:hidden border-t border-border overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col p-4 gap-3">
|
||||
<Link
|
||||
href="/guides"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Guides
|
||||
</Link>
|
||||
<Link
|
||||
href="/rechner"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Rechner
|
||||
</Link>
|
||||
<Link
|
||||
href="/faq"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
FAQ
|
||||
</Link>
|
||||
</div>
|
||||
</motion.nav>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
118
app/src/components/layout/Sidebar.tsx
Normal file
118
app/src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface SidebarSection {
|
||||
title: string;
|
||||
items: { label: string; href: string }[];
|
||||
}
|
||||
|
||||
const sections: SidebarSection[] = [
|
||||
{
|
||||
title: "Erste Schritte",
|
||||
items: [
|
||||
{ label: "Erstes Modell drucken", href: "/guides/erstes-modell-drucken" },
|
||||
{ label: "Druckbett leveln", href: "/guides/druckbett-leveln-z-offset" },
|
||||
{ label: "Erste Schicht kalibrieren", href: "/guides/erste-schicht-kalibrieren" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Materialien",
|
||||
items: [
|
||||
{ label: "PLA perfekt einstellen", href: "/guides/pla-perfekt-einstellen" },
|
||||
{ label: "PETG ohne Frust", href: "/guides/petg-ohne-frust" },
|
||||
{ label: "TPU drucken", href: "/guides/tpu-drucken" },
|
||||
{ label: "ASA/ABS Grundlagen", href: "/guides/asa-abs-grundlagen" },
|
||||
{ label: "Nylon (PA) drucken", href: "/guides/nylon-pa-drucken" },
|
||||
{ label: "Carbon & Glasfaser", href: "/guides/carbon-fiber-glasfaser-filamente" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Fehlerbehebung",
|
||||
items: [
|
||||
{ label: "Stringing reduzieren", href: "/guides/stringing-reduzieren" },
|
||||
{ label: "Warping vermeiden", href: "/guides/warping-vermeiden" },
|
||||
{ label: "Unterextrusion beheben", href: "/guides/unterextrusion-beheben" },
|
||||
{ label: "Layer Separation", href: "/guides/layer-separation-beheben" },
|
||||
{ label: "Elefantenfuss beheben", href: "/guides/elefantenfuss-beheben" },
|
||||
{ label: "Verstopfte Duese", href: "/guides/verstopfte-duese-diagnose-reinigung" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Kalibrierung",
|
||||
items: [
|
||||
{ label: "Retraction kalibrieren", href: "/guides/retraction-kalibrieren" },
|
||||
{ label: "Flow-Rate & E-Steps", href: "/guides/flow-rate-e-steps-kalibrieren" },
|
||||
{ label: "Pressure Advance", href: "/guides/pressure-advance-kalibrieren" },
|
||||
{ label: "Input Shaping", href: "/guides/input-shaping-kalibrieren" },
|
||||
{ label: "Temperaturturm", href: "/guides/temperaturturm-kalibrieren" },
|
||||
{ label: "Speed Tower", href: "/guides/speed-tower-druckgeschwindigkeit" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Slicer",
|
||||
items: [
|
||||
{ label: "OrcaSlicer Guide", href: "/guides/guide-orcaslicer-einsteiger" },
|
||||
{ label: "Cura Tipps", href: "/guides/guide-cura-tipps" },
|
||||
{ label: "Bambu Studio", href: "/guides/guide-bambu-studio" },
|
||||
{ label: "PrusaSlicer", href: "/guides/guide-prusaslicer" },
|
||||
{ label: "Slicer-Profil optimieren", href: "/guides/slicer-profil-optimieren" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Fortgeschritten",
|
||||
items: [
|
||||
{ label: "Adaptive Layer Height", href: "/guides/adaptive-layer-height" },
|
||||
{ label: "Modifier Meshes", href: "/guides/modifier-meshes-paint-on-supports" },
|
||||
{ label: "Ironing", href: "/guides/ironing-top-oberflaechen" },
|
||||
{ label: "Fuzzy Skin", href: "/guides/fuzzy-skin-strukturierte-oberflaechen" },
|
||||
{ label: "Multi-Material / AMS", href: "/guides/multi-material-ams" },
|
||||
{ label: "Klipper Grundlagen", href: "/guides/klipper-grundlagen-ersteinrichtung" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Sidebar = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside className="hidden lg:block w-64 shrink-0 border-r border-border bg-sidebar-bg overflow-y-auto h-[calc(100vh-4rem)] sticky top-16">
|
||||
<nav className="p-4 space-y-6">
|
||||
{sections.map((section) => (
|
||||
<div key={section.title}>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
||||
{section.title}
|
||||
</h3>
|
||||
<ul className="space-y-0.5">
|
||||
{section.items.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`block px-3 py-1.5 rounded-md text-sm transition-colors relative ${
|
||||
isActive
|
||||
? "bg-accent/10 text-accent font-medium"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-surface-hover"
|
||||
}`}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="sidebar-active"
|
||||
className="absolute left-0 top-0 bottom-0 w-0.5 bg-accent rounded-full"
|
||||
/>
|
||||
)}
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
27
app/src/components/ui/DifficultyBadge.tsx
Normal file
27
app/src/components/ui/DifficultyBadge.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
interface DifficultyBadgeProps {
|
||||
level: "einsteiger" | "fortgeschritten" | "experte";
|
||||
}
|
||||
|
||||
const config = {
|
||||
einsteiger: {
|
||||
label: "Einsteiger",
|
||||
className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400",
|
||||
},
|
||||
fortgeschritten: {
|
||||
label: "Fortgeschritten",
|
||||
className: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400",
|
||||
},
|
||||
experte: {
|
||||
label: "Experte",
|
||||
className: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
||||
},
|
||||
};
|
||||
|
||||
export const DifficultyBadge = ({ level }: DifficultyBadgeProps) => {
|
||||
const { label, className } = config[level];
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${className}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
20
app/src/components/ui/SearchButton.tsx
Normal file
20
app/src/components/ui/SearchButton.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
export const SearchButton = () => (
|
||||
<button
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border text-sm text-muted-foreground hover:text-foreground hover:border-accent/50 transition-colors"
|
||||
onClick={() => {
|
||||
// TODO: Search-Modal oeffnen
|
||||
document.dispatchEvent(new CustomEvent("open-search"));
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="M21 21l-4.35-4.35" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Suche...</span>
|
||||
<kbd className="hidden sm:inline-flex items-center text-xs border border-border rounded px-1.5 py-0.5 ml-2">
|
||||
⌘K
|
||||
</kbd>
|
||||
</button>
|
||||
);
|
||||
25
app/src/components/ui/ThemeToggle.tsx
Normal file
25
app/src/components/ui/ThemeToggle.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
interface ThemeToggleProps {
|
||||
theme: "light" | "dark";
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export const ThemeToggle = ({ theme, onToggle }: ThemeToggleProps) => (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-surface-hover transition-colors"
|
||||
aria-label={`Wechsle zu ${theme === "light" ? "Dark" : "Light"} Mode`}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
Reference in New Issue
Block a user