Stage 1: Course/Module/Lesson CRUD admin UI with TipTap editor
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { grantAccess, revokeAccess } from "@/app/admin/courses/[courseId]/actions";
|
||||
|
||||
interface Student {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
allStudents: Student[];
|
||||
enrolledIds: string[];
|
||||
}
|
||||
|
||||
export function EnrollmentManager({ courseId, allStudents, enrolledIds }: Props) {
|
||||
const [enrolled, setEnrolled] = useState(new Set(enrolledIds));
|
||||
const [search, setSearch] = useState("");
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const filtered = allStudents.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
s.email.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
function toggle(userId: string) {
|
||||
if (enrolled.has(userId)) {
|
||||
setEnrolled((prev) => { const s = new Set(prev); s.delete(userId); return s; });
|
||||
startTransition(() => revokeAccess(courseId, userId));
|
||||
} else {
|
||||
setEnrolled((prev) => new Set(prev).add(userId));
|
||||
startTransition(() => grantAccess(courseId, userId));
|
||||
}
|
||||
}
|
||||
|
||||
const enrolledStudents = allStudents.filter((s) => enrolled.has(s.id));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{enrolledStudents.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm text-slate-500 mb-2">Доступ открыт ({enrolledStudents.length}):</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{enrolledStudents.map((s) => (
|
||||
<Badge key={s.id} variant="secondary" className="gap-1.5 py-1 pr-1">
|
||||
{s.name}
|
||||
<button
|
||||
onClick={() => toggle(s.id)}
|
||||
disabled={pending}
|
||||
className="ml-1 text-slate-400 hover:text-red-500"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-slate-500 mb-2">Добавить ученика:</p>
|
||||
<Input
|
||||
placeholder="Поиск по имени или email..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="max-w-sm mb-3"
|
||||
/>
|
||||
<div className="space-y-1.5 max-h-48 overflow-y-auto">
|
||||
{filtered.map((student) => (
|
||||
<div key={student.id} className="flex items-center justify-between px-3 py-2 rounded-lg border border-slate-100 bg-slate-50">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700">{student.name}</p>
|
||||
<p className="text-xs text-slate-400">{student.email}</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={enrolled.has(student.id) ? "destructive" : "outline"}
|
||||
onClick={() => toggle(student.id)}
|
||||
disabled={pending}
|
||||
>
|
||||
{enrolled.has(student.id) ? "Убрать" : "Дать доступ"}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<p className="text-sm text-slate-400 py-2">Студентов не найдено</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user