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.
This commit is contained in:
@@ -28,7 +28,7 @@ export default function BanUserCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4">
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||||
<h2 className="font-medium">Ban User</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">Block abusive users by Telegram ID</p>
|
||||
<form onSubmit={handleBan} className="mt-4 space-y-3">
|
||||
|
||||
71
components/admin/InvitationsListCard.tsx
Normal file
71
components/admin/InvitationsListCard.tsx
Normal file
@@ -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<Invitation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const data = await fetchClient<Invitation[]>("/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 (
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||||
<h2 className="font-medium">Invitations</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">Active and recent invitations</p>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground mt-3">Loading...</p>
|
||||
) : error ? (
|
||||
<p className="text-sm text-muted-foreground mt-3">{error}</p>
|
||||
) : invites.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground mt-3">No invitations found</p>
|
||||
) : (
|
||||
<div className="mt-3 space-y-2">
|
||||
{invites.map((inv) => {
|
||||
const expired = new Date(inv.expiresAt).getTime() < Date.now();
|
||||
return (
|
||||
<div key={inv._id} className="rounded border border-border/50 p-3 text-sm flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
Code: <span className="font-mono px-1.5 py-0.5 rounded bg-muted">{inv.code}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Created: {new Date(inv.createdAt).toLocaleString()} · Expires: {new Date(inv.expiresAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${inv.isUsed ? 'bg-emerald-500/15 text-emerald-400' : expired ? 'bg-rose-500/15 text-rose-400' : 'bg-amber-500/15 text-amber-400'}`}>
|
||||
{inv.isUsed ? 'Used' : expired ? 'Expired' : 'Active'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [code, setCode] = useState<string | null>(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 (
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4">
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||||
<h2 className="font-medium">Invite Vendor</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">Generate and send an invite</p>
|
||||
<form onSubmit={handleInvite} className="mt-4 space-y-3">
|
||||
<input
|
||||
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
||||
placeholder="Vendor username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
||||
placeholder="Email (optional)"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
type="email"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">Generate a new invitation code</p>
|
||||
<div className="mt-4 space-y-3">
|
||||
<button
|
||||
type="submit"
|
||||
onClick={handleInvite}
|
||||
className="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground disabled:opacity-60"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Sending..." : "Send Invite"}
|
||||
{loading ? "Generating..." : "Generate Invite Code"}
|
||||
</button>
|
||||
{message && <p className="text-xs text-muted-foreground">{message}</p>}
|
||||
</form>
|
||||
{code && (
|
||||
<div className="text-sm">
|
||||
Code: <span className="font-mono px-1.5 py-0.5 rounded bg-muted">{code}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function RecentOrdersCard() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4">
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||||
<h2 className="font-medium">Recent Orders</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">Last 10 orders across stores</p>
|
||||
{loading ? (
|
||||
|
||||
72
components/admin/SystemStatusCard.tsx
Normal file
72
components/admin/SystemStatusCard.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchClient } from "@/lib/api-client";
|
||||
|
||||
interface Status {
|
||||
uptimeSeconds: number;
|
||||
memory: Record<string, number>;
|
||||
versions: Record<string, string>;
|
||||
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<Status | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetchClient<Status>("/admin/system-status");
|
||||
if (mounted) setData(res);
|
||||
} catch (e: any) {
|
||||
if (mounted) setError(e?.message || "Failed to load status");
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="font-medium">System status</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">Uptime, versions, environment</p>
|
||||
</div>
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400">OK</span>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-muted-foreground mt-3">{error}</p>}
|
||||
|
||||
{data && (
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground">Uptime</div>
|
||||
<div>{formatDuration(data.uptimeSeconds)}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground">Node</div>
|
||||
<div>{data.versions?.node}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground">Vendors</div>
|
||||
<div>{data.counts?.vendors}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground">Orders</div>
|
||||
<div>{data.counts?.orders}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user