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
+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 };