feat: scaffold MUI theme, providers, color mode and i18n
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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',
|
||||
};
|
||||
Reference in New Issue
Block a user