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:
2026-03-21 21:48:56 -05:00
parent 61a4d37d94
commit 231de3d005
79 changed files with 12983 additions and 0 deletions
@@ -0,0 +1,227 @@
import { useState, type FormEvent } from 'react';
import { toast } from 'sonner';
import type { ModuleType } from '../../types';
import { Modal } from '../ui/Modal';
import { Button } from '../ui/Button';
import { useRackStore } from '../../store/useRackStore';
import {
MODULE_TYPE_LABELS,
MODULE_TYPE_COLORS,
MODULE_U_DEFAULTS,
MODULE_PORT_DEFAULTS,
} from '../../lib/constants';
import { cn } from '../../lib/utils';
interface AddModuleModalProps {
open: boolean;
onClose: () => void;
rackId: string;
uPosition: number;
}
const ALL_TYPES: ModuleType[] = [
'SWITCH', 'AGGREGATE_SWITCH', 'ROUTER', 'FIREWALL', 'PATCH_PANEL',
'MODEM', 'SERVER', 'NAS', 'PDU', 'AP', 'BLANK', 'OTHER',
];
export function AddModuleModal({ open, onClose, rackId, uPosition }: AddModuleModalProps) {
const { addModule } = useRackStore();
const [selectedType, setSelectedType] = useState<ModuleType | null>(null);
const [name, setName] = useState('');
const [uSize, setUSize] = useState(1);
const [portCount, setPortCount] = useState(0);
const [ipAddress, setIpAddress] = useState('');
const [manufacturer, setManufacturer] = useState('');
const [model, setModel] = useState('');
const [loading, setLoading] = useState(false);
function handleTypeSelect(type: ModuleType) {
setSelectedType(type);
setName(MODULE_TYPE_LABELS[type]);
setUSize(MODULE_U_DEFAULTS[type]);
setPortCount(MODULE_PORT_DEFAULTS[type]);
}
function reset() {
setSelectedType(null);
setName('');
setUSize(1);
setPortCount(0);
setIpAddress('');
setManufacturer('');
setModel('');
}
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!selectedType || !name.trim()) return;
setLoading(true);
try {
await addModule(rackId, {
name: name.trim(),
type: selectedType,
uPosition,
uSize,
portCount,
ipAddress: ipAddress.trim() || undefined,
manufacturer: manufacturer.trim() || undefined,
model: model.trim() || undefined,
});
toast.success(`${name.trim()} added at U${uPosition}`);
reset();
onClose();
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to add module');
} finally {
setLoading(false);
}
}
function handleClose() {
if (!loading) {
reset();
onClose();
}
}
return (
<Modal open={open} onClose={handleClose} title={`Add Module — U${uPosition}`} size="md">
<form onSubmit={handleSubmit} className="space-y-4">
{/* Type selector */}
{!selectedType ? (
<div>
<p className="text-xs text-slate-400 mb-2">Select device type</p>
<div className="grid grid-cols-3 gap-1.5">
{ALL_TYPES.map((type) => {
const colors = MODULE_TYPE_COLORS[type];
return (
<button
key={type}
type="button"
onClick={() => handleTypeSelect(type)}
className={cn(
'flex flex-col items-center gap-1 px-2 py-2 rounded border text-center hover:brightness-125 transition-all',
colors.bg,
colors.border
)}
>
<span className="text-[11px] font-medium text-white leading-tight">
{MODULE_TYPE_LABELS[type]}
</span>
<span className="text-[10px] text-slate-400">
{MODULE_U_DEFAULTS[type]}U
</span>
</button>
);
})}
</div>
</div>
) : (
<>
<div className="flex items-center gap-2">
<div
className={cn(
'px-2 py-0.5 rounded text-xs font-medium border',
MODULE_TYPE_COLORS[selectedType].bg,
MODULE_TYPE_COLORS[selectedType].border,
'text-white'
)}
>
{MODULE_TYPE_LABELS[selectedType]}
</div>
<button
type="button"
onClick={() => setSelectedType(null)}
className="text-xs text-slate-500 hover:text-slate-300 underline"
>
Change type
</button>
</div>
<div className="space-y-1">
<label className="block text-sm text-slate-300">
Name <span className="text-red-400">*</span>
</label>
<input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
disabled={loading}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-1">
<label className="block text-sm text-slate-300">Size (U)</label>
<input
type="number"
min={1}
max={20}
value={uSize}
onChange={(e) => setUSize(Number(e.target.value))}
disabled={loading}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
/>
</div>
<div className="space-y-1">
<label className="block text-sm text-slate-300">Port count</label>
<input
type="number"
min={0}
max={128}
value={portCount}
onChange={(e) => setPortCount(Number(e.target.value))}
disabled={loading}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
/>
</div>
<div className="space-y-1">
<label className="block text-sm text-slate-300">IP Address</label>
<input
value={ipAddress}
onChange={(e) => setIpAddress(e.target.value)}
disabled={loading}
placeholder="192.168.1.1"
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="block text-sm text-slate-300">Manufacturer</label>
<input
value={manufacturer}
onChange={(e) => setManufacturer(e.target.value)}
disabled={loading}
placeholder="Cisco, Ubiquiti…"
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
/>
</div>
<div className="space-y-1">
<label className="block text-sm text-slate-300">Model</label>
<input
value={model}
onChange={(e) => setModel(e.target.value)}
disabled={loading}
placeholder="SG300-28…"
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
/>
</div>
</div>
<div className="flex justify-end gap-3 pt-1">
<Button variant="secondary" size="sm" type="button" onClick={handleClose} disabled={loading}>
Cancel
</Button>
<Button size="sm" type="submit" loading={loading} disabled={!name.trim()}>
Add Module
</Button>
</div>
</>
)}
</form>
</Modal>
);
}