diff --git a/src/components/AvailabilityCalendar.tsx b/src/components/AvailabilityCalendar.tsx
new file mode 100644
index 0000000..2626ab2
--- /dev/null
+++ b/src/components/AvailabilityCalendar.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { Box } from '@mui/material';
+import FullCalendar from '@fullcalendar/react';
+import dayGridPlugin from '@fullcalendar/daygrid';
+import interactionPlugin from '@fullcalendar/interaction';
+
+interface CalendarEvent {
+ title: string;
+ start: string;
+ end: string;
+ display?: 'background' | 'auto';
+ color?: string;
+}
+
+export default function AvailabilityCalendar({ events }: { events: CalendarEvent[] }) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx
new file mode 100644
index 0000000..53ae226
--- /dev/null
+++ b/src/components/HeroSection.tsx
@@ -0,0 +1,234 @@
+'use client';
+
+import { Box, Typography, Container, Chip } from '@mui/material';
+import { Sparkles, Wrench, Bot, Zap } from 'lucide-react';
+import { motion } from 'framer-motion';
+import { useI18n } from '../services/i18n';
+import AIInput from './AIInput';
+
+interface HeroSectionProps {
+ onAISend: (message: string, files?: File[]) => void;
+}
+
+const fadeUp = {
+ hidden: { opacity: 0, y: 20 },
+ show: (i: number) => ({
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.55, delay: 0.1 + i * 0.08, ease: [0.16, 1, 0.3, 1] },
+ }),
+};
+
+export default function HeroSection({ onAISend }: HeroSectionProps) {
+ const { t } = useI18n();
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ label={t('heroBadge')}
+ sx={{
+ bgcolor: 'primary.main',
+ color: 'white',
+ fontWeight: 700,
+ boxShadow: 'var(--pp-shadow-brand)',
+ mb: { xs: 3, md: 4 },
+ px: 1,
+ height: 30,
+ '& .MuiChip-icon': { color: 'white', ml: 0.5 },
+ '& .MuiChip-label': { px: 1.25 },
+ }}
+ />
+
+
+
+
+ {t('heroTitlePart1')}{' '}
+
+ {t('heroTitlePart2')}
+
+
+
+
+
+
+
+ {t('heroSubtitle')}
+
+
+
+
+
+
+
+
+
+
+
+ } label="Inventar" value="Live" />
+ } label="KI-Assistent" value="Gemini" />
+ } label="Antwort" value="< 2s" />
+
+
+
+
+ );
+}
+
+function MiniStat({
+ icon,
+ label,
+ value,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+}) {
+ return (
+
+
+ {icon}
+
+
+
+ {label}
+
+
+ {value}
+
+
+
+ );
+}
diff --git a/src/components/ReservationModal.tsx b/src/components/ReservationModal.tsx
new file mode 100644
index 0000000..7f5bdae
--- /dev/null
+++ b/src/components/ReservationModal.tsx
@@ -0,0 +1,370 @@
+'use client';
+
+import {
+ Dialog,
+ Button,
+ TextField,
+ Typography,
+ Box,
+ Divider,
+ Chip,
+ Paper,
+ IconButton,
+ Stack,
+ useMediaQuery,
+ CircularProgress,
+} from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import { useState, useEffect, Suspense, lazy, useMemo } from 'react';
+import { Tool, User } from '../types';
+import { useReservationStore } from '../services/reservation-store';
+import { useI18n } from '../services/i18n';
+import { Calendar, Info, ShieldCheck, X, Check, MapPin, Clock } from 'lucide-react';
+
+const AvailabilityCalendar = lazy(() => import('./AvailabilityCalendar'));
+
+interface ReservationModalProps {
+ tool: Tool | null;
+ user: User;
+ open: boolean;
+ onClose: () => void;
+}
+
+const pillSx = {
+ height: 22,
+ fontWeight: 600,
+ fontSize: '0.7rem',
+ borderRadius: 1.25,
+ '& .MuiChip-icon': { marginLeft: '6px', marginRight: '-2px' },
+ '& .MuiChip-label': { px: 0.75 },
+} as const;
+
+export default function ReservationModal({
+ tool,
+ user,
+ open,
+ onClose,
+}: ReservationModalProps) {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+ const { t } = useI18n();
+
+ const reservations = useReservationStore((s) => s.reservations);
+ const addReservation = useReservationStore((s) => s.addReservation);
+
+ const today = useMemo(() => new Date().toISOString().split('T')[0], []);
+ const [startDate, setStartDate] = useState(today);
+ const [endDate, setEndDate] = useState('');
+ const [submitted, setSubmitted] = useState(false);
+
+ useEffect(() => {
+ if (open) {
+ setStartDate(today);
+ setEndDate('');
+ setSubmitted(false);
+ }
+ }, [open, today]);
+
+ if (!tool) return null;
+
+ const toolEvents = reservations
+ .filter((r) => r.toolId === tool.id && r.status !== 'completed')
+ .map((r) => ({
+ title: 'Belegt',
+ start: r.startDate,
+ end: r.endDate,
+ display: 'background' as const,
+ color: '#F59E0B',
+ }));
+
+ const duration = (() => {
+ if (!startDate || !endDate) return 0;
+ const ms = new Date(endDate).getTime() - new Date(startDate).getTime();
+ return Math.max(0, Math.round(ms / 86_400_000) + 1);
+ })();
+ const exceedsMax = duration > tool.maxRentalDays;
+ const invalidRange = Boolean(endDate) && endDate < startDate;
+ const hasTraining =
+ !tool.trainingRequired || user.trainings.includes(tool.requiredTrainingId!);
+ const canReserve =
+ !!startDate &&
+ !!endDate &&
+ !exceedsMax &&
+ !invalidRange &&
+ (hasTraining || user.role === 'admin');
+
+ const handleSubmit = () => {
+ if (!canReserve) return;
+ addReservation({ userId: user.id, toolId: tool.id, startDate, endDate });
+ setSubmitted(true);
+ setTimeout(onClose, 1100);
+ };
+
+ return (
+
+ );
+}
+
+function LegendDot({
+ color,
+ label,
+ outline,
+}: {
+ color?: string;
+ label: string;
+ outline?: boolean;
+}) {
+ return (
+
+
+
+ {label}
+
+
+ );
+}
diff --git a/src/components/ToolCard.tsx b/src/components/ToolCard.tsx
new file mode 100644
index 0000000..36219bd
--- /dev/null
+++ b/src/components/ToolCard.tsx
@@ -0,0 +1,200 @@
+'use client';
+
+import { Card, CardContent, Typography, Box, Chip, Button } from '@mui/material';
+import { MapPin, Clock, ShieldCheck, AlertTriangle, Home } from 'lucide-react';
+import { Tool, User, CATEGORY_LABEL } from '../types';
+import { useI18n } from '../services/i18n';
+
+interface ToolCardProps {
+ tool: Tool;
+ user: User;
+ onReserve: (tool: Tool) => void;
+}
+
+const pillSx = {
+ height: 22,
+ fontWeight: 600,
+ fontSize: '0.7rem',
+ borderRadius: 1.25,
+ '& .MuiChip-icon': { marginLeft: '6px', marginRight: '-2px' },
+ '& .MuiChip-label': { px: 0.75 },
+} as const;
+
+export default function ToolCard({ tool, user, onReserve }: ToolCardProps) {
+ const hasTraining =
+ !tool.trainingRequired || user.trainings.includes(tool.requiredTrainingId!);
+ const isLocked = !hasTraining && user.role !== 'admin';
+ const { t } = useI18n();
+
+ return (
+
+
+
+ {/* Header strip with emoji + category */}
+
+
+ {tool.emoji}
+
+
+
+
+
+
+ {tool.name}
+
+
+ {tool.description}
+
+
+
+ {tool.trainingRequired ? (
+ : }
+ label={hasTraining ? t('certified') : t('trainingMissing')}
+ color={hasTraining ? 'success' : 'warning'}
+ size="small"
+ sx={pillSx}
+ />
+ ) : (
+
+ )}
+ {tool.homeAllowed && (
+ }
+ label={t('homeUse')}
+ size="small"
+ variant="outlined"
+ sx={pillSx}
+ />
+ )}
+
+
+
+
+
+ {tool.location}
+
+
+
+ max. {tool.maxRentalDays} {t('days')}
+
+
+
+
+
+
+ );
+}