diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..2e7299c --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,770 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { + Box, + Container, + TextField, + Button, + Typography, + Paper, + AppBar, + Toolbar, + IconButton, + Chip, + Menu, + MenuItem, + Drawer, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Divider, + useMediaQuery, + Tooltip, + InputAdornment, + Avatar, + ToggleButtonGroup, + ToggleButton, +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Sun, + Moon, + Languages, + LogOut, + User as UserIcon, + Settings, + Menu as MenuIcon, + MessageSquare, + Wrench, + ShieldCheck, + Lock, + ChevronRight, + Search, + X, +} from 'lucide-react'; +import Logo from '../components/Logo'; + +import { useAuthStore } from '../services/auth-store'; +import { useColorMode } from '../services/color-mode'; +import { useChatStore } from '../services/chat-store'; +import { useI18n } from '../services/i18n'; +import { useToolsStore } from '../services/tools-store'; +import { Tool, ToolCategory, CATEGORY_LABEL } from '../types'; + +import ChatSidebar from '../components/ChatSidebar'; +import ChatWindow from '../components/ChatWindow'; +import ToolCard from '../components/ToolCard'; +import ReservationModal from '../components/ReservationModal'; +import Onboarding from '../components/Onboarding'; +import HeroSection from '../components/HeroSection'; +import AdminDashboard from '../components/AdminDashboard'; +import AnimatedBackground from '../components/AnimatedBackground'; + +type View = 'tools' | 'chat' | 'admin'; + +const APPBAR_HEIGHT = 64; + +export default function Home() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const isSmall = useMediaQuery(theme.breakpoints.down('sm')); + + const { user, login, logout } = useAuthStore(); + const { mode, toggleColorMode } = useColorMode(); + const { t, lang, setLang } = useI18n(); + const { createSession, addMessage } = useChatStore(); + const tools = useToolsStore((s) => s.tools); + + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loginError, setLoginError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const [anchorEl, setAnchorEl] = useState(null); + const openLangMenu = Boolean(anchorEl); + + const [view, setView] = useState('tools'); + const [drawerOpen, setDrawerOpen] = useState(false); + + const [showOnboarding, setShowOnboarding] = useState(false); + const [selectedTool, setSelectedTool] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + // Tools page filters + const [search, setSearch] = useState(''); + const [categoryFilter, setCategoryFilter] = useState('all'); + + const navItems = useMemo( + () => + [ + { id: 'tools' as View, label: t('tools'), icon: }, + { id: 'chat' as View, label: t('chat'), icon: }, + ...(user?.role === 'admin' + ? [{ id: 'admin' as View, label: t('admin'), icon: }] + : []), + ], + [user, t] + ); + + const filteredTools = useMemo(() => { + const q = search.trim().toLowerCase(); + return tools.filter((tool) => { + if (categoryFilter !== 'all' && tool.category !== categoryFilter) return false; + if (!q) return true; + return ( + tool.name.toLowerCase().includes(q) || + tool.description.toLowerCase().includes(q) || + tool.location.toLowerCase().includes(q) || + CATEGORY_LABEL[tool.category].toLowerCase().includes(q) + ); + }); + }, [tools, search, categoryFilter]); + + const handleLangClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleLangClose = (newLang?: 'de' | 'en') => { + if (newLang) setLang(newLang); + setAnchorEl(null); + }; + + const handleLogin = (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + setLoginError(null); + const ok = login(username.trim(), password); + if (ok) { + setShowOnboarding(true); + } else { + setLoginError(t('loginError')); + } + setSubmitting(false); + }; + + const openReservation = (tool: Tool) => { + setSelectedTool(tool); + setIsModalOpen(true); + }; + + const handleAISend = (message: string) => { + const sessionId = createSession(message.substring(0, 30) || 'Neue Reparatur'); + setView('chat'); + addMessage(sessionId, { role: 'user', content: message }); + }; + + const navigate = (next: View) => { + setView(next); + setDrawerOpen(false); + }; + + if (!user) { + return ( + + ); + } + + const usedCategories = Array.from(new Set(tools.map((t) => t.category))) as ToolCategory[]; + + return ( + + + + + + + {isMobile && ( + setDrawerOpen(true)} + aria-label="Menü öffnen" + sx={{ mr: 0.5 }} + > + + + )} + + navigate('tools')} + > + + + PATCHPILOT + + + + {!isMobile && ( + + {navItems.map((item) => ( + + ))} + + )} + + {isMobile && } + + + + + + + handleLangClose()}> + handleLangClose('de')} selected={lang === 'de'}> + Deutsch + + handleLangClose('en')} selected={lang === 'en'}> + English + + + + + + {mode === 'dark' ? : } + + + + {!isSmall && ( + } + label={user.username} + sx={{ ml: 1, fontWeight: 600, borderRadius: 1.5 }} + size="small" + /> + )} + + + + + + + + + setDrawerOpen(false)}> + + + + + {user.username.charAt(0).toUpperCase()} + + + + {user.username} + + + {user.role === 'admin' ? t('administrator') : user.role === 'staff' ? t('staffRole') : t('guestRole')} + + + + + + + {navItems.map((item) => ( + + navigate(item.id)} + sx={{ + borderRadius: 2, + '&.Mui-selected': { + bgcolor: 'primary.main', + color: 'white', + '& .MuiListItemIcon-root': { color: 'white' }, + '&:hover': { bgcolor: 'primary.dark' }, + }, + }} + > + {item.icon} + + + + ))} + + + + + + + + + + + {view === 'tools' ? ( + + + + + + + {t('inventory')} + + + {filteredTools.length} / {tools.length} {t('inventoryHint')} + + + setSearch(e.target.value)} + sx={{ minWidth: { xs: '100%', sm: 280 } }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: search ? ( + + setSearch('')}> + + + + ) : null, + }} + /> + + + + v && setCategoryFilter(v)} + size="small" + sx={{ + '& .MuiToggleButton-root': { + textTransform: 'none', + fontWeight: 600, + fontSize: '0.78rem', + borderRadius: '999px !important', + mx: 0.25, + border: '1px solid', + borderColor: 'divider', + px: 1.5, + py: 0.5, + height: 30, + '&.Mui-selected': { + bgcolor: 'primary.main', + color: 'white', + borderColor: 'primary.main', + '&:hover': { bgcolor: 'primary.dark' }, + }, + }, + }} + > + {t('all')} + {usedCategories.map((cat) => ( + + {CATEGORY_LABEL[cat]} + + ))} + + + + {filteredTools.length === 0 ? ( + + {t('noToolsFound')} + + ) : ( + + {filteredTools.map((tool, idx) => ( + + + + ))} + + )} + + + ) : view === 'chat' ? ( + + + + + ) : ( + + )} + + + + + setIsModalOpen(false)} + /> + + setShowOnboarding(false)} /> + + ); +} + +/* ---------- Login Screen ---------- */ + +interface LoginScreenProps { + username: string; + password: string; + setUsername: (v: string) => void; + setPassword: (v: string) => void; + submitting: boolean; + loginError: string | null; + onSubmit: (e: React.FormEvent) => void; + t: (k: any) => string; + mode: 'light' | 'dark'; + toggleColorMode: () => void; +} + +function LoginScreen({ + username, + password, + setUsername, + setPassword, + submitting, + loginError, + onSubmit, + t, + mode, + toggleColorMode, +}: LoginScreenProps) { + return ( + + + + + {mode === 'dark' ? : } + + + + + + + + + + + PATCHPILOT + + + + + + + {t('heroTitlePart1')}
{t('heroTitlePart2')} +
+ + {t('heroSubtitle')} + + + } title={t('miniStatInventory')} desc="Werkzeug-Inventar" /> + } title={t('chat')} desc="Schritt für Schritt" /> + } title="Sicher" desc="Einweisungs-Tracking" /> + } title={t('admin')} desc="Verwaltung" /> + +
+
+ + + © {new Date().getFullYear()} PatchPilot · Hackathon Edition + +
+ + + + + + + + + PATCHPILOT + + + + + {t('loginWelcomeBack')} + + + {t('loginSubtitle')} + + +
+ setUsername(e.target.value)} + autoFocus + autoComplete="username" + margin="normal" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + setPassword(e.target.value)} + autoComplete="current-password" + margin="normal" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + {loginError && ( + + {loginError} + + )} + + + + + + + {t('loginDemoTitle')} + + + {t('loginDemoBody')} + + +
+
+
+ ); +} + +function Feature({ icon, title, desc }: { icon: React.ReactNode; title: string; desc: string }) { + return ( + + + {icon} + + {title} + + + + {desc} + + + ); +} diff --git a/src/components/Onboarding.tsx b/src/components/Onboarding.tsx new file mode 100644 index 0000000..44be34f --- /dev/null +++ b/src/components/Onboarding.tsx @@ -0,0 +1,701 @@ +'use client'; + +import { + Dialog, + Button, + Typography, + Box, + IconButton, + LinearProgress, + Chip, + Paper, + TextField, + Avatar, +} from '@mui/material'; +import { + Hammer, + MessageSquare, + ArrowRight, + Check, + Calendar, + Shield, + X, + Sparkles, + ShieldCheck, + MapPin, + Send, + Bot, + User as UserIcon, +} from 'lucide-react'; +import Logo from './Logo'; +import { useMemo, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useI18n } from '../services/i18n'; +import { useAuthStore } from '../services/auth-store'; + +interface OnboardingProps { + open: boolean; + onFinish: () => void; +} + +type Step = { + key: string; + title: string; + subtitle: string; + icon: React.ReactNode; + visual: React.ReactNode; + action?: { label: string; hint?: string }; +}; + +export default function Onboarding({ open, onFinish }: OnboardingProps) { + const [activeStep, setActiveStep] = useState(0); + const [pickedExample, setPickedExample] = useState(null); + const [demoInput, setDemoInput] = useState(''); + const { t } = useI18n(); + const { user } = useAuthStore(); + + const steps = useMemo(() => { + const base: Step[] = [ + { + key: 'welcome', + title: `Willkommen, ${user?.username ?? 'Helfer'}!`, + subtitle: + 'In wenigen Sekunden zeige ich dir, wie du Werkzeuge ausleihst, dich vom KI-Assistenten beraten lässt und alles auf einen Blick siehst.', + icon: , + visual: , + }, + { + key: 'tools', + title: 'Werkzeuge entdecken', + subtitle: + 'Wähle aus unserem Inventar — jedes Werkzeug zeigt dir Standort, Leihdauer und ob eine Einweisung nötig ist.', + icon: , + visual: ( + setPickedExample(name)} + /> + ), + action: { + label: 'Probier es: klick auf ein Werkzeug', + hint: pickedExample ? `✓ Gewählt: ${pickedExample}` : undefined, + }, + }, + { + key: 'chat', + title: 'KI-Assistent nutzen', + subtitle: + 'Beschreibe dein Problem in eigenen Worten — RepairBuddy hilft dir bei der Diagnose und schlägt Reparaturansätze vor.', + icon: , + visual: , + action: { + label: 'Tippe einen Beispiel-Satz oben ein', + hint: demoInput.length > 4 ? `✓ Beispiel erkannt (${demoInput.length} Zeichen)` : undefined, + }, + }, + { + key: 'calendar', + title: 'Verfügbarkeit prüfen', + subtitle: + 'Im interaktiven Kalender siehst du sofort, wann Werkzeuge frei sind und wann sie belegt sind.', + icon: , + visual: , + }, + ]; + + if (user?.role === 'admin') { + base.push({ + key: 'admin', + title: 'Admin-Bereich', + subtitle: + 'Als Admin verwaltest du Werkzeuge, Nutzer, das KI-Wissen und siehst alle Reservierungen auf einen Blick.', + icon: , + visual: , + }); + } + + return base; + }, [user, pickedExample, demoInput]); + + const totalSteps = steps.length; + const isLast = activeStep === totalSteps - 1; + const current = steps[activeStep]; + const progress = ((activeStep + 1) / totalSteps) * 100; + + const handleNext = () => { + if (isLast) { + reset(); + onFinish(); + } else { + setActiveStep((p) => p + 1); + } + }; + + const handleBack = () => setActiveStep((p) => Math.max(0, p - 1)); + + const handleSkip = () => { + reset(); + onFinish(); + }; + + const reset = () => { + setActiveStep(0); + setPickedExample(null); + setDemoInput(''); + }; + + return ( + + + {/* Header: progress + close */} + + + + + + + Onboarding · Schritt {activeStep + 1} von {totalSteps} + + + + + + + + + + + {/* Body */} + + {/* Visual */} + + + + {current.visual} + + + + + {/* Text */} + + + + + {current.icon} + {current.key} + + + {current.title} + + + {current.subtitle} + + + {current.action && ( + + + + + {current.action.label} + + {current.action.hint && ( + + {current.action.hint} + + )} + + + )} + + + + + + + {steps.map((s, i) => ( + + ))} + + + + + + + + ); +} + +/* ------------ Visuals ------------ */ + +function WelcomeVisual() { + return ( + + + + + + + + + + + + ); +} + +function ToolsVisual({ + picked, + onPick, +}: { + picked: string | null; + onPick: (name: string) => void; +}) { + const samples = [ + { name: 'Bohrmaschine', loc: 'Werkstatt A', cert: true }, + { name: '3D-Drucker', loc: 'Labor 1', cert: true }, + { name: 'Hammer', loc: 'Werkzeugwand', cert: false }, + ]; + return ( + + {samples.map((s) => { + const selected = picked === s.name; + return ( + onPick(s.name)} + sx={{ + p: 1.5, + borderRadius: 2.5, + display: 'flex', + alignItems: 'center', + gap: 1.5, + cursor: 'pointer', + transition: 'all 0.18s ease', + borderColor: selected ? 'primary.main' : 'divider', + bgcolor: selected ? 'rgba(255,107,0,0.08)' : 'background.paper', + boxShadow: selected ? 'var(--pp-shadow-brand)' : 'none', + }} + > + + + + + + {s.name} + + + + {s.loc} + + + {s.cert ? ( + } + label="Einweisung" + size="small" + color="warning" + sx={{ height: 22, fontSize: '0.7rem', borderRadius: 1.25 }} + /> + ) : ( + + )} + + ); + })} + + ); +} + +function ChatVisual({ + demoInput, + onChange, +}: { + demoInput: string; + onChange: (v: string) => void; +}) { + return ( + + + + + + + + Hi! Was möchtest du heute reparieren? + + + + {demoInput.length > 0 && ( + + + + + + + {demoInput} + + + + )} + + onChange(e.target.value)} + InputProps={{ disableUnderline: true, sx: { px: 1, fontSize: '0.85rem' } }} + /> + + + + + + ); +} + +function CalendarVisual() { + const days = Array.from({ length: 28 }, (_, i) => i + 1); + const reserved = new Set([5, 6, 12, 18, 19, 20, 25]); + const today = 10; + + return ( + + + + Mai 2026 + + + + + + Belegt + + + + + + {['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((d) => ( + + {d} + + ))} + {days.map((d) => { + const isReserved = reserved.has(d); + const isToday = d === today; + return ( + + {d} + + ); + })} + + + ); +} + +function AdminVisual() { + return ( + + {[ + { label: 'Werkzeuge', value: '3', accent: '#FF6B00' }, + { label: 'Aktive Reservierungen', value: '2', accent: '#3B82F6' }, + { label: 'Offene Anfragen', value: '1', accent: '#F59E0B' }, + { label: 'Nutzer', value: '2', accent: '#22C55E' }, + ].map((s) => ( + + + + + {s.label} + + + + {s.value} + + + ))} + + ); +}