Initial commit

This commit is contained in:
Developer
2026-02-06 21:44:04 -06:00
commit f85e93c7a6
151 changed files with 22916 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
import AdminPage from "../page";
export default function AdminAnalyticsPage() {
return <AdminPage />;
}

View File

@@ -0,0 +1,111 @@
"use client";
import { useState, useEffect } from "react";
interface ContactMessage {
id: string;
name: string;
email: string;
subject: string;
message: string;
status: string;
createdAt: string;
}
export default function AdminContactMessagesPage() {
const [messages, setMessages] = useState<ContactMessage[]>([]);
const [loading, setLoading] = useState(true);
const [selectedMessage, setSelectedMessage] = useState<ContactMessage | null>(null);
useEffect(() => {
fetchMessages();
}, []);
const fetchMessages = async () => {
try {
const response = await fetch("/api/admin/contact-messages");
if (response.ok) {
const data = await response.json();
setMessages(data.messages || []);
}
} catch (error) {
console.error("Failed to fetch messages:", error);
} finally {
setLoading(false);
}
};
return (
<main className="max-w-7xl mx-auto px-6 py-16">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Contact Messages
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
Review and respond to customer inquiries
</p>
</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 messages...</p>
</div>
) : messages.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 messages yet
</p>
<p className="text-gray-600 dark:text-gray-400">
Customer inquiries will appear here
</p>
</div>
) : (
<div className="grid gap-6">
{messages.map((message) => (
<div key={message.id} className="card p-6 hover:shadow-elevation-2 transition-all">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-1">
{message.subject}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
From: {message.name} ({message.email})
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
{new Date(message.createdAt).toLocaleString()}
</p>
</div>
<span
className={`px-3 py-1 rounded-full text-xs font-semibold ${
message.status === "NEW"
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
: message.status === "READ"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
}`}
>
{message.status}
</span>
</div>
<div className="bg-gray-50 dark:bg-slate-800 rounded-lg p-4 mb-4">
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
{message.message}
</p>
</div>
<div className="flex gap-2">
<a
href={`mailto:${message.email}?subject=Re: ${message.subject}`}
className="btn-primary text-sm"
>
📧 Reply via Email
</a>
</div>
</div>
))}
</div>
)}
</main>
);
}

26
app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { getSession } from "../../lib/auth/session";
import { redirect } from "next/navigation";
import AdminSidebar from "@/components/admin/AdminSidebar";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await getSession();
if (!session || session.role !== "ADMIN") {
redirect("/");
}
return (
<div className="flex min-h-screen bg-gray-50 dark:bg-darkbg">
<AdminSidebar userName={session.email?.split('@')[0]} />
<main className="flex-1 overflow-auto">
<div className="p-8">
{children}
</div>
</main>
</div>
);
}

233
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,233 @@
"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>
);
}

View File

