feat: add page course details
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import MainLayout from './layouts/MainLayout.jsx';
|
import MainLayout from './layouts/MainLayout.jsx';
|
||||||
import CoursesPage from './pages/Course/CoursesPage.jsx';
|
import CoursesPage from './pages/Course/CoursesPage.jsx';
|
||||||
|
import CourseDetail from './pages/Course/CourseDetails.jsx';
|
||||||
import StudentsPage from './pages/StudentsPage.jsx';
|
import StudentsPage from './pages/StudentsPage.jsx';
|
||||||
import ClassesPage from './pages/ClassesPage.jsx';
|
import ClassesPage from './pages/ClassesPage.jsx';
|
||||||
import ReportsPage from './pages/ReportsPage.jsx';
|
import ReportsPage from './pages/ReportsPage.jsx';
|
||||||
@@ -13,6 +14,7 @@ function App() {
|
|||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route index element={<Navigate to="/courses" replace />} />
|
<Route index element={<Navigate to="/courses" replace />} />
|
||||||
<Route path="/courses" element={<CoursesPage />} />
|
<Route path="/courses" element={<CoursesPage />} />
|
||||||
|
<Route path="/courses/:courseId" element={<CourseDetail />} />
|
||||||
<Route path="/students" element={<StudentsPage />} />
|
<Route path="/students" element={<StudentsPage />} />
|
||||||
<Route path="/classes" element={<ClassesPage />} />
|
<Route path="/classes" element={<ClassesPage />} />
|
||||||
<Route path="/reports" element={<ReportsPage />} />
|
<Route path="/reports" element={<ReportsPage />} />
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import { Card, Button } from '@heroui/react';
|
|||||||
import { PencilIcon, EllipsisVerticalIcon } from '@heroicons/react/24/outline';
|
import { PencilIcon, EllipsisVerticalIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
const statusStyles = {
|
const statusStyles = {
|
||||||
Published: 'bg-green-100 text-green-700',
|
Active: 'bg-green-100 text-green-700',
|
||||||
Draft: 'bg-yellow-100 text-yellow-700',
|
Inactive: 'bg-gray-100 text-gray-600'
|
||||||
Archived: 'bg-gray-100 text-gray-600'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function CourseCard({ course, image }) {
|
function CourseCard({ course, image, handleClick }) {
|
||||||
const { code, title, description, status, lastUpdate } = course;
|
const { code, title, description, status, lastUpdate } = course;
|
||||||
|
|
||||||
const formattedDate = new Date(lastUpdate).toLocaleDateString('en-GB', {
|
const formattedDate = new Date(lastUpdate).toLocaleDateString('en-GB', {
|
||||||
@@ -20,19 +19,19 @@ function CourseCard({ course, image }) {
|
|||||||
<Card
|
<Card
|
||||||
variant="default"
|
variant="default"
|
||||||
className="w-full max-w-100 overflow-hidden rounded-lg border border-gray-300 bg-white shadow-none"
|
className="w-full max-w-100 overflow-hidden rounded-lg border border-gray-300 bg-white shadow-none"
|
||||||
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="relative h-48 overflow-hidden border-b border-gray-300">
|
<div className="relative h-48 cursor-pointer overflow-hidden border-b border-gray-300">
|
||||||
<img src={image} alt={title} className="h-full w-full object-cover" />
|
<img src={image} alt={title} className="h-full w-full object-cover" />
|
||||||
|
|
||||||
{/* Course code */}
|
|
||||||
<div className="absolute left-4 top-4 rounded-sm border border-gray-300 bg-white px-3 py-1.5">
|
<div className="absolute left-4 top-4 rounded-sm border border-gray-300 bg-white px-3 py-1.5">
|
||||||
<span className="text-lg font-semibold text-gray-800">{code}</span>
|
<span className="text-lg font-semibold text-gray-800">{code}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<Card.Content className="px-5 py-5">
|
<Card.Content className="cursor-pointer px-5 py-5">
|
||||||
<h3 className="mb-3 text-xl font-bold leading-tight text-gray-900">
|
<h3 className="mb-3 text-xl font-bold leading-tight text-gray-900">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -43,7 +42,13 @@ function CourseCard({ course, image }) {
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<Card.Footer className="mx-5 flex items-center justify-between border-t border-gray-300 px-0 py-5">
|
<Card.Footer
|
||||||
|
className="mx-5 flex items-center justify-between border-t border-gray-300 px-0 py-5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// handle edit
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`rounded-full px-2.5 py-1 text-sm font-medium ${
|
className={`rounded-full px-2.5 py-1 text-sm font-medium ${
|
||||||
@@ -62,6 +67,10 @@ function CourseCard({ course, image }) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label={`Edit ${title}`}
|
aria-label={`Edit ${title}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// handle edit
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="h-5 w-5" />
|
<PencilIcon className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -71,6 +80,10 @@ function CourseCard({ course, image }) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label={`More actions for ${title}`}
|
aria-label={`More actions for ${title}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// handle edit
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<EllipsisVerticalIcon className="h-5 w-5" />
|
<EllipsisVerticalIcon className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Chip,
|
||||||
|
TextArea
|
||||||
|
} from '@heroui/react';
|
||||||
|
import PageHeader from '../../components/PageHeader';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function CourseDetail() {
|
||||||
|
const { courseId } = useParams();
|
||||||
|
const course = data.find((e) => e.code === courseId);
|
||||||
|
|
||||||
|
if (!course) {
|
||||||
|
return <div>Course not found</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader />
|
||||||
|
<div className="mb-4 flex flex-row items-center justify-between">
|
||||||
|
<Typography className="font-bold">{course?.title}</Typography>
|
||||||
|
<div className="flex flex-row item-center gap-2">
|
||||||
|
<Button variant="outline" className="rounded-lg">
|
||||||
|
Inactivate
|
||||||
|
</Button>
|
||||||
|
<Button className="rounded-lg">Publish</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Card
|
||||||
|
variant="default"
|
||||||
|
className="w-full max-w-100 overflow-hidden rounded-lg border border-gray-300 bg-white shadow-none"
|
||||||
|
>
|
||||||
|
<Card.Header>
|
||||||
|
<Typography className="font-medium">Course Details</Typography>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<TextField
|
||||||
|
className="w-full max-w-64 my-2"
|
||||||
|
name="Course Code"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
<Label>Course Code</Label>
|
||||||
|
<Input
|
||||||
|
className="rounded border border-gray-300"
|
||||||
|
value={course?.code}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
<Label>Status</Label>
|
||||||
|
{course?.status === 'Active' ? (
|
||||||
|
<Chip color="success" variant="primary">
|
||||||
|
Active
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<Chip>Inactive</Chip>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
className="w-full max-w-64 my-2"
|
||||||
|
name="Description"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
<Label>Description</Label>
|
||||||
|
<TextArea
|
||||||
|
className="max-h-50"
|
||||||
|
placeholder="..."
|
||||||
|
rows={4}
|
||||||
|
value={course?.description}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Footer />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CourseDetail;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Tabs
|
Tabs
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { FunnelIcon, PlusIcon } from '@heroicons/react/16/solid';
|
import { FunnelIcon, PlusIcon } from '@heroicons/react/16/solid';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{
|
{
|
||||||
@@ -15,7 +16,7 @@ const stats = [
|
|||||||
title: 'Foundations of Modern Leadership',
|
title: 'Foundations of Modern Leadership',
|
||||||
description:
|
description:
|
||||||
'An introductory course covering essential leadership principles for new managers.',
|
'An introductory course covering essential leadership principles for new managers.',
|
||||||
status: 'Published',
|
status: 'Active',
|
||||||
lastUpdate: '2026-08-19',
|
lastUpdate: '2026-08-19',
|
||||||
image:
|
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'
|
||||||
@@ -25,7 +26,7 @@ const stats = [
|
|||||||
title: 'Advanced Cloud Architecture Patterns',
|
title: 'Advanced Cloud Architecture Patterns',
|
||||||
description:
|
description:
|
||||||
'Deep dive into scalable cloud infrastructure design and implementation strategies.',
|
'Deep dive into scalable cloud infrastructure design and implementation strategies.',
|
||||||
status: 'Draft',
|
status: 'Inactive',
|
||||||
lastUpdate: '2026-08-19',
|
lastUpdate: '2026-08-19',
|
||||||
image:
|
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'
|
||||||
@@ -34,7 +35,7 @@ const stats = [
|
|||||||
code: 'CMP-099',
|
code: 'CMP-099',
|
||||||
title: '2022 Annual Compliance Review',
|
title: '2022 Annual Compliance Review',
|
||||||
description: 'Outdated compliance materials from the previous fiscal year.',
|
description: 'Outdated compliance materials from the previous fiscal year.',
|
||||||
status: 'Archived',
|
status: 'Inactive',
|
||||||
lastUpdate: '2026-08-19',
|
lastUpdate: '2026-08-19',
|
||||||
image:
|
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'
|
||||||
@@ -42,13 +43,18 @@ const stats = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function CoursePage() {
|
function CoursePage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleClick = (course) => {
|
||||||
|
navigate(`/courses/${course.code}`);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<div className="mb-4 flex flex-row items-center justify-between">
|
<div className="mb-4 flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h3" className="mb-2">
|
<Typography variant="h3" className="mb-2">
|
||||||
Course
|
Courses
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" className="text-gray-600">
|
<Typography variant="body1" className="text-gray-600">
|
||||||
Manage and organize your learning content.
|
Manage and organize your learning content.
|
||||||
@@ -85,7 +91,7 @@ function CoursePage() {
|
|||||||
id="draft"
|
id="draft"
|
||||||
className="h-7 rounded px-6 py-5 text-base font-normal text-gray-500"
|
className="h-7 rounded px-6 py-5 text-base font-normal text-gray-500"
|
||||||
>
|
>
|
||||||
Draft
|
Active
|
||||||
<Tabs.Indicator className="rounded bg-white shadow-sm" />
|
<Tabs.Indicator className="rounded bg-white shadow-sm" />
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
@@ -93,15 +99,7 @@ function CoursePage() {
|
|||||||
id="published"
|
id="published"
|
||||||
className="h-7 rounded px-6 py-5 text-base font-normal text-gray-500"
|
className="h-7 rounded px-6 py-5 text-base font-normal text-gray-500"
|
||||||
>
|
>
|
||||||
Published
|
Inactive
|
||||||
<Tabs.Indicator className="rounded bg-white shadow-sm" />
|
|
||||||
</Tabs.Tab>
|
|
||||||
|
|
||||||
<Tabs.Tab
|
|
||||||
id="archived"
|
|
||||||
className="h-7 rounded px-6 py-5 text-base font-normal text-gray-500"
|
|
||||||
>
|
|
||||||
Archived
|
|
||||||
<Tabs.Indicator className="rounded bg-white shadow-sm" />
|
<Tabs.Indicator className="rounded bg-white shadow-sm" />
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
@@ -119,7 +117,12 @@ function CoursePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{stats.map((course) => (
|
{stats.map((course) => (
|
||||||
<CourseCard key={course.code} course={course} image={course.image} />
|
<CourseCard
|
||||||
|
key={course.code}
|
||||||
|
course={course}
|
||||||
|
image={course.image}
|
||||||
|
handleClick={() => handleClick(course)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user