feat: add API course, lesson, section

This commit is contained in:
2026-09-09 22:16:47 +07:00
parent 7bc3cf4e73
commit f8eaa51d9d
14 changed files with 1224 additions and 274 deletions
+129 -121
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
@@ -23,116 +23,9 @@ import {
HiOutlineArrowUpTray,
HiOutlineFunnel,
} from 'react-icons/hi2';
const coursesData = [
{
code: 'LDR-101',
title: 'Foundations of Modern Leadership',
description:
'An introductory course covering essential leadership principles for new managers.',
status: 'Active',
lastUpdate: '2026-08-19',
image:
'https://images.unsplash.com/photo-1556761175-b413da4baf72?auto=format&fit=crop&w=800&q=80',
},
{
code: 'TEC-204',
title: 'Advanced Cloud Architecture Patterns',
description:
'Deep dive into scalable cloud infrastructure design and implementation strategies.',
status: 'Inactive',
lastUpdate: '2026-08-19',
image:
'https://images.unsplash.com/photo-1533090161767-e6ffed986c88?auto=format&fit=crop&w=800&q=80',
},
{
code: 'CMP-099',
title: '2022 Annual Compliance Review',
description: 'Outdated compliance materials from the previous fiscal year.',
status: 'Inactive',
lastUpdate: '2026-08-19',
image:
'https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=800&q=80',
},
];
const lessonsByCourse = {
'LDR-101': [
{
id: 'les_001',
title: '1.1 What is Leadership?',
position: 1,
type: 'video',
duration: '12:30',
status: 'Published',
isFreePreview: true,
updatedAt: '2026-08-15',
},
{
id: 'les_002',
title: '1.2 Leadership Styles Overview',
position: 2,
type: 'text',
duration: '~8 min read',
status: 'Published',
isFreePreview: false,
updatedAt: '2026-08-16',
},
{
id: 'les_003',
title: '1.3 Self-Assessment Quiz',
position: 3,
type: 'quiz',
duration: '10:00',
status: 'Draft',
isFreePreview: false,
updatedAt: '2026-08-17',
},
{
id: 'les_004',
title: '2.1 Communication Fundamentals',
position: 4,
type: 'video',
duration: '15:20',
status: 'Published',
isFreePreview: false,
updatedAt: '2026-08-18',
},
{
id: 'les_005',
title: '2.2 Active Listening Techniques',
position: 5,
type: 'text',
duration: '~6 min read',
status: 'Draft',
isFreePreview: false,
updatedAt: '2026-08-19',
},
],
'TEC-204': [
{
id: 'les_006',
title: '1.1 Cloud Fundamentals Recap',
position: 1,
type: 'video',
duration: '18:45',
status: 'Published',
isFreePreview: true,
updatedAt: '2026-08-10',
},
{
id: 'les_007',
title: '1.2 Microservices Architecture',
position: 2,
type: 'embed',
duration: 'Figma iFrame',
status: 'Draft',
isFreePreview: false,
updatedAt: '2026-08-12',
},
],
'CMP-099': [],
};
import courseService from '@/services/courseService';
import lessonService from '@/services/lessonService';
import sectionService from '@/services/sectionService';
const typeConfig = {
video: { label: 'Video', icon: HiOutlineVideoCamera, color: 'bg-blue-100 text-blue-700' },
@@ -146,20 +39,120 @@ const statusStyles = {
Draft: 'bg-app-neutral-badge text-app-neutral-badge-text',
};
function formatDuration(seconds) {
if (!seconds) return '00:00';
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
function mapCourse(c) {
return {
id: c.id,
code: c.courseCode,
title: c.title,
description: c.description,
status: c.isPublished ? 'Active' : 'Inactive',
lastUpdate: c.updatedAt,
image: c.image,
isPublished: c.isPublished,
};
}
function CourseDetail() {
const { courseId } = useParams();
const navigate = useNavigate();
const course = coursesData.find((e) => e.code === courseId);
const lessons = lessonsByCourse[courseId] || [];
const [course, setCourse] = useState(null);
const [lessons, setLessons] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState('overview');
if (!course) {
const handleTogglePublish = async () => {
try {
await courseService.update(courseId, {
courseCode: course.code,
title: course.title,
description: course.description,
image: course.image,
isPublished: !course.isPublished,
});
setCourse((prev) => ({ ...prev, isPublished: !prev.isPublished, status: prev.isPublished ? 'Inactive' : 'Active' }));
} catch (err) {
alert(err.message || 'Failed to update course');
}
};
const handleDeleteLesson = async (lessonId) => {
if (!window.confirm('Are you sure you want to delete this lesson?')) return;
try {
await lessonService.delete(lessonId);
setLessons((prev) => prev.filter((l) => l.id !== lessonId));
} catch (err) {
alert(err.message || 'Failed to delete lesson');
}
};
useEffect(() => {
const fetchData = async () => {
try {
const courseData = await courseService.getById(courseId);
setCourse(mapCourse(courseData));
const allLessons = await lessonService.getAll();
const courseLessons = allLessons.filter((l) => l.courseId === Number(courseId));
const lessonsWithSections = await Promise.all(
courseLessons.map(async (lesson) => {
const sections = await sectionService.getByLessonId(lesson.id);
const firstSection = sections[0];
return {
id: lesson.id,
title: lesson.title,
position: lesson.position,
type: firstSection?.type || 'text',
duration: firstSection?.type === 'video'
? formatDuration(firstSection.durationSeconds)
: firstSection?.type === 'text'
? `~${Math.max(1, Math.ceil((firstSection.content || '').split(/\s+/).length / 200))} min read`
: firstSection?.type === 'quiz'
? `${firstSection.durationSeconds ? Math.floor(firstSection.durationSeconds / 60) : 10}:00`
: firstSection?.type === 'embed'
? 'iFrame'
: '00:00',
status: lesson.isPublished ? 'Published' : 'Draft',
isFreePreview: firstSection?.isFreePreview || false,
updatedAt: lesson.updatedAt,
};
})
);
setLessons(lessonsWithSections);
} catch (err) {
setError(err.message || 'Failed to load course');
} finally {
setLoading(false);
}
};
fetchData();
}, [courseId]);
if (loading) {
return (
<>
<PageHeader />
<div className="flex items-center justify-center py-20 text-app-text-muted">Loading course...</div>
</>
);
}
if (error || !course) {
return (
<>
<PageHeader />
<div className="flex flex-col items-center justify-center py-20 text-app-text-muted">
<HiOutlineBookOpen className="w-16 h-16 mb-4 opacity-30" />
<p className="text-lg font-medium">Course not found</p>
<p className="text-lg font-medium">{error || 'Course not found'}</p>
<Button
variant="link"
className="mt-2 text-app-primary"
@@ -210,12 +203,24 @@ function CourseDetail() {
<Button
variant="outline"
className="rounded-xl bg-app-surface-muted text-app-text hover:bg-app-interactive-hover"
onClick={() => navigate(`/courses/${courseId}/edit`)}
>
<HiOutlinePencil className="w-4 h-4 mr-2" />
Edit
</Button>
<Button
variant="outline"
className="rounded-xl bg-app-surface-muted text-app-text hover:bg-app-interactive-hover"
onClick={handleTogglePublish}
>
<HiOutlineArrowUpTray className="w-4 h-4 mr-2" />
Inactivate
{course.isPublished ? 'Inactivate' : 'Activate'}
</Button>
<Button className="rounded-xl bg-app-primary text-white hover:bg-app-primary-dark">
Publish
<Button
className="rounded-xl bg-app-primary text-white hover:bg-app-primary-dark"
onClick={handleTogglePublish}
>
{course.isPublished ? 'Unpublish' : 'Publish'}
</Button>
</div>
</div>
@@ -283,7 +288,9 @@ function CourseDetail() {
<div className="flex items-center gap-6 text-sm text-app-text-muted">
<span>
Last updated:{' '}
<span className="font-medium text-app-text">{course.lastUpdate}</span>
<span className="font-medium text-app-text">
{course.lastUpdate ? new Date(course.lastUpdate).toLocaleDateString('en-GB') : '-'}
</span>
</span>
<span>
Total lessons:{' '}
@@ -368,7 +375,7 @@ function CourseDetail() {
{/* Table Rows */}
{lessons.map((lesson) => {
const tc = typeConfig[lesson.type];
const tc = typeConfig[lesson.type] || typeConfig.text;
const TypeIcon = tc.icon;
return (
<div
@@ -413,7 +420,7 @@ function CourseDetail() {
<div className="col-span-1 flex items-center justify-end gap-1">
<button
onClick={() =>
navigate(`/courses/${courseId}/lessons/new`)
navigate(`/courses/${courseId}/lessons/${lesson.id}/edit`)
}
className="w-8 h-8 rounded-lg flex items-center justify-center text-app-text-muted hover:text-app-primary hover:bg-app-primary-soft transition-colors"
title="Edit"
@@ -427,6 +434,7 @@ function CourseDetail() {
<HiOutlineEye className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteLesson(lesson.id)}
className="w-8 h-8 rounded-lg flex items-center justify-center text-app-text-muted hover:text-red-500 hover:bg-red-50 transition-colors"
title="Delete"
>