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

271 lines
10 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.
This file contains Unicode characters that might be confused with other characters. 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";
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>
);
}