Files
yourwillyourwish/app/webinars/[id]/WebinarDetailClient.tsx
2026-02-06 21:44:04 -06:00

267 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}