feat: Add LEVIS Holzbau — Kinder-Holzwerk-Website (Port 3013)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 39s
CI / test-python-voice (push) Successful in 37s
CI / test-bqas (push) Successful in 37s

Neue statische Website fuer Kinder (6-12 Jahre) mit 8 Holzprojekten,
SVG-Illustrationen, Sicherheitshinweisen und kindgerechtem Design.
Next.js 15 + Tailwind + Framer Motion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-11 10:03:21 +01:00
parent 32aade553d
commit 0770ff499b
31 changed files with 3329 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
export function AgeBadge({ range }: { range: string }) {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-accent/10 text-accent">
{range} Jahre
</span>
)
}

View File

@@ -0,0 +1,15 @@
import { Hammer } from 'lucide-react'
export function DifficultyBadge({ level }: { level: 1 | 2 | 3 }) {
const labels = ['Anfaenger', 'Fortgeschritten', 'Profi']
return (
<div className="flex items-center gap-1" title={labels[level - 1]}>
{Array.from({ length: 3 }).map((_, i) => (
<Hammer
key={i}
className={`w-4 h-4 ${i < level ? 'text-primary' : 'text-gray-300'}`}
/>
))}
</div>
)
}

View File

@@ -0,0 +1,17 @@
import { Heart } from 'lucide-react'
import { Logo } from './Logo'
export function Footer() {
return (
<footer className="bg-white border-t border-primary/10 mt-16">
<div className="max-w-6xl mx-auto px-4 py-8">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<Logo size={32} />
<p className="text-sm text-dark/50 flex items-center gap-1">
Gemacht mit <Heart className="w-4 h-4 text-red-400 fill-red-400" /> fuer junge Holzwerker
</p>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,95 @@
'use client'
import { motion } from 'framer-motion'
import Link from 'next/link'
import { ArrowRight } from 'lucide-react'
import { Logo } from './Logo'
export function HeroSection() {
return (
<section className="relative overflow-hidden bg-gradient-to-br from-cream via-white to-primary/5 py-16 sm:py-24">
<div className="max-w-6xl mx-auto px-4 flex flex-col lg:flex-row items-center gap-12">
<motion.div
className="flex-1 text-center lg:text-left"
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6 }}
>
<div className="flex justify-center lg:justify-start mb-6">
<Logo size={64} />
</div>
<h1 className="font-heading font-bold text-4xl sm:text-5xl text-dark mb-4 text-balance">
Willkommen in der{' '}
<span className="text-primary">Holzwerkstatt</span>!
</h1>
<p className="text-lg text-dark/70 mb-8 max-w-lg mx-auto lg:mx-0">
Hier lernst du, wie man aus Holz tolle Sachen baut und schnitzt.
Vom Zauberstab bis zum Vogelhaus fuer jeden ist etwas dabei!
</p>
<Link
href="/projekte"
className="inline-flex items-center gap-2 bg-primary hover:bg-primary/90 text-white font-bold px-8 py-4 rounded-2xl text-lg transition-colors shadow-lg shadow-primary/20"
>
Entdecke Projekte <ArrowRight className="w-5 h-5" />
</Link>
</motion.div>
<motion.div
className="flex-1 flex justify-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<HeroIllustration />
</motion.div>
</div>
</section>
)
}
function HeroIllustration() {
return (
<svg width="320" height="280" viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Workbench */}
<rect x="40" y="180" width="240" height="12" rx="4" fill="#D4915C" />
<rect x="60" y="192" width="12" height="60" rx="2" fill="#C4814C" />
<rect x="248" y="192" width="12" height="60" rx="2" fill="#C4814C" />
<rect x="50" y="248" width="32" height="8" rx="2" fill="#C4814C" />
<rect x="238" y="248" width="32" height="8" rx="2" fill="#C4814C" />
{/* Wood pieces on bench */}
<rect x="80" y="164" width="60" height="16" rx="3" fill="#E8A96C" />
<rect x="85" y="168" width="50" height="2" rx="1" fill="#D4915C" opacity="0.3" />
{/* Small boat */}
<path d="M180 170 Q200 155 220 170 Q200 178 180 170Z" fill="#E8A96C" />
<line x1="200" y1="148" x2="200" y2="170" stroke="#8B6F47" strokeWidth="2" />
<path d="M200 148 L215 158 L200 165Z" fill="#FF6B6B" opacity="0.8" />
{/* Hammer */}
<rect x="240" y="155" width="4" height="25" rx="1" fill="#8B6F47" transform="rotate(-20 240 155)" />
<rect x="232" y="148" width="20" height="10" rx="2" fill="#888" transform="rotate(-20 240 155)" />
{/* Tree background */}
<circle cx="60" cy="100" r="35" fill="#4CAF50" opacity="0.3" />
<circle cx="50" cy="85" r="25" fill="#4CAF50" opacity="0.4" />
<circle cx="70" cy="90" r="28" fill="#4CAF50" opacity="0.35" />
<rect x="56" y="120" width="8" height="60" rx="2" fill="#8B6F47" opacity="0.4" />
{/* Tree right */}
<circle cx="270" cy="110" r="30" fill="#4CAF50" opacity="0.25" />
<circle cx="280" cy="95" r="22" fill="#4CAF50" opacity="0.35" />
<rect x="268" y="130" width="6" height="50" rx="2" fill="#8B6F47" opacity="0.3" />
{/* Sun */}
<circle cx="280" cy="40" r="20" fill="#F5A623" opacity="0.3" />
<circle cx="280" cy="40" r="14" fill="#F5A623" opacity="0.5" />
{/* Sawdust particles */}
<circle cx="120" cy="175" r="1.5" fill="#D4915C" opacity="0.5" />
<circle cx="130" cy="172" r="1" fill="#D4915C" opacity="0.4" />
<circle cx="115" cy="178" r="1.2" fill="#D4915C" opacity="0.3" />
<circle cx="135" cy="176" r="0.8" fill="#D4915C" opacity="0.6" />
</svg>
)
}

