feat: scaffold MUI theme, providers, color mode and i18n

This commit is contained in:
MrFrostDev
2026-05-23 13:58:27 +02:00
parent cb662553d8
commit 4598f2c736
7 changed files with 1030 additions and 0 deletions
+249
View File
@@ -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;
}
+51
View File
@@ -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 (
<html lang="de" className={inter.variable}>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
+12
View File
@@ -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 (
<I18nProvider>
<ColorModeProvider>{children}</ColorModeProvider>
</I18nProvider>
);
}
+203
View File
@@ -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;
+71
View File
@@ -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<ColorModeContextValue>({
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<Mode>('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<ColorModeContextValue>(
() => ({
mode,
toggleColorMode: () => setMode((p) => (p === 'light' ? 'dark' : 'light')),
setColorMode: (next) => setMode(next),
}),
[mode]
);
const theme = useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
return (
<ColorModeContext.Provider value={value}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</ColorModeContext.Provider>
);
};
+385
View File
@@ -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<I18nContextType | undefined>(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<Language>('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<I18nContextType>(
() => ({
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 <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
export function useI18n() {
const context = useContext(I18nContext);
if (!context) throw new Error('useI18n must be used within I18nProvider');
return context;
}
export type { TranslationKey };
+59
View File
@@ -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<ToolCategory, string> = {
power: 'Elektrowerkzeuge',
measurement: 'Messen',
hand: 'Handwerkzeug',
electronics: 'Elektronik',
workshop: 'Werkstatt',
garden: 'Garten',
sewing: 'Nähen',
bike: 'Fahrrad',
};