Files
yourwillyourwish/app/admin/page.tsx
2026-02-06 21:44:04 -06:00

234 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
interface Stats {
totalUsers: number;
totalWebinars: number;
totalRegistrations: number;
revenue: number;
upcomingWebinars: number;
}
interface Webinar {
id: string;
title: string;
startAt: string;
speaker: string;
}
interface Registration {
id: string;
createdAt: string;
user: { firstName: string; lastName: string; email: string };
webinar: { title: string };
}
export default function AdminPage() {
const [stats, setStats] = useState<Stats>({
totalUsers: 0,
totalWebinars: 0,
totalRegistrations: 0,
revenue: 0,
upcomingWebinars: 0,
});
const [upcomingWebinars, setUpcomingWebinars] = useState<Webinar[]>([]);
const [recentRegistrations, setRecentRegistrations] = useState<Registration[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
try {
const [webinarsRes, registrationsRes] = await Promise.all([
fetch("/api/webinars?limit=100"),
fetch("/api/registrations?limit=10"),
]);
const webinarsData = await webinarsRes.ok ? await webinarsRes.json() : { webinars: [] };
const registrationsData = await registrationsRes.ok ? await registrationsRes.json() : { registrations: [] };
const now = new Date();
const upcoming = webinarsData.webinars?.filter((w: Webinar) => new Date(w.startAt) > now) || [];
const revenue = registrationsData.registrations?.reduce(
(sum: number, reg: any) => sum + (reg.webinar?.priceCents || 0),
0
) || 0;
// Get unique users from registrations
const uniqueUsers = new Set(registrationsData.registrations?.map((r: any) => r.userId) || []);
setStats({
totalUsers: uniqueUsers.size,
totalWebinars: webinarsData.webinars?.length || 0,
totalRegistrations: registrationsData.registrations?.length || 0,
revenue: revenue / 100,
upcomingWebinars: upcoming.length,
});
setUpcomingWebinars(upcoming.slice(0, 5));
setRecentRegistrations(registrationsData.registrations?.slice(0, 5) || []);
} catch (error) {
console.error("Failed to fetch dashboard data:", error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="text-center py-12">
<div className="inline-block w-8 h-8 border-4 border-primary border-r-transparent rounded-full animate-spin"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading dashboard...</p>
</div>
);
}
return (
<div>
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2 text-slate-900 dark:text-white">
Dashboard
</h1>
<p className="text-gray-600 dark:text-gray-400">
Welcome back, suman. Here's what's happening.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-600 dark:text-gray-400">Total Webinars</h3>
<div className="h-10 w-10 rounded-lg bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
<span className="text-xl">📹</span>
</div>
</div>
<div className="text-3xl font-bold text-slate-900 dark:text-white mb-1">
{stats.totalWebinars}
</div>
<p className="text-xs text-primary flex items-center gap-1">
<span className="inline-flex items-center"></span>
{stats.upcomingWebinars} upcoming
</p>
</div>
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-600 dark:text-gray-400">Total Users</h3>
<div className="h-10 w-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<span className="text-xl">👥</span>
</div>
</div>
<div className="text-3xl font-bold text-slate-900 dark:text-white mb-1">
{stats.totalUsers}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">Registered accounts</p>
</div>
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-600 dark:text-gray-400">Registrations</h3>
<div className="h-10 w-10 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
<span className="text-xl">📝</span>
</div>
</div>
<div className="text-3xl font-bold text-slate-900 dark:text-white mb-1">
{stats.totalRegistrations}
</div>
<p className="text-xs text-emerald-600 dark:text-emerald-400 flex items-center gap-1">
<span>📊</span> 0 this month
</p>
</div>
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-600 dark:text-gray-400">Total Revenue</h3>
<div className="h-10 w-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<span className="text-xl">💰</span>
</div>
</div>
<div className="text-3xl font-bold text-slate-900 dark:text-white mb-1">
${stats.revenue.toFixed(0)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">From paid webinars</p>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Recent Registrations</h2>
<a href="/admin/registrations" className="text-sm text-primary hover:underline flex items-center gap-1">
View All <span></span>
</a>
</div>
{recentRegistrations.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No registrations yet
</div>
) : (
<div className="space-y-3">
{recentRegistrations.map((reg) => (
<div
key={reg.id}
className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-slate-800/50"
>
<div>
<p className="font-medium text-slate-900 dark:text-white text-sm">
{reg.user?.firstName} {reg.user?.lastName}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{reg.webinar?.title}</p>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{new Date(reg.createdAt).toLocaleDateString()}
</p>
</div>
))}
</div>
)}
</div>
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Upcoming Webinars</h2>
<a href="/admin/webinars" className="text-sm text-primary hover:underline flex items-center gap-1">
Manage <span></span>
</a>
</div>
{upcomingWebinars.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400 mb-4">No upcoming webinars</p>
<a href="/admin/webinars" className="btn-primary btn-sm">
Create Webinar
</a>
</div>
) : (
<div className="space-y-3">
{upcomingWebinars.map((webinar) => (
<div
key={webinar.id}
className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-slate-800/50"
>
<div>
<p className="font-medium text-slate-900 dark:text-white text-sm">
{webinar.title}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{webinar.speaker}
</p>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{new Date(webinar.startAt).toLocaleDateString()}
</p>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}