Routers form working properly and listing

This commit is contained in:
litoral05
2026-05-05 17:46:29 +01:00
parent abaa40790c
commit 20a0dbe794
3 changed files with 242 additions and 7 deletions
+93
View File
@@ -200,3 +200,96 @@ th {
color: #6b7280; color: #6b7280;
background: #f3f4f6; background: #f3f4f6;
} }
.error-banner {
background: #fee2e2;
color: #991b1b;
padding: 12px 16px;
border-radius: 12px;
margin-bottom: 18px;
font-weight: 700;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: grid;
place-items: center;
z-index: 50;
}
.modal {
width: 520px;
background: white;
border-radius: 20px;
padding: 24px;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.25);
}
.modal-header {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.modal-header h2 {
margin: 0;
}
.modal-header p {
margin: 4px 0 0;
color: #6b7280;
}
.icon-button {
border: 0;
background: #f3f4f6;
border-radius: 10px;
width: 36px;
height: 36px;
display: grid;
place-items: center;
cursor: pointer;
}
.router-form {
display: grid;
gap: 16px;
}
.router-form label {
display: grid;
gap: 7px;
font-size: 14px;
font-weight: 700;
}
.router-form input {
height: 42px;
border: 1px solid #d1d5db;
border-radius: 10px;
padding: 0 12px;
font: inherit;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
}
.secondary {
border: 0;
border-radius: 12px;
padding: 12px 18px;
background: #f3f4f6;
cursor: pointer;
}
.primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
+122 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import { apiGet } from "./api"; import { apiGet, apiPost } from "./api";
import { import {
LayoutDashboard, LayoutDashboard,
Router, Router,
@@ -7,6 +7,7 @@ import {
Activity, Activity,
Server, Server,
Settings, Settings,
X,
} from "lucide-react"; } from "lucide-react";
import "./App.css"; import "./App.css";
@@ -19,20 +20,67 @@ type RouterItem = {
status: string; status: string;
}; };
type CreateRouterRequest = {
name: string;
serialNumber: string;
lanIp: string;
lanSubnet: string;
};
function App() { function App() {
const [page, setPage] = useState("dashboard"); const [page, setPage] = useState("dashboard");
const [routers, setRouters] = useState<RouterItem[]>([]); const [routers, setRouters] = useState<RouterItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [form, setForm] = useState<CreateRouterRequest>({
name: "",
serialNumber: "",
lanIp: "",
lanSubnet: "",
});
async function loadRouters() { async function loadRouters() {
try {
setLoading(true); setLoading(true);
setError(null);
const data = await apiGet<RouterItem[]>("/api/routers"); const data = await apiGet<RouterItem[]>("/api/routers");
setRouters(data); setRouters(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load routers");
} finally {
setLoading(false); setLoading(false);
} }
}
async function createRouter(event: FormEvent) {
event.preventDefault();
try {
setSaving(true);
setError(null);
await apiPost<RouterItem, CreateRouterRequest>("/api/routers", form);
setCreateOpen(false);
setForm({
name: "",
serialNumber: "",
lanIp: "",
lanSubnet: "",
});
await loadRouters();
setPage("routers");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create router");
} finally {
setSaving(false);
}
}
useEffect(() => { useEffect(() => {
loadRouters(); loadRouters();
@@ -77,6 +125,8 @@ function App() {
</aside> </aside>
<main className="content"> <main className="content">
{error && <div className="error-banner">{error}</div>}
{page === "dashboard" && ( {page === "dashboard" && (
<> <>
<div className="page-header"> <div className="page-header">
@@ -103,7 +153,9 @@ function App() {
<h1>Routers</h1> <h1>Routers</h1>
<p>Manage routers and OpenVPN provisioning</p> <p>Manage routers and OpenVPN provisioning</p>
</div> </div>
<button className="primary">+ New Router</button> <button className="primary" onClick={() => setCreateOpen(true)}>
+ New Router
</button>
</div> </div>
<div className="panel"> <div className="panel">
@@ -143,6 +195,72 @@ function App() {
</> </>
)} )}
</main> </main>
{createOpen && (
<div className="modal-backdrop">
<div className="modal">
<div className="modal-header">
<div>
<h2>New Router</h2>
<p>Create a router before allocating VPN details.</p>
</div>
<button className="icon-button" onClick={() => setCreateOpen(false)}>
<X size={18} />
</button>
</div>
<form onSubmit={createRouter} className="router-form">
<label>
Router Name
<input
required
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="Ex: Loja Braga"
/>
</label>
<label>
Serial Number
<input
value={form.serialNumber}
onChange={(e) => setForm({ ...form, serialNumber: e.target.value })}
placeholder="Ex: LR-001"
/>
</label>
<label>
LAN IP
<input
required
value={form.lanIp}
onChange={(e) => setForm({ ...form, lanIp: e.target.value })}
placeholder="Ex: 192.168.60.1"
/>
</label>
<label>
LAN Subnet
<input
required
value={form.lanSubnet}
onChange={(e) => setForm({ ...form, lanSubnet: e.target.value })}
placeholder="Ex: 192.168.60.0/24"
/>
</label>
<div className="modal-actions">
<button type="button" className="secondary" onClick={() => setCreateOpen(false)}>
Cancel
</button>
<button type="submit" className="primary" disabled={saving}>
{saving ? "Saving..." : "Save Router"}
</button>
</div>
</form>
</div>
</div>
)}
</div> </div>
); );
} }
+24
View File
@@ -2,6 +2,9 @@ const API_BASE = import.meta.env.VITE_API_BASE;
const API_KEY = import.meta.env.VITE_API_KEY; const API_KEY = import.meta.env.VITE_API_KEY;
export async function apiGet<T>(path: string): Promise<T> { export async function apiGet<T>(path: string): Promise<T> {
console.log("API_BASE:", API_BASE);
console.log("Calling:", `${API_BASE}${path}`);
const response = await fetch(`${API_BASE}${path}`, { const response = await fetch(`${API_BASE}${path}`, {
headers: { headers: {
"X-API-Key": API_KEY, "X-API-Key": API_KEY,
@@ -14,3 +17,24 @@ export async function apiGet<T>(path: string): Promise<T> {
return response.json(); return response.json();
} }
export async function apiPost<TResponse, TBody>(
path: string,
body: TBody
): Promise<TResponse> {
const response = await fetch(`${API_BASE}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const error = await response.json().catch(() => null);
throw new Error(error?.error || `API error ${response.status}`);
}
return response.json();
}