diff --git a/src/admin/package.json b/src/admin/package.json
index 56d07b1..303df87 100644
--- a/src/admin/package.json
+++ b/src/admin/package.json
@@ -11,8 +11,7 @@
"dependencies": {
"axios": "^1.4.0",
"react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-router-dom": "^6.14.0"
+ "react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^19.1.10",
diff --git a/src/admin/src/App.tsx b/src/admin/src/App.tsx
index 7797b61..b972969 100644
--- a/src/admin/src/App.tsx
+++ b/src/admin/src/App.tsx
@@ -1,43 +1,109 @@
-import React from "react";
-import { NavLink, Routes, Route, useNavigate } from "react-router-dom";
-import Dashboard from "./pages/Dashboard";
-import Send from "./pages/Send";
-import Scheduling from "./pages/Scheduling";
-import Alerts from "./pages/Alerts";
-import Qr from "./pages/Qr";
-
-const navClasses = ({ isActive }: { isActive: boolean }) =>
- `px-3 py-2 rounded-md text-sm font-medium ${isActive ? "bg-gray-700 text-white" : "text-gray-300 hover:bg-gray-700 hover:text-white"}`;
+import React, { useEffect, useState } from "react";
export default function App() {
+ const [dark, setDark] = useState(true);
+
+ useEffect(() => {
+ document.documentElement.classList.toggle("dark", dark);
+ }, [dark]);
+
+ const navItems = ["Dashboard", "Messages", "Analytics", "Settings"];
+
return (
-
-
-
-
- } />
- } />
- } />
- } />
- } />
-
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
Customers
+
+ -
+
+ SJ
+
+
+
Sarah Johnson
+
Active
+
+
+ -
+
+ RB
+
+
+
+ -
+
+ MK
+
+
+
+
+
+
);
}
+
diff --git a/src/admin/src/main.tsx b/src/admin/src/main.tsx
index 8de1488..49aeb45 100644
--- a/src/admin/src/main.tsx
+++ b/src/admin/src/main.tsx
@@ -1,16 +1,12 @@
import React from "react";
import ReactDOM from "react-dom/client";
-import { BrowserRouter } from "react-router-dom";
import App from "./App";
-import "./styles.css";
const root = document.getElementById("root");
if (root) {
ReactDOM.createRoot(root).render(
-
-
-
+
,
);
}
diff --git a/src/admin/src/pages/Alerts.tsx b/src/admin/src/pages/Alerts.tsx
deleted file mode 100644
index 29ee3b9..0000000
--- a/src/admin/src/pages/Alerts.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { fetchHealth } from "../lib/api";
-
-interface AlertItem {
- ts: number;
- message: string;
-}
-
-export default function Alerts() {
- const [alerts, setAlerts] = useState([]);
- const [lastReady, setLastReady] = useState(null);
-
- useEffect(() => {
- const interval = setInterval(check, 5000);
- check();
- return () => clearInterval(interval);
- }, []);
-
- async function check() {
- try {
- const health = await fetchHealth();
- if (lastReady === null) {
- setLastReady(health.session.ready);
- return;
- }
- if (health.session.ready !== lastReady) {
- const message = health.session.ready
- ? "Relinked / Ready"
- : "QR required or offline";
- setAlerts((prev) =>
- [{ ts: Date.now(), message }, ...prev].slice(0, 20),
- );
- setLastReady(health.session.ready);
- }
- } catch (err) {
- console.error(err);
- }
- }
-
- return (
-
- );
-}
diff --git a/src/admin/src/pages/Dashboard.tsx b/src/admin/src/pages/Dashboard.tsx
deleted file mode 100644
index fcd9388..0000000
--- a/src/admin/src/pages/Dashboard.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { fetchHealth, sendMessage, testPush, subscribePush } from "../lib/api";
-
-interface Health {
- session: { ready: boolean; qr: string | null };
- sentToday: number;
- perMinAvailable: number;
- dailyCap: number;
- headless: boolean;
-}
-
-export default function Dashboard() {
- const [health, setHealth] = useState(null);
- const [phone, setPhone] = useState("");
- const [text, setText] = useState("");
- const [disablePrefix, setDisablePrefix] = useState(false);
- const [message, setMessage] = useState(null);
-
- useEffect(() => {
- refresh();
- const id = setInterval(refresh, 5000);
- return () => clearInterval(id);
- }, []);
-
- async function refresh() {
- try {
- const data = await fetchHealth();
- setHealth(data);
- } catch (err) {
- console.error(err);
- }
- }
-
- async function handleSend(e: React.FormEvent) {
- e.preventDefault();
- try {
- await sendMessage({ phone, text, disablePrefix });
- setMessage("Message sent");
- setPhone("");
- setText("");
- } catch (err: any) {
- setMessage(err.response?.data?.error || "Error sending");
- }
- refresh();
- }
-
- async function handleSubscribe() {
- if (!("serviceWorker" in navigator)) return;
- const reg = await navigator.serviceWorker.ready;
- const subscription = await reg.pushManager.subscribe({
- userVisibleOnly: true,
- applicationServerKey: undefined,
- });
- await subscribePush(subscription);
- alert("Subscribed to push notifications");
- }
-
- return (
-
-
Dashboard
- {health && (
-
-
-
Session
-
{health.session.ready ? "Ready" : "Not ready"}
-
-
-
Daily Usage
-
- {health.sentToday} / {health.dailyCap}
-
-
-
-
Minute Tokens
-
{health.perMinAvailable} available
-
-
-
Headless
-
{health.headless ? "Enabled" : "Disabled"}
-
-
- )}
-
-
Quick Send
-
- {message &&
{message}
}
-
-
-
-
-
-
- );
-}
diff --git a/src/admin/src/pages/Qr.tsx b/src/admin/src/pages/Qr.tsx
deleted file mode 100644
index 3e8f30f..0000000
--- a/src/admin/src/pages/Qr.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { useEffect, useState } from "react";
-
-export default function Qr() {
- const [src, setSrc] = useState("");
- useEffect(() => {
- const update = () => {
- const ts = Date.now();
- setSrc(`/qr/latest?ts=${ts}`);
- };
- update();
- const id = setInterval(update, 3000);
- return () => clearInterval(id);
- }, []);
- return (
-
-
QR Code
-
- {src ? (
-

- ) : (
-
Loading...
- )}
-
-
- );
-}
diff --git a/src/admin/src/pages/Scheduling.tsx b/src/admin/src/pages/Scheduling.tsx
deleted file mode 100644
index 9bfeaf5..0000000
--- a/src/admin/src/pages/Scheduling.tsx
+++ /dev/null
@@ -1,436 +0,0 @@
-import React, { useEffect, useState } from "react";
-import {
- listSchedules,
- createSchedule,
- deleteSchedule,
- runSchedule,
- pauseSchedule,
- resumeSchedule,
- updateSchedule,
- fetchTopContacts,
- fetchAllContacts,
-} from "../lib/api";
-import type { Contact } from "../lib/types";
-
-interface Schedule {
- id: string;
- phone?: string;
- name?: string;
- text: string;
- disablePrefix: boolean;
- firstRunAt: string;
- nextRunAt: string;
- intervalMinutes: number | null;
- active: boolean;
- lastRunAt: string | null;
-}
-
-export default function Scheduling() {
- const [schedules, setSchedules] = useState([]);
- const [form, setForm] = useState({
- phone: "",
- text: "",
- disablePrefix: false,
- firstRunAt: "",
- intervalMinutes: "" as string | number,
- active: true,
- });
- const [selected, setSelected] = useState(null);
- const [topContacts, setTopContacts] = useState([]);
- const [allContacts, setAllContacts] = useState([]);
- const [showAll, setShowAll] = useState(false);
- const [edit, setEdit] = useState<{
- id: string;
- text: string;
- disablePrefix: boolean;
- firstRunAt: string;
- intervalMinutes: string | number;
- active: boolean;
- } | null>(null);
- const [status, setStatus] = useState(null);
-
- const suggestions = selected
- ? []
- : form.phone
- ? allContacts.filter(
- (c) =>
- c.phone?.includes(form.phone) ||
- c.name.toLowerCase().includes(form.phone.toLowerCase()),
- )
- : showAll
- ? allContacts
- : topContacts;
-
- useEffect(() => {
- refresh();
- fetchTopContacts().then((res) => setTopContacts(res.contacts));
- fetchAllContacts().then((res) => setAllContacts(res.contacts));
- }, []);
-
- async function refresh() {
- try {
- const data = await listSchedules();
- setSchedules(data.items);
- } catch (err) {
- console.error(err);
- }
- }
-
- async function handleCreate(e: React.FormEvent) {
- e.preventDefault();
- try {
- const payload: any = {
- text: form.text,
- disablePrefix: form.disablePrefix,
- active: form.active,
- };
- if (selected) {
- if (selected.phone) payload.phone = selected.phone;
- else payload.name = selected.name;
- } else if (form.phone) {
- payload.phone = form.phone;
- } else {
- throw new Error("Missing contact");
- }
- if (form.firstRunAt)
- payload.firstRunAt = new Date(form.firstRunAt).toISOString();
- if (form.intervalMinutes)
- payload.intervalMinutes = Number(form.intervalMinutes);
- await createSchedule(payload);
- setStatus("Schedule created");
- setForm({
- phone: "",
- text: "",
- disablePrefix: false,
- firstRunAt: "",
- intervalMinutes: "",
- active: true,
- });
- setSelected(null);
- refresh();
- } catch (err: any) {
- setStatus(err.response?.data?.error || err.message || "Error creating");
- }
- }
-
- async function handleDelete(id: string) {
- await deleteSchedule(id);
- refresh();
- }
- async function handleRun(id: string) {
- await runSchedule(id);
- refresh();
- }
- async function handlePause(id: string) {
- await pauseSchedule(id);
- refresh();
- }
- async function handleResume(id: string) {
- await resumeSchedule(id);
- refresh();
- }
- async function handleUpdate(e: React.FormEvent) {
- e.preventDefault();
- if (!edit) return;
- const payload: any = {
- text: edit.text,
- disablePrefix: edit.disablePrefix,
- active: edit.active,
- };
- if (edit.firstRunAt)
- payload.firstRunAt = new Date(edit.firstRunAt).toISOString();
- if (edit.intervalMinutes)
- payload.intervalMinutes = Number(edit.intervalMinutes);
- await updateSchedule(edit.id, payload);
- setEdit(null);
- refresh();
- }
-
- return (
-
-
Scheduling
- {/* Create form */}
-
-
Create Schedule
-
-
- {/* List */}
-
-
Existing Schedules
-
-
-
-
- | Contact |
- Message |
- Prefix |
- Next Run |
- Interval |
- Active |
- Actions |
-
-
-
- {schedules.map((s) => (
-
- |
- {s.name || s.phone}
- |
-
- {s.text}
- |
-
- {s.disablePrefix ? "Off" : "On"}
- |
-
- {new Date(s.nextRunAt).toLocaleString()}
- |
-
- {s.intervalMinutes ?? "None"}
- |
-
- {s.active ? "Yes" : "No"}
- |
-
-
-
- {s.active ? (
-
- ) : (
-
- )}
-
- |
-
- ))}
-
-
-
-
- {/* Edit modal */}
- {edit && (
-
- )}
-
- );
-}
diff --git a/src/admin/src/pages/Send.tsx b/src/admin/src/pages/Send.tsx
deleted file mode 100644
index 4499bd5..0000000
--- a/src/admin/src/pages/Send.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { sendMessage, fetchTopContacts, fetchAllContacts } from "../lib/api";
-import type { Contact } from "../lib/types";
-
-export default function Send() {
- const [phone, setPhone] = useState("");
- const [selected, setSelected] = useState(null);
- const [text, setText] = useState("");
- const [disablePrefix, setDisablePrefix] = useState(false);
- const [status, setStatus] = useState(null);
- const [topContacts, setTopContacts] = useState([]);
- const [allContacts, setAllContacts] = useState([]);
- const [showAll, setShowAll] = useState(false);
-
- useEffect(() => {
- fetchTopContacts().then((res) => setTopContacts(res.contacts));
- fetchAllContacts().then((res) => setAllContacts(res.contacts));
- }, []);
-
- const suggestions = selected
- ? []
- : phone
- ? allContacts.filter(
- (c) =>
- c.phone?.includes(phone) ||
- c.name.toLowerCase().includes(phone.toLowerCase()),
- )
- : showAll
- ? allContacts
- : topContacts;
-
- async function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- try {
- const payload: any = { text, disablePrefix };
- if (selected) {
- if (selected.phone) payload.phone = selected.phone;
- else payload.name = selected.name;
- } else if (phone) {
- payload.phone = phone;
- } else {
- throw new Error("Missing recipient");
- }
- await sendMessage(payload);
- setStatus("Message sent successfully");
- setPhone("");
- setSelected(null);
- setText("");
- } catch (err: any) {
- setStatus(err.response?.data?.error || "Failed to send");
- }
- }
-
- return (
-
-
Send Message
-
-
-
-
- );
-}
diff --git a/src/admin/src/styles.css b/src/admin/src/styles.css
deleted file mode 100644
index b5c61c9..0000000
--- a/src/admin/src/styles.css
+++ /dev/null
@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;