feat: add auth, mock data and zustand stores

This commit is contained in:
MrFrostDev
2026-05-23 13:58:27 +02:00
parent 4598f2c736
commit 9d8cf47d2a
4 changed files with 409 additions and 0 deletions
+36
View File
@@ -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<AuthState>()(
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 }),
}
)
);
+225
View File
@@ -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' },
];
+90
View File
@@ -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<Reservation, 'id' | 'status'>) => 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<ReservationState>()(
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,
}
)
);
+58
View File
@@ -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<Tool, 'id'>) => string;
updateTool: (id: string, patch: Partial<Tool>) => 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<ToolsState>()(
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,
}
)
);