271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import WebinarModal from "../../../components/admin/WebinarModal";
|
||
|
||
interface Webinar {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
speaker: string;
|
||
startAt: string;
|
||
duration: number;
|
||
priceCents: number;
|
||
visibility: string;
|
||
capacity: number;
|
||
isActive: boolean;
|
||
_count?: { registrations: number };
|
||
}
|
||
|
||
interface PaginationInfo {
|
||
page: number;
|
||
pageSize: number;
|
||
total: number;
|
||
pages: number;
|
||
hasMore: boolean;
|
||
}
|
||
|
||
export default function AdminWebinarsPage() {
|
||
const [webinars, setWebinars] = useState<Webinar[]>([]);
|
||
const [pagination, setPagination] = useState<PaginationInfo>({
|
||
page: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
pages: 1,
|
||
hasMore: false,
|
||
});
|
||
const [loading, setLoading] = useState(true);
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [editingWebinar, setEditingWebinar] = useState<Webinar | null>(null);
|
||
|
||
useEffect(() => {
|
||
fetchWebinars(1);
|
||
}, []);
|
||
|
||
const fetchWebinars = async (page: number) => {
|
||
setLoading(true);
|
||
try {
|
||
// API uses 0-indexed pages (page 0 is first page)
|
||
const apiPage = page - 1;
|
||
const response = await fetch(`/api/webinars?page=${apiPage}&limit=100`);
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
setWebinars(data.webinars || []);
|
||
setPagination({
|
||
page,
|
||
pageSize: data.limit || 10,
|
||
total: data.total || 0,
|
||
pages: Math.ceil((data.total || 0) / (data.limit || 10)),
|
||
hasMore: page < Math.ceil((data.total || 0) / (data.limit || 10)),
|
||
});
|
||
} else {
|
||
console.error("Failed to fetch webinars:", response.status);
|
||
setWebinars([]);
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to fetch webinars:", error);
|
||
setWebinars([]);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleCreateWebinar = () => {
|
||
setEditingWebinar(null);
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
const handleEditWebinar = (webinar: Webinar) => {
|
||
setEditingWebinar(webinar);
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
const handleDeleteWebinar = async (id: string) => {
|
||
if (!confirm("Are you sure you want to delete this webinar?")) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/webinars/${id}`, {
|
||
method: "DELETE",
|
||
});
|
||
|
||
if (response.ok) {
|
||
fetchWebinars(pagination.page);
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to delete webinar:", error);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<main className="max-w-7xl mx-auto px-6 py-16">
|
||
<div className="flex justify-between items-center mb-8">
|
||
<div>
|
||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 text-primary text-xs font-semibold mb-4">
|
||
🎬 Webinars
|
||
</div>
|
||
<h1 className="text-4xl font-bold text-slate-900 dark:text-white mb-2">
|
||
Webinars Management
|
||
</h1>
|
||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||
Create and manage your webinars ({pagination.total} total)
|
||
</p>
|
||
</div>
|
||
<button onClick={handleCreateWebinar} className="btn-primary">
|
||
➕ Create Webinar
|
||
</button>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<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 webinars...</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="grid gap-6">
|
||
{webinars.length === 0 ? (
|
||
<div className="card p-12 text-center">
|
||
<div className="text-6xl mb-4">📹</div>
|
||
<p className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||
No webinars yet
|
||
</p>
|
||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||
Create your first webinar to get started
|
||
</p>
|
||
<button onClick={handleCreateWebinar} className="btn-primary">
|
||
Create Webinar
|
||
</button>
|
||
</div>
|
||
) : (
|
||
webinars.map((webinar) => (
|
||
<div key={webinar.id} className="card p-6 hover:shadow-lg transition-all">
|
||
<div className="flex justify-between items-start">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<h3 className="text-xl font-bold text-gray-900 dark:text-white">
|
||
{webinar.title}
|
||
</h3>
|
||
<span
|
||
className={`px-3 py-1 rounded-full text-xs font-semibold ${
|
||
webinar.isActive
|
||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200"
|
||
: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
|
||
}`}
|
||
>
|
||
{webinar.isActive ? "✅ Active" : "⏸️ Inactive"}
|
||
</span>
|
||
<span
|
||
className={`px-3 py-1 rounded-full text-xs font-semibold ${
|
||
webinar.visibility === "PUBLIC"
|
||
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||
: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
|
||
}`}
|
||
>
|
||
{webinar.visibility === "PUBLIC" ? "🔓 Public" : "🔒 Private"}
|
||
</span>
|
||
</div>
|
||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||
{webinar.description}
|
||
</p>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||
<div>
|
||
<span className="text-gray-500 dark:text-gray-400">Speaker:</span>
|
||
<p className="font-semibold text-gray-900 dark:text-white">
|
||
{webinar.speaker}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-gray-500 dark:text-gray-400">Date:</span>
|
||
<p className="font-semibold text-gray-900 dark:text-white">
|
||
{new Date(webinar.startAt).toLocaleDateString()}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-gray-500 dark:text-gray-400">Price:</span>
|
||
<p className="font-semibold text-gray-900 dark:text-white">
|
||
{webinar.priceCents === 0 ? "FREE" : `$${(webinar.priceCents / 100).toFixed(2)}`}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-gray-500 dark:text-gray-400">Capacity:</span>
|
||
<p className="font-semibold text-gray-900 dark:text-white">
|
||
{webinar.capacity} seats
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2 ml-4">
|
||
<button
|
||
onClick={() => handleEditWebinar(webinar)}
|
||
className="px-3 py-2 text-sm font-semibold rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
|
||
>
|
||
✏️ Edit
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteWebinar(webinar.id)}
|
||
className="px-3 py-2 text-sm font-semibold rounded-lg bg-red-500/10 text-red-600 hover:bg-red-500/20 transition-colors"
|
||
>
|
||
🗑️ Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
{pagination.pages > 1 && (
|
||
<div className="mt-8 flex items-center justify-center gap-2">
|
||
<button
|
||
onClick={() => fetchWebinars(pagination.page - 1)}
|
||
disabled={pagination.page === 1}
|
||
className="px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-gray-900 dark:text-white font-semibold disabled:opacity-50"
|
||
>
|
||
← Previous
|
||
</button>
|
||
<div className="flex gap-1">
|
||
{Array.from({ length: pagination.pages }, (_, i) => i + 1).map((page) => (
|
||
<button
|
||
key={page}
|
||
onClick={() => fetchWebinars(page)}
|
||
className={`w-10 h-10 rounded-lg font-semibold transition-colors ${
|
||
pagination.page === page
|
||
? "bg-primary text-white"
|
||
: "bg-slate-100 dark:bg-slate-800 text-gray-900 dark:text-white hover:bg-slate-200 dark:hover:bg-slate-700"
|
||
}`}
|
||
>
|
||
{page}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button
|
||
onClick={() => fetchWebinars(pagination.page + 1)}
|
||
disabled={!pagination.hasMore}
|
||
className="px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-gray-900 dark:text-white font-semibold disabled:opacity-50"
|
||
>
|
||
Next →
|
||
</button>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{isModalOpen && (
|
||
<WebinarModal
|
||
webinar={editingWebinar}
|
||
onClose={() => {
|
||
setIsModalOpen(false);
|
||
setEditingWebinar(null);
|
||
}}
|
||
onSave={() => {
|
||
setIsModalOpen(false);
|
||
setEditingWebinar(null);
|
||
fetchWebinars(pagination.page);
|
||
}}
|
||
/>
|
||
)}
|
||
</main>
|
||
);
|
||
}
|