Compare commits

..

2 Commits

  1. 19
      frontend/edu-connect/next.config.mjs
  2. 628
      frontend/edu-connect/package-lock.json
  3. 2
      frontend/edu-connect/package.json
  4. 53
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailHeroSection.tsx
  5. 56
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseDetailStats.tsx
  6. 58
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseIndividualContentWrapper.tsx
  7. 39
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseMedia.tsx
  8. 149
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CoursePDFHolder.tsx
  9. 22
      frontend/edu-connect/src/app/courses/show/[id]/_partials/CourseProgress.tsx
  10. 97
      frontend/edu-connect/src/app/courses/show/[id]/_partials/TryPDF.tsx
  11. BIN
      frontend/edu-connect/src/app/courses/show/[id]/_partials/sample.pdf
  12. 18
      frontend/edu-connect/src/app/courses/show/[id]/page.tsx
  13. 6
      frontend/edu-connect/src/app/my-courses/_partials/CoursesActionModal.tsx
  14. 9
      frontend/edu-connect/src/app/my-courses/_partials/myCoursesTabWrapper.tsx
  15. 98
      frontend/edu-connect/src/app/search/_partials/SearchFeature.tsx
  16. 16
      frontend/edu-connect/src/app/search/page.tsx
  17. 10
      frontend/edu-connect/src/components/common/Header/_partials/searchBar.tsx
  18. 176
      frontend/edu-connect/src/components/elements/DiscussionForm.tsx
  19. 4
      frontend/edu-connect/src/components/ui/progress.tsx
  20. 24
      frontend/edu-connect/src/helpers/apiSchema/course.schema.ts
  21. 0
      frontend/edu-connect/src/helpers/apiSchema/user.schema.ts
  22. 2
      frontend/edu-connect/src/helpers/context/AuthProvider.tsx

@ -1,4 +1,21 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = {}; const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**',
port: '',
pathname: '**',
search: '',
},
],
},
webpack: (config) => {
config.resolve.alias.canvas = false;
return config;
},
};
export default nextConfig; export default nextConfig;

File diff suppressed because it is too large Load Diff

@ -23,10 +23,12 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.471.0", "lucide-react": "^0.471.0",
"next": "14.2.23", "next": "14.2.23",
"pdfjs-dist": "^4.8.69",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-pageflip": "^2.0.3", "react-pageflip": "^2.0.3",
"react-pdf": "^9.2.1", "react-pdf": "^9.2.1",
"swr": "^2.3.0",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },

