Initial commit
This commit is contained in:
266
app/webinars/[id]/WebinarDetailClient.tsx
Normal file
266
app/webinars/[id]/WebinarDetailClient.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface Webinar {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
speaker: string;
|
||||
startAt: string;
|
||||
duration: number;
|
||||
priceCents: number;
|
||||
visibility: "PUBLIC" | "PRIVATE";
|
||||
isActive: boolean;
|
||||
capacity: number;
|
||||
learningPoints?: string[];
|
||||
_count?: { registrations: number };
|
||||
}
|
||||
|
||||
interface WebinarDetailClientProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default function WebinarDetailClient({ id }: WebinarDetailClientProps) {
|
||||
const [webinar, setWebinar] = useState<Webinar | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [registering, setRegistering] = useState(false);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
fetchWebinar();
|
||||
checkAuth();
|
||||
}, [id]);
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/auth/me");
|
||||
setIsAuthenticated(response.ok);
|
||||
} catch (error) {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchWebinar = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/webinars/${id}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setWebinar(data.webinar);
|
||||
} else {
|
||||
router.push("/webinars");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch webinar:", error);
|
||||
router.push("/webinars");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!isAuthenticated) {
|
||||
setShowAuthModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setRegistering(true);
|
||||
try {
|
||||
const response = await fetch("/api/registrations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ webinarId: id }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert("Successfully registered for the webinar!");
|
||||
router.push("/account/webinars");
|
||||
} else {
|
||||
const data = await response.json();
|
||||
alert(data.error || "Registration failed");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Failed to register. Please try again.");
|
||||
} finally {
|
||||
setRegistering(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-slate-50 via-white to-slate-50 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900" />
|
||||
<div className="absolute -top-24 right-0 h-72 w-72 rounded-full bg-primary/20 blur-3xl" />
|
||||
<div className="absolute -bottom-24 left-0 h-72 w-72 rounded-full bg-secondary/20 blur-3xl" />
|
||||
|
||||
<div className="relative max-w-6xl mx-auto px-6 py-16 lg:py-20">
|
||||
<div className="rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-white/80 dark:bg-slate-900/70 backdrop-blur-md p-10 shadow-lg text-center">
|
||||
<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 webinar details...</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!webinar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const webinarDate = new Date(webinar.startAt);
|
||||
const registeredCount = webinar._count?.registrations || 0;
|
||||
const spotsLeft = webinar.capacity - registeredCount;
|
||||
const isFull = spotsLeft <= 0;
|
||||
|
||||
return (
|
||||
<main className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-slate-50 via-white to-slate-50 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900" />
|
||||
<div className="absolute -top-24 right-0 h-72 w-72 rounded-full bg-primary/20 blur-3xl" />
|
||||
<div className="absolute -bottom-24 left-0 h-72 w-72 rounded-full bg-secondary/20 blur-3xl" />
|
||||
|
||||
<div className="relative max-w-6xl mx-auto px-6 py-16 lg:py-20">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="mb-8 inline-flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 hover:text-primary transition-colors"
|
||||
>
|
||||
← Back to Webinars
|
||||
</button>
|
||||
|
||||
<div className="rounded-3xl border border-gray-200/70 dark:border-slate-800/70 bg-white/90 dark:bg-slate-900/80 backdrop-blur-md shadow-[0_24px_60px_rgba(15,23,42,0.15)] overflow-hidden">
|
||||
<div className="relative px-8 py-10 bg-gradient-to-r from-primary/90 via-primary to-secondary text-white">
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute top-6 right-10 h-24 w-24 rounded-full bg-white/30 blur-2xl" />
|
||||
<div className="absolute bottom-6 left-10 h-28 w-28 rounded-full bg-white/30 blur-2xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-wrap items-center gap-3 mb-5">
|
||||
<span className={`px-4 py-1.5 rounded-full text-xs font-bold ${webinar.isActive ? "bg-white/20 text-white" : "bg-white/10 text-white/70"}`}>
|
||||
{webinar.isActive ? "ACTIVE" : "INACTIVE"}
|
||||
</span>
|
||||
{isFull && (
|
||||
<span className="px-4 py-1.5 rounded-full text-xs font-bold bg-red-500/90 text-white">
|
||||
FULL
|
||||
</span>
|
||||
)}
|
||||
<span className="px-4 py-1.5 rounded-full text-xs font-bold bg-white/15 text-white">
|
||||
{webinar.visibility === "PRIVATE" ? "PRIVATE" : "PUBLIC"}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-black mb-4">{webinar.title}</h1>
|
||||
<p className="text-lg text-white/90 max-w-3xl">{webinar.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8 lg:p-10 grid lg:grid-cols-[1.4fr_0.9fr] gap-8">
|
||||
<div>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-slate-50/70 dark:bg-slate-900/60 p-6">
|
||||
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400">📅 Date & Time</p>
|
||||
<p className="mt-2 text-lg font-bold text-gray-900 dark:text-white">
|
||||
{webinarDate.toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{webinarDate.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-slate-50/70 dark:bg-slate-900/60 p-6">
|
||||
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400">👨🏫 Instructor</p>
|
||||
<p className="mt-2 text-lg font-bold text-gray-900 dark:text-white">
|
||||
{webinar.speaker}
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">Estate planning specialist</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-slate-50/70 dark:bg-slate-900/60 p-6">
|
||||
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400">⏱️ Duration</p>
|
||||
<p className="mt-2 text-lg font-bold text-gray-900 dark:text-white">{webinar.duration} minutes</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">Interactive Q&A included</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-slate-50/70 dark:bg-slate-900/60 p-6">
|
||||
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400">💰 Price</p>
|
||||
<p className="mt-2 text-3xl font-black text-primary">
|
||||
{webinar.priceCents === 0 ? "FREE" : `$${(webinar.priceCents / 100).toFixed(2)}`}
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">Secure checkout</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{webinar.learningPoints && webinar.learningPoints.length > 0 && (
|
||||
<div className="mt-8 rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-white/80 dark:bg-slate-900/70 p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">📚 What You'll Learn</h3>
|
||||
<ul className="grid sm:grid-cols-2 gap-3">
|
||||
{webinar.learningPoints.map((point, index) => (
|
||||
<li key={index} className="flex items-start gap-2 text-gray-700 dark:text-gray-300">
|
||||
<span className="text-primary font-bold">•</span>
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-white/80 dark:bg-slate-900/70 p-6 shadow-lg">
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">Registration</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{isFull
|
||||
? "This webinar is fully booked."
|
||||
: `${spotsLeft} spots left out of ${webinar.capacity}`}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleRegister}
|
||||
disabled={registering || isFull}
|
||||
className={`w-full py-3 rounded-xl text-sm font-semibold transition-colors ${
|
||||
isFull
|
||||
? "bg-gray-200 text-gray-500 cursor-not-allowed dark:bg-slate-800 dark:text-slate-500"
|
||||
: "bg-primary text-white hover:bg-primary/90"
|
||||
}`}
|
||||
>
|
||||
{registering ? "Registering..." : isFull ? "Fully Booked" : "Register Now"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAuthModal && (
|
||||
<div className="rounded-2xl border border-gray-200/70 dark:border-slate-800/70 bg-white/90 dark:bg-slate-900/80 p-6 shadow-lg">
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-3">Sign in required</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Please sign in to register for this webinar.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/signin?redirect=/webinars/" + id)}
|
||||
className="flex-1 py-2 rounded-lg bg-primary text-white text-sm font-semibold"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAuthModal(false)}
|
||||
className="flex-1 py-2 rounded-lg border border-gray-300 dark:border-slate-700 text-sm font-semibold"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
12
app/webinars/[id]/page.tsx
Normal file
12
app/webinars/[id]/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import WebinarDetailClient from "./WebinarDetailClient";
|
||||
|
||||
interface WebinarDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function WebinarDetailPage({
|
||||
params,
|
||||
}: WebinarDetailPageProps) {
|
||||
const { id } = await params;
|
||||
return <WebinarDetailClient id={id} />;
|
||||
}
|
||||
Reference in New Issue
Block a user