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