diff --git a/src/App.css b/src/App.css index ee5f81a..df74672 100644 --- a/src/App.css +++ b/src/App.css @@ -199,4 +199,97 @@ th { .badge.removed { 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; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 046b726..46b509d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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([]); const [loading, setLoading] = useState(false); + const [createOpen, setCreateOpen] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const [form, setForm] = useState({ + name: "", + serialNumber: "", + lanIp: "", + lanSubnet: "", + }); async function loadRouters() { - setLoading(true); + try { + setLoading(true); + setError(null); - const data = await apiGet("/api/routers"); + const data = await apiGet("/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("/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() {
+ {error &&
{error}
} + {page === "dashboard" && ( <>
@@ -103,7 +153,9 @@ function App() {

Routers

Manage routers and OpenVPN provisioning

- +
@@ -143,6 +195,72 @@ function App() { )}
+ + {createOpen && ( +
+
+
+
+

New Router

+

Create a router before allocating VPN details.

+
+ +
+ +
+ + + + + + + + +
+ + +
+
+
+
+ )} ); } diff --git a/src/api.ts b/src/api.ts index e0b54b8..cc1ceba 100644 --- a/src/api.ts +++ b/src/api.ts @@ -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(path: string): Promise { + 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, @@ -12,5 +15,26 @@ export async function apiGet(path: string): Promise { throw new Error(`API error ${response.status}`); } + return response.json(); +} + +export async function apiPost( + path: string, + body: TBody +): Promise { + 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(); } \ No newline at end of file