@@ -0,0 +1,196 @@
"use client";
import { useState, useEffect } from "react";
interface Registration {
id: string;
status: string;
paymentStatus: string;
createdAt: string;
user: {
email: string;
firstName: string;
lastName: string;
};
webinar: {
title: string;
dateTime: string;
price: number;
};
}
export default function AdminRegistrationsPage() {
const [registrations, setRegistrations] = useState<Registration[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<string>("ALL");
useEffect(() => {
fetchRegistrations();
}, []);
const fetchRegistrations = async () => {
try {
const response = await fetch("/api/registrations");
if (response.ok) {
const data = await response.json();
setRegistrations(data.registrations || []);
}
} catch (error) {
console.error("Failed to fetch registrations:", error);
} finally {
setLoading(false);
}
};
const filteredRegistrations =
filter === "ALL"
? registrations
: registrations.filter((reg) => reg.status === filter);
const stats = {
total: registrations.length,
confirmed: registrations.filter((r) => r.status === "CONFIRMED").length,
pending: registrations.filter((r) => r.status === "PENDING").length,
cancelled: registrations.filter((r) => r.status === "CANCELLED").length,
};
return (
<main className="max-w-7xl mx-auto px-6 py-16">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Registrations Management
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
View and manage webinar registrations
</p>
</div>
<div className="grid md:grid-cols-4 gap-6 mb-8">
<div className="card p-6">
<div className="text-3xl font-bold text-primary mb-2">{stats.total}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Total Registrations</div>
</div>
<div className="card p-6">
<div className="text-3xl font-bold text-green-600 mb-2">{stats.confirmed}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Confirmed</div>
</div>
<div className="card p-6">
<div className="text-3xl font-bold text-yellow-600 mb-2">{stats.pending}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Pending</div>
</div>
<div className="card p-6">
<div className="text-3xl font-bold text-red-600 mb-2">{stats.cancelled}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Cancelled</div>
</div>
</div>
<div className="card p-6 mb-6">
<div className="flex gap-2">
{["ALL", "CONFIRMED", "PENDING", "CANCELLED"].map((status) => (
<button
key={status}
onClick={() => setFilter(status)}
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
filter === status
? "bg-primary text-white"
: "bg-gray-100 dark:bg-slate-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-slate-700"
}`}
>
{status}
</button>
))}
</div>
</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 registrations...</p>
</div>
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-slate-800">
<tr>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
User
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
Webinar
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
Status
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
Payment
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
Date
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-slate-700">
{filteredRegistrations.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-12 text-center text-gray-500">
No registrations found
</td>
</tr>
) : (
filteredRegistrations.map((reg) => (
<tr key={reg.id} className="hover:bg-gray-50 dark:hover:bg-slate-800/50">
<td className="px-6 py-4">
<div className="font-semibold text-gray-900 dark:text-white">
{reg.user.firstName} {reg.user.lastName}
</div>
<div className="text-sm text-gray-500">{reg.user.email}</div>
</td>
<td className="px-6 py-4">
<div className="font-semibold text-gray-900 dark:text-white">
{reg.webinar.title}
</div>
<div className="text-sm text-gray-500">
{new Date(reg.webinar.dateTime).toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4">
<span
className={`inline-flex px-3 py-1 rounded-full text-xs font-semibold ${
reg.status === "CONFIRMED"
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: reg.status === "PENDING"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
}`}
>
{reg.status}
</span>
</td>
<td className="px-6 py-4">
<span
className={`inline-flex px-3 py-1 rounded-full text-xs font-semibold ${
reg.paymentStatus === "COMPLETED"
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: reg.paymentStatus === "FREE"
? "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"
}`}
>
{reg.paymentStatus}
</span>
</td>
<td className="px-6 py-4 text-gray-600 dark:text-gray-400">
{new Date(reg.createdAt).toLocaleDateString()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</main>
);
}

936
app/admin/setup/page.tsx Normal file
View File

@@ -0,0 +1,936 @@
"use client";
import { useState, useEffect } from "react";
interface SocialMedia {
url: string;
display: boolean;
}
interface Socials {
facebook?: SocialMedia;
instagram?: SocialMedia;
twitter?: SocialMedia;
linkedin?: SocialMedia;
youtube?: SocialMedia;
}
interface OAuthProvider {
enabled: boolean;
clientId: string;
clientSecret: string;
}
interface OAuthConfig {
google?: OAuthProvider;
github?: OAuthProvider;
facebook?: OAuthProvider;
discord?: OAuthProvider;
}
export default function AdminSetupPage() {
const [config, setConfig] = useState({
googleAuth: {
enabled: false,
clientId: "",
clientSecret: "",
},
oauth: {
google: { enabled: false, clientId: "", clientSecret: "" },
github: { enabled: false, clientId: "", clientSecret: "" },
facebook: { enabled: false, clientId: "", clientSecret: "" },
discord: { enabled: false, clientId: "", clientSecret: "" },
} as OAuthConfig,
googleCalendar: {
enabled: false,
serviceAccountEmail: "",
serviceAccountKey: "",
calendarId: "",
},
socials: {} as Socials,
email: {
smtp: {
enabled: false,
host: "",
port: 587,
username: "",
password: "",
from: "",
},
},
pagination: {
itemsPerPage: 10,
},
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState("");
useEffect(() => {
fetchConfig();
}, []);
const fetchConfig = async () => {
try {
const response = await fetch("/api/admin/setup");
const data = await response.json();
if (data.ok) {
// Ensure oauth object exists with all providers
const fetchedConfig = data.data;
if (!fetchedConfig.oauth) {
fetchedConfig.oauth = {
google: { enabled: false, clientId: "", clientSecret: "" },
github: { enabled: false, clientId: "", clientSecret: "" },
facebook: { enabled: false, clientId: "", clientSecret: "" },
discord: { enabled: false, clientId: "", clientSecret: "" },
};
}
setConfig(fetchedConfig);
} else {
setMessage(`❌ Failed to load config: ${data.message}`);
}
} catch (error) {
console.error("Error fetching config:", error);
setMessage(`❌ Error loading configuration: ${error}`);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
setSaving(true);
setMessage("");
try {
const response = await fetch("/api/admin/setup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config),
});
const data = await response.json();
if (data.ok) {
setMessage("✅ Settings saved successfully!");
} else {
setMessage(`${data.message}`);
}
} catch (error) {
setMessage("❌ Failed to save settings");
} finally {
setSaving(false);
setTimeout(() => setMessage(""), 3000);
}
};
const updateSocial = (platform: keyof Socials, field: "url" | "display", value: string | boolean) => {
setConfig({
...config,
socials: {
...config.socials,
[platform]: {
...config.socials[platform],
[field]: value,
},
},
});
};
if (loading) {
return (
<main className="max-w-5xl mx-auto px-6 py-16">
<div className="text-center">Loading configuration...</div>
</main>
);
}
return (
<main className="max-w-5xl mx-auto px-6 py-16">
<div className="mb-8">
<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">
🛠 Admin Settings
</div>
<h1 className="text-4xl font-bold text-slate-900 dark:text-white">
System Setup
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
Configure authentication, social media, and email settings
</p>
</div>
<div className="space-y-6">
{/* Google OAuth Configuration */}
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60 shadow-lg">
<div className="flex items-center gap-3 mb-4">
<div className="text-2xl">🔐</div>
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Google OAuth</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">Enable Google sign-in for users</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="googleEnabled"
checked={config.googleAuth.enabled}
onChange={(e) =>
setConfig({
...config,
googleAuth: { ...config.googleAuth, enabled: e.target.checked },
})
}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label
htmlFor="googleEnabled"
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
Enable Google Sign-In
</label>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Google Client ID
</label>
<input
type="text"
className="input-field"
placeholder="Your Google OAuth Client ID"
value={config.googleAuth.clientId}
onChange={(e) =>
setConfig({
...config,
googleAuth: { ...config.googleAuth, clientId: e.target.value },
})
}
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Google Client Secret
</label>
<input
type="password"
className="input-field"
placeholder="Your Google OAuth Client Secret"
value={config.googleAuth.clientSecret}
onChange={(e) =>
setConfig({
...config,
googleAuth: { ...config.googleAuth, clientSecret: e.target.value },
})
}
/>
</div>
</div>
</div>
{/* BetterAuth OAuth Providers */}
<div className="border-t border-slate-200 dark:border-slate-700 pt-6 mt-6">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 text-xs font-semibold mb-4">
🔑 OAuth Providers (BetterAuth)
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
Configure social OAuth providers for user authentication. Get credentials from each provider's developer console.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Google OAuth */}
<div className="card p-6 border border-blue-200/40 dark:border-blue-900/30 bg-blue-50/30 dark:bg-blue-950/20">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">🔍</span>
<h3 className="text-lg font-bold text-gray-900 dark:text-white">Google</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="googleOAuthEnabled"
checked={config.oauth.google?.enabled || false}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
google: { ...(config.oauth.google || {}), enabled: e.target.checked } as OAuthProvider,
},
})
}
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label
htmlFor="googleOAuthEnabled"
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
Enable Google OAuth
</label>
</div>
<input
type="text"
className="input-field text-sm"
placeholder="Client ID"
value={config.oauth.google?.clientId || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
google: { ...(config.oauth.google || {}), clientId: e.target.value } as OAuthProvider,
},
})
}
/>
<input
type="password"
className="input-field text-sm"
placeholder="Client Secret"
value={config.oauth.google?.clientSecret || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
google: { ...(config.oauth.google || {}), clientSecret: e.target.value } as OAuthProvider,
},
})
}
/>
</div>
</div>
{/* GitHub OAuth */}
<div className="card p-6 border border-gray-300/40 dark:border-gray-700/30 bg-gray-50/30 dark:bg-gray-950/20">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">🐙</span>
<h3 className="text-lg font-bold text-gray-900 dark:text-white">GitHub</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="githubOAuthEnabled"
checked={config.oauth.github?.enabled || false}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
github: { ...(config.oauth.github || {}), enabled: e.target.checked } as OAuthProvider,
},
})
}
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label
htmlFor="githubOAuthEnabled"
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
Enable GitHub OAuth
</label>
</div>
<input
type="text"
className="input-field text-sm"
placeholder="Client ID"
value={config.oauth.github?.clientId || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
github: { ...(config.oauth.github || {}), clientId: e.target.value } as OAuthProvider,
},
})
}
/>
<input
type="password"
className="input-field text-sm"
placeholder="Client Secret"
value={config.oauth.github?.clientSecret || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
github: { ...(config.oauth.github || {}), clientSecret: e.target.value } as OAuthProvider,
},
})
}
/>
</div>
</div>
{/* Facebook OAuth */}
<div className="card p-6 border border-blue-600/40 dark:border-blue-900/30 bg-blue-50/30 dark:bg-blue-950/20">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">👍</span>
<h3 className="text-lg font-bold text-gray-900 dark:text-white">Facebook</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="facebookOAuthEnabled"
checked={config.oauth.facebook?.enabled || false}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
facebook: { ...(config.oauth.facebook || {}), enabled: e.target.checked } as OAuthProvider,
},
})
}
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label
htmlFor="facebookOAuthEnabled"
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
Enable Facebook OAuth
</label>
</div>
<input
type="text"
className="input-field text-sm"
placeholder="App ID"
value={config.oauth.facebook?.clientId || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
facebook: { ...(config.oauth.facebook || {}), clientId: e.target.value } as OAuthProvider,
},
})
}
/>
<input
type="password"
className="input-field text-sm"
placeholder="App Secret"
value={config.oauth.facebook?.clientSecret || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
facebook: { ...(config.oauth.facebook || {}), clientSecret: e.target.value } as OAuthProvider,
},
})
}
/>
</div>
</div>
{/* Discord OAuth */}
<div className="card p-6 border border-indigo-500/40 dark:border-indigo-900/30 bg-indigo-50/30 dark:bg-indigo-950/20">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">💬</span>
<h3 className="text-lg font-bold text-gray-900 dark:text-white">Discord</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="discordOAuthEnabled"
checked={config.oauth.discord?.enabled || false}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
discord: { ...(config.oauth.discord || {}), enabled: e.target.checked } as OAuthProvider,
},
})
}
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label
htmlFor="discordOAuthEnabled"
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
Enable Discord OAuth
</label>
</div>
<input
type="text"
className="input-field text-sm"
placeholder="Client ID"
value={config.oauth.discord?.clientId || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
discord: { ...(config.oauth.discord || {}), clientId: e.target.value } as OAuthProvider,
},
})
}
/>
<input
type="password"
className="input-field text-sm"
placeholder="Client Secret"
value={config.oauth.discord?.clientSecret || ""}
onChange={(e) =>
setConfig({
...config,
oauth: {
...config.oauth,
discord: { ...(config.oauth.discord || {}), clientSecret: e.target.value } as OAuthProvider,
},
})
}
/>
</div>
</div>
</div>
</div>
{/* Google Calendar Configuration */}
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60 shadow-lg">
<div className="flex items-center gap-3 mb-4">
<div className="text-2xl">📅</div>
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Google Calendar</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">Enable calendar invites for webinar registrations</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="calendarEnabled"
checked={config.googleCalendar.enabled}
onChange={(e) =>
setConfig({
...config,
googleCalendar: { ...config.googleCalendar, enabled: e.target.checked },
})
}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label
htmlFor="calendarEnabled"
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
Enable Calendar Invites
</label>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Service Account Email
</label>
<input
type="email"
className="input-field"
placeholder="service-account@project.iam.gserviceaccount.com"
value={config.googleCalendar.serviceAccountEmail}
onChange={(e) =>
setConfig({
...config,
googleCalendar: { ...config.googleCalendar, serviceAccountEmail: e.target.value },
})
}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Create a service account in Google Cloud Console
</p>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Service Account Private Key (JSON)
</label>
<textarea
className="input-field font-mono text-xs"
rows={4}
placeholder='{"type": "service_account", "project_id": "...", "private_key": "...", ...}'
value={config.googleCalendar.serviceAccountKey}
onChange={(e) =>
setConfig({
...config,
googleCalendar: { ...config.googleCalendar, serviceAccountKey: e.target.value },
})
}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Paste the entire JSON key file content
</p>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Calendar ID
</label>
<input
type="text"
className="input-field"
placeholder="primary or your-calendar@group.calendar.google.com"
value={config.googleCalendar.calendarId}
onChange={(e) =>
setConfig({
...config,
googleCalendar: { ...config.googleCalendar, calendarId: e.target.value },
})
}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Use &quot;primary&quot; for the service account&apos;s calendar or specify a shared calendar ID
</p>
</div>
</div>
</div>
{/* Social Media Configuration */}
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60 shadow-lg">
<div className="flex items-center gap-3 mb-4">
<div className="text-2xl">🌐</div>
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Social Media</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">Configure social media links and visibility</p>
</div>
</div>
<div className="space-y-6">
{/* Facebook */}
<div className="border-b border-slate-200 dark:border-slate-700 pb-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-xl">📘</span>
<span className="font-semibold text-gray-900 dark:text-white">Facebook</span>
</div>
<div className="space-y-3">
<input
type="url"
className="input-field"
placeholder="https://facebook.com/yourpage"
value={config.socials.facebook?.url || ""}
onChange={(e) => updateSocial("facebook", "url", e.target.value)}
/>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="facebookDisplay"
checked={config.socials.facebook?.display || false}
onChange={(e) => updateSocial("facebook", "display", e.target.checked)}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label htmlFor="facebookDisplay" className="text-sm text-gray-700 dark:text-gray-300">
Display on landing page
</label>
</div>
</div>
</div>
{/* Instagram */}
<div className="border-b border-slate-200 dark:border-slate-700 pb-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-xl">📷</span>
<span className="font-semibold text-gray-900 dark:text-white">Instagram</span>
</div>
<div className="space-y-3">
<input
type="url"
className="input-field"
placeholder="https://instagram.com/yourprofile"
value={config.socials.instagram?.url || ""}
onChange={(e) => updateSocial("instagram", "url", e.target.value)}
/>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="instagramDisplay"
checked={config.socials.instagram?.display || false}
onChange={(e) => updateSocial("instagram", "display", e.target.checked)}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label htmlFor="instagramDisplay" className="text-sm text-gray-700 dark:text-gray-300">
Display on landing page
</label>
</div>
</div>
</div>
{/* Twitter */}
<div className="border-b border-slate-200 dark:border-slate-700 pb-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-xl">🐦</span>
<span className="font-semibold text-gray-900 dark:text-white">Twitter</span>
</div>
<div className="space-y-3">
<input
type="url"
className="input-field"
placeholder="https://twitter.com/yourhandle"
value={config.socials.twitter?.url || ""}
onChange={(e) => updateSocial("twitter", "url", e.target.value)}
/>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="twitterDisplay"
checked={config.socials.twitter?.display || false}
onChange={(e) => updateSocial("twitter", "display", e.target.checked)}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label htmlFor="twitterDisplay" className="text-sm text-gray-700 dark:text-gray-300">
Display on landing page
</label>
</div>
</div>
</div>
{/* LinkedIn */}
<div className="border-b border-slate-200 dark:border-slate-700 pb-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-xl">💼</span>
<span className="font-semibold text-gray-900 dark:text-white">LinkedIn</span>
</div>
<div className="space-y-3">
<input
type="url"
className="input-field"
placeholder="https://linkedin.com/company/yourcompany"
value={config.socials.linkedin?.url || ""}
onChange={(e) => updateSocial("linkedin", "url", e.target.value)}
/>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="linkedinDisplay"
checked={config.socials.linkedin?.display || false}
onChange={(e) => updateSocial("linkedin", "display", e.target.checked)}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label htmlFor="linkedinDisplay" className="text-sm text-gray-700 dark:text-gray-300">
Display on landing page
</label>
</div>
</div>
</div>
{/* YouTube */}
<div>
<div className="flex items-center gap-2 mb-3">
<span className="text-xl">📺</span>
<span className="font-semibold text-gray-900 dark:text-white">YouTube</span>
</div>
<div className="space-y-3">
<input
type="url"
className="input-field"
placeholder="https://youtube.com/channel/yourchannel"
value={config.socials.youtube?.url || ""}
onChange={(e) => updateSocial("youtube", "url", e.target.value)}
/>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="youtubeDisplay"
checked={config.socials.youtube?.display || false}
onChange={(e) => updateSocial("youtube", "display", e.target.checked)}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label htmlFor="youtubeDisplay" className="text-sm text-gray-700 dark:text-gray-300">
Display on landing page
</label>
</div>
</div>
</div>
</div>
</div>
{/* SMTP Email Configuration */}
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60 shadow-lg">
<div className="flex items-center gap-3 mb-4">
<div className="text-2xl">📧</div>
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Email (SMTP)</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">Configure email server for notifications and activation</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="smtpEnabled"
checked={config.email.smtp.enabled}
onChange={(e) =>
setConfig({
...config,
email: {
...config.email,
smtp: { ...config.email.smtp, enabled: e.target.checked },
},
})
}
className="w-5 h-5 text-primary border-gray-300 rounded focus:ring-primary"
/>
<label htmlFor="smtpEnabled" className="text-sm font-semibold text-gray-700 dark:text-gray-300">
Enable SMTP Email
</label>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
SMTP Host
</label>
<input
type="text"
className="input-field"
placeholder="smtp.gmail.com"
value={config.email.smtp.host}
onChange={(e) =>
setConfig({
...config,
email: {
...config.email,
smtp: { ...config.email.smtp, host: e.target.value },
},
})
}
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Port
</label>
<input
type="number"
className="input-field"
placeholder="587"
value={config.email.smtp.port}
onChange={(e) =>
setConfig({
...config,
email: {
...config.email,
smtp: { ...config.email.smtp, port: parseInt(e.target.value) || 587 },
},
})
}
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Username
</label>
<input
type="text"
className="input-field"
placeholder="your-email@gmail.com"
value={config.email.smtp.username}
onChange={(e) =>
setConfig({
...config,
email: {
...config.email,
smtp: { ...config.email.smtp, username: e.target.value },
},
})
}
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Password
</label>
<input
type="password"
className="input-field"
placeholder="Your SMTP password or app password"
value={config.email.smtp.password}
onChange={(e) =>
setConfig({
...config,
email: {
...config.email,
smtp: { ...config.email.smtp, password: e.target.value },
},
})
}
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
From Address
</label>
<input
type="email"
className="input-field"
placeholder="noreply@yourdomain.com"
value={config.email.smtp.from}
onChange={(e) =>
setConfig({
...config,
email: {
...config.email,
smtp: { ...config.email.smtp, from: e.target.value },
},
})
}
/>
</div>
</div>
</div>
{/* Pagination Configuration */}
<div className="card p-6 border border-slate-200/60 dark:border-slate-700/60 shadow-lg">
<div className="flex items-center gap-3 mb-4">
<div className="text-2xl">📄</div>
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Pagination</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">Configure pagination settings for lists</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Items Per Page
</label>
<input
type="number"
min="5"
max="100"
className="input-field"
placeholder="10"
value={config.pagination.itemsPerPage}
onChange={(e) =>
setConfig({
...config,
pagination: {
itemsPerPage: Math.max(5, Math.min(100, parseInt(e.target.value) || 10)),
},
})
}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Used for users and webinars list (min: 5, max: 100)
</p>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<button onClick={handleSave} disabled={saving} className="btn-primary">
{saving ? "⏳ Saving..." : "💾 Save Settings"}
</button>
{message && (
<span
className={`px-4 py-2 rounded-lg text-sm font-semibold ${
message.includes("✅")
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
: "bg-red-500/10 text-red-600 dark:text-red-400"
}`}
>
{message}
</span>
)}
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,133 @@
"use client";
import { useState, useEffect } from "react";
import { getSession } from "../../../lib/auth/session";
import { redirect } from "next/navigation";
interface User {
id: string;
email: string;
firstName: string;
lastName: string;
role: string;
createdAt: string;
}
export default function AdminUsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
const response = await fetch("/api/admin/users");
if (response.ok) {
const data = await response.json();
setUsers(data.users || []);
}
} catch (error) {
console.error("Failed to fetch users:", error);
} finally {
setLoading(false);
}
};
const filteredUsers = users.filter(
(user) =>
user.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
`${user.firstName} ${user.lastName}`.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<main className="max-w-7xl mx-auto px-6 py-16">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Users Management
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
Manage user accounts and permissions
</p>
</div>
<div className="card p-6 mb-6">
<input
type="text"
placeholder="Search users by name or email..."
className="input-field"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</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 users...</p>
</div>
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-slate-800">
<tr>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
User
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
Email
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
Role
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">
Joined
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-slate-700">
{filteredUsers.length === 0 ? (
<tr>
<td colSpan={4} className="px-6 py-12 text-center text-gray-500">
No users found
</td>
</tr>
) : (
filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50 dark:hover:bg-slate-800/50">
<td className="px-6 py-4">
<div className="font-semibold text-gray-900 dark:text-white">
{user.firstName} {user.lastName}
</div>
</td>
<td className="px-6 py-4 text-gray-600 dark:text-gray-400">
{user.email}
</td>
<td className="px-6 py-4">
<span
className={`inline-flex px-3 py-1 rounded-full text-xs font-semibold ${
user.role === "ADMIN"
? "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
}`}
>
{user.role}
</span>
</td>
<td className="px-6 py-4 text-gray-600 dark:text-gray-400">
{new Date(user.createdAt).toLocaleDateString()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</main>
);
}

345
app/admin/users/page.tsx Normal file
View File

@@ -0,0 +1,345 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
interface WebinarRegistration {
id: string;
status: string;
webinar: {
id: string;
title: string;
startAt: string;
};
}
interface User {
id: string;
email: string;
firstName: string;
lastName: string;
role: string;
isActive: boolean;
emailVerified: boolean;
createdAt: string;
gender?: string;
dob?: string;
address?: string;
image?: string;
_count: {
webinarRegistrations: number;
};
registeredWebinars: WebinarRegistration[];
}
interface PaginationInfo {
page: number;
pageSize: number;
total: number;
pages: number;
hasMore: boolean;
}
export default function AdminUsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [pagination, setPagination] = useState<PaginationInfo>({
page: 1,
pageSize: 10,
total: 0,
pages: 1,
hasMore: false,
});
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [message, setMessage] = useState("");
const [updatingUserId, setUpdatingUserId] = useState<string | null>(null);
useEffect(() => {
fetchUsers(1);
}, [searchQuery]);
const fetchUsers = async (page: number) => {
setLoading(true);
try {
const params = new URLSearchParams({ page: String(page) });
if (searchQuery) params.append("search", searchQuery);
const response = await fetch(`/api/admin/users?${params}`);
if (response.ok) {
const data = await response.json();
if (data.ok) {
setUsers(data.users);
setPagination(data.pagination);
}
}
} catch (error) {
console.error("Failed to fetch users:", error);
setMessage("❌ Failed to load users");
} finally {
setLoading(false);
}
};
const handleBlockUnblock = async (userId: string, currentlyActive: boolean) => {
setUpdatingUserId(userId);
try {
const response = await fetch("/api/admin/users", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId,
isActive: !currentlyActive,
}),
});
const data = await response.json();
if (data.ok) {
setMessage(`${data.message}`);
fetchUsers(pagination.page);
setTimeout(() => setMessage(""), 3000);
} else {
setMessage(`${data.message}`);
}
} catch (error) {
setMessage("❌ Failed to update user");
} finally {
setUpdatingUserId(null);
}
};
const handleRoleChange = async (userId: string, newRole: "USER" | "ADMIN") => {
setUpdatingUserId(userId);
try {
const response = await fetch("/api/admin/users", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId,
role: newRole,
}),
});
const data = await response.json();
if (data.ok) {
setMessage(`${data.message}`);
fetchUsers(pagination.page);
setTimeout(() => setMessage(""), 3000);
} else {
setMessage(`${data.message}`);
}
} catch (error) {
setMessage("❌ Failed to update user role");
} finally {
setUpdatingUserId(null);
}
};
return (
<main className="max-w-7xl mx-auto px-6 py-16">
<div className="mb-8">
<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">
👥 User Management
</div>
<h1 className="text-4xl font-bold text-slate-900 dark:text-white">
Users
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
Manage user accounts, roles, and permissions ({pagination.total} total)
</p>
</div>
{message && (
<div
className={`mb-4 px-4 py-3 rounded-lg font-semibold ${
message.includes("✅")
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
: "bg-red-500/10 text-red-600 dark:text-red-400"
}`}
>
{message}
</div>
)}
<div className="card p-6 mb-6">
<input
type="text"
placeholder="🔍 Search by name or email..."
className="input-field"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</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 users...</p>
</div>
) : users.length === 0 ? (
<div className="card p-12 text-center">
<p className="text-gray-500 dark:text-gray-400">No users found</p>
</div>
) : (
<>
<div className="space-y-4">
{users.map((user) => (
<div
key={user.id}
className="card p-6 border border-slate-200/60 dark:border-slate-700/60 hover:border-primary/50 transition-colors"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4">
{user.image ? (
<img
src={user.image}
alt={user.firstName}
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-white font-bold">
{user.firstName[0]}{user.lastName[0]}
</div>
)}
<div>
<h3 className="font-bold text-lg text-gray-900 dark:text-white">
{user.firstName} {user.lastName}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{user.email}
</p>
<div className="flex gap-2 mt-2">
{!user.isActive && (
<span className="px-2 py-1 bg-red-500/10 text-red-600 dark:text-red-400 text-xs font-semibold rounded">
🚫 BLOCKED
</span>
)}
{user.emailVerified && (
<span className="px-2 py-1 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-xs font-semibold rounded">
Verified
</span>
)}
{user.role === "ADMIN" && (
<span className="px-2 py-1 bg-blue-500/10 text-blue-600 dark:text-blue-400 text-xs font-semibold rounded">
👑 Admin
</span>
)}
</div>
</div>
</div>
<div className="text-right text-sm text-gray-500 dark:text-gray-400">
<p>Joined: {new Date(user.createdAt).toLocaleDateString()}</p>
<p>Registrations: {user._count.webinarRegistrations}</p>
</div>
</div>
{/* Registered Webinars */}
{user.registeredWebinars.length > 0 && (
<div className="mb-4 pb-4 border-b border-slate-200 dark:border-slate-700">
<p className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
📚 Recent Webinars ({user.registeredWebinars.length})
</p>
<div className="space-y-1">
{user.registeredWebinars.map((reg) => (
<div
key={reg.id}
className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-2"
>
<span className="w-2 h-2 rounded-full bg-primary"></span>
<Link
href={`/admin/webinars#${reg.webinar.id}`}
className="hover:text-primary hover:underline"
>
{reg.webinar.title}
</Link>
<span className="text-xs">
({new Date(reg.webinar.startAt).toLocaleDateString()})
</span>
<span
className={`px-2 py-0.5 text-xs font-semibold rounded ${
reg.status === "CONFIRMED"
? "bg-emerald-500/10 text-emerald-600"
: reg.status === "PAID"
? "bg-blue-500/10 text-blue-600"
: "bg-yellow-500/10 text-yellow-600"
}`}
>
{reg.status}
</span>
</div>
))}
</div>
</div>
)}
{/* Actions */}
<div className="flex items-center justify-between">
<div className="flex gap-2">
<select
value={user.role}
onChange={(e) => handleRoleChange(user.id, e.target.value as "USER" | "ADMIN")}
disabled={updatingUserId === user.id}
className="px-3 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-sm font-semibold text-gray-900 dark:text-white disabled:opacity-50"
>
<option value="USER">👤 User</option>
<option value="ADMIN">👑 Admin</option>
</select>
</div>
<button
onClick={() => handleBlockUnblock(user.id, user.isActive)}
disabled={updatingUserId === user.id}
className={`px-4 py-2 rounded-lg font-semibold text-sm transition-colors disabled:opacity-50 ${
user.isActive
? "bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20"
: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/20"
}`}
>
{updatingUserId === user.id
? "⏳"
: user.isActive
? "🚫 Block"
: "✅ Unblock"}
</button>
</div>
</div>
))}
</div>
{/* Pagination */}
{pagination.pages > 1 && (
<div className="mt-8 flex items-center justify-center gap-2">
<button
onClick={() => fetchUsers(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={() => fetchUsers(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={() => fetchUsers(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>
)}
</>
)}
</main>
);
}

270
app/admin/webinars/page.tsx Normal file
View File

@@ -0,0 +1,270 @@
"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>
);
}