@ -0,0 +1,53 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { CourseData } from "@/helpers/apiSchema/course.schema"
interface CourseDetailHeroSectionProps {
courseData: CourseData;
}
const CourseDetailHeroSection: React.FC<CourseDetailHeroSectionProps> = ({courseData}) => {
return(
<div className="bg-purple-700 text-white">
<div className="container mx-auto px-4 py-12">
<div className="grid md:grid-cols-2 gap-8 items-center">
<div className="space-y-4">
<h1 className="text-3xl font-bold">{courseData.title}</h1>
<p className="text-blue-100">{courseData.description}</p>
<div className="flex items-center space-x-4">
<Avatar className="h-12 w-12">
<AvatarImage src={courseData.instructor.avatar} className='aspect-1/1'/>
<AvatarFallback>SJ</AvatarFallback>
</Avatar>
<div>
<div className="font-semibold">{courseData.instructor.name}</div>
<div className="text-sm text-blue-100">{courseData.instructor.title}</div>
<div>Published at {courseData?.lastUpdated}</div>
</div>
</div>
</div>
{/* course image */}
<Card className="bg-white">
<CardContent className="p-6 space-y-4">
<img
src="https://placehold.co/600x400"
alt="Course Preview"
className="w-full rounded-lg"
/>
<Button className="w-full text-lg h-12 bg-purple-700">
Enroll Now
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
export default CourseDetailHeroSection

@ -0,0 +1,56 @@
import { Card, CardContent } from "@/components/ui/card"
import { CourseData } from "@/helpers/apiSchema/course.schema"
import { BookOpen, Clock, MessageSquare, Users } from "lucide-react"
const CourseDetailStats :React.FC<{courseData : CourseData}> = ({courseData}) => {
return(
<>
<div className="mx-auto container py-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 flex items-center space-x-4">
<Clock className="h-8 w-8 text-blue-500" />
<div>
<div className="text-sm text-gray-500">Duration</div>
<div className="font-semibold">{courseData.duration}</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center space-x-4">
<Users className="h-8 w-8 text-green-500" />
<div>
<div className="text-sm text-gray-500">Enrolled</div>
<div className="font-semibold">{courseData.enrolledStudents} students</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center space-x-4">
<MessageSquare className="h-8 w-8 text-purple-500" />
<div>
<div className="text-sm text-gray-500">Reviews</div>
<div className="font-semibold">{courseData.rating} ({courseData.totalReviews})</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center space-x-4">
<BookOpen className="h-8 w-8 text-orange-500" />
<div>
<div className="text-sm text-gray-500">Completion</div>
<div className="font-semibold">{courseData.completionRate}%</div>
</div>
</CardContent>
</Card>
</div>
</div>
</>
)
}
export default CourseDetailStats

@ -0,0 +1,58 @@
'use client';
import React from 'react';
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import CourseDetailHeroSection from './CourseDetailHeroSection';
import CourseDetailStats from './CourseDetailStats';
import CourseMedia from './CourseMedia';
import CourseProgress from './CourseProgress';
import TryPDF from './TryPDF';
const CourseIndividualContentWrapper : React.FC = () => {
// Dummy course data
const courseData = {
title: "Advanced JavaScript Programming",
instructor: {
name: "Dr. Sarah Johnson",
title: "Senior Software Engineer",
avatar: "https://placehold.co/600x400"
},
description: "Master modern JavaScript concepts including ES6+, async programming, and advanced frameworks. This comprehensive course covers everything from basic concepts to advanced topics in JavaScript development.",
category: "Programming",
duration: "12 weeks",
enrolledStudents: 1234,
rating: 4.8,
totalReviews: 456,
completionRate: 85,
lastUpdated: "2024-01-10",
chapters: [
{ title: "Introduction to ES6+", duration: "2.5 hours", isCompleted: true },
{ title: "Async Programming", duration: "3 hours", isCompleted: true },
{ title: "Advanced DOM Manipulation", duration: "2 hours", isCompleted: false },
{ title: "Modern Frameworks Overview", duration: "4 hours", isCompleted: false }
],
requirements: [
"Basic understanding of JavaScript",
"Familiarity with web development concepts",
"Node.js installed on your computer"
]
};
return (
<div className="min-h-screen bg-gray-50">
{/* Hero Section */}
<CourseDetailHeroSection courseData={courseData}/>
{/* Course Stats */}
<CourseDetailStats courseData={courseData}/>
<CourseProgress courseData={courseData}/>
{/* Course Content */}
<CourseMedia courseData={courseData}/>
</div>
);
};
export default CourseIndividualContentWrapper;

@ -0,0 +1,39 @@
import { Card, CardContent } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
import { CourseData } from "@/helpers/apiSchema/course.schema"
import PDFFlipBook from "./CoursePDFHolder"
import CustomPDFViewer from "./TryPDF"
import DiscussionSection from "@/components/elements/DiscussionForm"
const CourseMedia : React.FC<{courseData : CourseData}> = ({courseData}) => {
return(
<div className="container mx-auto py-8">
<div className="grid md:grid-cols-3 gap-8">
{/* Main Content */}
<div className="md:col-span-2 space-y-8">
<Card>
<CardContent className="p-6">
<h2 className="text-xl font-semibold mb-4">Course Content</h2>
<CustomPDFViewer />
</CardContent>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-4">Discussions</h3>
<DiscussionSection />
</CardContent>
</Card>
</div>
</div>
</div>
)
}
export default CourseMedia

@ -0,0 +1,149 @@
'use client'
import React, { forwardRef, useState, useEffect } from 'react';
import HTMLFlipBook from 'react-pageflip';
import { pdfjs, Document, Page as ReactPdfPage } from 'react-pdf';
// Configure PDF.js worker globally
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
interface PageProps {
pageNumber: number;
width: number;
}
interface PDFFlipBookProps {
pdfUrl: string;
width?: number;
height?: number;
numPages?: number;
}
const DEFAULT_WIDTH = 300;
const DEFAULT_HEIGHT = 424;
const Page = forwardRef<HTMLDivElement, PageProps>(({ pageNumber, width }, ref) => {
const [pageError, setPageError] = useState<string | null>(null);
return (
<div ref={ref} style={{ background: 'white' }}>
<ReactPdfPage
pageNumber={pageNumber}
width={width}
loading={
<div className="flex items-center justify-center p-4">
<div>Loading page {pageNumber}...</div>
</div>
}
error={
pageError && (
<div className="text-red-500 p-4">
Error loading page {pageNumber}: {pageError}
</div>
)
}
onLoadError={(error) => setPageError(error.message)}
/>
</div>
);
});
Page.displayName = 'Page';
const PDFFlipBook: React.FC<PDFFlipBookProps> = ({
pdfUrl,
width = DEFAULT_WIDTH,
height = DEFAULT_HEIGHT,
numPages = 3,
}) => {
const [totalPages, setTotalPages] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
console.log('PDF loaded successfully with', numPages, 'pages');
setTotalPages(numPages);
setIsLoading(false);
};
const onDocumentLoadError = (error: Error) => {
console.error('Error loading PDF:', error);
setError(error.message);
setIsLoading(false);
};
const LoadingComponent = () => (
<div className="flex items-center justify-center p-4">
<div className="text-center">
<div className="text-lg font-medium">Loading PDF...</div>
<div className="text-sm text-gray-500">Please wait</div>
</div>
</div>
);
const ErrorComponent = ({ message }: { message: string }) => (
<div className="flex items-center justify-center p-4">
<div className="text-center text-red-500">
<div className="text-lg font-medium">Error loading PDF</div>
<div className="text-sm">{message}</div>
</div>
</div>
);
if (error) {
return <ErrorComponent message={error} />;
}
return (
<Document
file={pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
loading={<LoadingComponent />}
error={<ErrorComponent message="Failed to load PDF" />}
options={{
cMapUrl: 'cmaps/',
cMapPacked: true,
standardFontDataUrl: 'standard_fonts/',
}}
>
{isLoading ? (
<LoadingComponent />
) : (
<HTMLFlipBook
style={{}}
width={width}
height={height}
size="fixed"
minWidth={0}
maxWidth={0}
minHeight={0}
maxHeight={0}
drawShadow={true}
flippingTime={1000}
usePortrait={true}
startZIndex={0}
autoSize={true}
maxShadowOpacity={1}
showCover={false}
mobileScrollSupport={true}
className="pdf-flipbook"
startPage={0}
swipeDistance={0}
showPageCorners={true}
disableFlipByClick={false}
clickEventForward={true}
useMouseEvents={true}
>
{Array.from(new Array(Math.min(totalPages, numPages)), (_, index) => (
<Page key={index + 1} pageNumber={index + 1} width={width} />
))}
</HTMLFlipBook>
)}
</Document>
);
};
export default PDFFlipBook;

@ -0,0 +1,22 @@
import CommonContainer from "@/components/elements/CommonContainer"
import { Card, CardContent } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
import { CourseData } from "@/helpers/apiSchema/course.schema"
const CourseProgress :React.FC<{courseData : CourseData}> = ({courseData}) => {
return(
<CommonContainer className="!px-0">
<Card>
<CardContent className="py-6">
<h3 className="text-lg font-semibold mb-4">Your Progress</h3>
<Progress value={courseData.completionRate} className="mb-2 " />
<p className="text-sm text-gray-500 text-center">
{courseData.completionRate}% Complete
</p>
</CardContent>
</Card>
</CommonContainer>
)
}
export default CourseProgress

@ -0,0 +1,97 @@
import { Button } from '@/components/ui/button';
import React, { useEffect, useState } from 'react';
export default function CustomPDFViewer() {
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pageUrl, setPageUrl] = useState('');
// Mock function to fetch PDF page as image URL
// In a real implementation, you would use a backend service to convert PDF pages to images
const fetchPageImage = async (pageNumber : number) => {
setLoading(true);
try {
// Simulating API call delay
await new Promise(resolve => setTimeout(resolve, 500));
// Using placeholder image for demonstration
setPageUrl(`https://placehold.co/600x400?text=Page ${pageNumber}`);
setTotalPages(5); // Mock total pages
setLoading(false);
} catch (err) {
setError('Failed to load PDF page');
setLoading(false);
}
};
useEffect(() => {
fetchPageImage(currentPage);
}, [currentPage]);
const goToPreviousPage = () => {
if (currentPage > 1) {
setCurrentPage(prev => prev - 1);
}
};
const goToNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(prev => prev + 1);
}
};
return (
<div className="flex flex-col items-center w-full max-w-3xl mx-auto p-4">
{/* Page Display */}
<div className="relative w-full h-auto bg-gray-100 rounded-lg shadow-lg mb-4">
{loading ? (
<div className=" inset-0 flex items-center justify-center min-h-96">
<div className="text-lg text-gray-600">Loading page {currentPage}...</div>
</div>
) : error ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-lg text-red-600">{error}</div>
</div>
) : (
<img
src={pageUrl}
alt={`Page ${currentPage}`}
className="w-full h-auto object-contain rounded-lg"
/>
)}
</div>
{/* Navigation Controls */}
<div className="flex items-center gap-4 mt-6">
<Button
onClick={goToPreviousPage}
disabled={currentPage <= 1 || loading}
className={`px-4 py-2 rounded-lg ${
currentPage <= 1 || loading
? 'bg-purple-700/50 cursor-not-allowed'
: 'bg-purple-700 hover:bg-purple-500 text-white'
} transition-colors`}
>
Previous
</Button>
<div className="text-lg font-medium">
Page {currentPage} of {totalPages}
</div>
<Button
onClick={goToNextPage}
disabled={currentPage >= totalPages || loading}
className={`px-4 py-2 rounded-lg ${
currentPage >= totalPages || loading
? 'bg-purple-700/50 cursor-not-allowed'
: 'bg-purple-700 hover:bg-purple-500 text-white'
} transition-colors`}
>
Next
</Button>
</div>
</div>
);
}