View File

@@ -0,0 +1,35 @@
'use client'
export function Logo({ size = 40 }: { size?: number }) {
return (
<div className="flex items-center gap-2">
<svg width={size} height={size} viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Wood log */}
<ellipse cx="24" cy="30" rx="16" ry="10" fill="#D4915C" />
<ellipse cx="24" cy="30" rx="16" ry="10" fill="url(#wood-grain)" opacity="0.3" />
<ellipse cx="24" cy="27" rx="16" ry="10" fill="#E8A96C" />
{/* Tree rings */}
<ellipse cx="24" cy="27" rx="10" ry="6" fill="none" stroke="#D4915C" strokeWidth="1" />
<ellipse cx="24" cy="27" rx="6" ry="3.5" fill="none" stroke="#D4915C" strokeWidth="0.8" />
<ellipse cx="24" cy="27" rx="2.5" ry="1.5" fill="#D4915C" />
{/* Saw */}
<rect x="30" y="6" width="3" height="18" rx="1" fill="#888" transform="rotate(15 30 6)" />
<rect x="29" y="4" width="5" height="5" rx="1" fill="#F5A623" transform="rotate(15 30 6)" />
{/* Saw teeth */}
<path d="M31 10 L34 11 L31 12 L34 13 L31 14 L34 15 L31 16 L34 17 L31 18 L34 19 L31 20" stroke="#666" strokeWidth="0.5" fill="none" transform="rotate(15 30 6)" />
{/* Leaf */}
<path d="M12 8 Q16 2 20 8 Q16 10 12 8Z" fill="#4CAF50" />
<line x1="16" y1="5" x2="16" y2="9" stroke="#388E3C" strokeWidth="0.5" />
<defs>
<pattern id="wood-grain" x="0" y="0" width="4" height="4" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="4" y2="4" stroke="#C4814C" strokeWidth="0.3" />
</pattern>
</defs>
</svg>
<div className="flex flex-col leading-tight">
<span className="font-heading font-bold text-xl text-primary">LEVIS</span>
<span className="font-heading text-sm text-dark/70 -mt-1">Holzbau</span>
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Logo } from './Logo'
const links = [
{ href: '/', label: 'Start' },
{ href: '/projekte', label: 'Projekte' },
{ href: '/sicherheit', label: 'Sicherheit' },
{ href: '/ueber', label: 'Ueber LEVIS' },
]
export function Navbar() {
const pathname = usePathname()
return (
<nav className="bg-white/80 backdrop-blur-sm border-b border-primary/10 sticky top-0 z-50">
<div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
<Link href="/">
<Logo />
</Link>
<div className="flex items-center gap-1 sm:gap-4">
{links.map(({ href, label }) => {
const isActive = href === '/' ? pathname === '/' : pathname.startsWith(href)
return (
<Link
key={href}
href={href}
className={`px-3 py-2 rounded-xl text-sm sm:text-base font-semibold transition-colors ${
isActive
? 'bg-primary/10 text-primary'
: 'text-dark/70 hover:text-primary hover:bg-primary/5'
}`}
>
{label}
</Link>
)
})}
</div>
</div>
</nav>
)
}

