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,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>
)
}

View 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>
);
}