Initial commit
This commit is contained in:
80
components/admin/AdminSidebar.tsx
Normal file
80
components/admin/AdminSidebar.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
interface AdminSidebarProps {
|
||||
userName?: string
|
||||
}
|
||||
|
||||
export default function AdminSidebar({ userName }: AdminSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
const isActive = (href: string) => {
|
||||
// Exact match for dashboard
|
||||
if (href === '/admin') {
|
||||
return pathname === '/admin' || pathname === '/admin/analytics'
|
||||
}
|
||||
// For other routes, check if pathname starts with href
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ href: '/admin', label: '📊 Dashboard', icon: '📊' },
|
||||
{ href: '/admin/users', label: '👥 Users', icon: '👥' },
|
||||
{ href: '/admin/webinars', label: '📹 Webinars', icon: '📹' },
|
||||
{ href: '/admin/registrations', label: '📝 Registrations', icon: '📝' },
|
||||
{ href: '/admin/contact-messages', label: '📧 Messages', icon: '📧' },
|
||||
{ href: '/admin/setup', label: '⚙️ Setup', icon: '⚙️' },
|
||||
]
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white dark:bg-slate-800 border-r border-gray-200 dark:border-slate-700 h-screen sticky top-0 overflow-y-auto shadow-sm">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-primary to-primary-dark text-white flex items-center justify-center font-bold text-lg">
|
||||
⚙️
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Admin</h2>
|
||||
</div>
|
||||
{userName && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 font-medium">
|
||||
👤 {userName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg transition-all duration-200 font-medium text-sm ${
|
||||
active
|
||||
? 'bg-primary text-white shadow-md'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{item.icon}</span>
|
||||
<span>{item.label.split(' ')[1]}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 border-t border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-800/50 backdrop-blur-sm">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-primary transition-colors py-2"
|
||||
>
|
||||
<span>←</span>
|
||||
<span>Back to Site</span>
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
338
components/admin/WebinarModal.tsx
Normal file
338
components/admin/WebinarModal.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface WebinarModalProps {
|
||||
webinar?: any;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export default function WebinarModal({ webinar, onClose, onSave }: WebinarModalProps) {
|
||||
const isEdit = !!webinar;
|
||||
const [form, setForm] = useState({
|
||||
title: "",
|
||||
description: "",
|
||||
speaker: "",
|
||||
startAt: new Date(Date.now() + 86400000).toISOString().slice(0, 16),
|
||||
duration: 90,
|
||||
bannerUrl: "",
|
||||
category: "Basics",
|
||||
visibility: "PUBLIC",
|
||||
isActive: true,
|
||||
capacity: 25,
|
||||
priceCents: 0,
|
||||
});
|
||||
const [learningPoints, setLearningPoints] = useState<string[]>([""]);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (webinar) {
|
||||
setForm({
|
||||
title: webinar.title || "",
|
||||
description: webinar.description || "",
|
||||
speaker: webinar.speaker || "",
|
||||
startAt: webinar.startAt ? new Date(webinar.startAt).toISOString().slice(0, 16) : "",
|
||||
duration: webinar.duration || 90,
|
||||
bannerUrl: webinar.bannerUrl || "",
|
||||
category: webinar.category || "Basics",
|
||||
visibility: webinar.visibility || "PUBLIC",
|
||||
isActive: webinar.isActive ?? true,
|
||||
capacity: webinar.capacity || 25,
|
||||
priceCents: webinar.priceCents || 0,
|
||||
});
|
||||
|
||||
// Load learning points if available
|
||||
if (webinar.learningPoints && Array.isArray(webinar.learningPoints) && webinar.learningPoints.length > 0) {
|
||||
setLearningPoints(webinar.learningPoints);
|
||||
} else {
|
||||
setLearningPoints([""]);
|
||||
}
|
||||
}
|
||||
}, [webinar]);
|
||||
|
||||
function addLearningPoint() {
|
||||
setLearningPoints([...learningPoints, ""]);
|
||||
}
|
||||
|
||||
function removeLearningPoint(index: number) {
|
||||
if (learningPoints.length > 1) {
|
||||
setLearningPoints(learningPoints.filter((_, i) => i !== index));
|
||||
}
|
||||
}
|
||||
|
||||
function updateLearningPoint(index: number, value: string) {
|
||||
const updated = [...learningPoints];
|
||||
updated[index] = value;
|
||||
setLearningPoints(updated);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
setMsg(null);
|
||||
setLoading(true);
|
||||
|
||||
// Validate required fields
|
||||
if (!form.title.trim()) {
|
||||
setLoading(false);
|
||||
setMsg("❌ Title is required");
|
||||
return;
|
||||
}
|
||||
if (!form.description.trim()) {
|
||||
setLoading(false);
|
||||
setMsg("❌ Description is required");
|
||||
return;
|
||||
}
|
||||
if (!form.speaker.trim()) {
|
||||
setLoading(false);
|
||||
setMsg("❌ Speaker is required");
|
||||
return;
|
||||
}
|
||||
if (form.duration < 15) {
|
||||
setLoading(false);
|
||||
setMsg("❌ Duration must be at least 15 minutes");
|
||||
return;
|
||||
}
|
||||
if (form.capacity < 1) {
|
||||
setLoading(false);
|
||||
setMsg("❌ Capacity must be at least 1");
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out empty learning points
|
||||
const filteredPoints = learningPoints.filter(p => p.trim() !== "");
|
||||
|
||||
const payload = {
|
||||
...form,
|
||||
duration: Number(form.duration),
|
||||
capacity: Number(form.capacity),
|
||||
priceCents: Number(form.priceCents),
|
||||
startAt: new Date(form.startAt).toISOString(),
|
||||
learningPoints: filteredPoints,
|
||||
};
|
||||
|
||||
const res = isEdit
|
||||
? await fetch(`/api/webinars/${webinar.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
: await fetch("/api/webinars", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const d = await res.json();
|
||||
setLoading(false);
|
||||
|
||||
if (!d.ok) {
|
||||
setMsg(d.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setMsg(isEdit ? "Webinar updated!" : "Webinar created!");
|
||||
setTimeout(() => {
|
||||
onSave();
|
||||
onClose();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">
|
||||
{isEdit ? "✏️ Edit Webinar" : "🎓 Create New Webinar"}
|
||||
</h2>
|
||||
<button onClick={onClose} className="modal-close-btn">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body space-y-4">
|
||||
{msg && (
|
||||
<div className={`p-4 rounded-xl text-sm font-medium ${msg.includes("!") || msg.includes("✅") ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" : "bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/20"}`}>
|
||||
{msg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">📝 Title *</label>
|
||||
<input
|
||||
className="input-field w-full"
|
||||
placeholder="e.g., Estate Planning Fundamentals"
|
||||
value={form.title}
|
||||
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">📄 Description *</label>
|
||||
<textarea
|
||||
className="input-field w-full"
|
||||
rows={4}
|
||||
placeholder="Describe what participants will learn..."
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">🎤 Speaker *</label>
|
||||
<input
|
||||
className="input-field w-full"
|
||||
placeholder="e.g., Dr. Jane Smith"
|
||||
value={form.speaker}
|
||||
onChange={(e) => setForm({ ...form, speaker: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">🏷️ Category *</label>
|
||||
<select
|
||||
className="input-field w-full"
|
||||
value={form.category}
|
||||
onChange={(e) => setForm({ ...form, category: e.target.value })}
|
||||
>
|
||||
<option value="Basics">Basics</option>
|
||||
<option value="Planning">Planning</option>
|
||||
<option value="Tax">Tax</option>
|
||||
<option value="Healthcare">Healthcare</option>
|
||||
<option value="Advanced">Advanced</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">📅 Start Date & Time *</label>
|
||||
<input
|
||||
className="input-field w-full"
|
||||
type="datetime-local"
|
||||
value={form.startAt}
|
||||
onChange={(e) => setForm({ ...form, startAt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">⏱️ Duration (minutes) *</label>
|
||||
<input
|
||||
className="input-field w-full"
|
||||
type="number"
|
||||
min="15"
|
||||
step="15"
|
||||
value={form.duration}
|
||||
onChange={(e) => setForm({ ...form, duration: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">👥 Capacity *</label>
|
||||
<input
|
||||
className="input-field w-full"
|
||||
type="number"
|
||||
min="1"
|
||||
value={form.capacity}
|
||||
onChange={(e) => setForm({ ...form, capacity: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">
|
||||
💰 Price {form.priceCents === 0 ? <span className="text-green-600 dark:text-green-400">(FREE)</span> : <span className="text-blue-600 dark:text-blue-400">(PAID)</span>}
|
||||
</label>
|
||||
<input
|
||||
className="input-field w-full"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0 for free"
|
||||
value={form.priceCents}
|
||||
onChange={(e) => setForm({ ...form, priceCents: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
${(form.priceCents / 100).toFixed(2)} {form.priceCents === 0 && "- This webinar will be free"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">🖼️ Banner Image URL (optional)</label>
|
||||
<input
|
||||
className="input-field w-full"
|
||||
placeholder="https://example.com/banner.jpg"
|
||||
value={form.bannerUrl}
|
||||
onChange={(e) => setForm({ ...form, bannerUrl: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">📚 Learning Points</label>
|
||||
<div className="space-y-2">
|
||||
{learningPoints.map((point, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<input
|
||||
className="input-field flex-1"
|
||||
placeholder={`Learning point ${index + 1}`}
|
||||
value={point}
|
||||
onChange={(e) => updateLearningPoint(index, e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeLearningPoint(index)}
|
||||
className="px-3 py-2 bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-400 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={learningPoints.length === 1}
|
||||
title="Remove point"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addLearningPoint}
|
||||
className="w-full px-4 py-2 bg-primary/10 hover:bg-primary/20 text-primary rounded-lg transition-colors font-medium text-sm"
|
||||
>
|
||||
➕ Add Learning Point
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">👁️ Visibility</label>
|
||||
<select
|
||||
className="input-field w-full"
|
||||
value={form.visibility}
|
||||
onChange={(e) => setForm({ ...form, visibility: e.target.value })}
|
||||
>
|
||||
<option value="PUBLIC">Public</option>
|
||||
<option value="PRIVATE">Private</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-5 h-5 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
checked={form.isActive}
|
||||
onChange={(e) => setForm({ ...form, isActive: e.target.checked })}
|
||||
/>
|
||||
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">✅ Active</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button onClick={onClose} className="btn-secondary" disabled={loading}>
|
||||
❌ Cancel
|
||||
</button>
|
||||
<button onClick={handleSubmit} className="btn-primary" disabled={loading}>
|
||||
{loading ? "⏳ Saving..." : isEdit ? "✅ Update Webinar" : "🎓 Create Webinar"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user