View File

@@ -0,0 +1,42 @@
'use client'
import Link from 'next/link'
import { motion } from 'framer-motion'
import { Clock } from 'lucide-react'
import { Project } from '@/lib/types'
import { DifficultyBadge } from './DifficultyBadge'
import { AgeBadge } from './AgeBadge'
import { ProjectIllustration } from './ProjectIllustration'
export function ProjectCard({ project }: { project: Project }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
whileHover={{ y: -4 }}
transition={{ duration: 0.3 }}
>
<Link href={`/projekte/${project.slug}`} className="block">
<div className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden border border-primary/5">
<div className="bg-cream p-6 flex items-center justify-center h-44">
<ProjectIllustration slug={project.slug} size={120} />
</div>
<div className="p-5">
<h3 className="font-heading font-bold text-lg mb-2">{project.name}</h3>
<p className="text-sm text-dark/60 mb-3 line-clamp-2">{project.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<AgeBadge range={project.ageRange} />
<DifficultyBadge level={project.difficulty} />
</div>
<div className="flex items-center gap-1 text-xs text-dark/40">
<Clock className="w-3.5 h-3.5" />
{project.duration}
</div>
</div>
</div>
</div>
</Link>
</motion.div>
)
}

View File

@@ -0,0 +1,132 @@
'use client'
export function ProjectIllustration({ slug, size = 100 }: { slug: string; size?: number }) {
const illustrations: Record<string, React.ReactNode> = {
zauberstab: (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
<rect x="20" y="80" width="60" height="4" rx="2" fill="#D4915C" transform="rotate(-45 50 50)" />
<circle cx="28" cy="28" r="4" fill="#F5A623" opacity="0.6" />
<circle cx="22" cy="35" r="2.5" fill="#FFC107" opacity="0.5" />
<circle cx="35" cy="22" r="2" fill="#FFC107" opacity="0.4" />
<path d="M25 25 L20 18 M25 25 L32 20 M25 25 L22 32" stroke="#F5A623" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="26" cy="26" r="6" fill="none" stroke="#F5A623" strokeWidth="0.5" opacity="0.3" />
</svg>
),
untersetzer: (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
<ellipse cx="50" cy="55" rx="32" ry="8" fill="#C4814C" />
<ellipse cx="50" cy="50" rx="32" ry="8" fill="#E8A96C" />
<ellipse cx="50" cy="50" rx="22" ry="5" fill="none" stroke="#D4915C" strokeWidth="0.8" />
<ellipse cx="50" cy="50" rx="12" ry="2.8" fill="none" stroke="#D4915C" strokeWidth="0.6" />
<circle cx="42" cy="48" r="3" fill="#FF6B6B" opacity="0.5" />
<circle cx="55" cy="46" r="2" fill="#4CAF50" opacity="0.5" />
<circle cx="48" cy="53" r="2.5" fill="#2196F3" opacity="0.4" />
</svg>
),
nagelbilder: (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
<rect x="20" y="20" width="60" height="60" rx="4" fill="#E8A96C" />
{/* Nails forming a star */}
<circle cx="50" cy="30" r="2" fill="#888" />
<circle cx="35" cy="45" r="2" fill="#888" />
<circle cx="65" cy="45" r="2" fill="#888" />
<circle cx="40" cy="65" r="2" fill="#888" />
<circle cx="60" cy="65" r="2" fill="#888" />
{/* String */}
<path d="M50 30 L35 45 L60 65 L40 65 L65 45 Z" stroke="#FF6B6B" strokeWidth="1.5" fill="none" />
<path d="M50 30 L40 65 M50 30 L60 65 M35 45 L65 45" stroke="#4CAF50" strokeWidth="1" fill="none" opacity="0.6" />
</svg>
),
bleistiftbox: (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
<path d="M25 75 L25 35 L75 35 L75 75 Z" fill="#E8A96C" />
<path d="M25 35 L30 30 L80 30 L75 35 Z" fill="#D4915C" />
<path d="M75 35 L80 30 L80 70 L75 75 Z" fill="#C4814C" />
{/* Pencils */}
<rect x="35" y="20" width="4" height="30" rx="1" fill="#FFC107" />
<polygon points="35,50 39,50 37,55" fill="#2C2C2C" />
<rect x="45" y="15" width="4" height="32" rx="1" fill="#2196F3" />
<polygon points="45,47 49,47 47,52" fill="#2C2C2C" />
<rect x="55" y="22" width="4" height="28" rx="1" fill="#FF6B6B" />
<polygon points="55,50 59,50 57,55" fill="#2C2C2C" />
</svg>
),
segelboot: (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
<path d="M20 65 Q50 55 80 65 Q50 72 20 65Z" fill="#E8A96C" />
<line x1="50" y1="25" x2="50" y2="62" stroke="#8B6F47" strokeWidth="2.5" />
<path d="M50 25 L70 50 L50 58Z" fill="white" stroke="#ddd" strokeWidth="0.5" />
<path d="M50 30 L38 52 L50 58Z" fill="#FF6B6B" opacity="0.8" />
{/* Water */}
<path d="M10 72 Q25 68 40 72 Q55 76 70 72 Q85 68 100 72" stroke="#2196F3" strokeWidth="1.5" fill="none" opacity="0.4" />
<path d="M5 78 Q20 74 35 78 Q50 82 65 78 Q80 74 95 78" stroke="#2196F3" strokeWidth="1" fill="none" opacity="0.3" />
</svg>
),
vogelhaus: (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
{/* Roof */}
<path d="M25 45 L50 25 L75 45 Z" fill="#C4814C" />
{/* Body */}
<rect x="30" y="45" width="40" height="35" fill="#E8A96C" />
{/* Entrance hole */}
<circle cx="50" cy="58" r="6" fill="#5D4037" />
{/* Perch */}
<rect x="47" y="65" width="6" height="2" rx="1" fill="#8B6F47" />
<rect x="48" y="67" width="4" height="6" rx="1" fill="#8B6F47" />
{/* Post */}
<rect x="46" y="80" width="8" height="15" rx="1" fill="#8B6F47" />
{/* Bird */}
<ellipse cx="68" cy="40" rx="5" ry="4" fill="#FF6B6B" />
<circle cx="71" cy="38" r="1.5" fill="#2C2C2C" />
<path d="M73 39 L77 38.5" stroke="#F5A623" strokeWidth="1.5" strokeLinecap="round" />
</svg>
),
'holztier-igel': (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
{/* Body */}
<ellipse cx="50" cy="60" rx="25" ry="18" fill="#C4814C" />
{/* Head */}
<ellipse cx="28" cy="58" rx="10" ry="9" fill="#D4915C" />
{/* Nose */}
<circle cx="20" cy="57" r="2" fill="#2C2C2C" />
{/* Eye */}
<circle cx="25" cy="54" r="1.5" fill="#2C2C2C" />
<circle cx="25.5" cy="53.5" r="0.5" fill="white" />
{/* Spines */}
{[0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150].map((angle, i) => {
const rad = (angle - 30) * Math.PI / 180
const x1 = 55 + Math.cos(rad) * 20
const y1 = 52 + Math.sin(rad) * 14
const x2 = 55 + Math.cos(rad) * 30
const y2 = 52 + Math.sin(rad) * 22
return <line key={i} x1={x1} y1={y1} x2={x2} y2={y2} stroke="#8B6F47" strokeWidth="2" strokeLinecap="round" />
})}
{/* Feet */}
<ellipse cx="35" cy="75" rx="4" ry="2" fill="#D4915C" />
<ellipse cx="60" cy="75" rx="4" ry="2" fill="#D4915C" />
</svg>
),
'schnitzfigur-pilz': (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
{/* Stem */}
<path d="M40 55 Q38 75 42 85 L58 85 Q62 75 60 55 Z" fill="#F5F5DC" />
<ellipse cx="50" cy="85" rx="10" ry="3" fill="#E8E0C8" />
{/* Cap */}
<ellipse cx="50" cy="48" rx="28" ry="18" fill="#D32F2F" />
<ellipse cx="50" cy="55" rx="22" ry="5" fill="#E8A96C" />
{/* White dots */}
<circle cx="38" cy="40" r="3" fill="white" opacity="0.9" />
<circle cx="55" cy="35" r="2.5" fill="white" opacity="0.9" />
<circle cx="48" cy="45" r="2" fill="white" opacity="0.8" />
<circle cx="62" cy="42" r="2.5" fill="white" opacity="0.85" />
<circle cx="42" cy="50" r="1.8" fill="white" opacity="0.7" />
{/* Grass */}
<path d="M30 85 Q32 78 34 85" stroke="#4CAF50" strokeWidth="1.5" fill="none" />
<path d="M65 85 Q67 79 69 85" stroke="#4CAF50" strokeWidth="1.5" fill="none" />
<path d="M72 85 Q73 80 75 85" stroke="#4CAF50" strokeWidth="1" fill="none" opacity="0.6" />
</svg>
),
}
return <>{illustrations[slug] || illustrations.zauberstab}</>
}

