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;
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;
}
+125 -7
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { apiGet } from "./api";
import { FormEvent, useEffect, useState } from "react";
import { apiGet, apiPost } from "./api";
import {
LayoutDashboard,
Router,
@@ -7,6 +7,7 @@ import {
Activity,
Server,
Settings,
X,
} from "lucide-react";
import "./App.css";
@@ -19,19 +20,66 @@ type RouterItem = {
status: string;
};
type CreateRouterRequest = {
name: string;
serialNumber: string;
lanIp: string;
lanSubnet: string;
};
function App() {
const [page, setPage] = useState("dashboard");
const [routers, setRouters] = useState<RouterItem[]>([]);
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() {
setLoading(true);
try {
setLoading(true);
setError(null);
const data = await apiGet<RouterItem[]>("/api/routers");
const data = await apiGet<RouterItem[]>("/api/routers");
setRouters(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load routers");
} finally {
setLoading(false);
}
}
setRouters(data);
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(() => {
@@ -77,6 +125,8 @@ function App() {
</aside>
<main className="content">
{error && <div className="error-banner">{error}</div>}
{page === "dashboard" && (
<>
<div className="page-header">
@@ -103,7 +153,9 @@ function App() {
<h1>Routers</h1>
<p>Manage routers and OpenVPN provisioning</p>
</div>
<button className="primary">+ New Router</button>
<button className="primary" onClick={() => setCreateOpen(true)}>
+ New Router
</button>
</div>
<div className="panel">
@@ -143,6 +195,72 @@ function App() {
</>
)}
</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>
);
}
+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;
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}`, {
headers: {
"X-API-Key": API_KEY,
@@ -14,3 +17,24 @@ export async function apiGet<T>(path: string): Promise<T> {
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();
}