253 lines
10 KiB
TypeScript
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>
|
|
);
|
|
} |