ayush 6 months ago
commit 3f777b170d
  1. 116
      frontend/edu-connect/src/app/(admin)/admin/category/_partials/CategoryTable.tsx
  2. 2
      frontend/edu-connect/src/app/(admin)/admin/course/page.tsx
  3. 96
      frontend/edu-connect/src/app/_partials/FeaturedCourses.tsx
  4. 92
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailHeroSection.tsx
  5. 12
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailStats.tsx
  6. 300
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDiscussion.tsx
  7. 262
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseGenerateQuestions.tsx
  8. 59
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseIndividualContentWrapper.tsx
  9. 21
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseMedia.tsx
  10. 6
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseProgress.tsx
  11. 136
      frontend/edu-connect/src/app/courses/show/[id]/_partials/TryPDF.tsx
  12. 26
      frontend/edu-connect/src/app/courses/show/[id]/page.tsx
  13. 2
      frontend/edu-connect/src/components/(dashboard)/common/Sidebar/_partials/menu.tsx
  14. 26
      frontend/edu-connect/src/components/common/Navbar/desktopNav.tsx
  15. 11
      frontend/edu-connect/src/components/elements/CourseCard.tsx
  16. 1
      frontend/edu-connect/src/helpers/apiSchema/course.schema.ts
  17. 3
      frontend/edu-connect/src/lib/routes.ts
  18. 51
      frontend/edu-connect/src/utils/menu-list.tsx

