This commit is contained in:
5t4l1n
2025-07-25 23:58:20 +05:30
parent b70e81d201
commit 4d8061616d
112 changed files with 14445 additions and 59 deletions
+11
View File
@@ -0,0 +1,11 @@
import { CodingProblemView } from "@/components/coding-problem-view"
interface CodingProblemPageProps {
params: {
problemId: string
}
}
export default function CodingProblemPage({ params }: CodingProblemPageProps) {
return <CodingProblemView problemId={params.problemId} />
}
+5
View File
@@ -0,0 +1,5 @@
import { CodingProblemList } from "@/components/coding-problem-list"
export default function CodingPage() {
return <CodingProblemList />
}
@@ -0,0 +1,91 @@
"use client"
import { CourseSidebar } from "@/components/course-sidebar"
import { LessonViewer } from "@/components/lesson-viewer"
import { Loader2 } from "lucide-react"
import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/navigation"
import { toast } from "react-hot-toast"
import { useState, useEffect } from "react"
import type { Course } from "@/lib/types"
import api from "@/lib/api" // Corrected import: default import
interface CourseDetailPageProps {
params: {
courseId: string
lessonId: string
}
}
export default function CourseDetailPage({ params }: CourseDetailPageProps) {
const { courseId, lessonId } = params
const { user, firebaseUser, isLoadingAuth } = useAuth() // Allow firebaseUser
const router = useRouter()
const [course, setCourse] = useState<Course | null>(null)
const [isLoadingCourse, setIsLoadingCourse] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) {
// Allow either MetaMask or Firebase user
toast.error("Please login to view courses.")
router.push("/")
return
}
const fetchCourse = async () => {
setIsLoadingCourse(true)
setError(null)
try {
// --- ORIGINAL API CALL (UNCOMMENT WHEN BACKEND IS READY) ---
const response = await api.get<Course>(`/api/courses/${courseId}`)
setCourse(response.data)
} catch (err: any) {
console.error("Failed to fetch course details:", err)
setError(err.response?.data?.message || "Failed to load course details.")
toast.error(err.response?.data?.message || "Failed to load course details.")
} finally {
setIsLoadingCourse(false)
}
}
if (user || firebaseUser) {
// Only fetch if either user type is logged in
fetchCourse()
}
}, [user, firebaseUser, isLoadingAuth, router, courseId])
if (isLoadingAuth || isLoadingCourse) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)]">
<Loader2 className="h-8 w-8 animate-spin text-primary-purple" />
<span className="ml-2 text-lg">Loading course...</span>
</div>
)
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-red-500">
<p>{error}</p>
</div>
)
}
if (!course) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-gray-600 dark:text-gray-300">
<p className="text-xl">Course not found.</p>
</div>
)
}
return (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]">
<CourseSidebar courseId={course.id} modules={course.modules} activeLessonId={lessonId} />
<div className="flex-1 p-4 md:p-8 overflow-y-auto">
<LessonViewer courseId={course.id} lessonId={lessonId} />
</div>
</div>
)
}
+105
View File
@@ -0,0 +1,105 @@
"use client"
import { CourseSidebar } from "@/components/course-sidebar"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2 } from "lucide-react"
import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/navigation"
import { toast } from "react-hot-toast"
import { useState, useEffect } from "react"
import type { Course } from "@/lib/types"
import api from "@/lib/api" // Corrected import: default import
interface CourseOverviewPageProps {
params: {
courseId: string
}
}
export default function CourseOverviewPage({ params }: CourseOverviewPageProps) {
const { courseId } = params
const { user, firebaseUser, isLoadingAuth } = useAuth() // Allow firebaseUser
const router = useRouter()
const [course, setCourse] = useState<Course | null>(null)
const [isLoadingCourse, setIsLoadingCourse] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) {
// Allow either MetaMask or Firebase user
toast.error("Please login to view courses.")
router.push("/")
return
}
const fetchCourse = async () => {
setIsLoadingCourse(true)
setError(null)
try {
// --- ORIGINAL API CALL (UNCOMMENT WHEN BACKEND IS READY) ---
const response = await api.get<Course>(`/api/courses/${courseId}`)
setCourse(response.data)
} catch (err: any) {
console.error("Failed to fetch course details:", err)
setError(err.response?.data?.message || "Failed to load course details.")
toast.error(err.response?.data?.message || "Failed to load course details.")
} finally {
setIsLoadingCourse(false)
}
}
if (user || firebaseUser) {
// Only fetch if either user type is logged in
fetchCourse()
}
}, [user, firebaseUser, isLoadingAuth, router, courseId])
useEffect(() => {
if (course && course.modules.length > 0 && course.modules[0].lessons.length > 0) {
// Redirect to the first lesson of the course
router.replace(`/courses/${courseId}/lesson/${course.modules[0].lessons[0].id}`)
}
}, [course, courseId, router])
if (isLoadingAuth || isLoadingCourse) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)]">
<Loader2 className="h-8 w-8 animate-spin text-primary-purple" />
<span className="ml-2 text-lg">Loading course...</span>
</div>
)
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-red-500">
<p>{error}</p>
</div>
)
}
if (!course) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-gray-600 dark:text-gray-300">
<p className="text-xl">Course not found.</p>
</div>
)
}
return (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]">
<CourseSidebar courseId={course.id} modules={course.modules} activeLessonId="" />
<div className="flex-1 p-4 md:p-8 overflow-y-auto">
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader>
<CardTitle className="text-2xl font-bold text-primary-purple">{course.title} Overview</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-lg text-gray-700 dark:text-gray-200">{course.description}</p>
<p className="text-gray-600 dark:text-gray-300">Select a lesson from the sidebar to begin.</p>
</CardContent>
</Card>
</div>
</div>
)
}
+5
View File
@@ -0,0 +1,5 @@
import { CourseList } from "@/components/course-list"
export default function CoursesPage() {
return <CourseList />
}
+5
View File
@@ -0,0 +1,5 @@
import { DashboardStatsOverview } from "@/components/dashboard-stats"
export default function DashboardPage() {
return <DashboardStatsOverview />
}
+90
View File
@@ -0,0 +1,90 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+36
View File
@@ -0,0 +1,36 @@
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { Toaster } from "react-hot-toast"
import { AuthProvider } from "@/context/auth-context"
import { Navbar } from "@/components/ui/navbar"
import { ThemeProvider } from "@/components/theme-provider"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "OpenLearnX - Decentralized Adaptive Learning",
description: "AI-powered adaptive testing with blockchain-secured credentials.",
generator: 'v0.dev'
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<AuthProvider>
<Navbar />
<main>{children}</main>
<Toaster position="top-right" />
</AuthProvider>
</ThemeProvider>
</body>
</html>
)
}
+92
View File
@@ -0,0 +1,92 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Brain, Zap, LinkIcon, BookOpen, Code, Lightbulb } from "lucide-react"
export default function Home() {
return (
<div className="flex flex-col min-h-[calc(100vh-64px)]">
<section className="relative w-full py-12 md:py-24 lg:py-32 bg-gradient-to-r from-primary-blue to-primary-purple text-white">
<div className="container px-4 md:px-6 text-center">
<div className="max-w-3xl mx-auto space-y-4">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
OpenLearnX Decentralized Adaptive Learning
</h1>
<p className="text-lg md:text-xl">
Unlock your potential with AI-powered adaptive learning, coding practice, and blockchain-secured
credentials.
</p>
<div className="flex flex-col gap-2 sm:flex-row justify-center">
<Link href="/courses">
<Button className="bg-white text-primary-purple hover:bg-gray-100 px-8 py-3 text-lg font-semibold rounded-full shadow-lg">
Start Learning
</Button>
</Link>
</div>
</div>
</div>
</section>
<section className="w-full py-12 md:py-24 lg:py-32 bg-gray-50 dark:bg-gray-900">
<div className="container px-4 md:px-6">
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader className="flex flex-row items-center space-x-4 pb-2">
<BookOpen className="h-8 w-8 text-primary-blue" />
<CardTitle className="text-xl font-semibold">Interactive Courses</CardTitle>
</CardHeader>
<CardContent className="text-gray-600 dark:text-gray-300">
Engage with rich multimedia content, track your progress, and master new subjects at your own pace.
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader className="flex flex-row items-center space-x-4 pb-2">
<Code className="h-8 w-8 text-primary-purple" />
<CardTitle className="text-xl font-semibold">LeetCode-Style Coding Practice</CardTitle>
</CardHeader>
<CardContent className="text-gray-600 dark:text-gray-300">
Sharpen your coding skills with interactive problems, instant feedback, and a built-in code editor.
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader className="flex flex-row items-center space-x-4 pb-2">
<Lightbulb className="h-8 w-8 text-primary-blue" />
<CardTitle className="text-xl font-semibold">Advanced Quiz Platform</CardTitle>
</CardHeader>
<CardContent className="text-gray-600 dark:text-gray-300">
Test your knowledge with timed multiple-choice quizzes, detailed explanations, and performance tracking.
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader className="flex flex-row items-center space-x-4 pb-2">
<Brain className="h-8 w-8 text-primary-purple" />
<CardTitle className="text-xl font-semibold">AI-powered Adaptive Learning</CardTitle>
</CardHeader>
<CardContent className="text-gray-600 dark:text-gray-300">
Our platform intelligently adjusts content and question difficulty based on your performance.
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader className="flex flex-row items-center space-x-4 pb-2">
<LinkIcon className="h-8 w-8 text-primary-blue" />
<CardTitle className="text-xl font-semibold">Blockchain-secured Credentials</CardTitle>
</CardHeader>
<CardContent className="text-gray-600 dark:text-gray-300">
Your achievements are secured on the blockchain, providing verifiable and tamper-proof records.
</CardContent>
</Card>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader className="flex flex-row items-center space-x-4 pb-2">
<Zap className="h-8 w-8 text-primary-purple" />
<CardTitle className="text-xl font-semibold">Personalized Dashboard</CardTitle>
</CardHeader>
<CardContent className="text-gray-600 dark:text-gray-300">
Track your progress, identify strengths and weaknesses, and visualize your learning journey.
</CardContent>
</Card>
</div>
</div>
</section>
</div>
)
}
+11
View File
@@ -0,0 +1,11 @@
import { QuizRunner } from "@/components/quiz-runner"
interface QuizPageProps {
params: {
quizId: string
}
}
export default function QuizPage({ params }: QuizPageProps) {
return <QuizRunner quizId={params.quizId} />
}
+5
View File
@@ -0,0 +1,5 @@
import { QuizList } from "@/components/quiz-list"
export default function QuizzesPage() {
return <QuizList />
}
+5
View File
@@ -0,0 +1,5 @@
import { TestingSession } from "@/components/testing-session"
export default function TestPage() {
return <TestingSession />
}