Compare commits
No commits in common. '6afb2a7cfbbe2f36250de330e97d974f60b8df19' and 'e70d869b76f0e0c10db0a5d04ad22ead6658439e' have entirely different histories.
6afb2a7cfb
...
e70d869b76
@ -1,21 +1,4 @@ |
||||
/** @type {import('next').NextConfig} */ |
||||
const nextConfig = { |
||||
images: { |
||||
remotePatterns: [ |
||||
{ |
||||
protocol: 'https', |
||||
hostname: '**', |
||||
port: '', |
||||
pathname: '**', |
||||
search: '', |
||||
}, |
||||
], |
||||
}, |
||||
webpack: (config) => { |
||||
config.resolve.alias.canvas = false; |
||||
|
||||
return config; |
||||
}, |
||||
}; |
||||
const nextConfig = {}; |
||||
|
||||
export default nextConfig; |
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,53 +0,0 @@ |
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" |
||||
import { Button } from "@/components/ui/button" |
||||
import { Card, CardContent } from "@/components/ui/card" |
||||
import { CourseData } from "@/helpers/apiSchema/course.schema" |
||||
|
||||
|
||||
interface CourseDetailHeroSectionProps { |
||||
courseData: CourseData; |
||||
} |
||||
|
||||
|
||||
const CourseDetailHeroSection: React.FC<CourseDetailHeroSectionProps> = ({courseData}) => { |
||||
return( |
||||
<div className="bg-purple-700 text-white"> |
||||
<div className="container mx-auto px-4 py-12"> |
||||
<div className="grid md:grid-cols-2 gap-8 items-center"> |
||||
<div className="space-y-4"> |
||||
<h1 className="text-3xl font-bold">{courseData.title}</h1> |
||||
<p className="text-blue-100">{courseData.description}</p> |
||||
|
||||
|
||||
<div className="flex items-center space-x-4"> |
||||
<Avatar className="h-12 w-12"> |
||||
<AvatarImage src={courseData.instructor.avatar} className='aspect-1/1'/> |
||||
<AvatarFallback>SJ</AvatarFallback> |
||||
</Avatar> |
||||
<div> |
||||
<div className="font-semibold">{courseData.instructor.name}</div> |
||||
<div className="text-sm text-blue-100">{courseData.instructor.title}</div> |
||||
<div>Published at {courseData?.lastUpdated}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{/* course image */} |
||||
<Card className="bg-white"> |
||||
<CardContent className="p-6 space-y-4"> |
||||
<img |
||||
src="https://placehold.co/600x400" |
||||
alt="Course Preview" |
||||
className="w-full rounded-lg" |
||||
/> |
||||
<Button className="w-full text-lg h-12 bg-purple-700"> |
||||
Enroll Now |
||||
</Button> |
||||
</CardContent> |
||||
</Card> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default CourseDetailHeroSection |
@ -1,56 +0,0 @@ |
||||
import { Card, CardContent } from "@/components/ui/card" |
||||
import { CourseData } from "@/helpers/apiSchema/course.schema" |
||||
import { BookOpen, Clock, MessageSquare, Users } from "lucide-react" |
||||
|
||||
|
||||
const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) => { |
||||
return( |
||||
<> |
||||
<div className="mx-auto container py-8"> |
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> |
||||
<Card> |
||||
<CardContent className="p-4 flex items-center space-x-4"> |
||||
<Clock className="h-8 w-8 text-blue-500" /> |
||||
<div> |
||||
<div className="text-sm text-gray-500">Duration</div> |
||||
<div className="font-semibold">{courseData.duration}</div> |
||||
</div> |
||||
</CardContent> |
||||
</Card> |
||||
|
||||
<Card> |
||||
<CardContent className="p-4 flex items-center space-x-4"> |
||||
<Users className="h-8 w-8 text-green-500" /> |
||||
<div> |
||||
<div className="text-sm text-gray-500">Enrolled</div> |
||||
<div className="font-semibold">{courseData.enrolledStudents} students</div> |
||||
</div> |
||||
</CardContent> |
||||
</Card> |
||||
|
||||
<Card> |
||||
<CardContent className="p-4 flex items-center space-x-4"> |
||||
<MessageSquare className="h-8 w-8 text-purple-500" /> |
||||
<div> |
||||
<div className="text-sm text-gray-500">Reviews</div> |
||||
<div className="font-semibold">{courseData.rating} ({courseData.totalReviews})</div> |
||||
</div> |
||||
</CardContent> |
||||
</Card> |
||||
|
||||
<Card> |
||||
<CardContent className="p-4 flex items-center space-x-4"> |
||||
<BookOpen className="h-8 w-8 text-orange-500" /> |
||||
<div> |
||||
<div className="text-sm text-gray-500">Completion</div> |
||||
<div className="font-semibold">{courseData.completionRate}%</div> |
||||
</div> |
||||
</CardContent> |
||||
</Card> |
||||
</div> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
export default CourseDetailStats |
@ -1,58 +0,0 @@ |
||||
'use client'; |
||||
import React from 'react'; |
||||
import { Card, CardContent } from "@/components/ui/card"; |
||||
import { Progress } from "@/components/ui/progress"; |
||||
import CourseDetailHeroSection from './CourseDetailHeroSection'; |
||||
import CourseDetailStats from './CourseDetailStats'; |
||||
import CourseMedia from './CourseMedia'; |
||||
import CourseProgress from './CourseProgress'; |
||||
import TryPDF from './TryPDF'; |
||||
|
||||
const CourseIndividualContentWrapper : React.FC = () => { |
||||
// Dummy course data
|
||||
const courseData = { |
||||
title: "Advanced JavaScript Programming", |
||||
instructor: { |
||||
name: "Dr. Sarah Johnson", |
||||
title: "Senior Software Engineer", |
||||
avatar: "https://placehold.co/600x400" |
||||
}, |
||||
description: "Master modern JavaScript concepts including ES6+, async programming, and advanced frameworks. This comprehensive course covers everything from basic concepts to advanced topics in JavaScript development.", |
||||
category: "Programming", |
||||
duration: "12 weeks", |
||||
enrolledStudents: 1234, |
||||
rating: 4.8, |
||||
totalReviews: 456, |
||||
completionRate: 85, |
||||
lastUpdated: "2024-01-10", |
||||
chapters: [ |
||||
{ title: "Introduction to ES6+", duration: "2.5 hours", isCompleted: true }, |
||||
{ title: "Async Programming", duration: "3 hours", isCompleted: true }, |
||||
{ title: "Advanced DOM Manipulation", duration: "2 hours", isCompleted: false }, |
||||
{ title: "Modern Frameworks Overview", duration: "4 hours", isCompleted: false } |
||||
], |
||||
requirements: [ |
||||
"Basic understanding of JavaScript", |
||||
"Familiarity with web development concepts", |
||||
"Node.js installed on your computer" |
||||
] |
||||
}; |
||||
|
||||
return ( |
||||
<div className="min-h-screen bg-gray-50"> |
||||
{/* Hero Section */} |
||||
<CourseDetailHeroSection courseData={courseData}/> |
||||
|
||||
{/* Course Stats */} |
||||
<CourseDetailStats courseData={courseData}/> |
||||
|
||||
<CourseProgress courseData={courseData}/> |
||||
|
||||
{/* Course Content */} |
||||
<CourseMedia courseData={courseData}/> |
||||
|
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default CourseIndividualContentWrapper; |
@ -1,39 +0,0 @@ |
||||
import { Card, CardContent } from "@/components/ui/card" |
||||
import { Progress } from "@/components/ui/progress" |
||||
import { CourseData } from "@/helpers/apiSchema/course.schema" |
||||
import PDFFlipBook from "./CoursePDFHolder" |
||||
import CustomPDFViewer from "./TryPDF" |
||||
import DiscussionSection from "@/components/elements/DiscussionForm" |
||||
|
||||
const CourseMedia : React.FC<{courseData : CourseData}> = ({courseData}) => { |
||||
return( |
||||
<div className="container mx-auto py-8"> |
||||
<div className="grid md:grid-cols-3 gap-8"> |
||||
{/* Main Content */} |
||||
<div className="md:col-span-2 space-y-8"> |
||||
<Card> |
||||
<CardContent className="p-6"> |
||||
<h2 className="text-xl font-semibold mb-4">Course Content</h2> |
||||
<CustomPDFViewer /> |
||||
</CardContent> |
||||
</Card> |
||||
</div> |
||||
|
||||
|
||||
{/* Sidebar */} |
||||
<div className="space-y-6"> |
||||
<Card> |
||||
<CardContent className="p-6"> |
||||
<h3 className="text-lg font-semibold mb-4">Discussions</h3> |
||||
<DiscussionSection /> |
||||
</CardContent> |
||||
</Card> |
||||
|
||||
|
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default CourseMedia |
@ -1,149 +0,0 @@ |
||||
'use client' |
||||
import React, { forwardRef, useState, useEffect } from 'react'; |
||||
import HTMLFlipBook from 'react-pageflip'; |
||||
import { pdfjs, Document, Page as ReactPdfPage } from 'react-pdf'; |
||||
|
||||
// Configure PDF.js worker globally
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL( |
||||
'pdfjs-dist/build/pdf.worker.min.mjs', |
||||
import.meta.url, |
||||
).toString(); |
||||
|
||||
interface PageProps { |
||||
pageNumber: number; |
||||
width: number; |
||||
} |
||||
|
||||
interface PDFFlipBookProps { |
||||
pdfUrl: string; |
||||
width?: number; |
||||
height?: number; |
||||
numPages?: number; |
||||
} |
||||
|
||||
const DEFAULT_WIDTH = 300; |
||||
const DEFAULT_HEIGHT = 424; |
||||
|
||||
const Page = forwardRef<HTMLDivElement, PageProps>(({ pageNumber, width }, ref) => { |
||||
const [pageError, setPageError] = useState<string | null>(null); |
||||
|
||||
return ( |
||||
<div ref={ref} style={{ background: 'white' }}> |
||||
<ReactPdfPage |
||||
pageNumber={pageNumber} |
||||
width={width} |
||||
loading={ |
||||
<div className="flex items-center justify-center p-4"> |
||||
<div>Loading page {pageNumber}...</div> |
||||
</div> |
||||
} |
||||
error={ |
||||
pageError && ( |
||||
<div className="text-red-500 p-4"> |
||||
Error loading page {pageNumber}: {pageError} |
||||
</div> |
||||
) |
||||
} |
||||
onLoadError={(error) => setPageError(error.message)} |
||||
/> |
||||
</div> |
||||
); |
||||
}); |
||||
|
||||
Page.displayName = 'Page'; |
||||
|
||||
const PDFFlipBook: React.FC<PDFFlipBookProps> = ({ |
||||
pdfUrl, |
||||
width = DEFAULT_WIDTH, |
||||
height = DEFAULT_HEIGHT, |
||||
numPages = 3, |
||||
}) => { |
||||
const [totalPages, setTotalPages] = useState<number>(0); |
||||
const [isLoading, setIsLoading] = useState<boolean>(true); |
||||
const [error, setError] = useState<string | null>(null); |
||||
|
||||
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { |
||||
console.log('PDF loaded successfully with', numPages, 'pages'); |
||||
setTotalPages(numPages); |
||||
setIsLoading(false); |
||||
}; |
||||
|
||||
const onDocumentLoadError = (error: Error) => { |
||||
console.error('Error loading PDF:', error); |
||||
setError(error.message); |
||||
setIsLoading(false); |
||||
}; |
||||
|
||||
const LoadingComponent = () => ( |
||||
<div className="flex items-center justify-center p-4"> |
||||
<div className="text-center"> |
||||
<div className="text-lg font-medium">Loading PDF...</div> |
||||
<div className="text-sm text-gray-500">Please wait</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
|
||||
const ErrorComponent = ({ message }: { message: string }) => ( |
||||
<div className="flex items-center justify-center p-4"> |
||||
<div className="text-center text-red-500"> |
||||
<div className="text-lg font-medium">Error loading PDF</div> |
||||
<div className="text-sm">{message}</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
|
||||
if (error) { |
||||
return <ErrorComponent message={error} />; |
||||
} |
||||
|
||||
return ( |
||||
<Document |
||||
file={pdfUrl} |
||||
onLoadSuccess={onDocumentLoadSuccess} |
||||
onLoadError={onDocumentLoadError} |
||||
loading={<LoadingComponent />} |
||||
error={<ErrorComponent message="Failed to load PDF" />} |
||||
options={{ |
||||
cMapUrl: 'cmaps/', |
||||
cMapPacked: true, |
||||
standardFontDataUrl: 'standard_fonts/', |
||||
}} |
||||
> |
||||
{isLoading ? ( |
||||
<LoadingComponent /> |
||||
) : ( |
||||
<HTMLFlipBook |
||||
style={{}} |
||||
width={width} |
||||
height={height} |
||||
size="fixed" |
||||
minWidth={0} |
||||
maxWidth={0} |
||||
minHeight={0} |
||||
maxHeight={0} |
||||
drawShadow={true} |
||||
flippingTime={1000} |
||||
usePortrait={true} |
||||
startZIndex={0} |
||||
autoSize={true} |
||||
maxShadowOpacity={1} |
||||
showCover={false} |
||||
mobileScrollSupport={true} |
||||
className="pdf-flipbook" |
||||
startPage={0} |
||||
swipeDistance={0} |
||||
showPageCorners={true} |
||||
disableFlipByClick={false} |
||||
clickEventForward={true} |
||||
useMouseEvents={true} |
||||
> |
||||
{Array.from(new Array(Math.min(totalPages, numPages)), (_, index) => ( |
||||
<Page key={index + 1} pageNumber={index + 1} width={width} /> |
||||
))} |
||||
</HTMLFlipBook> |
||||
)} |
||||
</Document> |
||||
); |
||||
}; |
||||
|
||||
export default PDFFlipBook; |
@ -1,22 +0,0 @@ |
||||
import CommonContainer from "@/components/elements/CommonContainer" |
||||
import { Card, CardContent } from "@/components/ui/card" |
||||
import { Progress } from "@/components/ui/progress" |
||||
import { CourseData } from "@/helpers/apiSchema/course.schema" |
||||
|
||||
const CourseProgress :React.FC<{courseData : CourseData}> = ({courseData}) => { |
||||
return( |
||||
<CommonContainer className="!px-0"> |
||||
<Card> |
||||
<CardContent className="py-6"> |
||||
<h3 className="text-lg font-semibold mb-4">Your Progress</h3> |
||||
<Progress value={courseData.completionRate} className="mb-2 " /> |
||||
<p className="text-sm text-gray-500 text-center"> |
||||
{courseData.completionRate}% Complete |
||||
</p> |
||||
</CardContent> |
||||
</Card> |
||||
</CommonContainer> |
||||
) |
||||
} |
||||
|
||||
export default CourseProgress |
@ -1,97 +0,0 @@ |
||||
import { Button } from '@/components/ui/button'; |
||||
import React, { useEffect, useState } from 'react'; |
||||
|
||||
export default function CustomPDFViewer() { |
||||
const [currentPage, setCurrentPage] = useState(1); |
||||
const [totalPages, setTotalPages] = useState(0); |
||||
const [loading, setLoading] = useState(true); |
||||
const [error, setError] = useState<string | null>(null); |
||||
const [pageUrl, setPageUrl] = useState(''); |
||||
|
||||
// Mock function to fetch PDF page as image URL
|
||||
// In a real implementation, you would use a backend service to convert PDF pages to images
|
||||
const fetchPageImage = async (pageNumber : number) => { |
||||
setLoading(true); |
||||
try { |
||||
// Simulating API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); |
||||
// Using placeholder image for demonstration
|
||||
setPageUrl(`https://placehold.co/600x400?text=Page ${pageNumber}`); |
||||
setTotalPages(5); // Mock total pages
|
||||
setLoading(false); |
||||
} catch (err) { |
||||
setError('Failed to load PDF page'); |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
fetchPageImage(currentPage); |
||||
}, [currentPage]); |
||||
|
||||
const goToPreviousPage = () => { |
||||
if (currentPage > 1) { |
||||
setCurrentPage(prev => prev - 1); |
||||
} |
||||
}; |
||||
|
||||
const goToNextPage = () => { |
||||
if (currentPage < totalPages) { |
||||
setCurrentPage(prev => prev + 1); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<div className="flex flex-col items-center w-full max-w-3xl mx-auto p-4"> |
||||
{/* Page Display */} |
||||
<div className="relative w-full h-auto bg-gray-100 rounded-lg shadow-lg mb-4"> |
||||
{loading ? ( |
||||
<div className=" inset-0 flex items-center justify-center min-h-96"> |
||||
<div className="text-lg text-gray-600">Loading page {currentPage}...</div> |
||||
</div> |
||||
) : error ? ( |
||||
<div className="absolute inset-0 flex items-center justify-center"> |
||||
<div className="text-lg text-red-600">{error}</div> |
||||
</div> |
||||
) : ( |
||||
<img
|
||||
src={pageUrl}
|
||||
alt={`Page ${currentPage}`} |
||||
className="w-full h-auto object-contain rounded-lg" |
||||
/> |
||||
)} |
||||
</div> |
||||
|
||||
{/* Navigation Controls */} |
||||
<div className="flex items-center gap-4 mt-6"> |
||||
<Button |
||||
onClick={goToPreviousPage} |
||||
disabled={currentPage <= 1 || loading} |
||||
className={`px-4 py-2 rounded-lg ${ |
||||
currentPage <= 1 || loading |
||||
? 'bg-purple-700/50 cursor-not-allowed' |
||||
: 'bg-purple-700 hover:bg-purple-500 text-white' |
||||
} transition-colors`}
|
||||
> |
||||
Previous |
||||
</Button> |
||||
|
||||
<div className="text-lg font-medium"> |
||||
Page {currentPage} of {totalPages} |
||||
</div> |
||||
|
||||
<Button |
||||
onClick={goToNextPage} |
||||
disabled={currentPage >= totalPages || loading} |
||||
className={`px-4 py-2 rounded-lg ${ |
||||
currentPage >= totalPages || loading |
||||
? 'bg-purple-700/50 cursor-not-allowed' |
||||
: 'bg-purple-700 hover:bg-purple-500 text-white' |
||||
} transition-colors`}
|
||||
> |
||||
Next |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
Binary file not shown.
@ -1,18 +0,0 @@ |
||||
'use client' |
||||
|
||||
import AppContextProvider from "@/helpers/context/AppContextProvider" |
||||
import CommonView from "@/views/CommonView" |
||||
import CourseIndividualContentWrapper from "./_partials/CourseIndividualContentWrapper" |
||||
|
||||
const CourseDetailPage : React.FC = () => { |
||||
return( |
||||
<AppContextProvider> |
||||
<CommonView> |
||||
|
||||
<CourseIndividualContentWrapper /> |
||||
</CommonView> |
||||
</AppContextProvider> |
||||
) |
||||
} |
||||
|
||||
export default CourseDetailPage |
@ -1,98 +0,0 @@ |
||||
// src/components/DiscussionList.tsx
|
||||
"use client"; |
||||
|
||||
import React, { useState } from "react"; |
||||
import { useSearchParams } from "next/navigation"; |
||||
import Image from "next/image"; |
||||
import { Card, CardContent } from "@/components/ui/card"; |
||||
|
||||
const SearchFeature: React.FC = () => { |
||||
const [searchQuery, setSearchQuery] = useState(""); |
||||
const searchParams = useSearchParams(); |
||||
|
||||
const discussions = [ |
||||
{ |
||||
id: 1, |
||||
title: "Introduction to Computer Science", |
||||
description: |
||||
"Learn the basics of programming and computer science concepts", |
||||
publishedDate: "2025-01-10", |
||||
enrolledStudent: 156, |
||||
activeDiscussionCount: 23, |
||||
publishedBy: "Prof. Smith", |
||||
}, |
||||
{ |
||||
id: 2, |
||||
title: "Advanced Mathematics", |
||||
description: "Covering calculus, linear algebra and statistics", |
||||
publishedDate: "2025-01-09", |
||||
enrolledStudent: 89, |
||||
activeDiscussionCount: 15, |
||||
publishedBy: "Dr. Johnson", |
||||
}, |
||||
{ |
||||
id: 3, |
||||
title: "Digital Marketing Fundamentals", |
||||
description: "Understanding modern marketing strategies", |
||||
publishedDate: "2025-01-08", |
||||
enrolledStudent: 234, |
||||
activeDiscussionCount: 45, |
||||
publishedBy: "Prof. Williams", |
||||
}, |
||||
]; |
||||
|
||||
return ( |
||||
<div className="container mx-auto p-6 space-y-6"> |
||||
{/* Discussion Cards */} |
||||
<div className="space-y-4"> |
||||
{discussions.map((discussion) => ( |
||||
<Card |
||||
key={discussion.id} |
||||
className="hover:shadow-lg transition-shadow" |
||||
> |
||||
<CardContent className="p-6 flex gap-4"> |
||||
<div > |
||||
<Image src="https://placehold.jp/400x250.png"alt="placeholder" width={400} height={250} className="w-[400] h-auto rounded-2xl"/> |
||||
</div> |
||||
<div> |
||||
<div className="flex items-start justify-between"> |
||||
<div className="space-y-2"> |
||||
<h3 className="text-xl font-semibold text-purple-700"> |
||||
{discussion.title} |
||||
</h3> |
||||
<p className="text-gray-600">{discussion.description}</p> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500"> |
||||
<span className="flex items-center"> |
||||
<span className="font-medium">Published:</span> |
||||
<span className="ml-1">{discussion.publishedDate}</span> |
||||
</span> |
||||
<span className="flex items-center"> |
||||
<span className="font-medium">Enrolled:</span> |
||||
<span className="ml-1"> |
||||
{discussion.enrolledStudent} students |
||||
</span> |
||||
</span> |
||||
<span className="flex items-center"> |
||||
<span className="font-medium">Active Discussions:</span> |
||||
<span className="ml-1"> |
||||
{discussion.activeDiscussionCount} |
||||
</span> |
||||
</span> |
||||
<span className="flex items-center"> |
||||
<span className="font-medium">Published by:</span> |
||||
<span className="ml-1">{discussion.publishedBy}</span> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</CardContent> |
||||
</Card> |
||||
))} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default SearchFeature; |
@ -1,16 +0,0 @@ |
||||
'use client' |
||||
import AppContextProvider from "@/helpers/context/AppContextProvider" |
||||
import SearchFeature from "./_partials/SearchFeature" |
||||
import CommonView from "@/views/CommonView" |
||||
|
||||
const SearchFeatureIndexPage : React.FC = () => { |
||||
return( |
||||
<AppContextProvider> |
||||
<CommonView> |
||||
<SearchFeature /> |
||||
</CommonView> |
||||
</AppContextProvider> |
||||
) |
||||
} |
||||
|
||||
export default SearchFeatureIndexPage |
@ -1,24 +0,0 @@ |
||||
export interface CourseData { |
||||
title: string; |
||||
instructor: { |
||||
name: string; |
||||
title: string; |
||||
avatar: string; |
||||
}; |
||||
description: string; |
||||
category: string; |
||||
duration: string; |
||||
enrolledStudents: number; |
||||
rating: number; |
||||
totalReviews: number; |
||||
completionRate: number; |
||||
lastUpdated: string; |
||||
chapters: Chapter[]; |
||||
requirements: string[]; |
||||
} |
||||
|
||||
interface Chapter { |
||||
title: string; |
||||
duration: string; |
||||
isCompleted: boolean; |
||||
} |
Loading…
Reference in new issue