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>
108 lines
3.9 KiB
TypeScript
108 lines
3.9 KiB
TypeScript
import { useState, type FormEvent } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { toast } from 'sonner';
|
|
import { useAuthStore } from '../../store/useAuthStore';
|
|
import { Button } from '../ui/Button';
|
|
|
|
export function LoginPage() {
|
|
const navigate = useNavigate();
|
|
const { login } = useAuthStore();
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
async function handleSubmit(e: FormEvent) {
|
|
e.preventDefault();
|
|
if (!username.trim() || !password) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
await login(username.trim(), password);
|
|
navigate('/rack', { replace: true });
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : 'Login failed');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#0f1117] flex items-center justify-center p-4">
|
|
<div className="w-full max-w-sm">
|
|
{/* Logo / wordmark */}
|
|
<div className="text-center mb-8">
|
|
<div className="inline-flex items-center gap-2 mb-2">
|
|
<div className="w-8 h-8 bg-blue-500 rounded flex items-center justify-center">
|
|
<svg
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 18 18"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<rect x="1" y="2" width="16" height="3" rx="1" fill="white" />
|
|
<rect x="1" y="7" width="16" height="3" rx="1" fill="white" opacity="0.7" />
|
|
<rect x="1" y="12" width="16" height="3" rx="1" fill="white" opacity="0.4" />
|
|
</svg>
|
|
</div>
|
|
<span className="text-xl font-bold text-white tracking-wider">RACKMAPPER</span>
|
|
</div>
|
|
<p className="text-slate-500 text-sm">Network infrastructure management</p>
|
|
</div>
|
|
|
|
{/* Card */}
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="bg-slate-800 border border-slate-700 rounded-xl p-6 space-y-4 shadow-2xl"
|
|
>
|
|
<div className="space-y-1">
|
|
<label htmlFor="username" className="block text-sm font-medium text-slate-300">
|
|
Username
|
|
</label>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
autoComplete="username"
|
|
autoFocus
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
disabled={loading}
|
|
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
|
|
placeholder="admin"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label htmlFor="password" className="block text-sm font-medium text-slate-300">
|
|
Password
|
|
</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
disabled={loading}
|
|
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
|
|
placeholder="••••••••"
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={loading || !username.trim() || !password}
|
|
loading={loading}
|
|
className="w-full mt-2"
|
|
>
|
|
Sign in
|
|
</Button>
|
|
</form>
|
|
|
|
<p className="text-center text-xs text-slate-600 mt-4">
|
|
Credentials are set via Docker environment variables.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|