feat: add onboarding walkthrough and wire all views into main page

This commit is contained in:
MrFrostDev
2026-05-23 13:58:27 +02:00
parent 14ede6aab0
commit 03ba312b89
2 changed files with 1471 additions and 0 deletions
+770
View File
@@ -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<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const openLangMenu = Boolean(anchorEl);
const [view, setView] = useState<View>('tools');
const [drawerOpen, setDrawerOpen] = useState(false);
const [showOnboarding, setShowOnboarding] = useState(false);
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
// Tools page filters
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState<ToolCategory | 'all'>('all');
const navItems = useMemo(
() =>
[
{ id: 'tools' as View, label: t('tools'), icon: <Wrench size={18} /> },
{ id: 'chat' as View, label: t('chat'), icon: <MessageSquare size={18} /> },
...(user?.role === 'admin'
? [{ id: 'admin' as View, label: t('admin'), icon: <Settings size={18} /> }]
: []),
],
[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<HTMLButtonElement>) => {
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 (
<LoginScreen
username={username}
password={password}
setUsername={setUsername}
setPassword={setPassword}
submitting={submitting}
loginError={loginError}
onSubmit={handleLogin}
t={t}
mode={mode}
toggleColorMode={toggleColorMode}
/>
);
}
const usedCategories = Array.from(new Set(tools.map((t) => t.category))) as ToolCategory[];
return (
<Box
sx={{
position: 'relative',
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
}}
>
<AnimatedBackground intensity={view === 'tools' ? 1 : 0.6} />
<Box sx={{ position: 'relative', zIndex: 1, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<AppBar position="sticky" elevation={0} sx={{ color: 'text.primary' }}>
<Toolbar sx={{ minHeight: { xs: APPBAR_HEIGHT, sm: APPBAR_HEIGHT }, gap: 1 }}>
{isMobile && (
<IconButton
edge="start"
onClick={() => setDrawerOpen(true)}
aria-label="Menü öffnen"
sx={{ mr: 0.5 }}
>
<MenuIcon size={22} />
</IconButton>
)}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.25,
cursor: 'pointer',
userSelect: 'none',
flexGrow: { xs: 1, md: 0 },
}}
onClick={() => navigate('tools')}
>
<Logo size={36} />
<Typography
variant="h6"
sx={{
fontWeight: 900,
letterSpacing: '-0.02em',
fontSize: { xs: '1.05rem', sm: '1.15rem' },
display: { xs: isSmall ? 'none' : 'block', sm: 'block' },
}}
>
PATCH<Box component="span" sx={{ color: 'primary.main' }}>PILOT</Box>
</Typography>
</Box>
{!isMobile && (
<Box sx={{ ml: 3, display: 'flex', gap: 0.5, flexGrow: 1 }}>
{navItems.map((item) => (
<Button
key={item.id}
onClick={() => navigate(item.id)}
startIcon={item.icon}
variant={view === item.id ? 'contained' : 'text'}
color={view === item.id ? 'primary' : 'inherit'}
sx={{
px: 2,
fontWeight: view === item.id ? 700 : 500,
boxShadow: view === item.id ? 'var(--pp-shadow-brand)' : 'none',
}}
>
{item.label}
</Button>
))}
</Box>
)}
{isMobile && <Box sx={{ flexGrow: 1 }} />}
<Tooltip title="Sprache wechseln">
<IconButton onClick={handleLangClick} size="small">
<Languages size={20} />
</IconButton>
</Tooltip>
<Menu anchorEl={anchorEl} open={openLangMenu} onClose={() => handleLangClose()}>
<MenuItem onClick={() => handleLangClose('de')} selected={lang === 'de'}>
Deutsch
</MenuItem>
<MenuItem onClick={() => handleLangClose('en')} selected={lang === 'en'}>
English
</MenuItem>
</Menu>
<Tooltip title={mode === 'dark' ? 'Light' : 'Dark'}>
<IconButton onClick={toggleColorMode} size="small">
{mode === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
</IconButton>
</Tooltip>
{!isSmall && (
<Chip
icon={<UserIcon size={14} />}
label={user.username}
sx={{ ml: 1, fontWeight: 600, borderRadius: 1.5 }}
size="small"
/>
)}
<Tooltip title={t('logout')}>
<IconButton onClick={logout} size="small">
<LogOut size={20} />
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>
<Drawer anchor="left" open={drawerOpen} onClose={() => setDrawerOpen(false)}>
<Box sx={{ width: 280, pt: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ px: 2.5, pb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Avatar
sx={{
bgcolor: 'primary.main',
width: 40,
height: 40,
fontWeight: 700,
}}
>
{user.username.charAt(0).toUpperCase()}
</Avatar>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
{user.username}
</Typography>
<Typography variant="caption" color="text.secondary">
{user.role === 'admin' ? t('administrator') : user.role === 'staff' ? t('staffRole') : t('guestRole')}
</Typography>
</Box>
</Box>
</Box>
<Divider />
<List sx={{ flexGrow: 1, p: 1 }}>
{navItems.map((item) => (
<ListItem key={item.id} disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
selected={view === item.id}
onClick={() => navigate(item.id)}
sx={{
borderRadius: 2,
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'white',
'& .MuiListItemIcon-root': { color: 'white' },
'&:hover': { bgcolor: 'primary.dark' },
},
}}
>
<ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>
<ListItemText primary={item.label} primaryTypographyProps={{ fontWeight: 600 }} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<Box sx={{ p: 1.5 }}>
<Button
fullWidth
variant="outlined"
startIcon={<LogOut size={18} />}
onClick={logout}
color="inherit"
>
{t('logout')}
</Button>
</Box>
</Box>
</Drawer>
<AnimatePresence mode="wait">
<motion.div
key={view}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
style={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}
>
{view === 'tools' ? (
<Box sx={{ pb: { xs: 6, md: 10 } }}>
<HeroSection onAISend={handleAISend} />
<Container maxWidth="lg" sx={{ mt: { xs: 2, md: 4 } }}>
<Box
sx={{
display: 'flex',
alignItems: { xs: 'stretch', md: 'center' },
justifyContent: 'space-between',
mb: { xs: 2, md: 3 },
flexDirection: { xs: 'column', md: 'row' },
gap: { xs: 1.5, md: 2 },
}}
>
<Box>
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.015em' }}>
{t('inventory')}
</Typography>
<Typography variant="caption" color="text.secondary">
{filteredTools.length} / {tools.length} {t('inventoryHint')}
</Typography>
</Box>
<TextField
size="small"
placeholder={t('search')}
value={search}
onChange={(e) => setSearch(e.target.value)}
sx={{ minWidth: { xs: '100%', sm: 280 } }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search size={16} />
</InputAdornment>
),
endAdornment: search ? (
<InputAdornment position="end">
<IconButton size="small" onClick={() => setSearch('')}>
<X size={14} />
</IconButton>
</InputAdornment>
) : null,
}}
/>
</Box>
<Box sx={{ mb: { xs: 2.5, md: 3 }, overflowX: 'auto', pb: 0.5 }}>
<ToggleButtonGroup
value={categoryFilter}
exclusive
onChange={(_, v) => 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' },
},
},
}}
>
<ToggleButton value="all">{t('all')}</ToggleButton>
{usedCategories.map((cat) => (
<ToggleButton key={cat} value={cat}>
{CATEGORY_LABEL[cat]}
</ToggleButton>
))}
</ToggleButtonGroup>
</Box>
{filteredTools.length === 0 ? (
<Paper
variant="outlined"
sx={{
p: 6,
textAlign: 'center',
borderRadius: 3,
borderStyle: 'dashed',
}}
>
<Typography color="text.secondary">{t('noToolsFound')}</Typography>
</Paper>
) : (
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
lg: 'repeat(3, 1fr)',
},
gap: { xs: 2, md: 2.5 },
}}
>
{filteredTools.map((tool, idx) => (
<motion.div
key={tool.id}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.03 * idx, ease: 'easeOut' }}
>
<ToolCard tool={tool} user={user} onReserve={openReservation} />
</motion.div>
))}
</Box>
)}
</Container>
</Box>
) : view === 'chat' ? (
<Box
sx={{
flexGrow: 1,
display: 'flex',
overflow: 'hidden',
height: `calc(100vh - ${APPBAR_HEIGHT}px)`,
}}
>
<ChatSidebar />
<ChatWindow />
</Box>
) : (
<AdminDashboard />
)}
</motion.div>
</AnimatePresence>
</Box>
<ReservationModal
open={isModalOpen}
tool={selectedTool}
user={user}
onClose={() => setIsModalOpen(false)}
/>
<Onboarding open={showOnboarding} onFinish={() => setShowOnboarding(false)} />
</Box>
);
}
/* ---------- 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 (
<Box
sx={{
position: 'relative',
minHeight: '100vh',
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
overflow: 'hidden',
}}
>
<AnimatedBackground intensity={0.7} />
<IconButton
onClick={toggleColorMode}
sx={{ position: 'absolute', top: 16, right: 16, zIndex: 5 }}
>
{mode === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
</IconButton>
<Box
sx={{
position: 'relative',
display: { xs: 'none', md: 'flex' },
flexDirection: 'column',
justifyContent: 'space-between',
color: 'white',
p: 6,
overflow: 'hidden',
background:
'radial-gradient(circle at 0% 0%, rgba(255,107,0,0.95), rgba(229,95,0,0.95) 45%, rgba(45,90,39,0.9) 100%)',
}}
>
<Box className="pp-grid-bg" sx={{ position: 'absolute', inset: 0, opacity: 0.18 }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, position: 'relative' }}>
<Box
sx={{
width: 48,
height: 48,
borderRadius: 2.5,
bgcolor: 'rgba(255,255,255,0.95)',
p: 0.5,
border: '1px solid rgba(255,255,255,0.28)',
boxShadow: '0 8px 20px -8px rgba(0,0,0,0.35)',
display: 'grid',
placeItems: 'center',
}}
>
<Logo size={36} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 900, letterSpacing: '-0.02em' }}>
PATCHPILOT
</Typography>
</Box>
<Box sx={{ position: 'relative', maxWidth: 460 }}>
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
<Typography
variant="h2"
sx={{ fontWeight: 900, letterSpacing: '-0.03em', lineHeight: 1.05, mb: 2 }}
>
{t('heroTitlePart1')}<br />{t('heroTitlePart2')}
</Typography>
<Typography variant="body1" sx={{ opacity: 0.92, fontSize: '1.05rem', mb: 4 }}>
{t('heroSubtitle')}
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
<Feature icon={<Wrench size={18} />} title={t('miniStatInventory')} desc="Werkzeug-Inventar" />
<Feature icon={<MessageSquare size={18} />} title={t('chat')} desc="Schritt für Schritt" />
<Feature icon={<ShieldCheck size={18} />} title="Sicher" desc="Einweisungs-Tracking" />
<Feature icon={<Settings size={18} />} title={t('admin')} desc="Verwaltung" />
</Box>
</motion.div>
</Box>
<Typography variant="caption" sx={{ opacity: 0.75, position: 'relative' }}>
© {new Date().getFullYear()} PatchPilot · Hackathon Edition
</Typography>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: { xs: 3, sm: 4, md: 6 },
position: 'relative',
zIndex: 1,
}}
>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
style={{ width: '100%', maxWidth: 420, position: 'relative' }}
>
<Box sx={{ display: { md: 'none' }, mb: 4, textAlign: 'center' }}>
<Box sx={{ display: 'inline-block', mb: 2 }}>
<Logo size={64} />
</Box>
<Typography variant="h4" sx={{ fontWeight: 900, letterSpacing: '-0.02em' }}>
PATCHPILOT
</Typography>
</Box>
<Typography variant="h4" sx={{ fontWeight: 800, mb: 1, letterSpacing: '-0.02em' }}>
{t('loginWelcomeBack')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
{t('loginSubtitle')}
</Typography>
<form onSubmit={onSubmit}>
<TextField
fullWidth
label={t('username')}
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
autoComplete="username"
margin="normal"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<UserIcon size={18} />
</InputAdornment>
),
}}
/>
<TextField
fullWidth
label={t('password')}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
margin="normal"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Lock size={18} />
</InputAdornment>
),
}}
/>
{loginError && (
<Paper
elevation={0}
sx={{
mt: 2,
p: 1.5,
bgcolor: 'error.main',
color: 'white',
borderRadius: 2,
fontSize: '0.85rem',
border: 'none',
}}
>
{loginError}
</Paper>
)}
<Button
fullWidth
size="large"
variant="contained"
type="submit"
disabled={submitting || !username || !password}
endIcon={<ChevronRight size={18} />}
sx={{ mt: 3, py: 1.5, fontSize: '1rem' }}
>
{submitting ? t('loginSubmitting') : t('loginSubmit')}
</Button>
</form>
<Paper
variant="outlined"
sx={{
mt: 4,
p: 2,
borderRadius: 2,
bgcolor: 'action.hover',
fontSize: '0.8rem',
border: '1px dashed',
borderColor: 'divider',
}}
>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.primary' }}>
{t('loginDemoTitle')}
</Typography>
<Typography
variant="caption"
sx={{ display: 'block', color: 'text.secondary', mt: 0.5, whiteSpace: 'pre-line' }}
>
{t('loginDemoBody')}
</Typography>
</Paper>
</motion.div>
</Box>
</Box>
);
}
function Feature({ icon, title, desc }: { icon: React.ReactNode; title: string; desc: string }) {
return (
<Box
sx={{
bgcolor: 'rgba(255,255,255,0.12)',
backdropFilter: 'blur(8px)',
borderRadius: 2.5,
p: 2,
border: '1px solid rgba(255,255,255,0.18)',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
{icon}
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{title}
</Typography>
</Box>
<Typography variant="caption" sx={{ opacity: 0.85 }}>
{desc}
</Typography>
</Box>
);
}
+701
View File
@@ -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<string | null>(null);
const [demoInput, setDemoInput] = useState('');
const { t } = useI18n();
const { user } = useAuthStore();
const steps = useMemo<Step[]>(() => {
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: <Hammer size={22} />,
visual: <WelcomeVisual />,
},
{
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: <Hammer size={22} />,
visual: (
<ToolsVisual
picked={pickedExample}
onPick={(name) => 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: <MessageSquare size={22} />,
visual: <ChatVisual demoInput={demoInput} onChange={setDemoInput} />,
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: <Calendar size={22} />,
visual: <CalendarVisual />,
},
];
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: <Shield size={22} />,
visual: <AdminVisual />,
});
}
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 (
<Dialog
open={open}
onClose={handleSkip}
fullWidth
maxWidth="md"
PaperProps={{
sx: {
borderRadius: { xs: 3, md: 4 },
overflow: 'hidden',
bgcolor: 'background.paper',
backgroundImage: 'none',
},
}}
>
<Box sx={{ position: 'relative' }}>
{/* Header: progress + close */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
px: { xs: 2, md: 3 },
py: 2,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 28,
height: 28,
borderRadius: 1.5,
display: 'grid',
placeItems: 'center',
background: 'linear-gradient(135deg, #FF6B00, #E55F00)',
color: 'white',
}}
>
<Sparkles size={14} />
</Box>
<Typography variant="caption" sx={{ fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
Onboarding · Schritt {activeStep + 1} von {totalSteps}
</Typography>
</Box>
<Box sx={{ flexGrow: 1 }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 999,
bgcolor: 'action.hover',
'& .MuiLinearProgress-bar': {
background: 'linear-gradient(90deg, #FF6B00, #E55F00)',
borderRadius: 999,
},
}}
/>
</Box>
<IconButton onClick={handleSkip} size="small" aria-label="Überspringen">
<X size={16} />
</IconButton>
</Box>
{/* Body */}
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
minHeight: { md: 400 },
}}
>
{/* Visual */}
<Box
sx={{
position: 'relative',
p: { xs: 3, md: 4 },
bgcolor: 'background.default',
backgroundImage: 'none',
borderRight: { md: '1px solid' },
borderBottom: { xs: '1px solid', md: 'none' },
borderColor: 'divider',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}
>
<AnimatePresence mode="wait">
<motion.div
key={current.key}
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.02 }}
transition={{ duration: 0.28, ease: 'easeOut' }}
style={{ width: '100%' }}
>
{current.visual}
</motion.div>
</AnimatePresence>
</Box>
{/* Text */}
<Box
sx={{
p: { xs: 3, md: 4 },
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
gap: 2,
}}
>
<AnimatePresence mode="wait">
<motion.div
key={current.key}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
transition={{ duration: 0.22 }}
>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
color: 'primary.main',
fontWeight: 700,
fontSize: '0.75rem',
letterSpacing: '0.06em',
textTransform: 'uppercase',
mb: 1.5,
}}
>
{current.icon}
<span>{current.key}</span>
</Box>
<Typography variant="h5" sx={{ fontWeight: 800, mb: 1.5, letterSpacing: '-0.015em' }}>
{current.title}
</Typography>
<Typography color="text.secondary" sx={{ lineHeight: 1.6, mb: 2 }}>
{current.subtitle}
</Typography>
{current.action && (
<Paper
variant="outlined"
sx={{
p: 1.5,
borderRadius: 2,
borderStyle: 'dashed',
bgcolor: 'action.hover',
display: 'flex',
alignItems: 'center',
gap: 1.25,
}}
>
<Sparkles size={16} color="#FF6B00" />
<Box>
<Typography variant="caption" sx={{ display: 'block', fontWeight: 700 }}>
{current.action.label}
</Typography>
{current.action.hint && (
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 600 }}>
{current.action.hint}
</Typography>
)}
</Box>
</Paper>
)}
</motion.div>
</AnimatePresence>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 1,
mt: 'auto',
pt: 2,
}}
>
<Button
onClick={activeStep === 0 ? handleSkip : handleBack}
color="inherit"
size="small"
>
{activeStep === 0 ? 'Überspringen' : 'Zurück'}
</Button>
<Box sx={{ display: 'flex', gap: 0.5 }}>
{steps.map((s, i) => (
<Box
key={s.key}
sx={{
width: i === activeStep ? 20 : 8,
height: 8,
borderRadius: 999,
bgcolor: i === activeStep ? 'primary.main' : 'action.disabledBackground',
transition: 'all 0.3s',
}}
/>
))}
</Box>
<Button
onClick={handleNext}
variant="contained"
endIcon={isLast ? <Check size={16} /> : <ArrowRight size={16} />}
>
{isLast ? 'Loslegen' : 'Weiter'}
</Button>
</Box>
</Box>
</Box>
</Box>
</Dialog>
);
}
/* ------------ Visuals ------------ */
function WelcomeVisual() {
return (
<Box sx={{ textAlign: 'center', width: '100%' }}>
<Box
sx={{
width: 110,
height: 110,
mx: 'auto',
borderRadius: 4,
display: 'grid',
placeItems: 'center',
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
boxShadow: 'var(--pp-shadow-elevated)',
position: 'relative',
p: 1.5,
}}
>
<Logo size={76} />
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 14, repeat: Infinity, ease: 'linear' }}
style={{
position: 'absolute',
inset: -10,
borderRadius: 26,
border: '2px dashed rgba(255,107,0,0.35)',
}}
/>
</Box>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'center', gap: 0.75 }}>
<Chip label="🔧 Werkzeuge" size="small" />
<Chip label="🤖 KI-Chat" size="small" />
<Chip label="📅 Kalender" size="small" />
</Box>
</Box>
);
}
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 (
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1.25 }}>
{samples.map((s) => {
const selected = picked === s.name;
return (
<Paper
key={s.name}
variant="outlined"
onClick={() => 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',
}}
>
<Box
sx={{
width: 36,
height: 36,
borderRadius: 1.75,
display: 'grid',
placeItems: 'center',
background: 'linear-gradient(135deg, #FF6B00, #E55F00)',
color: 'white',
flexShrink: 0,
}}
>
<Hammer size={16} />
</Box>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
{s.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: 'text.secondary' }}>
<MapPin size={11} />
<Typography variant="caption">{s.loc}</Typography>
</Box>
</Box>
{s.cert ? (
<Chip
icon={<ShieldCheck size={11} />}
label="Einweisung"
size="small"
color="warning"
sx={{ height: 22, fontSize: '0.7rem', borderRadius: 1.25 }}
/>
) : (
<Chip
label="Frei"
size="small"
color="success"
sx={{ height: 22, fontSize: '0.7rem', borderRadius: 1.25 }}
/>
)}
</Paper>
);
})}
</Box>
);
}
function ChatVisual({
demoInput,
onChange,
}: {
demoInput: string;
onChange: (v: string) => void;
}) {
return (
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1.25 }}>
<Box sx={{ display: 'flex', gap: 1.25, alignItems: 'flex-start' }}>
<Avatar sx={{ width: 28, height: 28, bgcolor: 'secondary.main' }}>
<Bot size={14} />
</Avatar>
<Paper
variant="outlined"
sx={{
p: 1.25,
borderRadius: 2.5,
borderTopLeftRadius: 6,
maxWidth: '85%',
}}
>
<Typography variant="body2" sx={{ fontSize: '0.82rem' }}>
Hi! Was möchtest du heute reparieren?
</Typography>
</Paper>
</Box>
{demoInput.length > 0 && (
<Box sx={{ display: 'flex', gap: 1.25, alignItems: 'flex-start', flexDirection: 'row-reverse' }}>
<Avatar sx={{ width: 28, height: 28, bgcolor: 'primary.main' }}>
<UserIcon size={14} />
</Avatar>
<Paper
sx={{
p: 1.25,
borderRadius: 2.5,
borderTopRightRadius: 6,
maxWidth: '85%',
bgcolor: 'primary.main',
color: 'white',
border: 'none',
}}
elevation={0}
>
<Typography variant="body2" sx={{ fontSize: '0.82rem' }}>
{demoInput}
</Typography>
</Paper>
</Box>
)}
<Box
sx={{
mt: 1,
display: 'flex',
alignItems: 'center',
gap: 1,
p: 1,
borderRadius: 2.5,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'background.paper',
}}
>
<TextField
fullWidth
size="small"
variant="standard"
placeholder="z. B. Mein Fahrrad bremst nicht mehr"
value={demoInput}
onChange={(e) => onChange(e.target.value)}
InputProps={{ disableUnderline: true, sx: { px: 1, fontSize: '0.85rem' } }}
/>
<IconButton
size="small"
sx={{
width: 30,
height: 30,
bgcolor: 'primary.main',
color: 'white',
borderRadius: 2,
'&:hover': { bgcolor: 'primary.dark' },
}}
>
<Send size={14} />
</IconButton>
</Box>
</Box>
);
}
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 (
<Box
sx={{
width: '100%',
maxWidth: 320,
mx: 'auto',
p: 2,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'background.paper',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700 }}>
Mai 2026
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4 }}>
<Box sx={{ width: 8, height: 8, borderRadius: 0.75, bgcolor: '#F59E0B' }} />
<Typography variant="caption" sx={{ fontSize: '0.65rem' }}>
Belegt
</Typography>
</Box>
</Box>
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 0.4,
}}
>
{['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((d) => (
<Box
key={d}
sx={{
textAlign: 'center',
fontSize: '0.6rem',
color: 'text.secondary',
fontWeight: 700,
py: 0.5,
}}
>
{d}
</Box>
))}
{days.map((d) => {
const isReserved = reserved.has(d);
const isToday = d === today;
return (
<Box
key={d}
sx={{
aspectRatio: '1',
display: 'grid',
placeItems: 'center',
fontSize: '0.7rem',
fontWeight: 600,
borderRadius: 1,
bgcolor: isToday
? 'primary.main'
: isReserved
? 'rgba(245,158,11,0.18)'
: 'transparent',
color: isToday ? 'white' : isReserved ? '#B45309' : 'text.primary',
}}
>
{d}
</Box>
);
})}
</Box>
</Box>
);
}
function AdminVisual() {
return (
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1.25 }}>
{[
{ 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) => (
<Paper
key={s.label}
variant="outlined"
sx={{
p: 1.5,
borderRadius: 2.5,
display: 'flex',
alignItems: 'center',
gap: 1.5,
}}
>
<Box
sx={{
width: 8,
height: 36,
borderRadius: 999,
bgcolor: s.accent,
}}
/>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
{s.label}
</Typography>
</Box>
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.02em' }}>
{s.value}
</Typography>
</Paper>
))}
</Box>
);
}