Initial scaffold: full-stack RackMapper application
Complete project scaffold with working auth, REST API, Prisma/SQLite schema, Docker config, and React frontend for both Rack Planner and Service Mapper modules. Both server and client pass TypeScript strict mode with zero errors. Initial migration applied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
37
client/src/store/useAuthStore.ts
Normal file
37
client/src/store/useAuthStore.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { create } from 'zustand';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
loading: boolean;
|
||||
checkAuth: () => Promise<void>;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
isAuthenticated: false,
|
||||
loading: true,
|
||||
|
||||
checkAuth: async () => {
|
||||
try {
|
||||
await apiClient.auth.me();
|
||||
set({ isAuthenticated: true, loading: false });
|
||||
} catch {
|
||||
set({ isAuthenticated: false, loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
login: async (username, password) => {
|
||||
await apiClient.auth.login(username, password);
|
||||
set({ isAuthenticated: true });
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
await apiClient.auth.logout();
|
||||
} finally {
|
||||
set({ isAuthenticated: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
58
client/src/store/useMapStore.ts
Normal file
58
client/src/store/useMapStore.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { create } from 'zustand';
|
||||
import type { ServiceMap, ServiceMapSummary } from '../types';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
interface MapState {
|
||||
maps: ServiceMapSummary[];
|
||||
activeMap: ServiceMap | null;
|
||||
loading: boolean;
|
||||
fetchMaps: () => Promise<void>;
|
||||
loadMap: (id: string) => Promise<void>;
|
||||
createMap: (name: string, description?: string) => Promise<ServiceMap>;
|
||||
deleteMap: (id: string) => Promise<void>;
|
||||
setActiveMap: (map: ServiceMap | null) => void;
|
||||
}
|
||||
|
||||
export const useMapStore = create<MapState>((set) => ({
|
||||
maps: [],
|
||||
activeMap: null,
|
||||
loading: false,
|
||||
|
||||
fetchMaps: async () => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const maps = await apiClient.maps.list();
|
||||
set({ maps, loading: false });
|
||||
} catch {
|
||||
set({ loading: false });
|
||||
throw new Error('Failed to load maps');
|
||||
}
|
||||
},
|
||||
|
||||
loadMap: async (id) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const map = await apiClient.maps.get(id);
|
||||
set({ activeMap: map, loading: false });
|
||||
} catch {
|
||||
set({ loading: false });
|
||||
throw new Error('Failed to load map');
|
||||
}
|
||||
},
|
||||
|
||||
createMap: async (name, description) => {
|
||||
const map = await apiClient.maps.create({ name, description });
|
||||
set((s) => ({ maps: [{ id: map.id, name: map.name, description: map.description, createdAt: map.createdAt, updatedAt: map.updatedAt }, ...s.maps] }));
|
||||
return map;
|
||||
},
|
||||
|
||||
deleteMap: async (id) => {
|
||||
await apiClient.maps.delete(id);
|
||||
set((s) => ({
|
||||
maps: s.maps.filter((m) => m.id !== id),
|
||||
activeMap: s.activeMap?.id === id ? null : s.activeMap,
|
||||
}));
|
||||
},
|
||||
|
||||
setActiveMap: (map) => set({ activeMap: map }),
|
||||
}));
|
||||
90
client/src/store/useRackStore.ts
Normal file
90
client/src/store/useRackStore.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Rack, Module } from '../types';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
interface RackState {
|
||||
racks: Rack[];
|
||||
loading: boolean;
|
||||
selectedModuleId: string | null;
|
||||
// Fetch
|
||||
fetchRacks: () => Promise<void>;
|
||||
// Rack CRUD
|
||||
addRack: (name: string, totalU?: number, location?: string) => Promise<Rack>;
|
||||
updateRack: (id: string, data: Partial<{ name: string; totalU: number; location: string; displayOrder: number }>) => Promise<void>;
|
||||
deleteRack: (id: string) => Promise<void>;
|
||||
// Module CRUD (optimistic update helpers)
|
||||
addModule: (rackId: string, data: Parameters<typeof apiClient.racks.addModule>[1]) => Promise<Module>;
|
||||
updateModuleLocal: (moduleId: string, data: Partial<Module>) => void;
|
||||
removeModuleLocal: (moduleId: string) => void;
|
||||
// Selection
|
||||
setSelectedModule: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export const useRackStore = create<RackState>((set, get) => ({
|
||||
racks: [],
|
||||
loading: false,
|
||||
selectedModuleId: null,
|
||||
|
||||
fetchRacks: async () => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const racks = await apiClient.racks.list();
|
||||
set({ racks, loading: false });
|
||||
} catch {
|
||||
set({ loading: false });
|
||||
throw new Error('Failed to load racks');
|
||||
}
|
||||
},
|
||||
|
||||
addRack: async (name, totalU = 42, location) => {
|
||||
const rack = await apiClient.racks.create({ name, totalU, location });
|
||||
set((s) => ({ racks: [...s.racks, rack].sort((a, b) => a.displayOrder - b.displayOrder) }));
|
||||
return rack;
|
||||
},
|
||||
|
||||
updateRack: async (id, data) => {
|
||||
const updated = await apiClient.racks.update(id, data);
|
||||
set((s) => ({
|
||||
racks: s.racks
|
||||
.map((r) => (r.id === id ? updated : r))
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder),
|
||||
}));
|
||||
},
|
||||
|
||||
deleteRack: async (id) => {
|
||||
await apiClient.racks.delete(id);
|
||||
set((s) => ({ racks: s.racks.filter((r) => r.id !== id) }));
|
||||
},
|
||||
|
||||
addModule: async (rackId, data) => {
|
||||
const module = await apiClient.racks.addModule(rackId, data);
|
||||
set((s) => ({
|
||||
racks: s.racks.map((r) =>
|
||||
r.id === rackId
|
||||
? { ...r, modules: [...r.modules, module].sort((a, b) => a.uPosition - b.uPosition) }
|
||||
: r
|
||||
),
|
||||
}));
|
||||
return module;
|
||||
},
|
||||
|
||||
updateModuleLocal: (moduleId, data) => {
|
||||
set((s) => ({
|
||||
racks: s.racks.map((r) => ({
|
||||
...r,
|
||||
modules: r.modules.map((m) => (m.id === moduleId ? { ...m, ...data } : m)),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
removeModuleLocal: (moduleId) => {
|
||||
set((s) => ({
|
||||
racks: s.racks.map((r) => ({
|
||||
...r,
|
||||
modules: r.modules.filter((m) => m.id !== moduleId),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
setSelectedModule: (id) => set({ selectedModuleId: id }),
|
||||
}));
|
||||
Reference in New Issue
Block a user