From e7c06e435235980ca31a2f97dab05606852f3ab6 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:40:54 +0100 Subject: [PATCH] Add system status and invitations cards to admin page Introduces SystemStatusCard and InvitationsListCard components to the admin dashboard for displaying system metrics and active invitations. Refactors InviteVendorCard to generate and display invitation codes, and updates card layouts for consistent sizing. Improves admin page structure and enhances visibility of system and invitation data. --- app/admin/page.tsx | 21 +++---- components/admin/BanUserCard.tsx | 2 +- components/admin/InvitationsListCard.tsx | 71 +++++++++++++++++++++++ components/admin/InviteVendorCard.tsx | 48 ++++++---------- components/admin/RecentOrdersCard.tsx | 2 +- components/admin/SystemStatusCard.tsx | 72 ++++++++++++++++++++++++ 6 files changed, 170 insertions(+), 46 deletions(-) create mode 100644 components/admin/InvitationsListCard.tsx create mode 100644 components/admin/SystemStatusCard.tsx diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 9e26fe5..5920293 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -3,6 +3,8 @@ export const dynamic = "force-dynamic"; import InviteVendorCard from "@/components/admin/InviteVendorCard"; import BanUserCard from "@/components/admin/BanUserCard"; import RecentOrdersCard from "@/components/admin/RecentOrdersCard"; +import SystemStatusCard from "@/components/admin/SystemStatusCard"; +import InvitationsListCard from "@/components/admin/InvitationsListCard"; export default function AdminPage() { return ( @@ -12,18 +14,10 @@ export default function AdminPage() {

Restricted area. Only admin1 can access.

-
- -
-
-

System status

-

Uptime, versions, environment

-
- OK -
-
+
+ - +

Logs

@@ -36,6 +30,7 @@ export default function AdminPage() { +
@@ -47,7 +42,7 @@ export default function AdminPage() {
- +

Config

@@ -57,7 +52,7 @@ export default function AdminPage() {
- +

Payments

diff --git a/components/admin/BanUserCard.tsx b/components/admin/BanUserCard.tsx index cc04ed8..9ddcf0e 100644 --- a/components/admin/BanUserCard.tsx +++ b/components/admin/BanUserCard.tsx @@ -28,7 +28,7 @@ export default function BanUserCard() { } return ( -
+

Ban User

Block abusive users by Telegram ID

diff --git a/components/admin/InvitationsListCard.tsx b/components/admin/InvitationsListCard.tsx new file mode 100644 index 0000000..111b1a3 --- /dev/null +++ b/components/admin/InvitationsListCard.tsx @@ -0,0 +1,71 @@ +"use client"; +import { useEffect, useState } from "react"; +import { fetchClient } from "@/lib/api-client"; + +interface Invitation { + _id: string; + code: string; + isUsed: boolean; + createdAt: string; + expiresAt: string; + usedAt?: string | null; +} + +export default function InvitationsListCard() { + const [invites, setInvites] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + (async () => { + try { + const data = await fetchClient("/admin/invitations"); + if (mounted) setInvites(data); + } catch (e: any) { + if (mounted) setError(e?.message || "Failed to load invitations"); + } finally { + if (mounted) setLoading(false); + } + })(); + return () => { mounted = false; }; + }, []); + + return ( +
+

Invitations

+

Active and recent invitations

+ + {loading ? ( +

Loading...

+ ) : error ? ( +

{error}

+ ) : invites.length === 0 ? ( +

No invitations found

+ ) : ( +
+ {invites.map((inv) => { + const expired = new Date(inv.expiresAt).getTime() < Date.now(); + return ( +
+
+
+ Code: {inv.code} +
+
+ Created: {new Date(inv.createdAt).toLocaleString()} ยท Expires: {new Date(inv.expiresAt).toLocaleString()} +
+
+ + {inv.isUsed ? 'Used' : expired ? 'Expired' : 'Active'} + +
+ ); + })} +
+ )} +
+ ); +} + + diff --git a/components/admin/InviteVendorCard.tsx b/components/admin/InviteVendorCard.tsx index 0a26e4c..b7295b6 100644 --- a/components/admin/InviteVendorCard.tsx +++ b/components/admin/InviteVendorCard.tsx @@ -3,23 +3,18 @@ import { useState } from "react"; import { fetchClient } from "@/lib/api-client"; export default function InviteVendorCard() { - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(null); + const [code, setCode] = useState(null); - async function handleInvite(e: React.FormEvent) { - e.preventDefault(); + async function handleInvite() { setLoading(true); setMessage(null); + setCode(null); try { - await fetchClient("/admin/invitations", { - method: "POST", - body: { username, email } - }); - setMessage("Invitation sent"); - setUsername(""); - setEmail(""); + const res = await fetchClient<{ code: string }>("/admin/invitations", { method: "POST" }); + setMessage("Invitation created"); + setCode(res.code); } catch (e: any) { setMessage(e?.message || "Failed to send invitation"); } finally { @@ -28,33 +23,24 @@ export default function InviteVendorCard() { } return ( -
+

Invite Vendor

-

Generate and send an invite

- - setUsername(e.target.value)} - required - /> - setEmail(e.target.value)} - type="email" - /> +

Generate a new invitation code

+
{message &&

{message}

} - + {code && ( +
+ Code: {code} +
+ )} +
); } diff --git a/components/admin/RecentOrdersCard.tsx b/components/admin/RecentOrdersCard.tsx index 7d4c3b5..9c6fe05 100644 --- a/components/admin/RecentOrdersCard.tsx +++ b/components/admin/RecentOrdersCard.tsx @@ -36,7 +36,7 @@ export default function RecentOrdersCard() { }, []); return ( -
+

Recent Orders

Last 10 orders across stores

{loading ? ( diff --git a/components/admin/SystemStatusCard.tsx b/components/admin/SystemStatusCard.tsx new file mode 100644 index 0000000..01a583b --- /dev/null +++ b/components/admin/SystemStatusCard.tsx @@ -0,0 +1,72 @@ +"use client"; +import { useEffect, useState } from "react"; +import { fetchClient } from "@/lib/api-client"; + +interface Status { + uptimeSeconds: number; + memory: Record; + versions: Record; + counts: { vendors: number; orders: number; products: number; chats: number }; +} + +function formatDuration(seconds: number) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${h}h ${m}m ${s}s`; +} + +export default function SystemStatusCard() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + (async () => { + try { + const res = await fetchClient("/admin/system-status"); + if (mounted) setData(res); + } catch (e: any) { + if (mounted) setError(e?.message || "Failed to load status"); + } + })(); + return () => { mounted = false; }; + }, []); + + return ( +
+
+
+

System status

+

Uptime, versions, environment

+
+ OK +
+ + {error &&

{error}

} + + {data && ( +
+
+
Uptime
+
{formatDuration(data.uptimeSeconds)}
+
+
+
Node
+
{data.versions?.node}
+
+
+
Vendors
+
{data.counts?.vendors}
+
+
+
Orders
+
{data.counts?.orders}
+
+
+ )} +
+ ); +} + +