@ -1,9 +1,113 @@
const CategoryTable = () => {
return(
<>
import DataTable from "@/components/(dashboard)/common/DataTable/DataTable"
import { Button } from "@/components/(dashboard)/ui/button"
import { Badge } from "@/components/ui/badge"
import { routes } from "@/lib/routes"
import { ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
</>
)
const CategoryTable: React.FC<{
mutate: () => void
Data: Array<any>
isLoading: boolean
}> = ({
mutate,
Data,
isLoading
}) => {
const columns: ColumnDef<any>[] = [
{
accessorKey: "sn",
header: "SN",
cell: ({ row }) => (
<div className="capitalize">{row.index + 1}</div>
),
},
{
id: 'name',
accessorFn: (row: any) => row.original?.name,
header: ({ column }) => (
<Button
variant="ghost"
className="!px-0"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="capitalize">
<p>{row.original?.name}</p>
</div>
),
},
{
id: 'description',
accessorFn: (row: any) => row.original?.description,
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="!px-0"
>
Description
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="max-w-[300px] truncate">{row.original?.description ?? '-'}</div>
),
},
{
id: 'creationDate',
accessorFn: (row: any) => row.original?.creationDate,
header: ({ column }) => (
<Button
variant="ghost"
className="!px-0"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Created Date
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="whitespace-nowrap">
{row.original?.creationDate ? new Date(row.original.creationDate).toLocaleDateString() : '-'}
</div>
),
},
{
id: 'isActive',
accessorFn: (row: any) => row.original?.isActive,
header: ({ column }) => (
<Button
variant="ghost"
className="!px-0"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div>{row.original?.isActive ? <Badge variant={'success'}>Active</Badge> : <Badge variant={'destructive'}>Deactive</Badge>}</div>
),
},
]
return(
<>
<DataTable
data={Data}
columns={columns}
mutate={mutate}
searchKey="name"
isLoading={isLoading}
/>
</>
)
}
export default Categ
export default CategoryTable

@ -12,7 +12,7 @@ import useSWR from "swr"
import CourseTable from "./_partials/CourseTable"
const CourseIndexPage = () => {
const CourseListURL = `${APP_BASE_URL}/api/course/listall`
const CourseListURL = `${APP_BASE_URL}/api/course/listAll`
const { data, mutate, isLoading } = useSWR(CourseListURL, defaultFetcher);
return (

@ -3,75 +3,13 @@ import CourseCard from '@/components/elements/CourseCard';
import useSWR from 'swr';
import { APP_BASE_URL } from '@/utils/constants';
import { defaultFetcher } from '@/helpers/fetch.helper';
import Link from 'next/link';
import { routes } from '@/lib/routes';
const FeaturedCourses = () => {
const { data : CourseList } = useSWR(APP_BASE_URL + '' , defaultFetcher)
const courses = [
{
id: 1,
title: 'Foundation course to under stand about software',
category: 'Data & Tech',
lessons: 23,
duration: '1hr 30 min',
price: 32.00,
originalPrice: 89.00,
instructor: {
name : 'Micie John',
image: 'https://dummyimage.com/300'
},
rating: 4.5,
reviews: 44,
image: 'https://dummyimage.com/300'
},
{
id: 2,
title: 'Nidhies course to under stand about softwere',
category: 'Mechanical',
lessons: 29,
duration: '2hr 30 min',
price: 32.00,
originalPrice: 89.00,
instructor: {
name : 'Micie John',
image: 'https://dummyimage.com/300'
},
rating: 4.5,
reviews: 44,
image: 'https://dummyimage.com/300'
},
{
id: 3,
title: 'Minws course to under stand about solution',
category: 'Development',
lessons: 25,
duration: '1hr 40 min',
price: 40.00,
originalPrice: 89.00,
instructor: {
name : 'Micie John',
image: 'https://dummyimage.com/300'
},
rating: 4.5,
reviews: 44,
image: 'https://dummyimage.com/300'
},
{
id: 3,
title: 'Minws course to under stand about solution',
category: 'Development',
lessons: 25,
duration: '1hr 40 min',
price: 40.00,
originalPrice: 89.00,
instructor: {
name : 'Micie John',
image: 'https://dummyimage.com/300'
},
rating: 4.5,
reviews: 44,
image: 'https://dummyimage.com/300'
}
];
const { data : CourseList } = useSWR(APP_BASE_URL + '/api/course/listAll' , defaultFetcher)
console.log(CourseList)
return (
<div className="max-w-7xl mx-auto p-6">
@ -86,18 +24,20 @@ const FeaturedCourses = () => {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{
courses?.map((course : Record<string,any> , index) => {
CourseList?.data?.length && CourseList?.data?.map((course : Record<string,any> , index) => {
return(
<CourseCard
key={index}
id={course.id}
image ={course?.image}
title ={course?.title}
category = {course?.category}
lessons = {course?.lessons}
instructor = {course?.instructor}
duration={course?.duration}
/>
<Link href={routes.COURSE_INDIVIDUAL_PAGE.replace(':id' , course?.id)}>
<CourseCard
key={index}
id={course.id}
image ={APP_BASE_URL + course?.coverImage}
title ={course?.title}
category = {course?.category?.name}
lessons = {course?.lessons}
instructor = {course?.instructor}
duration={course?.duration}
/>
</Link>
)
})
}

@ -2,46 +2,112 @@ 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"
import { AuthContext } from "@/helpers/context/AuthProvider";
import { fetchHeader } from "@/helpers/fetch.helper";
import { useToast } from "@/hooks/use-toast";
import { APP_BASE_URL } from "@/utils/constants";
import { Loader } from "lucide-react";
import Image from "next/image";
import { useContext, useState } from "react";
interface CourseDetailHeroSectionProps {
courseData: CourseData;
courseData: Record<string,any>;
mutate : () => void
}
const CourseDetailHeroSection: React.FC<CourseDetailHeroSectionProps> = ({courseData}) => {
const CourseDetailHeroSection: React.FC<CourseDetailHeroSectionProps> = ({ courseData , mutate }) => {
console.log(courseData)
const { user } = useContext(AuthContext)
const { toast } = useToast();
const [loading , setLoading] = useState<boolean>(false)
const handleEnrollNow = async () : Promise<void> => {
setLoading(true)
const formData = new FormData();
formData.append('course_uuid' , courseData.id)
try{
const res = await fetch(APP_BASE_URL + '' , {
headers : fetchHeader() ,
method : 'POST' ,
body : formData
})
const data = await res.json();
if(res.status == 200){
toast({
title : 'Successfully enrolled !',
description : data?.message ,
variant : 'success'
})
mutate()
}else{
toast({
title : 'Successfully enrolled !',
description : data?.message ,
variant : 'destructive'
})
}
}catch(error : any){
toast({
title : 'Successfully enrolled !',
description : 'something went wrong , please try again' ,
variant : 'destructive'
})
}finally{
setLoading(false)
}
}
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>
<h1 className="text-3xl font-bold">{courseData?.courseName}</h1>
<p className="text-blue-100">{courseData?.courseDescription}</p>
<div className="flex items-center space-x-4">
<Avatar className="h-12 w-12">
<AvatarImage src={courseData.instructor.avatar} className='aspect-1/1'/>
<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 className="font-semibold">{courseData?.instructor?.firstName}</div>
<div className="text-sm text-blue-100">{courseData?.instructor?.title}</div>
<div>Published at {courseData?.creationDate}</div>
</div>
</div>
</div>
{/* course image */}
<Card className="bg-white">
<CardContent className="p-6 space-y-4">
<div className="relative max-h-124">
<img
src="https://placehold.co/600x400"
src={ courseData?.coverImage ? (APP_BASE_URL + courseData?.coverImage) : "https://placehold.co/600x400"}
alt="Course Preview"
className="w-full rounded-lg"
className="w-full rounded-lg h-auto aspect-1/1 max-h-96 object-cover"
/>
<Button className="w-full text-lg h-12 bg-purple-700">
Enroll Now
</Button>
</div>
{
courseData?.selfEnrollment?.isEnrolled &&
<Button className="w-full text-lg h-12 bg-purple-700" onClick={handleEnrollNow}>
{
loading ?
<>
<Loader />
Enrolling ..
</> : 'Enroll Now'
}
</Button>
}
</CardContent>
</Card>
</div>

@ -3,7 +3,7 @@ import { CourseData } from "@/helpers/apiSchema/course.schema"
import { BookOpen, Clock, MessageSquare, Users } from "lucide-react"
const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) => {
const CourseDetailStats :React.FC<{courseData : Record<string,any>}> = ({courseData}) => {
return(
<>
<div className="mx-auto container py-8">
@ -13,7 +13,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
<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 className="font-semibold">{courseData?.duration ?? 0}</div>
</div>
</CardContent>
</Card>
@ -23,7 +23,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
<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 className="font-semibold">{courseData?.enrollmentSummary?.courseData ?? 0} students</div>
</div>
</CardContent>
</Card>
@ -33,7 +33,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
<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 className="font-semibold">{courseData?.enrollmentSummary?.totalChats ?? 0}</div>
</div>
</CardContent>
</Card>
@ -42,8 +42,8 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
<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 className="text-sm text-gray-500">Discussion group member</div>
<div className="font-semibold">{courseData?.enrollmentSummary?.usersInChat ?? 0}</div>
</div>
</CardContent>
</Card>

@ -0,0 +1,300 @@
import React, { useState, useRef } from 'react';
import useSWRInfinite from 'swr/infinite';
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import { MessageCircle, Reply, X } from "lucide-react";
import { APP_BASE_URL } from '@/utils/constants';
import { getEduConnectAccessToken } from '@/helpers/token.helper';
// Types
interface Reply {
id: number;
user: string;
avatar: string;
content: string;
timestamp: string;
}
interface Discussion {
id: number;
user: string;
avatar: string;
content: string;
likes: number;
replies: Reply[];
timestamp: string;
}
interface PageData {
discussions: Discussion[];
hasMore: boolean;
}
interface DiscussionSectionProps {
courseUuid: string;
}
// Key generator for SWR
const getKey = (courseUuid: string) => (pageIndex: number, previousPageData: PageData | null) => {
if (previousPageData && !previousPageData.discussions.length) return null;
return `${APP_BASE_URL}/api/chat/get?page=${pageIndex}`;
};
// Fetcher function
const fetcher = async (url: string, courseUuid: string): Promise<PageData> => {
const formData = new FormData();
formData.append('course_uuid', courseUuid);
const response = await fetch(url, {
method: 'POST',
body: formData ,
headers : {
Authorization : `Bearer ${getEduConnectAccessToken()}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch discussions');
}
const data = await response.json();
return {
discussions: data.discussions.map((discussion: any) => ({
...discussion,
timestamp: new Date(discussion.timestamp).toLocaleString()
})),
hasMore: data.hasMore
};
};
const EnhancedDiscussionSection: React.FC<DiscussionSectionProps> = ({ courseUuid }) => {
const [comment, setComment] = useState('');
const [replyingTo, setReplyingTo] = useState<{ commentId: number; userName: string } | null>(null);
const loadingRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const {
data,
error,
size,
setSize,
isValidating,
mutate
} = useSWRInfinite(
getKey(courseUuid),
(url) => fetcher(url, courseUuid),
{
revalidateFirstPage: false,
revalidateOnFocus: false,
persistSize: true
}
);
const allDiscussions = data ? data.flatMap(page => page.discussions) : [];
const hasMore = data ? data[data.length - 1]?.hasMore : true;
const isLoadingMore = !data && !error || (size > 0 && data && typeof data[size - 1] === "undefined");
// Intersection Observer for infinite loading
React.useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !isValidating) {
setSize(size + 1);
}
},
{ threshold: 0.1 }
);
if (loadingRef.current) {
observer.observe(loadingRef.current);
}
return () => observer.disconnect();
}, [hasMore, isValidating, setSize, size]);
const handleSubmit = async () => {
if (comment.trim()) {
const formData = new FormData();
formData.append('course_uuid', courseUuid);
formData.append('content', comment);
if (replyingTo) {
formData.append('parent_id', replyingTo.commentId.toString());
}
try {
const response = await fetch('/api/chat/post', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Failed to post comment');
}
const newComment = await response.json();
// Optimistically update the UI
const updatedData = [...(data || [])];
if (replyingTo) {
// Update the discussion with the new reply
updatedData.forEach(page => {
page.discussions = page.discussions.map(discussion => {
if (discussion.id === replyingTo.commentId) {
return {
...discussion,
replies: [...discussion.replies, {
...newComment,
timestamp: 'Just now'
}]
};
}
return discussion;
});
});
} else {
// Add new top-level comment
if (updatedData[0]) {
updatedData[0].discussions = [{
...newComment,
replies: [],
timestamp: 'Just now'
}, ...updatedData[0].discussions];
}
}
await mutate(updatedData, false);
setComment('');
setReplyingTo(null);
} catch (error) {
console.error('Failed to post comment:', error);
// You might want to show an error message to the user here
}
}
};
const handleReply = (commentId: number, userName: string) => {
setReplyingTo({ commentId, userName });
setComment(`@${userName} `);
};
const cancelReply = () => {
setReplyingTo(null);
setComment('');
};
if (error) {
return (
<div className="text-red-500 p-4 text-center">
Error loading discussions. Please try again later.
</div>
);
}
return (
<div className="w-full max-w-3xl mx-auto">
<ScrollArea className="h-[600px]">
<div className="space-y-6 p-4">
{allDiscussions.map((discussion) => (
<Card key={discussion.id} className="p-4">
<div className="space-y-4">
<div className="flex gap-4">
<Avatar className="w-8 h-8">
<AvatarImage src={discussion.avatar} alt={discussion.user} />
<AvatarFallback>{discussion.user[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{discussion.user}</span>
<span className="text-sm text-gray-500">{discussion.timestamp}</span>
</div>
<p className="text-gray-700 mb-2">{discussion.content}</p>
<div className="flex items-center gap-4">
<button
className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700"
onClick={() => handleReply(discussion.id, discussion.user)}
>
<Reply className="w-4 h-4" />
Reply
</button>
<div className="flex items-center gap-1 text-sm text-gray-500">
<MessageCircle className="w-4 h-4" />
{discussion.replies.length} replies
</div>
</div>
</div>
</div>
{discussion.replies.length > 0 && (
<div className="ml-12 space-y-4">
{discussion.replies.map((reply) => (
<div key={reply.id} className="flex gap-4">
<Avatar className="w-8 h-8">
<AvatarImage src={reply.avatar} alt={reply.user} />
<AvatarFallback>{reply.user[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{reply.user}</span>
<span className="text-sm text-gray-500">{reply.timestamp}</span>
</div>
<p className="text-gray-700">{reply.content}</p>
</div>
</div>
))}
</div>
)}
</div>
</Card>
))}
<div ref={loadingRef} className="h-10 flex items-center justify-center">
{isLoadingMore && (
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900" />
)}
{!hasMore && allDiscussions.length > 0 && (
<div className="text-gray-500">No more discussions</div>
)}
</div>
</div>
</ScrollArea>
<div className="my-6 px-4">
<div className="flex items-start gap-4">
<Avatar className="w-8 h-8">
<AvatarImage src="/api/placeholder/32/32" alt="User" />
<AvatarFallback>U</AvatarFallback>
</Avatar>
<div className="flex-1">
{replyingTo && (
<div className="flex items-center gap-2 mb-2 text-sm text-gray-500">
<span>Replying to {replyingTo.userName}</span>
<button
onClick={cancelReply}
className="p-1 hover:bg-gray-100 rounded-full"
>
<X className="w-4 h-4" />
</button>
</div>
)}
<Textarea
placeholder={replyingTo ? `Reply to ${replyingTo.userName}...` : "Add to the discussion..."}
value={comment}
onChange={(e) => setComment(e.target.value)}
className="min-h-24 mb-6"
/>
<div className="w-fit ml-auto">
<Button onClick={handleSubmit} className="bg-purple-700">
{replyingTo ? 'Post Reply' : 'Post Comment'}
</Button>
</div>
</div>
</div>
</div>
</div>
);
};
export default EnhancedDiscussionSection;

@ -6,77 +6,76 @@ import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Check, X } from "lucide-react";
import useSWR from 'swr';
import { APP_BASE_URL } from '@/utils/constants';
import { defaultFetcher } from '@/helpers/fetch.helper';
import { getEduConnectAccessToken } from '@/helpers/token.helper';
import { useToast } from '@/hooks/use-toast';
const QuizGenerator = () => {
// Submitted questions history
const [submittedQuestions] = useState([
{
id: 1,
question: "What is React's Virtual DOM?",
options: [
"A complete DOM copy",
"A lightweight copy of the DOM",
"A browser feature",
"A routing system"
],
selectedOption: 0, // User selected
correctOption: 1, // Correct answer
},
{
id: 2,
question: "What hook manages side effects?",
options: [
"useState",
"useReducer",
"useEffect",
"useContext"
],
selectedOption: 2,
correctOption: 2,
},
]);
// Unsubmitted questions
const [pendingQuestions, setPendingQuestions] = useState([
{
id: 1,
question: "Which of these is a state management library?",
options: ["Redux", "Axios", "Lodash", "Moment"],
selectedOption: null
},
{
id: 2,
question: "What does CSS stand for?",
options: [
"Computer Style Sheets",
"Cascading Style Sheets",
"Creative Style Sheets",
"Colorful Style Sheets"
],
selectedOption: null
},
]);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const handleOptionSelect = (value) => {
const updatedQuestions = [...pendingQuestions];
updatedQuestions[currentQuestionIndex].selectedOption = parseInt(value);
setPendingQuestions(updatedQuestions);
};
interface Course {
id: number;
name: string;
description: string;
}
interface Creator {
id: number;
firstName: string;
lastName: string;
username: string;
pfpFilename: string;
}
interface QuizAttempt {
id: number;
quizID: number;
isActive: boolean;
creationDate: string;
quizAnswers: string[];
quizQuestion: string;
userAnswer: string | null;
quizCorrectAnswer: string;
isCorrect: boolean;
course: Course;
creator: Creator;
}
interface QuizGeneratorProps {
current_page: number;
course_uuid: string;
}
const QuizGenerator: React.FC<QuizGeneratorProps> = ({
current_page,
course_uuid ,
}) => {
const { data: completedQuizzes, error: completedError } = useSWR<any>(
`${APP_BASE_URL}/api/quiz/get/allComplete?course_uuid=${course_uuid}`,
defaultFetcher
);
const generateQuiz = () => {
// Reset selections and shuffle pending questions
const shuffledQuestions = [...pendingQuestions].map(q => ({
...q,
selectedOption: null
const { toast } = useToast()
const { data: uncompletedQuizzes, error: uncompletedError , mutate } = useSWR<any>(
`${APP_BASE_URL}/api/quiz/get/personalIncomplete?course_uuid=${course_uuid}`,
defaultFetcher
);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState<number>(0);
const [pendingAnswers, setPendingAnswers] = useState<Record<number, string>>({});
const isLoading = !completedQuizzes || !uncompletedQuizzes;
const isError = completedError || uncompletedError;
const handleOptionSelect = (quizId: number, value: string) => {
setPendingAnswers(prev => ({
...prev,
[quizId]: value
}));
setPendingQuestions(shuffledQuestions);
setCurrentQuestionIndex(0);
};
const handleNext = () => {
if (currentQuestionIndex < pendingQuestions.length - 1) {
if (uncompletedQuizzes && currentQuestionIndex < uncompletedQuizzes.length - 1) {
setCurrentQuestionIndex(prev => prev + 1);
}
};
@ -87,93 +86,150 @@ const QuizGenerator = () => {
}
};
if (isError) {
return (
<Card>
<CardContent className="p-6 text-center text-red-500">
Error loading quiz data
</CardContent>
</Card>
);
}
if (isLoading) {
return (
<Card>
<CardContent className="p-6 text-center text-gray-500">
Loading quiz data...
</CardContent>
</Card>
);
}
const handleGenerateQuiz = async() : Promise<void> => {
try{
const formData = new FormData();
formData.append('course_uuid' , course_uuid);
formData.append('page' , (current_page + 1)?.toString())
const res = await fetch(APP_BASE_URL + '/api/quiz/generate' , {
headers : {
Authorization : `Bearer ${getEduConnectAccessToken()}`
} ,
body : formData,
method : 'POST'
});
if(res.status == 200){
mutate()
toast({
title : 'Sucessfully fetched !',
variant : 'success'
})
}
}catch(error : any){
toast({
title : 'Failed to fetch',
variant : 'destructive'
})
}
}
const currentQuiz = uncompletedQuizzes?.[currentQuestionIndex];
return (
<div className="py-6 container mx- mt-8">
{/* Submitted Questions History */}
<div className='grid grid-cols-3 space-x-6'>
<Card className="w-full col-span-2">
<div className="py-6 container mx-auto mt-8">
<div className="grid grid-cols-3 gap-6">
<Card className="col-span-2">
<CardHeader>
<CardTitle>Quiz History</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] pr-4">
{submittedQuestions.map((q, index) => (
<div key={q.id} className="mb-4">
{completedQuizzes?.data?.map((quiz, index) => (
<div key={quiz.id} className="mb-4">
<div className="p-4 border rounded-lg border-gray-200">
<h3 className="font-medium mb-2">{q.question}</h3>
<div className="flex justify-between items-center mb-2">
<h3 className="font-medium">{quiz.quizQuestion}</h3>
<span className="text-sm text-gray-500">
by {quiz.creator.firstName} {quiz.creator.lastName}
</span>
</div>
<div className="text-sm space-y-2">
{q.options.map((option, i) => (
{quiz.quizAnswers.map((answer, i) => (
<div
key={i}
className={`p-2 rounded flex justify-between items-center ${
i === q.correctOption && i === q.selectedOption
answer === quiz.quizCorrectAnswer && answer === quiz.userAnswer
? 'bg-green-100'
: i === q.correctOption
: answer === quiz.quizCorrectAnswer
? 'bg-green-50'
: i === q.selectedOption
: answer === quiz.userAnswer
? 'bg-red-50'
: 'bg-gray-50'
}`}
>
<span>{option}</span>
{i === q.correctOption && (
<span>{answer}</span>
{answer === quiz.quizCorrectAnswer && (
<Check className="w-4 h-4 text-green-600" />
)}
{i === q.selectedOption && i !== q.correctOption && (
{answer === quiz.userAnswer && answer !== quiz.quizCorrectAnswer && (
<X className="w-4 h-4 text-red-600" />
)}
</div>
))}
</div>
</div>
{index < submittedQuestions.length - 1 && <Separator className="my-4" />}
{index < completedQuizzes?.length - 1 && <Separator className="my-4" />}
</div>
))}
{
completedQuizzes?.data?.length === 0 && 'No Quiz taken yet'
}
</ScrollArea>
</CardContent>
</Card>
{/* Pending Questions Interface */}
<div className="w-full space-y-6">
<Button
onClick={generateQuiz}
className="w-full bg-purple-700"
>
Generate Quiz
</Button>
{pendingQuestions.length > 0 ? (
<div className="space-y-6">
{currentQuiz ? (
<Card>
<CardHeader>
<CardTitle>
Question {currentQuestionIndex + 1} of {pendingQuestions.length}
Question {currentQuestionIndex + 1} of {uncompletedQuizzes?.length}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<h3 className="text-lg font-medium">
{pendingQuestions[currentQuestionIndex].question}
{currentQuiz?.quizQuestion}
</h3>
<RadioGroup
value={pendingQuestions[currentQuestionIndex].selectedOption?.toString()}
onValueChange={handleOptionSelect}
value={pendingAnswers[currentQuiz.id] || ''}
onValueChange={(value) => handleOptionSelect(currentQuiz.id, value)}
>
{pendingQuestions[currentQuestionIndex].options.map((option, i) => (
{currentQuiz.quizAnswers.map((answer, i) => (
<div key={i} className="flex items-center space-x-2">
<RadioGroupItem value={i.toString()} id={`option-${i}`} />
<Label htmlFor={`option-${i}`}>{option}</Label>
<RadioGroupItem value={answer} id={`option-${i}`} />
<Label htmlFor={`option-${i}`}>{answer}</Label>
</div>
))}
</RadioGroup>
<div className="flex justify-end mt-6">
<div className="flex justify-between mt-6">
<Button
onClick={handlePrevious}
disabled={currentQuestionIndex === 0}
variant="outline"
>
Previous
</Button>
<Button
onClick={handleNext}
disabled={currentQuestionIndex === pendingQuestions.length - 1}
className='bg-purple-700'
disabled={currentQuestionIndex === (uncompletedQuizzes?.length ?? 0) - 1}
className="bg-purple-700 hover:bg-purple-800"
>
Submit
Next
</Button>
</div>
</div>
@ -182,7 +238,9 @@ const QuizGenerator = () => {
) : (
<Card>
<CardContent className="p-6 text-center text-gray-500">
No pending questions available
<Button className='bg-purple-700' onClick={handleGenerateQuiz}>
Generate Quiz Questions
</Button>
</CardContent>
</Card>
)}

@ -1,5 +1,5 @@
'use client';
import React from 'react';
import React, { useContext } from 'react';
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import CourseDetailHeroSection from './CourseDetailHeroSection';
@ -7,49 +7,40 @@ import CourseDetailStats from './CourseDetailStats';
import CourseMedia from './CourseMedia';
import CourseProgress from './CourseProgress';
import TryPDF from './TryPDF';
import { CourseData } from '@/helpers/apiSchema/course.schema';
import useSWR from 'swr';
import { APP_BASE_URL } from '@/utils/constants';
import { defaultFetcher } from '@/helpers/fetch.helper';
import { AuthContext, AuthProvider } from '@/helpers/context/AuthProvider';
const CourseIndividualContentWrapper : React.FC<{
course_uuid : string
}> = ({
course_uuid
}) => {
const { user } = useContext(AuthContext)
const { data , mutate } = useSWR(APP_BASE_URL + `/api/course/info/${course_uuid}` , defaultFetcher)
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}/>
<CourseDetailHeroSection courseData={data?.data as CourseData} mutate={mutate}/>
{/* Course Stats */}
<CourseDetailStats courseData={courseData}/>
<CourseDetailStats courseData={data?.data}/>
{
user && data?.data?.selfEnrollment?.isEnrolled && (
<CourseProgress courseData={data?.data}/>
)
}
<CourseProgress courseData={courseData}/>
{/* Course Content */}
<CourseMedia courseData={courseData}/>
<CourseMedia courseData={data?.data}/>
</div>
);

@ -1,12 +1,16 @@
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"
import QuizGenerator from "./CourseGenerateQuestions"
import EnhancedDiscussionSection from "./CourseDiscussion"
import { useState } from "react"
const CourseMedia : React.FC<{courseData : CourseData}> = ({courseData}) => {
const CourseMedia : React.FC<{courseData : Record<string,any>}> = ({courseData}) => {
const [currentPage, setCurrentPage] = useState<number>(0);
return(
<div className="container mx-auto py-8">
<div className="grid md:grid-cols-3 gap-8">
@ -15,7 +19,11 @@ const CourseMedia : React.FC<{courseData : CourseData}> = ({courseData}) => {
<Card>
<CardContent className="p-6">
<h2 className="text-xl font-semibold mb-4">Course Content</h2>
<CustomPDFViewer />
<CustomPDFViewer
pdfPages={courseData?.pages}
onChange={setCurrentPage}
currentPage={currentPage}
/>
</CardContent>
</Card>
</div>
@ -26,14 +34,17 @@ const CourseMedia : React.FC<{courseData : CourseData}> = ({courseData}) => {
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-4">Discussions</h3>
<DiscussionSection />
{/* <DiscussionSection /> */}
<EnhancedDiscussionSection
courseUuid={courseData?.id}
/>
</CardContent>
</Card>
</div>
</div>
<QuizGenerator />
<QuizGenerator current_page={currentPage} course_uuid={courseData?.id}/>
</div>
)
}

@ -3,15 +3,15 @@ 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}) => {
const CourseProgress :React.FC<{courseData : Record<string,any>}> = ({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 " />
<Progress value={(((courseData?.selfEnrollment?.maxPage) / courseData?.totalPages) * 100)} className="mb-2 " />
<p className="text-sm text-gray-500 text-center">
{courseData.completionRate}% Complete
{((courseData?.selfEnrollment?.maxPage) / courseData?.totalPages) * 100 }% Complete
</p>
</CardContent>
</Card>

@ -1,97 +1,73 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import React, { useEffect, useState } from 'react';
import { APP_BASE_URL } from '@/utils/constants';
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('');
const CustomPDFViewer = ({
pdfPages ,
currentPage ,
onChange
} : {
pdfPages : Array<string>
currentPage : number
onChange : (value : any) => void
}) => {
// const [currentPage, setCurrentPage] = useState(0);
// Your PDF pages array
// 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 totalPages = pdfPages?.length;
const goToPreviousPage = () => {
if (currentPage > 1) {
setCurrentPage(prev => prev - 1);
if (currentPage > 0) {
onChange((prev : number) => prev - 1);
}
};
const goToNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(prev => prev + 1);
if (currentPage < totalPages - 1) {
onChange((prev : number) => 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>
<>
{
pdfPages && (
<div className="flex flex-col items-center space-y-4">
{/* PDF Display */}
<div className="w-full max-w-3xl border rounded-lg shadow-lg">
<iframe
src={APP_BASE_URL + pdfPages[currentPage]}
className="w-full h-[800px]"
title={`PDF Page ${currentPage + 1}`}
/>
</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}
{/* Navigation Controls */}
<div className="flex items-center space-x-4">
<Button
onClick={goToPreviousPage}
disabled={currentPage === 0}
variant="outline"
>
Previous
</Button>
<span className="text-sm">
Page {currentPage + 1} of {totalPages}
</span>
<Button
onClick={goToNextPage}
disabled={currentPage === totalPages - 1}
variant="outline"
>
Next
</Button>
</div>
</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>
)
}
</>
);
}
};
export default CustomPDFViewer;

@ -3,13 +3,33 @@
import AppContextProvider from "@/helpers/context/AppContextProvider"
import CommonView from "@/views/CommonView"
import CourseIndividualContentWrapper from "./_partials/CourseIndividualContentWrapper"
import { APP_BASE_URL } from "@/utils/constants"
import { defaultFetcher } from "@/helpers/fetch.helper"
import useSWR from "swr"
import { useEffect } from "react"
import { useRouter } from "next/navigation"
import { routes } from "@/lib/routes"
const CourseDetailPage : React.FC<{
params : Record<string,any>
}> = ({params}) => {
const { id } = params
const router = useRouter()
const CourseDetailPage = `${APP_BASE_URL}/api/course/info/${id}`
const { data } = useSWR(CourseDetailPage, defaultFetcher);
console.log(data)
useEffect(() => {
if(!id) router.replace(routes.COURSE_LIST_INDEX)
},[])
const CourseDetailPage : React.FC = () => {
return(
<AppContextProvider>
<CommonView>
<CourseIndividualContentWrapper />
<CourseIndividualContentWrapper course_uuid={id}/>
</CommonView>
</AppContextProvider>
)

@ -32,7 +32,7 @@ export function Menu({ isOpen }: MenuProps) {
return (
<ScrollArea className="[&>div>div[style]]:!block">
<nav className="mt-8 h-full w-full">
<ul className="flex flex-col min-h-[calc(100vh-48px-36px-16px-32px)] lg:min-h-[calc(100vh-32px-60px-32px)] items-start space-y-1 px-2">
<ul className="flex flex-col min-h-[calc(100vh-48px-36px-16px-32px-300px)] lg:min-h-[calc(100vh-32px-60px-32px-100px)] items-start space-y-1 px-2">
{menuList.map(({ groupLabel, menus }, index) => (
<li className={cn("w-full", groupLabel ? "pt-3" : "")} key={index}>
{(isOpen && groupLabel) || isOpen === undefined ? (

@ -2,35 +2,16 @@ import React from 'react';
import Link from "next/link";
import { NavigationMenu,NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
import { Book, Home, User } from "lucide-react";
import { routes } from '@/lib/routes';
export const DesktopNav = () => {
return (
<div className="hidden md:block">
<NavigationMenu>
<NavigationMenuList>
{/* <NavigationMenuItem>
<NavigationMenuTrigger className='bg-zinc-900 text-white hover:bg-zinc-600'>
<Home className="mr-2 h-4 w-4" />
Menu
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
{navigationItems.map((item : Record<string,any> , index : number) => (
<ListItem
key={item.title}
title={item.title}
href={item.href}
icon={item.icon}
>
{item.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem> */}
<NavigationMenuItem>
<Link href="/docs" legacyBehavior passHref>
<Link href={routes.INDEX_PAGE} legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
<Home className="mr-2 h-4 w-4" />
Home
@ -39,7 +20,7 @@ export const DesktopNav = () => {
</NavigationMenuItem>
<NavigationMenuItem>
<Link href="/profile" legacyBehavior passHref>
<Link href={routes.COURSE_LIST_INDEX} legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
<Book className="mr-2 h-4 w-4" />
All Courses
@ -47,6 +28,7 @@ export const DesktopNav = () => {
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
);

@ -2,6 +2,7 @@ import { BookOpen, Clock, Heart } from "lucide-react"
import { Card, CardContent } from "../ui/card"
import Link from "next/link"
import { routes } from "@/lib/routes"
import { APP_BASE_URL } from "@/utils/constants"
interface CourseInterface {
@ -12,8 +13,8 @@ interface CourseInterface {
lessons ?: string
duration? : string
instructor ?: {
image : string
name : string
pfpFilename : string
firstName : string
}
}
@ -60,13 +61,13 @@ const CourseCard : React.FC<CourseInterface> = ({
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<img
src={instructor?.image}
alt={instructor?.name}
src={APP_BASE_URL + instructor?.pfpFilename}
alt={instructor?.firstName}
className="w-8 h-8 rounded-full"
/>
<span className="text-sm">
<Link href={routes.PROFILE_ROUTE}>
{instructor?.name}
{instructor?.firstName}
</Link>
</span>
</div>

@ -15,6 +15,7 @@ export interface CourseData {
lastUpdated: string;
chapters: Chapter[];
requirements: string[];
course_uuid : string
}
interface Chapter {

@ -8,7 +8,8 @@ export const routes = {
USER_INDEX_PAGE : '/admin/users',
COURSE_INDEX_PAGE: '/admin/course',
CATEGORY_INDEX_PAGE: '/admin/category',
COURSE_INDIVIDUAL_PAGE : '/course/show/:id'
COURSE_INDIVIDUAL_PAGE : '/courses/show/:id',
COURSE_LIST_INDEX : '/courses'
}
export const privateRoutes = ['/user/profile']

@ -42,70 +42,35 @@ export function getMenuList(): Group[] {
{
groupLabel: "Contents",
menus: [
{
href : '' ,
label : 'Company',
icon : Building,
submenus : [
{
href: routes.DASHBOARD_ROUTE,
label: "All Branch"
},
{
href: routes.DASHBOARD_ROUTE,
label: "All Department"
},
]
},
{
href: "",
label: "Employees",
label: "Users",
icon: Users,
submenus: [
{
href: routes.DASHBOARD_ROUTE,
label: "All Employees"
href: routes.USER_INDEX_PAGE,
label: "All Users"
},
]
},
{
href : '' ,
label : 'Assets' ,
label : 'Courses' ,
icon : Warehouse ,
submenus : [
{
label : 'All Assets' ,
href : routes.DASHBOARD_ROUTE
label : 'All Courses' ,
href : routes.COURSE_INDEX_PAGE
} ,
{
label : 'Categories' ,
href : routes.DASHBOARD_ROUTE
label : 'All Categories' ,
href : routes.CATEGORY_INDEX_PAGE
} ,
{
label : 'Usage',
href : routes.DASHBOARD_ROUTE
} ,
]
}
]
},
{
groupLabel: "Settings",
menus: [
// {
// href: "/users",
// label: "Users",
// icon: UserCog
// },
{
href: "/account",
label: "Account",
icon: Settings
}
]
}
];
}
Loading…
Cancel
Save