feat: add create lesson UI
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user