From 9d8cf47d2a3fab77604cdf3c7399c3cda0f602c1 Mon Sep 17 00:00:00 2001 From: MrFrostDev <25389627+MrFrostDev@users.noreply.github.com> Date: Sat, 23 May 2026 13:58:27 +0200 Subject: [PATCH] feat: add auth, mock data and zustand stores --- src/services/auth-store.ts | 36 +++++ src/services/mock-data.ts | 225 ++++++++++++++++++++++++++++++ src/services/reservation-store.ts | 90 ++++++++++++ src/services/tools-store.ts | 58 ++++++++ 4 files changed, 409 insertions(+) create mode 100644 src/services/auth-store.ts create mode 100644 src/services/mock-data.ts create mode 100644 src/services/reservation-store.ts create mode 100644 src/services/tools-store.ts diff --git a/src/services/auth-store.ts b/src/services/auth-store.ts new file mode 100644 index 0000000..0dcb397 --- /dev/null +++ b/src/services/auth-store.ts @@ -0,0 +1,36 @@ +'use client'; + +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import { User } from '../types'; +import { MOCK_USERS } from './mock-data'; + +interface AuthState { + user: User | null; + login: (username: string, password: string) => boolean; + logout: () => void; +} + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + login: (username, password) => { + if (!username || !password) return false; + if (password !== username) return false; + const foundUser = MOCK_USERS.find( + (u) => u.username.toLowerCase() === username.toLowerCase() + ); + if (!foundUser) return false; + set({ user: foundUser }); + return true; + }, + logout: () => set({ user: null }), + }), + { + name: 'patchpilot:auth', + storage: createJSONStorage(() => localStorage), + partialize: (state) => ({ user: state.user }), + } + ) +); diff --git a/src/services/mock-data.ts b/src/services/mock-data.ts new file mode 100644 index 0000000..bfe8202 --- /dev/null +++ b/src/services/mock-data.ts @@ -0,0 +1,225 @@ +import type { Manual, Tool, User } from '../types'; + +export const MOCK_USERS: User[] = [ + { + id: '1', + username: 'admin', + role: 'admin', + trainings: ['drill', '3d-print', 'laser', 'cnc', 'soldering'], + email: 'admin@repaircafe.dev', + }, + { + id: '2', + username: 'guest', + role: 'guest', + trainings: [], + email: 'guest@repaircafe.dev', + }, + { + id: '3', + username: 'staff', + role: 'staff', + trainings: ['drill', 'soldering'], + email: 'staff@repaircafe.dev', + }, +]; + +export const MOCK_TOOLS: Tool[] = [ + { + id: 't1', + name: 'Bosch Schlagbohrmaschine', + description: + 'Leistungsstarke Schlagbohrmaschine 850 W für Beton, Stein und Mauerwerk. Inklusive Bohrer-Set bis 13 mm.', + location: 'Werkstatt A · Regal 2', + category: 'power', + trainingRequired: true, + requiredTrainingId: 'drill', + homeAllowed: true, + maxRentalDays: 3, + emoji: '🪛', + }, + { + id: 't2', + name: '3D-Drucker Prusa i3 MK3S', + description: + 'Präziser FDM-Drucker für PLA, PETG und ABS. Druckraum 250×210×210 mm. PLA-Filament ist verfügbar.', + location: 'Labor 1 · Schrank B', + category: 'electronics', + trainingRequired: true, + requiredTrainingId: '3d-print', + homeAllowed: false, + maxRentalDays: 1, + emoji: '🖨️', + }, + { + id: 't3', + name: 'Schlosserhammer 500 g', + description: + 'Klassischer Schlosserhammer mit Hickory-Stiel. Für allgemeine Heimwerker-Arbeiten geeignet.', + location: 'Werkstatt A · Werkzeugwand', + category: 'hand', + trainingRequired: false, + homeAllowed: true, + maxRentalDays: 7, + emoji: '🔨', + }, + { + id: 't4', + name: 'Lötstation Weller WE 1010', + description: + 'Digitale Lötstation 70 W mit Temperaturregelung 100-450 °C. Inkl. Lötspitzen-Set und Entlötlitze.', + location: 'Labor 2 · Tisch 3', + category: 'electronics', + trainingRequired: true, + requiredTrainingId: 'soldering', + homeAllowed: false, + maxRentalDays: 2, + emoji: '🔌', + }, + { + id: 't5', + name: 'Akku-Schrauber Makita 18V', + description: + 'Kompakter Akku-Schrauber mit 18 V Lithium-Ionen-Akku. Inklusive Bit-Set und Ersatzakku.', + location: 'Werkstatt A · Schrank 1', + category: 'power', + trainingRequired: false, + homeAllowed: true, + maxRentalDays: 5, + emoji: '🔧', + }, + { + id: 't6', + name: 'Laser-Schneider K40', + description: + 'CO2-Laserschneider 40 W für Holz, Acryl und Pappe. Arbeitsfläche 300×200 mm. Achtung: Materialliste beachten!', + location: 'Labor 1 · Standalone', + category: 'workshop', + trainingRequired: true, + requiredTrainingId: 'laser', + homeAllowed: false, + maxRentalDays: 1, + emoji: '⚡', + }, + { + id: 't7', + name: 'Digital-Multimeter Fluke 117', + description: + 'Profi-Multimeter mit AutoVolt, True-RMS und kontaktloser Spannungsprüfung. Für Elektronik-Diagnose.', + location: 'Labor 2 · Werkzeugkoffer', + category: 'measurement', + trainingRequired: false, + homeAllowed: true, + maxRentalDays: 3, + emoji: '🔋', + }, + { + id: 't8', + name: 'Nähmaschine Bernina B215', + description: + 'Robuste Nähmaschine für Anfänger und Fortgeschrittene. 18 Nähstiche, automatisches Knopfloch.', + location: 'Textil-Ecke · Tisch 1', + category: 'sewing', + trainingRequired: false, + homeAllowed: false, + maxRentalDays: 2, + emoji: '🧵', + }, + { + id: 't9', + name: 'Fahrrad-Montageständer Park Tool', + description: + 'Professioneller Werkstatt-Montageständer für alle Fahrrad-Reparaturen. Tragkraft bis 36 kg.', + location: 'Fahrrad-Werkstatt · Eingang', + category: 'bike', + trainingRequired: false, + homeAllowed: true, + maxRentalDays: 5, + emoji: '🚲', + }, + { + id: 't10', + name: 'Akku-Heckenschere Bosch', + description: + 'Akku-Heckenschere AHS 50-20 LI mit 50 cm Schwertlänge. Für Hecken bis 20 mm Astdurchmesser.', + location: 'Garten-Schuppen', + category: 'garden', + trainingRequired: false, + homeAllowed: true, + maxRentalDays: 4, + emoji: '🌿', + }, + { + id: 't11', + name: 'Oszilloskop Rigol DS1054Z', + description: + '4-Kanal-Oszilloskop 50 MHz, 1 GSa/s Abtastrate. Für anspruchsvolle Elektronik-Messungen.', + location: 'Labor 2 · Tisch 2', + category: 'measurement', + trainingRequired: true, + requiredTrainingId: 'soldering', + homeAllowed: false, + maxRentalDays: 2, + emoji: '📊', + }, + { + id: 't12', + name: 'CNC-Fräse Stepcraft D.420', + description: + 'Tisch-CNC-Fräse mit Arbeitsbereich 420×300×140 mm. Für Holz, Aluminium und Kunststoff.', + location: 'Labor 1 · CNC-Bereich', + category: 'workshop', + trainingRequired: true, + requiredTrainingId: 'cnc', + homeAllowed: false, + maxRentalDays: 1, + emoji: '⚙️', + }, +]; + +export const MOCK_MANUALS: Manual[] = [ + { + toolId: 't1', + title: 'Bohrmaschine richtig nutzen', + content: + 'Spannfutter gegen den Uhrzeigersinn lösen, passenden Bohrer einsetzen, fest zudrehen. Bei Beton den Schlag-Modus aktivieren. Augenschutz tragen!', + }, + { + toolId: 't2', + title: 'Prusa i3 vorbereiten', + content: + 'Druckbett mit Isopropanol reinigen, Filament laden, Erstschicht-Kalibrierung durchführen. Erste 10 Minuten am Drucker bleiben.', + }, + { + toolId: 't4', + title: 'Löten — Grundlagen', + content: + 'Lötkolben auf 320-360 °C aufheizen. Erst Pad und Bauteil erhitzen, dann Lötzinn zuführen. Niemals direkt auf den Lötkolben löten — nur indirekt über Pad/Pin.', + }, + { + toolId: 't6', + title: 'Laser-Cutter Sicherheit', + content: + 'NIEMALS verbotene Materialien (PVC, Vinyl, Polycarbonat) schneiden — Chlorgas! Absaugung VOR Start prüfen. Während des Schnitts nie unbeaufsichtigt lassen.', + }, + { + toolId: 't8', + title: 'Nähmaschine einfädeln', + content: + 'Oberfaden vom Garnrollenhalter durch alle markierten Führungen. Spule unten einsetzen — Faden gegen den Uhrzeigersinn. Probelauf an Stoffrest.', + }, + { + toolId: 't11', + title: 'Oszilloskop-Trigger', + content: + 'Trigger immer auf das stabilste Signal setzen. AutoSetup gibt einen guten Startpunkt. Pegel manuell anpassen, bis das Bild "steht".', + }, +]; + +export const MOCK_TRAININGS = [ + { id: 'drill', name: 'Bohrmaschinen-Einweisung', duration: '30 min' }, + { id: '3d-print', name: '3D-Druck Grundlagen', duration: '90 min' }, + { id: 'laser', name: 'Laser-Cutter Sicherheit', duration: '60 min' }, + { id: 'cnc', name: 'CNC-Fräse Einführung', duration: '120 min' }, + { id: 'soldering', name: 'Löten für Anfänger', duration: '45 min' }, +]; diff --git a/src/services/reservation-store.ts b/src/services/reservation-store.ts new file mode 100644 index 0000000..4b38202 --- /dev/null +++ b/src/services/reservation-store.ts @@ -0,0 +1,90 @@ +'use client'; + +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import { Reservation } from '../types'; + +interface ReservationState { + reservations: Reservation[]; + addReservation: (reservation: Omit) => string; + updateStatus: (id: string, status: Reservation['status']) => void; + removeReservation: (id: string) => void; +} + +function randomId() { + if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { + return crypto.randomUUID(); + } + return Math.random().toString(36).slice(2, 11); +} + +const isoOffset = (offsetDays: number) => + new Date(Date.now() + offsetDays * 86_400_000).toISOString().split('T')[0]; + +const INITIAL_RESERVATIONS: Reservation[] = [ + { + id: 'res1', + userId: '1', + toolId: 't1', + startDate: isoOffset(0), + endDate: isoOffset(2), + status: 'approved', + }, + { + id: 'res2', + userId: '2', + toolId: 't2', + startDate: isoOffset(1), + endDate: isoOffset(3), + status: 'requested', + }, + { + id: 'res3', + userId: '3', + toolId: 't4', + startDate: isoOffset(-1), + endDate: isoOffset(1), + status: 'active', + }, + { + id: 'res4', + userId: '1', + toolId: 't6', + startDate: isoOffset(5), + endDate: isoOffset(6), + status: 'requested', + }, +]; + +export const useReservationStore = create()( + persist( + (set) => ({ + reservations: INITIAL_RESERVATIONS, + addReservation: (res) => { + const id = randomId(); + const newReservation: Reservation = { + ...res, + id, + status: 'requested', + }; + set((state) => ({ reservations: [...state.reservations, newReservation] })); + return id; + }, + updateStatus: (id, status) => + set((state) => ({ + reservations: state.reservations.map((r) => + r.id === id ? { ...r, status } : r + ), + })), + removeReservation: (id) => + set((state) => ({ + reservations: state.reservations.filter((r) => r.id !== id), + })), + }), + { + name: 'patchpilot:reservations', + storage: createJSONStorage(() => localStorage), + version: 1, + } + ) +); diff --git a/src/services/tools-store.ts b/src/services/tools-store.ts new file mode 100644 index 0000000..697d577 --- /dev/null +++ b/src/services/tools-store.ts @@ -0,0 +1,58 @@ +'use client'; + +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import type { Manual, Tool } from '../types'; +import { MOCK_TOOLS, MOCK_MANUALS } from './mock-data'; + +interface ToolsState { + tools: Tool[]; + manuals: Manual[]; + addTool: (tool: Omit) => string; + updateTool: (id: string, patch: Partial) => void; + removeTool: (id: string) => void; + addManual: (manual: Manual) => void; + removeManual: (toolId: string, idx: number) => void; + resetToMockData: () => void; +} + +function randomId() { + if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { + return crypto.randomUUID(); + } + return Math.random().toString(36).slice(2, 11); +} + +export const useToolsStore = create()( + persist( + (set) => ({ + tools: MOCK_TOOLS, + manuals: MOCK_MANUALS, + addTool: (tool) => { + const id = `t${randomId()}`; + set((state) => ({ tools: [...state.tools, { ...tool, id }] })); + return id; + }, + updateTool: (id, patch) => + set((state) => ({ + tools: state.tools.map((t) => (t.id === id ? { ...t, ...patch } : t)), + })), + removeTool: (id) => + set((state) => ({ tools: state.tools.filter((t) => t.id !== id) })), + addManual: (manual) => + set((state) => ({ manuals: [...state.manuals, manual] })), + removeManual: (toolId, idx) => + set((state) => ({ + manuals: state.manuals.filter( + (m, i) => !(m.toolId === toolId && i === idx) + ), + })), + resetToMockData: () => set({ tools: MOCK_TOOLS, manuals: MOCK_MANUALS }), + }), + { + name: 'patchpilot:tools', + storage: createJSONStorage(() => localStorage), + version: 1, + } + ) +);