diff --git a/frontend/edu-connect/src/app/(admin)/admin/category/_partials/CategoryTable.tsx b/frontend/edu-connect/src/app/(admin)/admin/category/_partials/CategoryTable.tsx index 6a25b7d..33cac53 100644 --- a/frontend/edu-connect/src/app/(admin)/admin/category/_partials/CategoryTable.tsx +++ b/frontend/edu-connect/src/app/(admin)/admin/category/_partials/CategoryTable.tsx @@ -110,4 +110,4 @@ const CategoryTable: React.FC<{ ) } -export default CategoryTable \ No newline at end of file +export default CategoryTable diff --git a/frontend/edu-connect/src/app/(admin)/admin/course/page.tsx b/frontend/edu-connect/src/app/(admin)/admin/course/page.tsx index 3f840e6..71dcd68 100644 --- a/frontend/edu-connect/src/app/(admin)/admin/course/page.tsx +++ b/frontend/edu-connect/src/app/(admin)/admin/course/page.tsx @@ -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 ( diff --git a/frontend/edu-connect/src/app/_partials/CommunitySection.tsx b/frontend/edu-connect/src/app/_partials/CommunitySection.tsx index 69e4724..0647476 100644 --- a/frontend/edu-connect/src/app/_partials/CommunitySection.tsx +++ b/frontend/edu-connect/src/app/_partials/CommunitySection.tsx @@ -6,8 +6,10 @@ import useSWR from 'swr' const CommunitySection = () => { - const {data : AuthorCount} = useSWR(APP_BASE_URL+'' , defaultFetcher); - const { data : TotalCourses } = 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 + '/api/public/stats/total-courses' , defaultFetcher) + + console.log(AuthorCount , TotalCourses) return(
@@ -19,11 +21,11 @@ const CommunitySection = () => {

-

{}+

+

{AuthorCount?.totalAuthors}+

Expert Contributors

-

1000+

+

{TotalCourses?.totalCourses}+

Research-Based Courses

diff --git a/frontend/edu-connect/src/app/_partials/FeaturedCourses.tsx b/frontend/edu-connect/src/app/_partials/FeaturedCourses.tsx index b3d5b93..b017e53 100644 --- a/frontend/edu-connect/src/app/_partials/FeaturedCourses.tsx +++ b/frontend/edu-connect/src/app/_partials/FeaturedCourses.tsx @@ -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 (
@@ -86,18 +24,20 @@ const FeaturedCourses = () => {
{ - courses?.map((course : Record , index) => { + CourseList?.data?.length && CourseList?.data?.map((course : Record , index) => { return( - + + + ) }) } diff --git a/frontend/edu-connect/src/app/_partials/HeroSection.tsx b/frontend/edu-connect/src/app/_partials/HeroSection.tsx index 6888b7e..eb8d7bb 100644 --- a/frontend/edu-connect/src/app/_partials/HeroSection.tsx +++ b/frontend/edu-connect/src/app/_partials/HeroSection.tsx @@ -5,7 +5,8 @@ import Image from 'next/image'; import useSWR from "swr" 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 (
@@ -40,7 +41,7 @@ const HeroSection: React.FC = () => { />
-

{}+

+

{TotalUserCount?.totalUsers}+

Active Learners

diff --git a/frontend/edu-connect/src/app/auth/login/_partials/LoginForm.tsx b/frontend/edu-connect/src/app/auth/login/_partials/LoginForm.tsx index bf2a3dc..b7d959f 100644 --- a/frontend/edu-connect/src/app/auth/login/_partials/LoginForm.tsx +++ b/frontend/edu-connect/src/app/auth/login/_partials/LoginForm.tsx @@ -3,7 +3,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/com import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; 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 { routes } from '@/lib/routes'; import { useToast } from '@/hooks/use-toast'; @@ -82,8 +82,8 @@ export default function LoginForm() { },[router]) return ( -
- +
+ Login @@ -127,7 +127,14 @@ export default function LoginForm() { type="submit" className="w-full h-11 bg-purple-600 hover:bg-purple-700 text-white font-semibold" > - Login + { + loading ? + <> + + Login + + : 'Login' + }
diff --git a/frontend/edu-connect/src/app/courses/page.tsx b/frontend/edu-connect/src/app/courses/page.tsx new file mode 100644 index 0000000..a15be68 --- /dev/null +++ b/frontend/edu-connect/src/app/courses/page.tsx @@ -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( + + + + All Courses +
+ { + data?.data?.length && data?.data?.map((course) => { + return( + + ) + }) + } +
+
+
+
+ ) +} + +export default AllCourseList \ No newline at end of file diff --git a/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailHeroSection.tsx b/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailHeroSection.tsx index 129aa18..42b170e 100644 --- a/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailHeroSection.tsx +++ b/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailHeroSection.tsx @@ -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; + mutate : () => void } -const CourseDetailHeroSection: React.FC = ({courseData}) => { +const CourseDetailHeroSection: React.FC = ({ courseData , mutate }) => { + + console.log(courseData) + + const { user } = useContext(AuthContext) + const { toast } = useToast(); + + const [loading , setLoading] = useState(false) + + + const handleEnrollNow = async () : Promise => { + 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(
-

{courseData.title}

-

{courseData.description}

+

{courseData?.courseName}

+

{courseData?.courseDescription}

- + SJ
-
{courseData.instructor.name}
-
{courseData.instructor.title}
-
Published at {courseData?.lastUpdated}
+
{courseData?.instructor?.firstName}
+
{courseData?.instructor?.title}
+
Published at {courseData?.creationDate}
{/* course image */} +
Course Preview - +
+ { + courseData?.selfEnrollment?.isEnrolled && + + }
diff --git a/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailStats.tsx b/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailStats.tsx index 21e3280..aade949 100644 --- a/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailStats.tsx +++ b/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailStats.tsx @@ -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}> = ({courseData}) => { return( <>
@@ -13,7 +13,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
Duration
-
{courseData.duration}
+
{courseData?.duration ?? 0}
@@ -23,7 +23,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
Enrolled
-
{courseData.enrolledStudents} students
+
{courseData?.enrollmentSummary?.courseData ?? 0} students
@@ -33,7 +33,7 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
Reviews
-
{courseData.rating} ({courseData.totalReviews})
+
{courseData?.enrollmentSummary?.totalChats ?? 0}
@@ -42,8 +42,8 @@ const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) =>
-
Completion
-
{courseData.completionRate}%
+
Discussion group member
+
{courseData?.enrollmentSummary?.usersInChat ?? 0}
diff --git a/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDiscussion.tsx b/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDiscussion.tsx new file mode 100644 index 0000000..3c950b5 --- /dev/null +++ b/frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDiscussion.tsx @@ -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 => { + 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 = ({ courseUuid }) => { + const [comment, setComment] = useState(''); + const [replyingTo, setReplyingTo] = useState<{ commentId: number; userName: string } | null>(null); + const loadingRef = useRef(null); + const containerRef = useRef(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 ( +
+ Error loading discussions. Please try again later. +
+ ); + } + + return ( +
+ +
+ {allDiscussions.map((discussion) => ( + +
+
+ + + {discussion.user[0]} + +
+
+ {discussion.user} + {discussion.timestamp} +
+

{discussion.content}

+
+ +
+ + {discussion.replies.length} replies +
+
+
+
+ + {discussion.replies.length > 0 && ( +
+ {discussion.replies.map((reply) => ( +
+ + + {reply.user[0]} + +
+
+ {reply.user} + {reply.timestamp} +
+

{reply.content}

+
+
+ ))} +
+ )} +
+
+ ))} + +
+ {isLoadingMore && ( +
+ )} + {!hasMore && allDiscussions.length > 0 && ( +
No more discussions
+ )} +
+
+ + +
+
+ + + U + +
+ {replyingTo && ( +
+ Replying to {replyingTo.userName} + +
+ )} +