From 4598f2c736d060aa9f8afdace1a7cc9f6e834c0d Mon Sep 17 00:00:00 2001
From: MrFrostDev <25389627+MrFrostDev@users.noreply.github.com>
Date: Sat, 23 May 2026 13:58:27 +0200
Subject: [PATCH] feat: scaffold MUI theme, providers, color mode and i18n
---
src/app/globals.css | 249 +++++++++++++++++++++++
src/app/layout.tsx | 51 +++++
src/app/providers.tsx | 12 ++
src/config/theme.ts | 203 +++++++++++++++++++
src/services/color-mode.tsx | 71 +++++++
src/services/i18n.tsx | 385 ++++++++++++++++++++++++++++++++++++
src/types/index.ts | 59 ++++++
7 files changed, 1030 insertions(+)
create mode 100644 src/app/globals.css
create mode 100644 src/app/layout.tsx
create mode 100644 src/app/providers.tsx
create mode 100644 src/config/theme.ts
create mode 100644 src/services/color-mode.tsx
create mode 100644 src/services/i18n.tsx
create mode 100644 src/types/index.ts
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..f161468
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,249 @@
+:root {
+ --pp-shadow-soft: 0 1px 2px rgba(15, 23, 42, 0.04),
+ 0 4px 12px -2px rgba(15, 23, 42, 0.06);
+ --pp-shadow-elevated: 0 12px 32px -12px rgba(15, 23, 42, 0.18),
+ 0 2px 6px -2px rgba(15, 23, 42, 0.08);
+ --pp-shadow-brand: 0 8px 24px -8px rgba(255, 107, 0, 0.45);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ scroll-behavior: smooth;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+}
+
+body {
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+
+::selection {
+ background: rgba(255, 107, 0, 0.25);
+ color: inherit;
+}
+
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+::-webkit-scrollbar-thumb {
+ background: rgba(120, 120, 120, 0.35);
+ border-radius: 8px;
+ border: 2px solid transparent;
+ background-clip: padding-box;
+}
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(120, 120, 120, 0.6);
+ background-clip: padding-box;
+}
+
+@keyframes pp-pulse {
+ 0%, 100% {
+ opacity: 0.8;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 1;
+ transform: scale(1.05);
+ }
+}
+
+@keyframes pp-dot {
+ 0%, 80%, 100% {
+ transform: scale(0);
+ opacity: 0.4;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.pp-typing span {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ margin: 0 2px;
+ border-radius: 50%;
+ background: currentColor;
+ animation: pp-dot 1.2s infinite ease-in-out;
+}
+.pp-typing span:nth-child(2) {
+ animation-delay: 0.15s;
+}
+.pp-typing span:nth-child(3) {
+ animation-delay: 0.3s;
+}
+
+.pp-grid-bg {
+ background-image: radial-gradient(
+ circle at 1px 1px,
+ rgba(120, 120, 120, 0.12) 1px,
+ transparent 0
+ );
+ background-size: 28px 28px;
+}
+
+/* ===========================
+ FullCalendar — themed to PatchPilot
+ =========================== */
+.fc {
+ font-family: inherit;
+ --fc-border-color: rgba(120, 120, 120, 0.18);
+ --fc-page-bg-color: transparent;
+ --fc-neutral-bg-color: transparent;
+ --fc-list-event-hover-bg-color: rgba(255, 107, 0, 0.06);
+ --fc-today-bg-color: rgba(255, 107, 0, 0.08);
+ --fc-now-indicator-color: #ff6b00;
+ --fc-event-bg-color: #ff6b00;
+ --fc-event-border-color: #ff6b00;
+ --fc-event-text-color: #ffffff;
+ color: inherit;
+ font-size: 0.85rem;
+}
+
+.fc .fc-toolbar {
+ margin-bottom: 14px;
+ gap: 8px;
+}
+.fc .fc-toolbar-chunk {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.fc .fc-toolbar-title {
+ font-size: 0.95rem !important;
+ font-weight: 700 !important;
+ letter-spacing: -0.005em;
+}
+
+.fc .fc-button {
+ background: transparent !important;
+ border: 1px solid var(--fc-border-color) !important;
+ color: inherit !important;
+ text-transform: none !important;
+ font-weight: 600 !important;
+ font-size: 0.78rem !important;
+ padding: 4px 10px !important;
+ border-radius: 8px !important;
+ box-shadow: none !important;
+ transition: background-color 0.15s, border-color 0.15s, color 0.15s;
+}
+.fc .fc-button:not(:disabled):hover {
+ background: rgba(255, 107, 0, 0.08) !important;
+ border-color: rgba(255, 107, 0, 0.45) !important;
+ color: #ff6b00 !important;
+}
+.fc .fc-button-primary:not(:disabled).fc-button-active {
+ background: #ff6b00 !important;
+ border-color: #ff6b00 !important;
+ color: #fff !important;
+}
+.fc .fc-button-group > .fc-button {
+ border-radius: 0 !important;
+}
+.fc .fc-button-group > .fc-button:first-child {
+ border-top-left-radius: 8px !important;
+ border-bottom-left-radius: 8px !important;
+}
+.fc .fc-button-group > .fc-button:last-child {
+ border-top-right-radius: 8px !important;
+ border-bottom-right-radius: 8px !important;
+}
+
+.fc .fc-col-header-cell {
+ background: transparent !important;
+ font-weight: 700;
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: rgba(120, 120, 120, 1);
+ padding: 8px 0 !important;
+}
+.fc .fc-col-header-cell-cushion {
+ text-decoration: none !important;
+ color: inherit !important;
+ padding: 4px 8px !important;
+}
+
+.fc-theme-standard td,
+.fc-theme-standard th,
+.fc-theme-standard .fc-scrollgrid {
+ border-color: var(--fc-border-color) !important;
+}
+
+.fc .fc-daygrid-day-frame {
+ min-height: 56px;
+ padding: 2px;
+}
+.fc .fc-daygrid-day-number {
+ text-decoration: none !important;
+ color: inherit !important;
+ font-size: 0.78rem;
+ font-weight: 600;
+ padding: 4px 6px !important;
+ opacity: 0.8;
+}
+.fc .fc-day-today .fc-daygrid-day-number {
+ background: #ff6b00;
+ color: #fff !important;
+ border-radius: 8px;
+ width: 22px;
+ height: 22px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 !important;
+ opacity: 1;
+}
+.fc .fc-day-other .fc-daygrid-day-number {
+ opacity: 0.35;
+}
+
+.fc-event {
+ border-radius: 6px !important;
+ border: none !important;
+ font-size: 0.72rem !important;
+ font-weight: 600 !important;
+ padding: 2px 6px !important;
+ cursor: pointer;
+ transition: transform 0.15s, box-shadow 0.15s;
+}
+.fc-event:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 6px 14px -6px rgba(0, 0, 0, 0.25);
+}
+.fc-event.fc-bg-event {
+ opacity: 0.4 !important;
+}
+
+.fc-h-event,
+.fc-v-event {
+ background: var(--fc-event-bg-color) !important;
+ border: none !important;
+}
+
+.fc .fc-timegrid-slot-label-cushion {
+ font-size: 0.72rem;
+ font-weight: 500;
+ color: rgba(120, 120, 120, 1);
+}
+
+/* Compact mode for in-modal calendar */
+.pp-calendar-compact .fc-toolbar-title {
+ font-size: 0.85rem !important;
+}
+.pp-calendar-compact .fc-daygrid-day-frame {
+ min-height: 38px;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..d83fc05
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,51 @@
+import type { Metadata, Viewport } from 'next';
+import { Inter } from 'next/font/google';
+import Providers from './providers';
+import './globals.css';
+
+const inter = Inter({
+ subsets: ['latin'],
+ display: 'swap',
+ variable: '--pp-font-sans',
+});
+
+export const metadata: Metadata = {
+ title: {
+ default: 'PatchPilot — KI-gestütztes Repair Café',
+ template: '%s · PatchPilot',
+ },
+ description:
+ 'Buche Werkzeuge, finde Anleitungen und repariere klüger — mit KI-gestützter Hilfe in deinem Repair Café.',
+ applicationName: 'PatchPilot',
+ keywords: ['Repair Café', 'Werkzeuge', 'Reparatur', 'KI', 'Reservierung'],
+ authors: [{ name: 'PatchPilot Team' }],
+ icons: {
+ icon: [{ url: '/logo.png', type: 'image/png' }],
+ apple: [{ url: '/logo.png', type: 'image/png' }],
+ },
+ openGraph: {
+ title: 'PatchPilot — KI-gestütztes Repair Café',
+ description: 'Buche Werkzeuge, finde Anleitungen, repariere klüger.',
+ type: 'website',
+ locale: 'de_DE',
+ },
+};
+
+export const viewport: Viewport = {
+ width: 'device-width',
+ initialScale: 1,
+ themeColor: [
+ { media: '(prefers-color-scheme: light)', color: '#F9FAFB' },
+ { media: '(prefers-color-scheme: dark)', color: '#121212' },
+ ],
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
new file mode 100644
index 0000000..a3ccdba
--- /dev/null
+++ b/src/app/providers.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import { I18nProvider } from '../services/i18n';
+import { ColorModeProvider } from '../services/color-mode';
+
+export default function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/config/theme.ts b/src/config/theme.ts
new file mode 100644
index 0000000..6b45b2b
--- /dev/null
+++ b/src/config/theme.ts
@@ -0,0 +1,203 @@
+import type { ThemeOptions } from '@mui/material/styles';
+
+export const brandColors = {
+ orange: '#FF6B00',
+ orangeDark: '#E55F00',
+ orangeLight: '#FFB17A',
+ green: '#2D5A27',
+ greenLight: '#4A8F3F',
+ grey: {
+ 50: '#F9FAFB',
+ 100: '#F3F4F6',
+ 200: '#E5E7EB',
+ 300: '#D1D5DB',
+ 400: '#9CA3AF',
+ 500: '#6B7280',
+ 600: '#4B5563',
+ 700: '#374151',
+ 800: '#1F2937',
+ 900: '#111827',
+ },
+} as const;
+
+const fontStack =
+ 'var(--pp-font-sans), "Inter", "system-ui", "-apple-system", "Segoe UI", Roboto, sans-serif';
+
+const getDesignTokens = (mode: 'light' | 'dark'): ThemeOptions => {
+ const isLight = mode === 'light';
+
+ return {
+ palette: {
+ mode,
+ primary: {
+ main: brandColors.orange,
+ dark: brandColors.orangeDark,
+ light: brandColors.orangeLight,
+ contrastText: '#FFFFFF',
+ },
+ secondary: {
+ main: brandColors.green,
+ light: brandColors.greenLight,
+ contrastText: '#FFFFFF',
+ },
+ background: {
+ default: isLight ? brandColors.grey[50] : '#0B0F1A',
+ paper: isLight ? '#FFFFFF' : '#141826',
+ },
+ text: {
+ primary: isLight ? brandColors.grey[900] : '#F3F4F6',
+ secondary: isLight ? brandColors.grey[600] : brandColors.grey[400],
+ },
+ divider: isLight ? brandColors.grey[200] : 'rgba(255,255,255,0.08)',
+ success: { main: '#22C55E' },
+ warning: { main: '#F59E0B' },
+ error: { main: '#EF4444' },
+ info: { main: '#3B82F6' },
+ },
+ shape: { borderRadius: 12 },
+ typography: {
+ fontFamily: fontStack,
+ h1: { fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1.05 },
+ h2: { fontWeight: 800, letterSpacing: '-0.02em', lineHeight: 1.1 },
+ h3: { fontWeight: 700, letterSpacing: '-0.015em' },
+ h4: { fontWeight: 700, letterSpacing: '-0.01em' },
+ h5: { fontWeight: 700, letterSpacing: '-0.005em' },
+ h6: { fontWeight: 600 },
+ subtitle1: { fontWeight: 600 },
+ body1: { lineHeight: 1.6 },
+ body2: { lineHeight: 1.55 },
+ button: { textTransform: 'none', fontWeight: 600, letterSpacing: '0.005em' },
+ },
+ components: {
+ MuiCssBaseline: {
+ styleOverrides: {
+ body: {
+ background: isLight
+ ? `linear-gradient(180deg, ${brandColors.grey[50]} 0%, #F1F3F7 100%)`
+ : 'linear-gradient(180deg, #0B0F1A 0%, #0E1424 100%)',
+ backgroundAttachment: 'fixed',
+ },
+ },
+ },
+ MuiButton: {
+ defaultProps: { disableElevation: true },
+ styleOverrides: {
+ root: {
+ borderRadius: 12,
+ padding: '10px 18px',
+ fontWeight: 600,
+ transition: 'transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease',
+ },
+ contained: {
+ boxShadow: 'var(--pp-shadow-brand)',
+ '&:hover': {
+ boxShadow: '0 12px 28px -10px rgba(255,107,0,0.55)',
+ transform: 'translateY(-1px)',
+ },
+ '&:active': { transform: 'translateY(0)' },
+ },
+ outlined: {
+ borderColor: isLight ? brandColors.grey[300] : 'rgba(255,255,255,0.12)',
+ '&:hover': {
+ borderColor: brandColors.orange,
+ background: isLight ? 'rgba(255,107,0,0.06)' : 'rgba(255,107,0,0.1)',
+ },
+ },
+ sizeSmall: { padding: '6px 12px' },
+ },
+ },
+ MuiPaper: {
+ styleOverrides: {
+ root: {
+ backgroundImage: 'none',
+ borderRadius: 16,
+ },
+ outlined: {
+ border: `1px solid ${isLight ? brandColors.grey[200] : 'rgba(255,255,255,0.08)'}`,
+ },
+ },
+ },
+ MuiCard: {
+ styleOverrides: {
+ root: {
+ borderRadius: 16,
+ backgroundImage: 'none',
+ },
+ },
+ },
+ MuiAppBar: {
+ styleOverrides: {
+ root: {
+ backgroundImage: 'none',
+ backdropFilter: 'saturate(180%) blur(12px)',
+ WebkitBackdropFilter: 'saturate(180%) blur(12px)',
+ background: isLight ? 'rgba(255,255,255,0.78)' : 'rgba(20,24,38,0.78)',
+ borderBottom: `1px solid ${
+ isLight ? brandColors.grey[200] : 'rgba(255,255,255,0.06)'
+ }`,
+ },
+ },
+ },
+ MuiDialog: {
+ styleOverrides: {
+ paper: { borderRadius: 20 },
+ },
+ },
+ MuiTextField: {
+ defaultProps: { variant: 'outlined' },
+ },
+ MuiOutlinedInput: {
+ styleOverrides: {
+ root: {
+ borderRadius: 12,
+ transition: 'box-shadow 0.18s ease',
+ '&.Mui-focused': {
+ boxShadow: `0 0 0 3px ${isLight ? 'rgba(255,107,0,0.18)' : 'rgba(255,107,0,0.25)'}`,
+ },
+ },
+ },
+ },
+ MuiChip: {
+ styleOverrides: {
+ root: { fontWeight: 600 },
+ },
+ },
+ MuiTooltip: {
+ styleOverrides: {
+ tooltip: {
+ fontSize: '0.75rem',
+ borderRadius: 8,
+ padding: '6px 10px',
+ background: isLight ? brandColors.grey[900] : brandColors.grey[700],
+ },
+ },
+ },
+ MuiTab: {
+ styleOverrides: {
+ root: {
+ textTransform: 'none',
+ fontWeight: 600,
+ minHeight: 48,
+ },
+ },
+ },
+ MuiTableCell: {
+ styleOverrides: {
+ head: {
+ fontWeight: 700,
+ fontSize: '0.78rem',
+ letterSpacing: '0.04em',
+ textTransform: 'uppercase',
+ color: isLight ? brandColors.grey[500] : brandColors.grey[400],
+ borderBottom: `1px solid ${isLight ? brandColors.grey[200] : 'rgba(255,255,255,0.08)'}`,
+ },
+ root: {
+ borderBottom: `1px solid ${isLight ? brandColors.grey[100] : 'rgba(255,255,255,0.04)'}`,
+ },
+ },
+ },
+ },
+ };
+};
+
+export default getDesignTokens;
diff --git a/src/services/color-mode.tsx b/src/services/color-mode.tsx
new file mode 100644
index 0000000..85d8f6a
--- /dev/null
+++ b/src/services/color-mode.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { ThemeProvider, createTheme, CssBaseline } from '@mui/material';
+import getDesignTokens from '../config/theme';
+
+type Mode = 'light' | 'dark';
+
+interface ColorModeContextValue {
+ mode: Mode;
+ toggleColorMode: () => void;
+ setColorMode: (mode: Mode) => void;
+}
+
+const STORAGE_KEY = 'patchpilot:color-mode';
+
+const ColorModeContext = createContext({
+ mode: 'light',
+ toggleColorMode: () => {},
+ setColorMode: () => {},
+});
+
+export const useColorMode = () => useContext(ColorModeContext);
+
+function detectInitialMode(): Mode {
+ if (typeof window === 'undefined') return 'light';
+ const stored = window.localStorage.getItem(STORAGE_KEY);
+ if (stored === 'light' || stored === 'dark') return stored;
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+}
+
+export const ColorModeProvider = ({ children }: { children: React.ReactNode }) => {
+ const [mode, setMode] = useState('light');
+ const [hydrated, setHydrated] = useState(false);
+
+ useEffect(() => {
+ setMode(detectInitialMode());
+ setHydrated(true);
+ }, []);
+
+ useEffect(() => {
+ if (!hydrated) return;
+ window.localStorage.setItem(STORAGE_KEY, mode);
+ }, [mode, hydrated]);
+
+ const value = useMemo(
+ () => ({
+ mode,
+ toggleColorMode: () => setMode((p) => (p === 'light' ? 'dark' : 'light')),
+ setColorMode: (next) => setMode(next),
+ }),
+ [mode]
+ );
+
+ const theme = useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
+
+ return (
+
+
+
+ {children}
+
+
+ );
+};
diff --git a/src/services/i18n.tsx b/src/services/i18n.tsx
new file mode 100644
index 0000000..e332035
--- /dev/null
+++ b/src/services/i18n.tsx
@@ -0,0 +1,385 @@
+'use client';
+
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ ReactNode,
+ useMemo,
+} from 'react';
+
+type Language = 'de' | 'en';
+
+const translations = {
+ de: {
+ // App + Navigation
+ appName: 'PATCHPILOT',
+ tools: 'Werkzeuge',
+ chat: 'KI-Assistent',
+ admin: 'Admin',
+ logout: 'Abmelden',
+ administrator: 'Administrator',
+ guestRole: 'Gast',
+ staffRole: 'Mitarbeiter',
+ user: 'Nutzer',
+ role: 'Rolle',
+
+ // Hero
+ heroBadge: 'KI-gestützte Reparaturlösungen',
+ heroTitlePart1: 'Repariere klüger,',
+ heroTitlePart2: 'nicht härter.',
+ heroSubtitle:
+ 'Dein intelligenter Begleiter für jede Reparatur. Erhalte Schritt-für-Schritt Anleitungen, Werkzeug-Empfehlungen und sofortige Hilfe.',
+ heroCTA: 'Jetzt Hilfe anfordern',
+ aiPlaceholder: 'Frage RepairBuddy nach Hilfe…',
+ miniStatInventory: 'Inventar',
+ miniStatAssistant: 'KI-Assistent',
+ miniStatResponse: 'Antwort',
+
+ // Login
+ loginWelcomeBack: 'Willkommen zurück',
+ loginSubtitle:
+ 'Melde dich an, um Werkzeuge zu reservieren und mit dem KI-Assistenten zu chatten.',
+ loginDemoTitle: 'Demo-Zugänge',
+ loginDemoBody:
+ 'admin / admin · vollständiger Zugriff\nguest / guest · Gast-Modus\nstaff / staff · Mitarbeiter',
+ loginError: 'Falscher Benutzername oder Passwort. (Tipp: admin/admin oder guest/guest)',
+ loginSubmit: 'Einloggen',
+ loginSubmitting: 'Einloggen…',
+
+ // Tools page
+ inventory: 'Werkzeug-Inventar',
+ inventoryHint: 'Werkzeuge verfügbar',
+ search: 'Werkzeuge durchsuchen…',
+ filter: 'Filter',
+ all: 'Alle',
+ reserve: 'Jetzt reservieren',
+ certified: 'Zertifiziert',
+ trainingMissing: 'Schulung fehlt',
+ trainingRequired: 'Einweisung benötigt',
+ homeUse: 'Heimnutzung',
+ freeAccess: 'Frei zugänglich',
+ days: 'Tage',
+ noToolsFound: 'Keine Werkzeuge gefunden. Passe Suche oder Filter an.',
+
+ // Chat
+ newChat: 'Neuer Chat',
+ chatHistory: 'Verlauf',
+ chatEmptyTitle: 'Womit darf ich helfen?',
+ chatEmptyHint:
+ 'Frag mich nach Werkzeugen, Reparatur-Tipps oder beschreibe dein Problem.',
+ chatNoSessionTitle: 'Worüber sollen wir reden?',
+ chatNoSessionHint: 'Wähle einen Vorschlag unten oder schreibe deine Frage.',
+ chatRename: 'Umbenennen',
+ chatDelete: 'Löschen',
+ chatRetry: 'Letzte Anfrage erneut senden',
+ chatCopy: 'Kopieren',
+ chatCopied: 'Kopiert!',
+ chatTypingPlaceholder: 'Tippe deine Frage…',
+ modeLive: 'Live',
+ modeFallback: 'Fallback',
+ modeDemo: 'Demo',
+
+ // Reservation
+ reservation: 'Reservierung',
+ availability: 'Verfügbarkeit',
+ booked: 'Reserviert',
+ free: 'Frei',
+ from: 'Von',
+ to: 'Bis',
+ maxRental: 'Maximale Leihdauer',
+ cancel: 'Abbrechen',
+ request: 'Anfragen',
+ confirmReservation: 'Reservieren',
+ reservedSuccess: 'Reserviert!',
+ durationTotal: 'gesamt',
+ notice:
+ 'Bitte bringe das Werkzeug rechtzeitig und unbeschädigt zurück.',
+
+ // Login form fields
+ username: 'Benutzername',
+ password: 'Passwort',
+ login: 'Einloggen',
+
+ // Onboarding
+ welcome: 'Willkommen bei PatchPilot!',
+ onboarding1Desc:
+ 'Hier kannst du professionelle Werkzeuge für deine Reparaturen ausleihen. Achte auf die Einweisungspflicht!',
+ onboarding2Title: 'Dein Reparatur-Assistent',
+ onboarding2Desc:
+ 'Nutze unseren Chatbot, um Tipps für deine Reparatur zu bekommen oder das passende Werkzeug zu finden.',
+ onboarding3Title: 'Verfügbarkeit prüfen',
+ onboarding3Desc:
+ 'In unserem interaktiven Kalender siehst du sofort, wann Werkzeuge verfügbar sind.',
+ onboarding4Title: 'Admin-Funktionen',
+ onboarding4Desc:
+ 'Als Administrator kannst du das gesamte System verwalten — von Werkzeugen bis hin zu KI-Wissensquellen.',
+ getStarted: 'Loslegen',
+ next: 'Weiter',
+ back: 'Zurück',
+ skip: 'Überspringen',
+ onboardingStep: 'Schritt',
+ of: 'von',
+
+ // Admin
+ adminDashboard: 'Admin Dashboard',
+ adminSubtitle: 'Verwalte Werkzeuge, Reservierungen und Wissen',
+ manageTools: 'Werkzeuge',
+ manageRAG: 'KI-Wissen',
+ manageUsers: 'Nutzer',
+ calendar: 'Kalender',
+ addTool: 'Werkzeug hinzufügen',
+ editTool: 'Werkzeug bearbeiten',
+ deleteTool: 'Werkzeug löschen',
+ confirmDelete: 'Wirklich löschen?',
+ delete: 'Löschen',
+ edit: 'Bearbeiten',
+ save: 'Speichern',
+ name: 'Name',
+ description: 'Beschreibung',
+ location: 'Standort',
+ category: 'Kategorie',
+ emoji: 'Symbol',
+ status: 'Status',
+ actions: 'Aktionen',
+ available: 'Verfügbar',
+ statsTools: 'Werkzeuge',
+ statsUsers: 'Nutzer',
+ statsActive: 'Aktive Reservierungen',
+ statsPending: 'Offen',
+ addManual: 'Dokument hochladen',
+ manualPreview: 'Vorschau',
+ forTool: 'Werkzeug',
+ title: 'Titel',
+ content: 'Inhalt',
+ certifications: 'Zertifizierungen',
+ none: 'keine',
+ pendingReservations: 'Offene Reservierungs-Anfragen',
+ approve: 'Genehmigen',
+ reject: 'Ablehnen',
+ requestedBy: 'Angefragt von',
+ period: 'Zeitraum',
+ statusRequested: 'Angefragt',
+ statusApproved: 'Genehmigt',
+ statusActive: 'Aktiv',
+ statusCompleted: 'Erledigt',
+ statusOverdue: 'Überfällig',
+ reservationCalendar: 'Reservierungskalender',
+ },
+ en: {
+ // App + Navigation
+ appName: 'PATCHPILOT',
+ tools: 'Tools',
+ chat: 'AI Assistant',
+ admin: 'Admin',
+ logout: 'Sign out',
+ administrator: 'Administrator',
+ guestRole: 'Guest',
+ staffRole: 'Staff',
+ user: 'User',
+ role: 'Role',
+
+ // Hero
+ heroBadge: 'AI-powered repair solutions',
+ heroTitlePart1: 'Repair smarter,',
+ heroTitlePart2: 'not harder.',
+ heroSubtitle:
+ 'Your intelligent companion for every repair. Get step-by-step instructions, tool recommendations, and instant help.',
+ heroCTA: 'Get help now',
+ aiPlaceholder: 'Ask RepairBuddy for help…',
+ miniStatInventory: 'Inventory',
+ miniStatAssistant: 'AI assistant',
+ miniStatResponse: 'Response',
+
+ // Login
+ loginWelcomeBack: 'Welcome back',
+ loginSubtitle:
+ 'Sign in to reserve tools and chat with the AI assistant.',
+ loginDemoTitle: 'Demo accounts',
+ loginDemoBody:
+ 'admin / admin · full access\nguest / guest · guest mode\nstaff / staff · staff member',
+ loginError: 'Wrong username or password. (Hint: admin/admin or guest/guest)',
+ loginSubmit: 'Sign in',
+ loginSubmitting: 'Signing in…',
+
+ // Tools page
+ inventory: 'Tool Inventory',
+ inventoryHint: 'tools available',
+ search: 'Search tools…',
+ filter: 'Filter',
+ all: 'All',
+ reserve: 'Reserve now',
+ certified: 'Certified',
+ trainingMissing: 'Training missing',
+ trainingRequired: 'Training required',
+ homeUse: 'Home use',
+ freeAccess: 'Free access',
+ days: 'days',
+ noToolsFound: 'No tools found. Adjust your search or filter.',
+
+ // Chat
+ newChat: 'New chat',
+ chatHistory: 'History',
+ chatEmptyTitle: 'How can I help?',
+ chatEmptyHint:
+ 'Ask me about tools, repair tips, or describe your problem.',
+ chatNoSessionTitle: 'What shall we talk about?',
+ chatNoSessionHint: 'Pick a suggestion below or type your question.',
+ chatRename: 'Rename',
+ chatDelete: 'Delete',
+ chatRetry: 'Retry last request',
+ chatCopy: 'Copy',
+ chatCopied: 'Copied!',
+ chatTypingPlaceholder: 'Type your question…',
+ modeLive: 'Live',
+ modeFallback: 'Fallback',
+ modeDemo: 'Demo',
+
+ // Reservation
+ reservation: 'Reservation',
+ availability: 'Availability',
+ booked: 'Booked',
+ free: 'Free',
+ from: 'From',
+ to: 'To',
+ maxRental: 'Max rental period',
+ cancel: 'Cancel',
+ request: 'Request',
+ confirmReservation: 'Reserve',
+ reservedSuccess: 'Reserved!',
+ durationTotal: 'total',
+ notice: 'Please return the tool on time and undamaged.',
+
+ // Login form fields
+ username: 'Username',
+ password: 'Password',
+ login: 'Sign in',
+
+ // Onboarding
+ welcome: 'Welcome to PatchPilot!',
+ onboarding1Desc:
+ 'Here you can rent professional tools for your repairs. Mind the training requirements!',
+ onboarding2Title: 'Your repair assistant',
+ onboarding2Desc:
+ 'Use our chatbot to get tips for your repair or find the right tool.',
+ onboarding3Title: 'Check availability',
+ onboarding3Desc:
+ 'See exactly when tools are available in our interactive calendar.',
+ onboarding4Title: 'Admin features',
+ onboarding4Desc:
+ 'As an administrator, you can manage the entire system — from tools to AI knowledge sources.',
+ getStarted: 'Get started',
+ next: 'Next',
+ back: 'Back',
+ skip: 'Skip',
+ onboardingStep: 'Step',
+ of: 'of',
+
+ // Admin
+ adminDashboard: 'Admin Dashboard',
+ adminSubtitle: 'Manage tools, reservations, and knowledge',
+ manageTools: 'Tools',
+ manageRAG: 'AI Knowledge',
+ manageUsers: 'Users',
+ calendar: 'Calendar',
+ addTool: 'Add tool',
+ editTool: 'Edit tool',
+ deleteTool: 'Delete tool',
+ confirmDelete: 'Really delete?',
+ delete: 'Delete',
+ edit: 'Edit',
+ save: 'Save',
+ name: 'Name',
+ description: 'Description',
+ location: 'Location',
+ category: 'Category',
+ emoji: 'Symbol',
+ status: 'Status',
+ actions: 'Actions',
+ available: 'Available',
+ statsTools: 'Tools',
+ statsUsers: 'Users',
+ statsActive: 'Active reservations',
+ statsPending: 'Pending',
+ addManual: 'Upload manual',
+ manualPreview: 'Preview',
+ forTool: 'Tool',
+ title: 'Title',
+ content: 'Content',
+ certifications: 'Certifications',
+ none: 'none',
+ pendingReservations: 'Pending reservation requests',
+ approve: 'Approve',
+ reject: 'Reject',
+ requestedBy: 'Requested by',
+ period: 'Period',
+ statusRequested: 'Requested',
+ statusApproved: 'Approved',
+ statusActive: 'Active',
+ statusCompleted: 'Completed',
+ statusOverdue: 'Overdue',
+ reservationCalendar: 'Reservation calendar',
+ },
+} as const;
+
+type TranslationKey = keyof (typeof translations)['de'];
+
+interface I18nContextType {
+ t: (key: TranslationKey) => string;
+ lang: Language;
+ setLang: (lang: Language) => void;
+}
+
+const I18nContext = createContext(undefined);
+
+const STORAGE_KEY = 'patchpilot:lang';
+
+function detectInitialLang(): Language {
+ if (typeof window === 'undefined') return 'de';
+ const stored = window.localStorage.getItem(STORAGE_KEY);
+ if (stored === 'de' || stored === 'en') return stored;
+ const browser = window.navigator.language.toLowerCase();
+ return browser.startsWith('en') ? 'en' : 'de';
+}
+
+export function I18nProvider({ children }: { children: ReactNode }) {
+ const [lang, setLangState] = useState('de');
+ const [hydrated, setHydrated] = useState(false);
+
+ useEffect(() => {
+ setLangState(detectInitialLang());
+ setHydrated(true);
+ }, []);
+
+ useEffect(() => {
+ if (!hydrated) return;
+ window.localStorage.setItem(STORAGE_KEY, lang);
+ document.documentElement.lang = lang;
+ }, [lang, hydrated]);
+
+ const value = useMemo(
+ () => ({
+ lang,
+ setLang: (next: Language) => setLangState(next),
+ t: (key: TranslationKey) => {
+ const dict = translations[lang];
+ const value = dict[key];
+ if (typeof value === 'string') return value;
+ const fallback = translations.de[key];
+ return typeof fallback === 'string' ? fallback : String(key);
+ },
+ }),
+ [lang]
+ );
+
+ return {children};
+}
+
+export function useI18n() {
+ const context = useContext(I18nContext);
+ if (!context) throw new Error('useI18n must be used within I18nProvider');
+ return context;
+}
+
+export type { TranslationKey };
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..0a032c6
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,59 @@
+export type Role = 'admin' | 'staff' | 'user' | 'guest';
+
+export type ToolCategory =
+ | 'power'
+ | 'measurement'
+ | 'hand'
+ | 'electronics'
+ | 'workshop'
+ | 'garden'
+ | 'sewing'
+ | 'bike';
+
+export interface User {
+ id: string;
+ username: string;
+ role: Role;
+ trainings: string[];
+ email?: string;
+}
+
+export interface Tool {
+ id: string;
+ name: string;
+ description: string;
+ location: string;
+ category: ToolCategory;
+ trainingRequired: boolean;
+ requiredTrainingId?: string;
+ homeAllowed: boolean;
+ maxRentalDays: number;
+ emoji: string;
+}
+
+export interface Reservation {
+ id: string;
+ userId: string;
+ toolId: string;
+ startDate: string;
+ endDate: string;
+ status: 'requested' | 'approved' | 'active' | 'completed' | 'overdue';
+ note?: string;
+}
+
+export interface Manual {
+ toolId: string;
+ title?: string;
+ content: string;
+}
+
+export const CATEGORY_LABEL: Record = {
+ power: 'Elektrowerkzeuge',
+ measurement: 'Messen',
+ hand: 'Handwerkzeug',
+ electronics: 'Elektronik',
+ workshop: 'Werkstatt',
+ garden: 'Garten',
+ sewing: 'Nähen',
+ bike: 'Fahrrad',
+};