Compare commits

...

4 Commits

  1. 2
      frontend/edu-connect/src/app/(admin)/admin/course/page.tsx
  2. 10
      frontend/edu-connect/src/app/_partials/CommunitySection.tsx
  3. 80
      frontend/edu-connect/src/app/_partials/FeaturedCourses.tsx
  4. 5
      frontend/edu-connect/src/app/_partials/HeroSection.tsx
  5. 13
      frontend/edu-connect/src/app/auth/login/_partials/LoginForm.tsx
  6. 46
      frontend/edu-connect/src/app/courses/page.tsx
  7. 90
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailHeroSection.tsx
  8. 12
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailStats.tsx
  9. 300
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDiscussion.tsx
  10. 262
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseGenerateQuestions.tsx
  11. 59
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseIndividualContentWrapper.tsx
  12. 21
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseMedia.tsx
  13. 6
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseProgress.tsx
  14. 108
      frontend/edu-connect/src/app/courses/show/[id]/_partials/TryPDF.tsx
  15. 26
      frontend/edu-connect/src/app/courses/show/[id]/page.tsx
  16. 2
      frontend/edu-connect/src/app/my-courses/_partials/EnrolledCourseList.tsx
  17. 30
      frontend/edu-connect/src/app/my-courses/_partials/MyCoursesListTabContent.tsx
  18. 2
      frontend/edu-connect/src/app/my-courses/_partials/myCoursesTabWrapper.tsx
  19. 2
      frontend/edu-connect/src/components/(dashboard)/common/Sidebar/_partials/menu.tsx
  20. 6
      frontend/edu-connect/src/components/common/Header/header.tsx
  21. 26
      frontend/edu-connect/src/components/common/Navbar/desktopNav.tsx
  22. 22
      frontend/edu-connect/src/components/elements/CourseCard.tsx
  23. 1
      frontend/edu-connect/src/helpers/apiSchema/course.schema.ts
  24. 4
      frontend/edu-connect/src/lib/routes.ts
  25. 51
      frontend/edu-connect/src/utils/menu-list.tsx

