Files
yourwillyourwish/app/webinars/page.tsx
2026-02-06 21:44:04 -06:00

265 lines
12 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 { useEffect, useState } from "react";
interface Webinar {
id: string;
title: string;
description: string;
speaker: string;
startAt: string;
duration: number;
bannerUrl?: string;
category: string;
capacity: number;
priceCents: number;
}
interface RegistrationItem {
webinarId: string;
registeredAt: string;
webinar: {
startAt: string;
};
}
function formatDate(dateString: string) {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
export default function WebinarsPage() {
const [webinars, setWebinars] = useState<Webinar[]>([]);
const [loading, setLoading] = useState(true);
const [registeredMap, setRegisteredMap] = useState<Record<string, { registeredAt: string; startAt: string }>>({});
useEffect(() => {
async function fetchWebinars() {
try {
const [webinarsRes, registrationsRes] = await Promise.all([
fetch("/api/webinars?limit=50"),
fetch("/api/account/webinars"),
]);
const webinarsData = await webinarsRes.json();
setWebinars(webinarsData.webinars || []);
if (registrationsRes.ok) {
const registrationsData = await registrationsRes.json();
const nextMap: Record<string, { registeredAt: string; startAt: string }> = {};
(registrationsData.registrations || []).forEach((reg: RegistrationItem) => {
nextMap[reg.webinarId] = {
registeredAt: reg.registeredAt,
startAt: reg.webinar?.startAt,
};
});
setRegisteredMap(nextMap);
}
} catch (error) {
console.error("Failed to fetch webinars:", error);
} finally {
setLoading(false);
}
}
fetchWebinars();
}, []);
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-7xl mx-auto px-6 py-16 lg:py-20">
<div className="text-center mb-14">
<p className="inline-flex items-center gap-2 text-xs font-semibold tracking-[0.2em] uppercase text-primary/80 bg-primary/10 px-4 py-2 rounded-full">
Estate Planning Academy
</p>
<h1 className="mt-5 text-4xl md:text-5xl lg:text-6xl font-black text-gray-900 dark:text-white">
Professional Webinars, Clear Outcomes
</h1>
<p className="mt-4 text-lg text-gray-600 dark:text-gray-400 max-w-3xl mx-auto">
Curated sessions built by attorneys and planners to help you protect wealth, reduce tax exposure, and plan with confidence.
</p>
</div>
<div className="grid md:grid-cols-3 gap-6 mb-10">
{[
{ label: "Expert Sessions", value: "40+" },
{ label: "Live Each Month", value: "8" },
{ label: "Average Rating", value: "4.9" },
].map((stat) => (
<div
key={stat.label}
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-6 text-center shadow-lg"
>
<div className="text-3xl font-black text-gray-900 dark:text-white">{stat.value}</div>
<p className="mt-2 text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
{stat.label}
</p>
</div>
))}
</div>
<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 text-gray-600 dark:text-gray-400">
Loading webinars...
</div>
</div>
</main>
);
}
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-7xl mx-auto px-6 py-16 lg:py-20">
<div className="grid lg:grid-cols-[1.3fr_0.7fr] gap-8 items-end mb-12">
<div>
<p className="inline-flex items-center gap-2 text-xs font-semibold tracking-[0.2em] uppercase text-primary/80 bg-primary/10 px-4 py-2 rounded-full">
Estate Planning Academy
</p>
<h1 className="mt-5 text-4xl md:text-5xl lg:text-6xl font-black text-gray-900 dark:text-white">
Professional Webinars, Clear Outcomes
</h1>
<p className="mt-4 text-lg text-gray-600 dark:text-gray-400 max-w-2xl">
Curated sessions built by attorneys and planners to help you protect wealth, reduce tax exposure, and plan with confidence.
</p>
</div>
<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-6 shadow-lg">
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">Quick stats</p>
<div className="mt-4 space-y-3">
<div className="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300">
<span>Expert Sessions</span>
<span className="font-semibold text-gray-900 dark:text-white">40+</span>
</div>
<div className="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300">
<span>Live Each Month</span>
<span className="font-semibold text-gray-900 dark:text-white">8</span>
</div>
<div className="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300">
<span>Average Rating</span>
<span className="font-semibold text-gray-900 dark:text-white">4.9</span>
</div>
</div>
</div>
</div>
{webinars.length === 0 ? (
<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 text-gray-600 dark:text-gray-400">
No webinars available at the moment.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-7">
{webinars.map((webinar) => {
const registration = registeredMap[webinar.id];
const isRegistered = Boolean(registration);
const startAt = new Date(webinar.startAt);
const isPast = startAt.getTime() < Date.now();
return (
<div
key={webinar.id}
className="group rounded-3xl border border-gray-200/70 dark:border-slate-800/70 bg-white/90 dark:bg-slate-900/80 backdrop-blur-md overflow-hidden shadow-[0_20px_50px_rgba(15,23,42,0.12)] hover:shadow-[0_24px_60px_rgba(15,23,42,0.18)] transition-all duration-300"
>
<div className="relative h-44 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-primary/25 via-primary/5 to-secondary/25" />
{webinar.bannerUrl ? (
<img
src={webinar.bannerUrl}
alt={webinar.title}
className="relative w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
) : (
<div className="relative h-full w-full flex items-center justify-center text-sm text-gray-500 dark:text-gray-400">
Webinar preview
</div>
)}
<div className="absolute top-4 left-4 flex flex-wrap gap-2">
<span className="rounded-full bg-white/90 text-primary text-xs font-semibold px-3 py-1 shadow">
{webinar.category}
</span>
{webinar.priceCents === 0 ? (
<span className="rounded-full bg-emerald-500/90 text-white text-xs font-semibold px-3 py-1 shadow">
Free
</span>
) : null}
</div>
{isRegistered && (
<div className="absolute top-4 right-4">
<span className={`rounded-full text-xs font-semibold px-3 py-1 shadow ${isPast ? "bg-slate-700 text-white" : "bg-emerald-500 text-white"}`}>
{isPast ? "Completed" : "Registered"}
</span>
</div>
)}
</div>
<div className="p-6">
<div className="flex items-start justify-between gap-3">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">
{webinar.title}
</h3>
{webinar.priceCents > 0 && (
<span className="text-sm font-bold text-primary dark:text-secondary">
${(webinar.priceCents / 100).toFixed(2)}
</span>
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2 line-clamp-2">
{webinar.description}
</p>
<div className="mt-4 grid grid-cols-2 gap-3 text-sm">
<div className="flex items-center gap-2 text-gray-700 dark:text-gray-300">
<span>🎤</span>
<span className="font-medium">{webinar.speaker}</span>
</div>
<div className="flex items-center gap-2 text-gray-700 dark:text-gray-300">
<span>📅</span>
<span>{formatDate(webinar.startAt)}</span>
</div>
<div className="flex items-center gap-2 text-gray-700 dark:text-gray-300">
<span></span>
<span>{webinar.duration} min</span>
</div>
<div className="flex items-center gap-2 text-gray-700 dark:text-gray-300">
<span>👥</span>
<span>{webinar.capacity} seats</span>
</div>
</div>
<div className="mt-5 flex items-center justify-between">
<span className={`text-xs font-semibold px-3 py-1 rounded-full ${isPast ? "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300" : "bg-primary/10 text-primary"}`}>
{isPast ? "Past session" : "Upcoming"}
</span>
<a
href={`/webinars/${webinar.id}`}
className="inline-flex items-center justify-center gap-2 rounded-full px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-primary to-primary-dark shadow-md hover:shadow-lg transition-all duration-300"
>
View details
</a>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</main>
);
}