View File

@@ -0,0 +1,10 @@
import { AlertTriangle } from 'lucide-react'
export function SafetyTip({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-start gap-3 bg-warning/10 border border-warning/30 rounded-xl p-4">
<AlertTriangle className="w-5 h-5 text-warning flex-shrink-0 mt-0.5" />
<p className="text-sm font-medium">{children}</p>
</div>
)
}

View File

@@ -0,0 +1,15 @@
import { Step } from '@/lib/types'
export function StepCard({ step, index }: { step: Step; index: number }) {
return (
<div className="flex gap-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white font-bold text-lg">
{index + 1}
</div>
<div className="flex-1 pb-8 border-l-2 border-primary/20 pl-6 -ml-5 mt-5">
<h3 className="font-heading font-bold text-lg mb-1">{step.title}</h3>
<p className="text-dark/70 leading-relaxed">{step.description}</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,14 @@
import { Hammer, Scissors, Ruler, Paintbrush, Wrench } from 'lucide-react'
const iconMap: Record<string, React.ElementType> = {
hammer: Hammer,
schnitzmesser: Scissors,
lineal: Ruler,
pinsel: Paintbrush,
}
export function ToolIcon({ name }: { name: string }) {
const key = name.toLowerCase()
const Icon = Object.entries(iconMap).find(([k]) => key.includes(k))?.[1] || Wrench
return <Icon className="w-5 h-5 text-primary" />
}