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 d40abfd..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 @@ -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 + isLoading: boolean +}> = ({ + mutate, + Data, + isLoading + }) => { + + const columns: ColumnDef[] = [ + { + accessorKey: "sn", + header: "SN", + cell: ({ row }) => ( +
{row.index + 1}
+ ), + }, + { + id: 'name', + accessorFn: (row: any) => row.original?.name, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+

{row.original?.name}

+
+ ), + }, + { + id: 'description', + accessorFn: (row: any) => row.original?.description, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.original?.description ?? '-'}
+ ), + }, + { + id: 'creationDate', + accessorFn: (row: any) => row.original?.creationDate, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original?.creationDate ? new Date(row.original.creationDate).toLocaleDateString() : '-'} +
+ ), + }, + { + id: 'isActive', + accessorFn: (row: any) => row.original?.isActive, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.original?.isActive ? Active : Deactive}
+ ), + }, + ] + + return( + <> + + + ) } -export default Categ \ 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/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/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} + +
+ )} +