FitnessApp/app/[tenant]/instructor/classes/page.tsx

253 lines
10 KiB
TypeScript

export const dynamic = 'force-dynamic';
import React from 'react';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { prisma, getTenantPrisma } from '@/lib/prisma';
import { generateJitsiRoomName } from '@/lib/jitsi';
interface PageProps {
params: Promise<{ tenant: string }>;
}
export default async function InstructorClassesPage({
params }: PageProps) {
const { tenant: tenantSlug } = await params;
const tenant = await prisma.tenant.findUnique({
where: { slug: tenantSlug },
include: {
users: {
where: { role: { in: ['INSTRUCTOR', 'SUPER_ADMIN'] } },
},
classSessions: {
orderBy: { startTime: 'desc' },
include: {
bookings: {
include: { user: true },
},
},
},
},
});
if (!tenant) {
notFound();
}
// Server Action to schedule class
async function scheduleClassAction(formData: FormData): Promise<void> {
'use server';
const title = formData.get('title') as string;
const description = (formData.get('description') as string) || '';
const dateStr = formData.get('date') as string;
const timeStr = formData.get('time') as string;
const durationMins = parseInt(formData.get('duration') as string, 10) || 60;
const capacity = parseInt(formData.get('capacity') as string, 10) || 15;
const start = new Date(`${dateStr}T${timeStr}:00`);
const end = new Date(start.getTime() + durationMins * 60 * 1000);
const timestamp = Date.now();
const tempId = Math.random().toString(36).slice(2, 7);
const jitsiRoomId = generateJitsiRoomName(tenantSlug, tempId, timestamp);
const tenantPrisma = getTenantPrisma(tenant!.id);
await tenantPrisma.classSession.create({
data: {
tenantId: tenant!.id,
title,
description,
startTime: start,
endTime: end,
capacity,
jitsiRoomId,
},
});
revalidatePath(`/${tenantSlug}/instructor/classes`);
revalidatePath(`/${tenantSlug}/classes`);
}
const defaultDate = new Date().toISOString().split('T')[0];
const defaultTime = '10:00';
return (
<div className="min-h-screen bg-neutral-950 text-neutral-100 p-6 md:p-12 font-sans">
<div className="max-w-6xl mx-auto space-y-8">
{/* Navigation Breadcrumb */}
<div className="flex items-center justify-between border-b border-neutral-800 pb-4">
<div className="flex items-center gap-3">
<Link
href={`/${tenantSlug}/classes`}
className="text-xs text-neutral-400 hover:text-white transition"
>
← Student Class Schedule
</Link>
<span className="text-neutral-600">/</span>
<span className="text-xs text-emerald-400 font-semibold uppercase tracking-wider">
Instructor Portal
</span>
</div>
<span className="text-xs font-mono text-neutral-500 bg-neutral-900 px-2.5 py-1 rounded border border-neutral-800">
Tenant: {tenant.name} ({tenant.slug})
</span>
</div>
{/* Header */}
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">Live Classroom Management</h1>
<p className="text-sm text-neutral-400 mt-1">
Schedule virtual workout classes, configure WebRTC room parameters, and launch live unbranded sessions.
</p>
</div>
{/* Schedule New Class Form */}
<section className="bg-neutral-900/90 border border-neutral-800 rounded-xl p-6 shadow-xl">
<h2 className="text-base font-semibold text-white mb-4 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-400" />
Schedule New Virtual Class
</h2>
<form action={scheduleClassAction} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="lg:col-span-2">
<label className="block text-xs font-medium text-neutral-300 mb-1">Class Title</label>
<input
type="text"
name="title"
required
placeholder="e.g. HIIT Strength & Conditioning"
className="w-full bg-neutral-950 border border-neutral-800 rounded-lg px-3 py-2 text-sm text-white placeholder-neutral-600 focus:outline-none focus:border-emerald-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-neutral-300 mb-1">Date</label>
<input
type="date"
name="date"
required
defaultValue={defaultDate}
className="w-full bg-neutral-950 border border-neutral-800 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-neutral-300 mb-1">Start Time</label>
<input
type="time"
name="time"
required
defaultValue={defaultTime}
className="w-full bg-neutral-950 border border-neutral-800 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500"
/>
</div>
<div className="lg:col-span-2">
<label className="block text-xs font-medium text-neutral-300 mb-1">Description (Optional)</label>
<input
type="text"
name="description"
placeholder="Equipment needed: dumbbells, resistance band"
className="w-full bg-neutral-950 border border-neutral-800 rounded-lg px-3 py-2 text-sm text-white placeholder-neutral-600 focus:outline-none focus:border-emerald-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-neutral-300 mb-1">Duration (Minutes)</label>
<input
type="number"
name="duration"
defaultValue={45}
min={15}
max={180}
className="w-full bg-neutral-950 border border-neutral-800 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-neutral-300 mb-1">Capacity</label>
<input
type="number"
name="capacity"
defaultValue={15}
min={1}
max={100}
className="w-full bg-neutral-950 border border-neutral-800 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500"
/>
</div>
<div className="md:col-span-2 lg:col-span-4 flex justify-end mt-2">
<button
type="submit"
className="px-5 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-semibold rounded-lg shadow transition flex items-center gap-2"
>
<span>Schedule & Generate Room</span>
<span>→</span>
</button>
</div>
</form>
</section>
{/* Existing Classes List */}
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-white">Classroom Sessions ({tenant.classSessions.length})</h2>
<span className="text-xs text-neutral-400">Deterministic room names with host JWT signing</span>
</div>
{tenant.classSessions.length === 0 ? (
<div className="bg-neutral-900 border border-dashed border-neutral-800 rounded-xl p-10 text-center text-neutral-400 text-sm">
No classes scheduled yet. Use the form above to create your first live class session.
</div>
) : (
<div className="grid grid-cols-1 gap-4">
{tenant.classSessions.map((session) => (
<div
key={session.id}
className="bg-neutral-900 border border-neutral-800 rounded-xl p-5 hover:border-neutral-700 transition flex flex-col md:flex-row md:items-center justify-between gap-4"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="text-base font-semibold text-white">{session.title}</h3>
<span className="px-2 py-0.5 rounded text-[10px] bg-neutral-800 text-neutral-400 font-mono">
Cap: {session.bookings.length} / {session.capacity}
</span>
</div>
{session.description && (
<p className="text-xs text-neutral-400">{session.description}</p>
)}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-neutral-500 font-mono pt-1">
<span>Start: {new Date(session.startTime).toLocaleString()}</span>
<span>•</span>
<span>Room: {session.jitsiRoomId}</span>
</div>
</div>
<div className="flex items-center gap-3">
<Link
href={`/${tenantSlug}/classroom/${session.id}?role=INSTRUCTOR`}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold rounded-lg shadow transition flex items-center gap-1.5 whitespace-nowrap"
>
<span className="w-2 h-2 rounded-full bg-white animate-pulse" />
Start Live Class (Host View)
</Link>
<Link
href={`/${tenantSlug}/classroom/${session.id}?role=CLIENT`}
className="px-3 py-2 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 text-xs font-medium rounded-lg transition whitespace-nowrap"
>
Preview Student View
</Link>
</div>
</div>
))}
</div>
)}
</section>
</div>
</div>
);
}