@ -12,7 +12,7 @@ import useSWR from "swr"
import CourseTable from "./_partials/CourseTable" import CourseTable from "./_partials/CourseTable"
const CourseIndexPage = () => { 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); const { data, mutate, isLoading } = useSWR(CourseListURL, defaultFetcher);
return ( return (

@ -6,8 +6,10 @@ import useSWR from 'swr'
const CommunitySection = () => { const CommunitySection = () => {
const {data : AuthorCount} = useSWR(APP_BASE_URL+'' , defaultFetcher); const {data : AuthorCount} = useSWR(APP_BASE_URL+'/api/public/stats/total-authors' , defaultFetcher);
const { data : TotalCourses } = useSWR(APP_BASE_URL + '' , defaultFetcher) const { data : TotalCourses } = useSWR(APP_BASE_URL + '/api/public/stats/total-courses' , defaultFetcher)
console.log(AuthorCount , TotalCourses)
return( return(
<div className="bg-gray-50 border-t"> <div className="bg-gray-50 border-t">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
@ -19,11 +21,11 @@ const CommunitySection = () => {
</p> </p>
<div className="grid grid-cols-2 gap-4 text-center"> <div className="grid grid-cols-2 gap-4 text-center">
<div className="bg-white p-4 rounded-lg shadow-sm border border-purple-700"> <div className="bg-white p-4 rounded-lg shadow-sm border border-purple-700">
<p className="text-2xl font-bold text-purple-600">{}+</p> <p className="text-2xl font-bold text-purple-600">{AuthorCount?.totalAuthors}+</p>
<p className="text-gray-600">Expert Contributors</p> <p className="text-gray-600">Expert Contributors</p>
</div> </div>
<div className="bg-white p-4 rounded-lg shadow-sm border border-purple-700"> <div className="bg-white p-4 rounded-lg shadow-sm border border-purple-700">
<p className="text-2xl font-bold text-purple-600">1000+</p> <p className="text-2xl font-bold text-purple-600">{TotalCourses?.totalCourses}+</p>
<p className="text-gray-600">Research-Based Courses</p> <p className="text-gray-600">Research-Based Courses</p>
</div> </div>
</div> </div>

@ -3,75 +3,13 @@ import CourseCard from '@/components/elements/CourseCard';
import useSWR from 'swr'; import useSWR from 'swr';
import { APP_BASE_URL } from '@/utils/constants'; import { APP_BASE_URL } from '@/utils/constants';
import { defaultFetcher } from '@/helpers/fetch.helper'; import { defaultFetcher } from '@/helpers/fetch.helper';
import Link from 'next/link';
import { routes } from '@/lib/routes';
const FeaturedCourses = () => { const FeaturedCourses = () => {
const { data : CourseList } = useSWR(APP_BASE_URL + '' , defaultFetcher) const { data : CourseList } = useSWR(APP_BASE_URL + '/api/course/listAll' , defaultFetcher)
const courses = [
{ console.log(CourseList)
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'
}
];
return ( return (
<div className="max-w-7xl mx-auto p-6"> <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"> <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( return(
<Link href={routes.COURSE_INDIVIDUAL_PAGE.replace(':id' , course?.id)}>
<CourseCard <CourseCard
key={index} key={index}
id={course.id} id={course.id}
image ={course?.image} image ={APP_BASE_URL + course?.coverImage}
title ={course?.title} title ={course?.title}
category = {course?.category} category = {course?.category?.name}
lessons = {course?.lessons} lessons = {course?.lessons}
instructor = {course?.instructor} instructor = {course?.instructor}
duration={course?.duration} duration={course?.duration}
/> />
</Link>
) )
}) })
} }

@ -5,7 +5,8 @@ import Image from 'next/image';
import useSWR from "swr" import useSWR from "swr"
const HeroSection: React.FC = () => { const HeroSection: React.FC = () => {
const {data : TotalUserCount } = useSWR(APP_BASE_URL + '/api/user/count' , defaultFetcher) const {data : TotalUserCount } = useSWR(APP_BASE_URL + '/api/public/stats/total-users' , defaultFetcher)
console.log(TotalUserCount)
return ( return (
<div className="bg-gradient-to-br from-purple-900 to-indigo-900 text-white"> <div className="bg-gradient-to-br from-purple-900 to-indigo-900 text-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-24"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-24">
@ -40,7 +41,7 @@ const HeroSection: React.FC = () => {
/> />
</div> </div>
<div className="absolute -bottom-4 -right-4 bg-purple-700 text-white p-4 rounded-lg shadow-lg"> <div className="absolute -bottom-4 -right-4 bg-purple-700 text-white p-4 rounded-lg shadow-lg">
<p className="font-bold">{}+</p> <p className="font-bold">{TotalUserCount?.totalUsers}+</p>
<p className="text-sm">Active Learners</p> <p className="text-sm">Active Learners</p>
</div> </div>
</div> </div>

@ -3,7 +3,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/com
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { EyeIcon, EyeOffIcon } from 'lucide-react'; import { EyeIcon, EyeOffIcon, Loader } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { routes } from '@/lib/routes'; import { routes } from '@/lib/routes';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@ -82,8 +82,8 @@ export default function LoginForm() {
},[router]) },[router])
return ( return (
<div className="flex items-center justify-center min-h-[60vh] border border-purple-700/20 shadow shadow-purple-700/40"> <div className="flex items-center justify-center min-h-[60vh]">
<Card className="w-full max-w-md shadow-lg border-0"> <Card className="w-full max-w-md border border-purple-700/20 shadow shadow-purple-700/40">
<CardHeader className="space-y-3"> <CardHeader className="space-y-3">
<CardTitle className="text-2xl font-bold text-purple-600">Login</CardTitle> <CardTitle className="text-2xl font-bold text-purple-600">Login</CardTitle>
<CardDescription className="text-gray-600"> <CardDescription className="text-gray-600">
@ -127,7 +127,14 @@ export default function LoginForm() {
type="submit" type="submit"
className="w-full h-11 bg-purple-600 hover:bg-purple-700 text-white font-semibold" className="w-full h-11 bg-purple-600 hover:bg-purple-700 text-white font-semibold"
> >
{
loading ?
<>
<Loader />
Login Login
</>
: 'Login'
}
</Button> </Button>
<div className="text-center text-sm text-gray-600"> <div className="text-center text-sm text-gray-600">

@ -0,0 +1,46 @@
'use client'
import { PageHeading } from "@/components/(dashboard)/ui/title"
import CommonContainer from "@/components/elements/CommonContainer"
import CourseCard from "@/components/elements/CourseCard"
import AppContextProvider from "@/helpers/context/AppContextProvider"
import { defaultFetcher } from "@/helpers/fetch.helper"
import { APP_BASE_URL } from "@/utils/constants"
import CommonView from "@/views/CommonView"
import React from "react"
import useSWR from "swr"
const AllCourseList : React.FC = () => {
const { data } = useSWR(APP_BASE_URL + '/api/course/listAll' , defaultFetcher)
console.log(data)
return(
<AppContextProvider>
<CommonView>
<CommonContainer>
<PageHeading>All Courses</PageHeading>
<div className="grid grid-cols-4 gap-4 mt-8">
{
data?.data?.length && data?.data?.map((course) => {
return(
<CourseCard
id={course?.id}
image={APP_BASE_URL + course?.coverImage}
title={course?.name}
category={course?.category?.name}
instructor={{
image : APP_BASE_URL + course?.author?.pfpFilename ,
name :course?.author?.firstName
}}
/>
)
})
}
</div>
</CommonContainer>
</CommonView>
</AppContextProvider>
)
}
export default AllCourseList

@ -2,46 +2,112 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { CourseData } from "@/helpers/apiSchema/course.schema" 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 { 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( return(
<div className="bg-purple-700 text-white"> <div className="bg-purple-700 text-white">
<div className="container mx-auto px-4 py-12"> <div className="container mx-auto px-4 py-12">
<div className="grid md:grid-cols-2 gap-8 items-center"> <div className="grid md:grid-cols-2 gap-8 items-center">
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-3xl font-bold">{courseData.title}</h1> <h1 className="text-3xl font-bold">{courseData?.courseName}</h1>
<p className="text-blue-100">{courseData.description}</p> <p className="text-blue-100">{courseData?.courseDescription}</p>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Avatar className="h-12 w-12"> <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> <AvatarFallback>SJ</AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<div className="font-semibold">{courseData.instructor.name}</div> <div className="font-semibold">{courseData?.instructor?.firstName}</div>
<div className="text-sm text-blue-100">{courseData.instructor.title}</div> <div className="text-sm text-blue-100">{courseData?.instructor?.title}</div>
<div>Published at {courseData?.lastUpdated}</div> <div>Published at {courseData?.creationDate}</div>
</div> </div>
</div> </div>
</div> </div>
{/* course image */} {/* course image */}
<Card className="bg-white"> <Card className="bg-white">
<CardContent className="p-6 space-y-4"> <CardContent className="p-6 space-y-4">
<div className="relative max-h-124">
<img <img
src="https://placehold.co/600x400" src={ courseData?.coverImage ? (APP_BASE_URL + courseData?.coverImage) : "https://placehold.co/600x400"}
alt="Course Preview" 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"> </div>
Enroll Now {
courseData?.selfEnrollment?.isEnrolled &&
<Button className="w-full text-lg h-12 bg-purple-700" onClick={handleEnrollNow}>
{
loading ?
<>
<Loader />
Enrolling ..
</> : 'Enroll Now'
}
</Button> </Button>
}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

@ -3,7 +3,7 @@ import { CourseData } from "@/helpers/apiSchema/course.schema"
import { BookOpen, Clock, MessageSquare, Users } from "lucide-react" 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( return(
<> <>
<div className="mx-auto container py-8"> <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" /> <Clock className="h-8 w-8 text-blue-500" />
<div> <div>
<div className="text-sm text-gray-500">Duration</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> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -23,7 +23,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
<Users className="h-8 w-8 text-green-500" /> <Users className="h-8 w-8 text-green-500" />
<div> <div>
<div className="text-sm text-gray-500">Enrolled</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> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -33,7 +33,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
<MessageSquare className="h-8 w-8 text-purple-500" /> <MessageSquare className="h-8 w-8 text-purple-500" />
<div> <div>
<div className="text-sm text-gray-500">Reviews</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> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -42,8 +42,8 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
<CardContent className="p-4 flex items-center space-x-4"> <CardContent className="p-4 flex items-center space-x-4">
<BookOpen className="h-8 w-8 text-orange-500" /> <BookOpen className="h-8 w-8 text-orange-500" />
<div> <div>
<div className="text-sm text-gray-500">Completion</div> <div className="text-sm text-gray-500">Discussion group member</div>
<div className="font-semibold">{courseData.completionRate}%</div> <div className="font-semibold">{courseData?.enrollmentSummary?.usersInChat ?? 0}</div>
</div> </div>
</CardContent> </CardContent>
</Card> </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 { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Check, X } from "lucide-react"; 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 = () => { interface Course {
// Submitted questions history id: number;
const [submittedQuestions] = useState([ name: string;
{ description: string;
id: 1, }
question: "What is React's Virtual DOM?",
options: [ interface Creator {
"A complete DOM copy", id: number;
"A lightweight copy of the DOM", firstName: string;
"A browser feature", lastName: string;
"A routing system" username: string;
], pfpFilename: string;
selectedOption: 0, // User selected }
correctOption: 1, // Correct answer
}, interface QuizAttempt {
{ id: number;
id: 2, quizID: number;
question: "What hook manages side effects?", isActive: boolean;
options: [ creationDate: string;
"useState", quizAnswers: string[];
"useReducer", quizQuestion: string;
"useEffect", userAnswer: string | null;
"useContext" quizCorrectAnswer: string;
], isCorrect: boolean;
selectedOption: 2, course: Course;
correctOption: 2, creator: Creator;
}, }
]);
interface QuizGeneratorProps {
// Unsubmitted questions current_page: number;
const [pendingQuestions, setPendingQuestions] = useState([ course_uuid: string;
{ }
id: 1,
question: "Which of these is a state management library?", const QuizGenerator: React.FC<QuizGeneratorProps> = ({
options: ["Redux", "Axios", "Lodash", "Moment"], current_page,
selectedOption: null course_uuid ,
}, }) => {
{ const { data: completedQuizzes, error: completedError } = useSWR<any>(
id: 2, `${APP_BASE_URL}/api/quiz/get/allComplete?course_uuid=${course_uuid}`,
question: "What does CSS stand for?", defaultFetcher
options: [ );
"Computer Style Sheets",
"Cascading Style Sheets", const { toast } = useToast()
"Creative Style Sheets",
"Colorful Style Sheets" const { data: uncompletedQuizzes, error: uncompletedError , mutate } = useSWR<any>(
], `${APP_BASE_URL}/api/quiz/get/personalIncomplete?course_uuid=${course_uuid}`,
selectedOption: null defaultFetcher
}, );
]);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const handleOptionSelect = (value) => {
const updatedQuestions = [...pendingQuestions];
updatedQuestions[currentQuestionIndex].selectedOption = parseInt(value);
setPendingQuestions(updatedQuestions);
};
const generateQuiz = () => { const [currentQuestionIndex, setCurrentQuestionIndex] = useState<number>(0);
// Reset selections and shuffle pending questions const [pendingAnswers, setPendingAnswers] = useState<Record<number, string>>({});
const shuffledQuestions = [...pendingQuestions].map(q => ({
...q, const isLoading = !completedQuizzes || !uncompletedQuizzes;
selectedOption: null const isError = completedError || uncompletedError;
const handleOptionSelect = (quizId: number, value: string) => {
setPendingAnswers(prev => ({
...prev,
[quizId]: value
})); }));
setPendingQuestions(shuffledQuestions);
setCurrentQuestionIndex(0);
}; };
const handleNext = () => { const handleNext = () => {
if (currentQuestionIndex < pendingQuestions.length - 1) { if (uncompletedQuizzes && currentQuestionIndex < uncompletedQuizzes.length - 1) {
setCurrentQuestionIndex(prev => prev + 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 ( return (
<div className="py-6 container mx- mt-8"> <Card>
{/* Submitted Questions History */} <CardContent className="p-6 text-center text-gray-500">
<div className='grid grid-cols-3 space-x-6'> Loading quiz data...
<Card className="w-full col-span-2"> </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-auto mt-8">
<div className="grid grid-cols-3 gap-6">
<Card className="col-span-2">
<CardHeader> <CardHeader>
<CardTitle>Quiz History</CardTitle> <CardTitle>Quiz History</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ScrollArea className="h-[400px] pr-4"> <ScrollArea className="h-[400px] pr-4">
{submittedQuestions.map((q, index) => ( {completedQuizzes?.data?.map((quiz, index) => (
<div key={q.id} className="mb-4"> <div key={quiz.id} className="mb-4">
<div className="p-4 border rounded-lg border-gray-200"> <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"> <div className="text-sm space-y-2">
{q.options.map((option, i) => ( {quiz.quizAnswers.map((answer, i) => (
<div <div
key={i} key={i}
className={`p-2 rounded flex justify-between items-center ${ className={`p-2 rounded flex justify-between items-center ${
i === q.correctOption && i === q.selectedOption answer === quiz.quizCorrectAnswer && answer === quiz.userAnswer
? 'bg-green-100' ? 'bg-green-100'
: i === q.correctOption : answer === quiz.quizCorrectAnswer
? 'bg-green-50' ? 'bg-green-50'
: i === q.selectedOption : answer === quiz.userAnswer
? 'bg-red-50' ? 'bg-red-50'
: 'bg-gray-50' : 'bg-gray-50'
}`} }`}
> >
<span>{option}</span> <span>{answer}</span>
{i === q.correctOption && ( {answer === quiz.quizCorrectAnswer && (
<Check className="w-4 h-4 text-green-600" /> <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" /> <X className="w-4 h-4 text-red-600" />
)} )}
</div> </div>
))} ))}
</div> </div>
</div> </div>
{index < submittedQuestions.length - 1 && <Separator className="my-4" />} {index < completedQuizzes?.length - 1 && <Separator className="my-4" />}
</div> </div>
))} ))}
{
completedQuizzes?.data?.length === 0 && 'No Quiz taken yet'
}
</ScrollArea> </ScrollArea>
</CardContent> </CardContent>
</Card> </Card>
{/* Pending Questions Interface */} <div className="space-y-6">
<div className="w-full space-y-6"> {currentQuiz ? (
<Button
onClick={generateQuiz}
className="w-full bg-purple-700"
>
Generate Quiz
</Button>
{pendingQuestions.length > 0 ? (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
Question {currentQuestionIndex + 1} of {pendingQuestions.length} Question {currentQuestionIndex + 1} of {uncompletedQuizzes?.length}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<h3 className="text-lg font-medium"> <h3 className="text-lg font-medium">
{pendingQuestions[currentQuestionIndex].question} {currentQuiz?.quizQuestion}
</h3> </h3>
<RadioGroup <RadioGroup
value={pendingQuestions[currentQuestionIndex].selectedOption?.toString()} value={pendingAnswers[currentQuiz.id] || ''}
onValueChange={handleOptionSelect} 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"> <div key={i} className="flex items-center space-x-2">
<RadioGroupItem value={i.toString()} id={`option-${i}`} /> <RadioGroupItem value={answer} id={`option-${i}`} />
<Label htmlFor={`option-${i}`}>{option}</Label> <Label htmlFor={`option-${i}`}>{answer}</Label>
</div> </div>
))} ))}
</RadioGroup> </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 <Button
onClick={handleNext} onClick={handleNext}
disabled={currentQuestionIndex === pendingQuestions.length - 1} disabled={currentQuestionIndex === (uncompletedQuizzes?.length ?? 0) - 1}
className='bg-purple-700' className="bg-purple-700 hover:bg-purple-800"
> >
Submit Next
</Button> </Button>
</div> </div>
</div> </div>
@ -182,7 +238,9 @@ const QuizGenerator = () => {
) : ( ) : (
<Card> <Card>
<CardContent className="p-6 text-center text-gray-500"> <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> </CardContent>
</Card> </Card>
)} )}

@ -1,5 +1,5 @@
'use client'; 'use client';
import React from 'react'; import React, { useContext } from 'react';
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import CourseDetailHeroSection from './CourseDetailHeroSection'; import CourseDetailHeroSection from './CourseDetailHeroSection';
@ -7,49 +7,40 @@ import CourseDetailStats from './CourseDetailStats';
import CourseMedia from './CourseMedia'; import CourseMedia from './CourseMedia';
import CourseProgress from './CourseProgress'; import CourseProgress from './CourseProgress';
import TryPDF from './TryPDF'; 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 ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Hero Section */} {/* Hero Section */}
<CourseDetailHeroSection courseData={courseData}/> <CourseDetailHeroSection courseData={data?.data as CourseData} mutate={mutate}/>
{/* Course Stats */} {/* Course Stats */}
<CourseDetailStats courseData={courseData}/> <CourseDetailStats courseData={data?.data}/>
{
user && data?.data?.selfEnrollment?.isEnrolled && (
<CourseProgress courseData={data?.data}/>
)
}
<CourseProgress courseData={courseData}/>
{/* Course Content */} {/* Course Content */}
<CourseMedia courseData={courseData}/> <CourseMedia courseData={data?.data}/>
</div> </div>
); );

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

@ -3,15 +3,15 @@ import { Card, CardContent } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress"
import { CourseData } from "@/helpers/apiSchema/course.schema" import { CourseData } from "@/helpers/apiSchema/course.schema"
const CourseProgress :React.FC<{courseData : CourseData}> = ({courseData}) => { const CourseProgress :React.FC<{courseData : Record<string,any>}> = ({courseData}) => {
return( return(
<CommonContainer className="!px-0"> <CommonContainer className="!px-0">
<Card> <Card>
<CardContent className="py-6"> <CardContent className="py-6">
<h3 className="text-lg font-semibold mb-4">Your Progress</h3> <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"> <p className="text-sm text-gray-500 text-center">
{courseData.completionRate}% Complete {((courseData?.selfEnrollment?.maxPage) / courseData?.totalPages) * 100 }% Complete
</p> </p>
</CardContent> </CardContent>
</Card> </Card>

@ -1,97 +1,73 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import React, { useEffect, useState } from 'react'; import { APP_BASE_URL } from '@/utils/constants';
export default function CustomPDFViewer() { const CustomPDFViewer = ({
const [currentPage, setCurrentPage] = useState(1); pdfPages ,
const [totalPages, setTotalPages] = useState(0); currentPage ,
const [loading, setLoading] = useState(true); onChange
const [error, setError] = useState<string | null>(null); } : {
const [pageUrl, setPageUrl] = useState(''); pdfPages : Array<string>
currentPage : number
onChange : (value : any) => void
}) => {
// const [currentPage, setCurrentPage] = useState(0);
// Mock function to fetch PDF page as image URL // Your PDF pages array
// 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(() => { const totalPages = pdfPages?.length;
fetchPageImage(currentPage);
}, [currentPage]);
const goToPreviousPage = () => { const goToPreviousPage = () => {
if (currentPage > 1) { if (currentPage > 0) {
setCurrentPage(prev => prev - 1); onChange((prev : number) => prev - 1);
} }
}; };
const goToNextPage = () => { const goToNextPage = () => {
if (currentPage < totalPages) { if (currentPage < totalPages - 1) {
setCurrentPage(prev => prev + 1); onChange((prev : number) => prev + 1);
} }
}; };
return ( 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"> pdfPages && (
{loading ? ( <div className="flex flex-col items-center space-y-4">
<div className=" inset-0 flex items-center justify-center min-h-96"> {/* PDF Display */}
<div className="text-lg text-gray-600">Loading page {currentPage}...</div> <div className="w-full max-w-3xl border rounded-lg shadow-lg">
</div> <iframe
) : error ? ( src={APP_BASE_URL + pdfPages[currentPage]}
<div className="absolute inset-0 flex items-center justify-center"> className="w-full h-[800px]"
<div className="text-lg text-red-600">{error}</div> title={`PDF Page ${currentPage + 1}`}
</div>
) : (
<img
src={pageUrl}
alt={`Page ${currentPage}`}
className="w-full h-auto object-contain rounded-lg"
/> />
)}
</div> </div>
{/* Navigation Controls */} {/* Navigation Controls */}
<div className="flex items-center gap-4 mt-6"> <div className="flex items-center space-x-4">
<Button <Button
onClick={goToPreviousPage} onClick={goToPreviousPage}
disabled={currentPage <= 1 || loading} disabled={currentPage === 0}
className={`px-4 py-2 rounded-lg ${ variant="outline"
currentPage <= 1 || loading
? 'bg-purple-700/50 cursor-not-allowed'
: 'bg-purple-700 hover:bg-purple-500 text-white'
} transition-colors`}
> >
Previous Previous
</Button> </Button>
<span className="text-sm">
<div className="text-lg font-medium"> Page {currentPage + 1} of {totalPages}
Page {currentPage} of {totalPages} </span>
</div>
<Button <Button
onClick={goToNextPage} onClick={goToNextPage}
disabled={currentPage >= totalPages || loading} disabled={currentPage === totalPages - 1}
className={`px-4 py-2 rounded-lg ${ variant="outline"
currentPage >= totalPages || loading
? 'bg-purple-700/50 cursor-not-allowed'
: 'bg-purple-700 hover:bg-purple-500 text-white'
} transition-colors`}
> >
Next Next
</Button> </Button>
</div> </div>
</div> </div>
)
}
</>
); );
} };
export default CustomPDFViewer;

@ -3,13 +3,33 @@
import AppContextProvider from "@/helpers/context/AppContextProvider" import AppContextProvider from "@/helpers/context/AppContextProvider"
import CommonView from "@/views/CommonView" import CommonView from "@/views/CommonView"
import CourseIndividualContentWrapper from "./_partials/CourseIndividualContentWrapper" 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( return(
<AppContextProvider> <AppContextProvider>
<CommonView> <CommonView>
<CourseIndividualContentWrapper course_uuid={id}/>
<CourseIndividualContentWrapper />
</CommonView> </CommonView>
</AppContextProvider> </AppContextProvider>
) )

@ -5,7 +5,7 @@ import Image from "next/image"
import useSWR from "swr" import useSWR from "swr"
const EnrolledCourseTabContent : React.FC = () => { const EnrolledCourseTabContent : React.FC = () => {
const { data } = useSWR(APP_BASE_URL + '/' , defaultFetcher); const { data } = useSWR(APP_BASE_URL + '/api/course/enrolled' , defaultFetcher);
return ( return (
<> <>

@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { defaultFetcher } from "@/helpers/fetch.helper" import { defaultFetcher } from "@/helpers/fetch.helper"
import { APP_BASE_URL } from "@/utils/constants" import { APP_BASE_URL } from "@/utils/constants"
@ -5,7 +6,8 @@ import Image from "next/image"
import useSWR from "swr" import useSWR from "swr"
const MyCoursesListTabContent = () => { const MyCoursesListTabContent = () => {
const { data } = useSWR(APP_BASE_URL + '/' , defaultFetcher); const { data } = useSWR(APP_BASE_URL + '/api/course/myCourses' , defaultFetcher);
console.log(data)
return ( return (
<> <>
@ -20,7 +22,7 @@ const MyCoursesListTabContent = () => {
<> <>
<Card <Card
key={course.id} key={course.id}
className="hover:shadow-lg transition-shadow" className="hover:shadow-lg transition-shadow mb-4"
> >
<CardContent className="p-6 flex gap-4"> <CardContent className="p-6 flex gap-4">
<div > <div >
@ -30,7 +32,7 @@ const MyCoursesListTabContent = () => {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-xl font-semibold text-purple-700"> <h3 className="text-xl font-semibold text-purple-700">
{course.title} {course.name}
</h3> </h3>
<p className="text-gray-600">{course.description}</p> <p className="text-gray-600">{course.description}</p>
</div> </div>
@ -38,24 +40,34 @@ const MyCoursesListTabContent = () => {
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500"> <div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500">
<span className="flex items-center"> <span className="flex items-center">
<span className="font-medium">Published:</span> <span className="ml-1">{
<span className="ml-1">{course.publishedDate}</span> course.publishedDate == '1'
? <Badge variant={'success'}>Published</Badge>
: course.publishedDate == '1'
? <Badge variant={'default'}>Pending</Badge>
: <Badge variant={'destructive'}>Rejected</Badge>
}
</span>
</span> </span>
</div>
<div className="mt-2 flex gap-4">
<span className="flex items-center"> <span className="flex items-center">
<span className="font-medium">Enrolled:</span> <span className="font-medium">Enrolled:</span>
<span className="ml-1"> <span className="ml-1">
{course.enrolledStudent} students {course.totalEnrolled ?? 0} students
</span> </span>
</span> </span>
<span className="flex items-center"> <span className="flex items-center">
<span className="font-medium">Active Discussions:</span> <span className="font-medium"></span>
<span className="ml-1"> <span className="ml-1">
{course.activeDiscussionCount} {course.activeDiscussionCount ?? 0} discussions
</span> </span>
</span> </span>
<span className="flex items-center"> <span className="flex items-center">
<span className="font-medium">Published by:</span> <span className="font-medium">Published by:</span>
<span className="ml-1">{course.publishedBy}</span> <span className="ml-1">{course?.author?.firstName ?? '-'}</span>
</span> </span>
</div> </div>
</div> </div>

@ -20,7 +20,7 @@ const MyCoursesWrapper = () => {
value="enrolled" value="enrolled"
className="justify-start w-full px-3 py-2 text-left data-[state=active]:bg-gray-50 data-[state=active]:border data-[state=active]:border-purple-700/50 rounded-md" className="justify-start w-full px-3 py-2 text-left data-[state=active]:bg-gray-50 data-[state=active]:border data-[state=active]:border-purple-700/50 rounded-md"
> >
Enrolled Course List Enrolled Course
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="action-form" value="action-form"

@ -32,7 +32,7 @@ export function Menu({ isOpen }: MenuProps) {
return ( return (
<ScrollArea className="[&>div>div[style]]:!block"> <ScrollArea className="[&>div>div[style]]:!block">
<nav className="mt-8 h-full w-full"> <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) => ( {menuList.map(({ groupLabel, menus }, index) => (
<li className={cn("w-full", groupLabel ? "pt-3" : "")} key={index}> <li className={cn("w-full", groupLabel ? "pt-3" : "")} key={index}>
{(isOpen && groupLabel) || isOpen === undefined ? ( {(isOpen && groupLabel) || isOpen === undefined ? (

@ -26,12 +26,12 @@ const Header = () => {
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex items-center justify-between py-4"> <div className="flex items-center justify-between py-4">
{/* Logo */} {/* Logo */}
<div className='flex gap-2 items-center'> <Link href={routes.INDEX_PAGE} className='flex gap-2 items-center'>
<Image src={BrandLogo} width={50} height={30} alt={'brand_logo'} className='bg-blend-darken'/> <Image src={BrandLogo} width={50} height={30} alt={'brand_logo'} className='bg-blend-darken'/>
<h1 className="brand-logo text-purple-700 font-bold text-2xl"> <h1 className="brand-logo text-purple-700 font-bold text-2xl">
EDU CONNECT EduCONNECT
</h1> </h1>
</div> </Link>
<SearchBar /> <SearchBar />
<div className='flex gap-8'> <div className='flex gap-8'>
<DesktopNav /> <DesktopNav />

@ -2,35 +2,16 @@ import React from 'react';
import Link from "next/link"; import Link from "next/link";
import { NavigationMenu,NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; import { NavigationMenu,NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
import { Book, Home, User } from "lucide-react"; import { Book, Home, User } from "lucide-react";
import { routes } from '@/lib/routes';
export const DesktopNav = () => { export const DesktopNav = () => {
return ( return (
<div className="hidden md:block"> <div className="hidden md:block">
<NavigationMenu> <NavigationMenu>
<NavigationMenuList> <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> <NavigationMenuItem>
<Link href="/docs" legacyBehavior passHref> <Link href={routes.INDEX_PAGE} legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}> <NavigationMenuLink className={navigationMenuTriggerStyle()}>
<Home className="mr-2 h-4 w-4" /> <Home className="mr-2 h-4 w-4" />
Home Home
@ -39,7 +20,7 @@ export const DesktopNav = () => {
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<Link href="/profile" legacyBehavior passHref> <Link href={routes.COURSE_LIST_INDEX} legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}> <NavigationMenuLink className={navigationMenuTriggerStyle()}>
<Book className="mr-2 h-4 w-4" /> <Book className="mr-2 h-4 w-4" />
All Courses All Courses
@ -47,6 +28,7 @@ export const DesktopNav = () => {
</Link> </Link>
</NavigationMenuItem> </NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</div> </div>
); );

@ -2,18 +2,19 @@ import { BookOpen, Clock, Heart } from "lucide-react"
import { Card, CardContent } from "../ui/card" import { Card, CardContent } from "../ui/card"
import Link from "next/link" import Link from "next/link"
import { routes } from "@/lib/routes" import { routes } from "@/lib/routes"
import { APP_BASE_URL } from "@/utils/constants"
interface CourseInterface { interface CourseInterface {
id : string , id : string ,
image :string | null image :string | null
title : string title : string
category : string category ?: string
lessons : string lessons ?: string
duration : string duration? : string
instructor : { instructor ?: {
image : string pfpFilename : string
name : string firstName : string
} }
} }
@ -28,6 +29,8 @@ const CourseCard : React.FC<CourseInterface> = ({
}) => { }) => {
return( return(
<Card key={id} className="overflow-hidden"> <Card key={id} className="overflow-hidden">
<Link href={routes?.COURSE_INDIVIDUAL_PAGE.replace(':id',id)}>
<div className="relative"> <div className="relative">
<img <img
@ -42,6 +45,7 @@ const CourseCard : React.FC<CourseInterface> = ({
<Heart className="w-4 h-4 fill-red-500 text-red-500" /> <Heart className="w-4 h-4 fill-red-500 text-red-500" />
</button> */} </button> */}
</div> </div>
</Link>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex gap-4 text-sm text-gray-600 mb-3"> <div className="flex gap-4 text-sm text-gray-600 mb-3">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -57,13 +61,13 @@ const CourseCard : React.FC<CourseInterface> = ({
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<img <img
src={instructor?.image} src={APP_BASE_URL + instructor?.pfpFilename}
alt={instructor?.name} alt={instructor?.firstName}
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
/> />
<span className="text-sm"> <span className="text-sm">
<Link href={routes.PROFILE_ROUTE}> <Link href={routes.PROFILE_ROUTE}>
{instructor?.name} {instructor?.firstName}
</Link> </Link>
</span> </span>
</div> </div>

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

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

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