@ -0,0 +1,18 @@
'use client'
import AppContextProvider from "@/helpers/context/AppContextProvider"
import CommonView from "@/views/CommonView"
import CourseIndividualContentWrapper from "./_partials/CourseIndividualContentWrapper"
const CourseDetailPage : React.FC = () => {
return(
<AppContextProvider>
<CommonView>
<CourseIndividualContentWrapper />
</CommonView>
</AppContextProvider>
)
}
export default CourseDetailPage

@ -105,10 +105,8 @@ const CourseActionFormTabContent :React.FC = () => {
</div> </div>
{/* Submit Button */} {/* Submit Button */}
<div className="pt-4"> <div className="mx-auto w-fit">
<Button type="submit" className="w-full"> <Button className="mt-6 w-full md:min-w-[600px] min-w-[300px] bg-purple-700">Save Changes</Button>
Create Course
</Button>
</div> </div>
</form> </form>
</CardContent> </CardContent>

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import MyCoursesActionContent from './MyCoursesTabContent';
import CourseActionFormTabContent from './CoursesActionModal'; import CourseActionFormTabContent from './CoursesActionModal';
const MyCoursesWrapper = () => { const MyCoursesWrapper = () => {
@ -9,17 +8,17 @@ const MyCoursesWrapper = () => {
<Tabs defaultValue="general" className="flex gap-6 min-h-[50vh]"> <Tabs defaultValue="general" className="flex gap-6 min-h-[50vh]">
<div className="min-w-[240px]"> <div className="min-w-[240px]">
<TabsList className="flex flex-col h-auto bg-transparent p-0"> <TabsList className="flex flex-col h-auto bg-transparent p-0">
<TabsTrigger <TabsTrigger
value="generali" value="generali"
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 "
> >
Profile Course action
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="email" value="email"
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"
> >
Account Course List
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="password" value="password"
@ -35,8 +34,6 @@ const MyCoursesWrapper = () => {
{/* <MyCoursesActionContent /> */} {/* <MyCoursesActionContent /> */}
<CourseActionFormTabContent /> <CourseActionFormTabContent />
</TabsContent> </TabsContent>
{/* <UserEmailUpdateTabContent />
<UserPasswordUpdateTabContent /> */}
</div> </div>
</Tabs> </Tabs>
</div> </div>

@ -0,0 +1,98 @@
// src/components/DiscussionList.tsx
"use client";
import React, { useState } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { Card, CardContent } from "@/components/ui/card";
const SearchFeature: React.FC = () => {
const [searchQuery, setSearchQuery] = useState("");
const searchParams = useSearchParams();
const discussions = [
{
id: 1,
title: "Introduction to Computer Science",
description:
"Learn the basics of programming and computer science concepts",
publishedDate: "2025-01-10",
enrolledStudent: 156,
activeDiscussionCount: 23,
publishedBy: "Prof. Smith",
},
{
id: 2,
title: "Advanced Mathematics",
description: "Covering calculus, linear algebra and statistics",
publishedDate: "2025-01-09",
enrolledStudent: 89,
activeDiscussionCount: 15,
publishedBy: "Dr. Johnson",
},
{
id: 3,
title: "Digital Marketing Fundamentals",
description: "Understanding modern marketing strategies",
publishedDate: "2025-01-08",
enrolledStudent: 234,
activeDiscussionCount: 45,
publishedBy: "Prof. Williams",
},
];
return (
<div className="container mx-auto p-6 space-y-6">
{/* Discussion Cards */}
<div className="space-y-4">
{discussions.map((discussion) => (
<Card
key={discussion.id}
className="hover:shadow-lg transition-shadow"
>
<CardContent className="p-6 flex gap-4">
<div >
<Image src="https://placehold.jp/400x250.png"alt="placeholder" width={400} height={250} className="w-[400] h-auto rounded-2xl"/>
</div>
<div>
<div className="flex items-start justify-between">
<div className="space-y-2">
<h3 className="text-xl font-semibold text-purple-700">
{discussion.title}
</h3>
<p className="text-gray-600">{discussion.description}</p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500">
<span className="flex items-center">
<span className="font-medium">Published:</span>
<span className="ml-1">{discussion.publishedDate}</span>
</span>
<span className="flex items-center">
<span className="font-medium">Enrolled:</span>
<span className="ml-1">
{discussion.enrolledStudent} students
</span>
</span>
<span className="flex items-center">
<span className="font-medium">Active Discussions:</span>
<span className="ml-1">
{discussion.activeDiscussionCount}
</span>
</span>
<span className="flex items-center">
<span className="font-medium">Published by:</span>
<span className="ml-1">{discussion.publishedBy}</span>
</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default SearchFeature;

@ -0,0 +1,16 @@
'use client'
import AppContextProvider from "@/helpers/context/AppContextProvider"
import SearchFeature from "./_partials/SearchFeature"
import CommonView from "@/views/CommonView"
const SearchFeatureIndexPage : React.FC = () => {
return(
<AppContextProvider>
<CommonView>
<SearchFeature />
</CommonView>
</AppContextProvider>
)
}
export default SearchFeatureIndexPage

@ -1,18 +1,24 @@
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Search } from "lucide-react"; import { Search } from "lucide-react";
import { useState } from "react"; import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
const SearchBar = () => { const SearchBar = () => {
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
const searchParams = useSearchParams()
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Handle search logic here
console.log('Searching for:', searchQuery); console.log('Searching for:', searchQuery);
}; };
useEffect(() => {
setSearchQuery(searchParams?.get('query') ?? '')
},[])
return( return(
<> <>
<form <form

@ -4,10 +4,11 @@ import { Button } from "@/components/ui/button";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { MessageCircle, ThumbsUp, Reply } from "lucide-react"; import { MessageCircle, ThumbsUp, Reply, X } from "lucide-react";
const DiscussionSection = () => { const DiscussionSection = () => {
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [replyingTo, setReplyingTo] = useState(null);
const [discussions, setDiscussions] = useState([ const [discussions, setDiscussions] = useState([
{ {
id: 1, id: 1,
@ -15,7 +16,15 @@ const DiscussionSection = () => {
avatar: '/api/placeholder/32/32', avatar: '/api/placeholder/32/32',
content: 'This is really interesting! I particularly liked the section about state management.', content: 'This is really interesting! I particularly liked the section about state management.',
likes: 12, likes: 12,
replies: 2, replies: [
{
id: 101,
user: 'Alex Kim',
avatar: '/api/placeholder/32/32',
content: '@Sarah Chen Thanks for sharing your thoughts!',
timestamp: '1 hour ago'
}
],
timestamp: '2 hours ago' timestamp: '2 hours ago'
}, },
{ {
@ -24,54 +33,66 @@ const DiscussionSection = () => {
avatar: '/api/placeholder/32/32', avatar: '/api/placeholder/32/32',
content: 'Great explanation! Could you elaborate more on the useEffect implementation?', content: 'Great explanation! Could you elaborate more on the useEffect implementation?',
likes: 8, likes: 8,
replies: 1, replies: [],
timestamp: '1 hour ago' timestamp: '1 hour ago'
} }
]); ]);
const handleSubmit = () => { const handleSubmit = () => {
if (comment.trim()) { if (comment.trim()) {
const newComment = { if (replyingTo) {
id: discussions.length + 1, // Add reply to existing comment
user: 'Current User', const updatedDiscussions = discussions.map(discussion => {
avatar: '/api/placeholder/32/32', if (discussion.id === replyingTo.commentId) {
content: comment, return {
likes: 0, ...discussion,
replies: 0, replies: [...discussion.replies, {
timestamp: 'Just now' id: Date.now(),
}; user: 'Current User',
setDiscussions([...discussions, newComment]); avatar: '/api/placeholder/32/32',
content: comment,
timestamp: 'Just now'
}]
};
}
return discussion;
});
setDiscussions(updatedDiscussions);
setReplyingTo(null);
} else {
// Add new top-level comment
const newComment = {
id: Date.now(),
user: 'Current User',
avatar: '/api/placeholder/32/32',
content: comment,
likes: 0,
replies: [],
timestamp: 'Just now'
};
setDiscussions([...discussions, newComment]);
}
setComment(''); setComment('');
} }
}; };
return ( const handleReply = (commentId, userName) => {
<Card className="w-full max-w-2xl mx-auto"> setReplyingTo({ commentId, userName });
<CardContent className="p-6"> setComment(`@${userName} `);
<div className="mb-6"> };
<div className="flex items-start gap-4">
<Avatar className="w-8 h-8"> const cancelReply = () => {
<AvatarImage src="/api/placeholder/32/32" alt="User" /> setReplyingTo(null);
<AvatarFallback>U</AvatarFallback> setComment('');
</Avatar> };
<div className="flex-1">
<Textarea
placeholder="Add to the discussion..."
value={comment}
onChange={(e) => setComment(e.target.value)}
className="min-h-24 mb-2"
/>
<Button onClick={handleSubmit} className="float-right">
Post Comment
</Button>
</div>
</div>
</div>
<ScrollArea className="h-96"> return (
<div className="space-y-6"> <>
{discussions.map((discussion) => ( <ScrollArea className="h-auto max-h-128">
<div key={discussion.id} className="flex gap-4"> <div className="space-y-6">
{discussions.map((discussion) => (
<div key={discussion.id} className="space-y-4">
<div className="flex gap-4">
<Avatar className="w-8 h-8"> <Avatar className="w-8 h-8">
<AvatarImage src={discussion.avatar} alt={discussion.user} /> <AvatarImage src={discussion.avatar} alt={discussion.user} />
<AvatarFallback>{discussion.user[0]}</AvatarFallback> <AvatarFallback>{discussion.user[0]}</AvatarFallback>
@ -83,26 +104,79 @@ const DiscussionSection = () => {
</div> </div>
<p className="text-gray-700 mb-2">{discussion.content}</p> <p className="text-gray-700 mb-2">{discussion.content}</p>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700"> <button
<ThumbsUp className="w-4 h-4" /> className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700"
{discussion.likes} onClick={() => handleReply(discussion.id, discussion.user)}
</button> >
<button className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700">
<Reply className="w-4 h-4" /> <Reply className="w-4 h-4" />
Reply Reply
</button> </button>
<button className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700"> <div className="flex items-center gap-1 text-sm text-gray-500">
<MessageCircle className="w-4 h-4" /> <MessageCircle className="w-4 h-4" />
{discussion.replies} replies {discussion.replies.length} replies
</button> </div>
</div> </div>
</div> </div>
</div> </div>
))}
{/* Nested replies */}
{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>
))}
</div>
</ScrollArea>
<div className="my-6">
<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>
</ScrollArea> </div>
</CardContent> </div>
</Card> </>
); );
}; };

@ -12,13 +12,13 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root <ProgressPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20", "relative h-2 w-full overflow-hidden rounded-full bg-purple-700/20",
className className
)} )}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all" className="h-full w-full flex-1 bg-purple-700 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>

@ -0,0 +1,24 @@
export interface CourseData {
title: string;
instructor: {
name: string;
title: string;
avatar: string;
};
description: string;
category: string;
duration: string;
enrolledStudents: number;
rating: number;
totalReviews: number;
completionRate: number;
lastUpdated: string;
chapters: Chapter[];
requirements: string[];
}
interface Chapter {
title: string;
duration: string;
isCompleted: boolean;
}

@ -2,7 +2,7 @@
import React, { ReactNode, createContext, useEffect, useState } from "react"; import React, { ReactNode, createContext, useEffect, useState } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { User } from "../apiSchema/User"; import { User } from "../apiSchema/user.schema";
import { getEduConnectAccessToken } from "../token.helper"; import { getEduConnectAccessToken } from "../token.helper";
interface AuthContextProps { interface AuthContextProps {

Loading…
Cancel
Save