206 lines
10 KiB
TypeScript
206 lines
10 KiB
TypeScript
import LiveClassStatus from '@/components/live-class-status';
|
|
import Tabs from '@/components/tabs';
|
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardHeader } from '@/components/ui/card';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { CoursePlayerProps } from '@/types/page';
|
|
import { Link, router, usePage } from '@inertiajs/react';
|
|
import { format, parseISO } from 'date-fns';
|
|
import { Calendar, Clock } from 'lucide-react';
|
|
import { Renderer } from 'richtor';
|
|
import 'richtor/styles';
|
|
import Lesson from './lesson';
|
|
import Quiz from './quiz';
|
|
|
|
interface ContentListProps {
|
|
completedContents: CompletedContent[];
|
|
courseCompletion: {
|
|
percentage: string;
|
|
totalContents: number;
|
|
completedContents: number;
|
|
};
|
|
}
|
|
|
|
const ContentList = ({ completedContents, courseCompletion }: ContentListProps) => {
|
|
const { props } = usePage<CoursePlayerProps>();
|
|
const { course, zoomConfig, section, watchHistory, translate, direction, system } = props;
|
|
const { button, common, frontend } = translate;
|
|
const showCourseCertificate = system?.fields?.show_course_certificate ?? true;
|
|
const showCourseMarksheet = system?.fields?.show_course_marksheet ?? showCourseCertificate;
|
|
const showCertificateTab = showCourseCertificate || showCourseMarksheet;
|
|
const certificateLabel = showCourseCertificate ? frontend.course_certificate : button.marksheet || 'Notenblatt';
|
|
|
|
// Get live classes from course data
|
|
const liveClasses = course.live_classes || [];
|
|
|
|
// Get the last section
|
|
const lastSection = props.course.sections[props.course.sections.length - 1];
|
|
|
|
// Get the last content based on current_watching_type
|
|
let lastContent;
|
|
if (watchHistory.current_watching_type === 'lesson') {
|
|
lastContent = lastSection.section_lessons.find((lesson) => lesson.id.toString() === watchHistory.current_watching_id);
|
|
} else {
|
|
lastContent = lastSection.section_quizzes.find((quiz) => quiz.id.toString() === watchHistory.current_watching_id);
|
|
}
|
|
|
|
const finishCourseHandler = () => {
|
|
router.get(route('course.player.finish', { watch_history: watchHistory.id }));
|
|
};
|
|
|
|
return (
|
|
<div className="relative h-full md:h-[calc(100vh-60px)]">
|
|
<Tabs defaultValue="lessons" className="w-full">
|
|
<TabsList className="h-12 w-full">
|
|
<TabsTrigger value="lessons" className="h-10 w-full cursor-pointer">
|
|
{button.lessons}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="live-classes" className="h-10 w-full cursor-pointer">
|
|
{button.live_classes}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<ScrollArea className="h-[calc(100vh-48px)] md:h-[calc(100vh-108px)]">
|
|
<TabsContent dir={direction} value="lessons" className="mt-0">
|
|
<CardHeader className="px-4 py-6 text-center">
|
|
<div className="w-full space-y-1">
|
|
<p className="text-muted-foreground flex items-center text-xs font-medium">
|
|
<span>{courseCompletion.percentage}%</span>
|
|
<span>
|
|
{common.completed} {courseCompletion.completedContents}/{courseCompletion.totalContents}
|
|
</span>
|
|
</p>
|
|
|
|
<Progress value={Number(courseCompletion.percentage)} className="[&>div]:bg-secondary-foreground h-1" />
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<Separator />
|
|
|
|
{section ? (
|
|
<Accordion
|
|
type="single"
|
|
collapsible
|
|
// type="multiple"
|
|
className="space-y-4 px-4 py-6"
|
|
defaultValue={section.id as string}
|
|
>
|
|
{props.course.sections.map((section, ind) => (
|
|
<AccordionItem key={section.id} value={section.id as string} className="overflow-hidden rounded-lg border">
|
|
<AccordionTrigger className="[&[data-state=open]]:!bg-muted px-4 py-3 text-base hover:no-underline">
|
|
{ind + 1}. {section.title}
|
|
</AccordionTrigger>
|
|
<AccordionContent className="space-y-2 p-2">
|
|
{section.section_lessons.length > 0 ? (
|
|
<>
|
|
{section.section_lessons.map((lesson) => (
|
|
<Lesson key={lesson.id} lesson={lesson} completed={completedContents} />
|
|
))}
|
|
|
|
{section.section_quizzes.map((quiz) => (
|
|
<Quiz key={quiz.id} quiz={quiz} completed={completedContents} />
|
|
))}
|
|
</>
|
|
) : (
|
|
<div className="px-4 py-3 text-center">
|
|
<p>Keine Lektion vorhanden</p>
|
|
</div>
|
|
)}
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
))}
|
|
|
|
{watchHistory.completion_date ? (
|
|
<div>
|
|
<Link
|
|
href={route('student.course.show', {
|
|
id: course.id,
|
|
tab: showCertificateTab ? 'certificate' : 'modules',
|
|
})}
|
|
>
|
|
<Button className="w-full" variant="secondary" size="lg" disabled={courseCompletion.percentage !== '100.00'}>
|
|
{showCertificateTab ? certificateLabel : 'Module'}
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
{!watchHistory.next_watching_id ? (
|
|
<Button className="w-full" variant="secondary" size="lg" onClick={finishCourseHandler}>
|
|
Kurs abschließen
|
|
</Button>
|
|
) : (
|
|
<Button className="w-full" variant="secondary" size="lg" disabled>
|
|
Kurs abschließen
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Accordion>
|
|
) : (
|
|
<div className="p-6 text-center">
|
|
<p>Kein Abschnitt vorhanden</p>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
<TabsContent value="live-classes" className="mt-0">
|
|
<div className="space-y-4 p-4">
|
|
{liveClasses.length <= 0 ? (
|
|
<Card className="p-8 text-center">
|
|
<Calendar className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
|
<h3 className="mb-2 text-lg font-medium">Keine Live-Sitzungen geplant</h3>
|
|
<p className="text-gray-500">Plane deine erste Live-Session, um mit Zoom zu starten.</p>
|
|
</Card>
|
|
) : (
|
|
liveClasses.map((liveClass) => {
|
|
return (
|
|
<Card key={liveClass.id} className="space-y-4 p-4">
|
|
<p className="text-base font-medium">{liveClass.class_topic}</p>
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="text-muted-foreground space-y-3 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<Clock className="h-4 w-4" />
|
|
<span>{format(parseISO(liveClass.class_date_and_time), 'p')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="h-4 w-4" />
|
|
<span>{format(parseISO(liveClass.class_date_and_time), 'PPP')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<LiveClassStatus courseId={course.id} liveClass={liveClass} zoomConfig={zoomConfig} />
|
|
</div>
|
|
</div>
|
|
|
|
{liveClass.class_note && (
|
|
<Accordion type="single" collapsible className="w-full">
|
|
<AccordionItem value="item-1" className="bg-muted overflow-hidden rounded-lg border-none">
|
|
<AccordionTrigger className="[&[data-state=open]]:!bg-secondary-lighter px-3 py-1.5 text-sm font-normal hover:no-underline">
|
|
Hinweis zur Sitzung
|
|
</AccordionTrigger>
|
|
<AccordionContent className="p-3">
|
|
<Renderer value={liveClass.class_note} />
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
)}
|
|
</Card>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
</ScrollArea>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ContentList;
|