feat: add create lesson UI

This commit is contained in:
2026-09-08 20:39:30 +07:00
parent b55e7f97c9
commit 7bc3cf4e73
14 changed files with 1534 additions and 46 deletions
+385 -46
View File
@@ -1,13 +1,30 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge';
import { Label } from '../../components/ui/label';
import { Input } from '../../components/ui/input';
import { Textarea } from '../../components/ui/textarea';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../components/ui/tabs';
import { Separator } from '../../components/ui/separator';
import PageHeader from '../../components/PageHeader';
import { useParams } from 'react-router-dom';
import {
HiOutlinePlus,
HiOutlinePencil,
HiOutlineTrash,
HiOutlineEye,
HiOutlineChevronRight,
HiOutlineBookOpen,
HiOutlineVideoCamera,
HiOutlineDocumentText,
HiOutlineQuestionMarkCircle,
HiOutlineCodeBracket,
HiOutlineArrowUpTray,
HiOutlineFunnel,
} from 'react-icons/hi2';
const data = [
const coursesData = [
{
code: 'LDR-101',
title: 'Foundations of Modern Leadership',
@@ -16,7 +33,7 @@ const data = [
status: 'Active',
lastUpdate: '2026-08-19',
image:
'https://images.unsplash.com/photo-1556761175-b413da4baf72?auto=format&fit=crop&w=800&q=80'
'https://images.unsplash.com/photo-1556761175-b413da4baf72?auto=format&fit=crop&w=800&q=80',
},
{
code: 'TEC-204',
@@ -26,7 +43,7 @@ const data = [
status: 'Inactive',
lastUpdate: '2026-08-19',
image:
'https://images.unsplash.com/photo-1533090161767-e6ffed986c88?auto=format&fit=crop&w=800&q=80'
'https://images.unsplash.com/photo-1533090161767-e6ffed986c88?auto=format&fit=crop&w=800&q=80',
},
{
code: 'CMP-099',
@@ -35,27 +52,166 @@ const data = [
status: 'Inactive',
lastUpdate: '2026-08-19',
image:
'https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=800&q=80'
}
'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': [],
};
const typeConfig = {
video: { label: 'Video', icon: HiOutlineVideoCamera, color: 'bg-blue-100 text-blue-700' },
text: { label: 'Text', icon: HiOutlineDocumentText, color: 'bg-purple-100 text-purple-700' },
quiz: { label: 'Quiz', icon: HiOutlineQuestionMarkCircle, color: 'bg-orange-100 text-orange-700' },
embed: { label: 'Embed', icon: HiOutlineCodeBracket, color: 'bg-gray-100 text-gray-700' },
};
const statusStyles = {
Published: 'bg-app-success text-app-success-text',
Draft: 'bg-app-neutral-badge text-app-neutral-badge-text',
};
function CourseDetail() {
const { courseId } = useParams();
const course = data.find((e) => e.code === courseId);
const navigate = useNavigate();
const course = coursesData.find((e) => e.code === courseId);
const lessons = lessonsByCourse[courseId] || [];
const [activeTab, setActiveTab] = useState('overview');
if (!course) {
return <div>Course not found</div>;
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>
<Button
variant="link"
className="mt-2 text-app-primary"
onClick={() => navigate('/courses')}
>
Back to Courses
</Button>
</div>
</>
);
}
const publishedCount = lessons.filter((l) => l.status === 'Published').length;
const draftCount = lessons.filter((l) => l.status === 'Draft').length;
return (
<>
<PageHeader />
<div className="mb-4 flex flex-row items-center justify-between">
<h2 className="text-xl font-bold text-app-text">{course?.title}</h2>
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-app-text-muted mb-4">
<button
onClick={() => navigate('/courses')}
className="hover:text-app-primary transition-colors flex items-center gap-1"
>
<HiOutlineBookOpen className="w-4 h-4" />
Courses
</button>
<HiOutlineChevronRight className="w-3.5 h-3.5" />
<span className="font-medium text-app-text">{course.title}</span>
</div>
{/* Header */}
<div className="flex flex-row items-center justify-between mb-6">
<div className="flex items-center gap-3">
<h2 className="text-xl font-bold text-app-text">{course.title}</h2>
<Badge
className={`rounded-[6px] text-xs font-medium ${
course.status === 'Active'
? 'bg-app-success text-app-success-text'
: 'bg-app-neutral-badge text-app-neutral-badge-text'
}`}
>
{course.status}
</Badge>
</div>
<div className="flex flex-row items-center gap-2">
<Button
color="gray"
variant="outline"
className="rounded-xl bg-app-surface-muted text-app-text hover:bg-app-interactive-hover"
>
<HiOutlineArrowUpTray className="w-4 h-4 mr-2" />
Inactivate
</Button>
<Button className="rounded-xl bg-app-primary text-white hover:bg-app-primary-dark">
@@ -63,45 +219,228 @@ function CourseDetail() {
</Button>
</div>
</div>
<div>
<Card
className="w-full max-w-100 overflow-hidden rounded-xl border border-app-border bg-white"
style={{ boxShadow: 'rgba(0,0,0,0.03) 0px 4px 24px' }}
>
<div className="p-5">
<h4 className="font-medium text-app-text">Course Details</h4>
</div>
<div className="px-5 pb-5">
<div className="my-2 w-full max-w-64">
<Label className="mb-1 block text-app-text">Course Code</Label>
<Input
value={course?.code}
readOnly
/>
<Separator className="mb-6" />
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="mb-6">
<TabsTrigger value="overview" className="gap-2">
<HiOutlineBookOpen className="w-4 h-4" />
Overview
</TabsTrigger>
<TabsTrigger value="lessons" className="gap-2">
<HiOutlineDocumentText className="w-4 h-4" />
Lessons
<Badge className="ml-1 rounded-full bg-app-primary-soft text-app-primary text-[10px] px-1.5 py-0">
{lessons.length}
</Badge>
</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview">
<Card
className="w-full max-w-2xl overflow-hidden rounded-xl border border-app-border bg-white"
style={{ boxShadow: 'rgba(0,0,0,0.03) 0px 4px 24px' }}
>
<div className="p-5 border-b border-app-border">
<h4 className="font-semibold text-app-text">Course Details</h4>
</div>
<Label className="mb-1 block text-app-text">Status</Label>
{course?.status === 'Active' ? (
<Badge className="rounded-[6px] bg-app-success text-app-success-text">
Active
</Badge>
<div className="px-5 pb-5 pt-4">
<div className="mb-4 w-full max-w-sm">
<Label className="mb-1.5 block text-app-text text-xs font-semibold uppercase tracking-wider">
Course Code
</Label>
<Input value={course.code} readOnly className="bg-app-surface-muted" />
</div>
<div className="mb-4">
<Label className="mb-1.5 block text-app-text text-xs font-semibold uppercase tracking-wider">
Status
</Label>
<Badge
className={`rounded-[6px] text-xs font-medium ${
course.status === 'Active'
? 'bg-app-success text-app-success-text'
: 'bg-app-neutral-badge text-app-neutral-badge-text'
}`}
>
{course.status}
</Badge>
</div>
<div className="mb-4 w-full max-w-sm">
<Label className="mb-1.5 block text-app-text text-xs font-semibold uppercase tracking-wider">
Description
</Label>
<Textarea
className="max-h-40 bg-app-surface-muted"
placeholder="Course description..."
rows={4}
value={course.description}
readOnly
/>
</div>
<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>
<span>
Total lessons:{' '}
<span className="font-medium text-app-text">{lessons.length}</span>
</span>
</div>
</div>
</Card>
</TabsContent>
{/* Lessons Tab */}
<TabsContent value="lessons">
<div className="flex flex-col gap-4">
{/* Toolbar */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-app-text">Lessons</h3>
<Badge className="rounded-full bg-app-surface-muted text-app-text-muted text-xs">
{lessons.length} items
</Badge>
</div>
<p className="text-sm text-app-text-muted mt-0.5">
{publishedCount} published, {draftCount} drafts
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="rounded-xl bg-app-surface-muted text-app-text hover:bg-app-interactive-hover"
>
<HiOutlineFunnel className="w-4 h-4 mr-1.5" />
Filter
</Button>
<Button
className="rounded-xl bg-app-primary text-white hover:bg-app-primary-dark shadow-sm"
onClick={() => navigate(`/courses/${courseId}/lessons/new`)}
>
<HiOutlinePlus className="w-4 h-4 mr-1.5" />
Add Lesson
</Button>
</div>
</div>
{/* Lessons Table */}
{lessons.length === 0 ? (
<Card
className="rounded-xl border border-app-border bg-white p-12 flex flex-col items-center justify-center text-center"
style={{ boxShadow: 'rgba(0,0,0,0.03) 0px 4px 24px' }}
>
<div className="w-16 h-16 rounded-2xl bg-app-surface-muted flex items-center justify-center mb-4">
<HiOutlineBookOpen className="w-8 h-8 text-app-text-muted/40" />
</div>
<p className="text-lg font-medium text-app-text mb-1">No lessons yet</p>
<p className="text-sm text-app-text-muted mb-4">
Start building your course by adding the first lesson.
</p>
<Button
className="rounded-xl bg-app-primary text-white hover:bg-app-primary-dark"
onClick={() => navigate(`/courses/${courseId}/lessons/new`)}
>
<HiOutlinePlus className="w-4 h-4 mr-2" />
Add First Lesson
</Button>
</Card>
) : (
<Badge className="rounded-[8px] bg-app-neutral-badge text-app-neutral-badge-text">
Inactive
</Badge>
<Card
className="rounded-xl border border-app-border bg-white overflow-hidden"
style={{ boxShadow: 'rgba(0,0,0,0.03) 0px 4px 24px' }}
>
{/* Table Header */}
<div className="grid grid-cols-12 gap-4 px-5 py-3 bg-app-surface-muted/50 border-b border-app-border text-xs font-semibold text-app-text-muted uppercase tracking-wider">
<div className="col-span-1">#</div>
<div className="col-span-1">Type</div>
<div className="col-span-4">Title</div>
<div className="col-span-2">Duration</div>
<div className="col-span-2">Status</div>
<div className="col-span-1">Preview</div>
<div className="col-span-1 text-right">Actions</div>
</div>
{/* Table Rows */}
{lessons.map((lesson) => {
const tc = typeConfig[lesson.type];
const TypeIcon = tc.icon;
return (
<div
key={lesson.id}
className="grid grid-cols-12 gap-4 px-5 py-3.5 border-b border-app-border/50 last:border-b-0 items-center hover:bg-app-surface-muted/30 transition-colors"
>
<div className="col-span-1 text-sm text-app-text-muted font-mono">
{lesson.position}
</div>
<div className="col-span-1">
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[11px] font-semibold uppercase ${tc.color}`}
>
<TypeIcon className="w-3 h-3" />
{tc.label}
</span>
</div>
<div className="col-span-4">
<span className="text-sm font-medium text-app-text truncate block">
{lesson.title}
</span>
</div>
<div className="col-span-2 text-sm text-app-text-muted font-mono">
{lesson.duration}
</div>
<div className="col-span-2">
<Badge
className={`rounded-[6px] text-[11px] font-medium ${
statusStyles[lesson.status]
}`}
>
{lesson.status}
</Badge>
</div>
<div className="col-span-1">
{lesson.isFreePreview && (
<Badge className="rounded-full bg-blue-50 text-blue-600 text-[10px]">
Free
</Badge>
)}
</div>
<div className="col-span-1 flex items-center justify-end gap-1">
<button
onClick={() =>
navigate(`/courses/${courseId}/lessons/new`)
}
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"
>
<HiOutlinePencil className="w-4 h-4" />
</button>
<button
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="Preview"
>
<HiOutlineEye className="w-4 h-4" />
</button>
<button
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"
>
<HiOutlineTrash className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</Card>
)}
<div className="my-2 w-full max-w-64">
<Label className="mb-1 block text-app-text">Description</Label>
<Textarea
className="max-h-50"
placeholder="..."
rows={4}
value={course?.description}
readOnly
/>
</div>
</div>
</Card>
</div>
</TabsContent>
</Tabs>
</>
);
}