Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 109 additions & 16 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,140 @@
'use client'

import { useState } from 'react'
import { useState, useEffect } from 'react'
import Navigation from '@/components/navigation'
import Dashboard from '@/components/pages/dashboard'
import GoalCreation from '@/components/pages/goal-creation'
import StudyPlan from '@/components/pages/study-plan'
import LearningScreen from '@/components/pages/learning-screen'
import QuizPage from '@/components/pages/quiz-page'
import AIChat from '@/components/pages/ai-chat'
import ResourceLibrary from '@/components/pages/resource-library'
import Analytics from '@/components/pages/analytics'
import Settings from '@/components/pages/settings'
import Auth from '@/components/pages/auth'

type Goal = { id: number; title: string; progress: number; daysLeft: number }

export default function Home() {
const [currentPage, setCurrentPage] = useState('dashboard')
const [goals, setGoals] = useState([
{ id: 1, title: "Master Calculus", progress: 65, daysLeft: 12 },
{ id: 2, title: "Biology Fundamentals", progress: 42, daysLeft: 18 },
{ id: 3, title: "Physics Concepts", progress: 78, daysLeft: 8 },
])
const [goals, setGoals] = useState<Goal[]>([])
const [dashboardRefreshKey, setDashboardRefreshKey] = useState(0)

const [learningScreenState, setLearningScreenState] = useState({
isCompleted: false,
notes: "",
messages: [
{ id: 1, role: "tutor", text: "Hi! I'm your AI tutor. What would you like to learn about derivatives?" },
],
inputMessage: ""
inputMessage: "",
chatLoading: false,
selectedGoalTitle: null as string | null,
selectedModule: null as any,
});

const handleDeleteGoal = (id: number) => {
setGoals(goals.filter(goal => goal.id !== id));
const [auth, setAuth] = useState<{ token: string | null; name?: string; email?: string; role?: string }>({ token: null })
const [selectedGoal, setSelectedGoal] = useState<Goal | null>(null)

useEffect(() => {
const t = typeof window !== 'undefined' ? localStorage.getItem('token') : null
if (!t) return
const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080'
fetch(`${base}/api/auth/me`, { headers: { Authorization: `Bearer ${t}` } })
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data) setAuth({ token: t, name: data.name, email: data.email, role: data.role })
else setAuth({ token: null })
})
.catch(() => setAuth({ token: null }))
}, [])

useEffect(() => {
const t = auth.token
if (!t) return
const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080'
fetch(`${base}/api/goals`, { headers: { Authorization: `Bearer ${t}` } })
.then(res => res.ok ? res.json() : [])
.then((list) => setGoals(Array.isArray(list) ? list : []))
.catch(() => setGoals([]))
}, [auth.token])

const refreshGoals = async () => {
const t = typeof window !== 'undefined' ? localStorage.getItem('token') : null
if (!t) return
const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080'
try {
const res = await fetch(`${base}/api/goals`, { headers: { Authorization: `Bearer ${t}` } })
const list = await res.json().catch(() => [])
setGoals(Array.isArray(list) ? list : [])
setDashboardRefreshKey((k) => k + 1)
} catch {
// ignore
}
}

const handleDeleteGoal = async (id: number) => {
const t = typeof window !== 'undefined' ? localStorage.getItem('token') : null
const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080'
try {
if (!t) {
console.warn('Not authenticated; cannot delete goal')
return
}
const res = await fetch(`${base}/api/goals/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${t}` } })
const data = await res.json().catch(() => ({}))
if (!res.ok) {
// Fallback path for environments blocking DELETE (403/405): try POST /delete
if (res.status === 403 || res.status === 405) {
const res2 = await fetch(`${base}/api/goals/delete/${id}`, { method: 'POST', headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' } })
const data2 = await res2.json().catch(() => ({}))
if (!res2.ok) {
throw new Error(data2?.error || 'Failed to delete goal')
}
} else {
throw new Error(data?.error || 'Failed to delete goal')
}
}
setGoals(prev => prev.filter(goal => goal.id !== id))
if (selectedGoal && selectedGoal.id === id) setSelectedGoal(null)
} catch (e) {
console.error(e)
}
};

const renderPage = () => {
switch (currentPage) {
case 'dashboard':
return <Dashboard onNavigate={setCurrentPage} goals={goals} onDeleteGoal={handleDeleteGoal} />
return (
<Dashboard
onNavigate={setCurrentPage}
goals={goals}
onDeleteGoal={handleDeleteGoal}
onSelectGoal={(goal: Goal) => { setSelectedGoal(goal); setCurrentPage('study-plan'); }}
refreshKey={dashboardRefreshKey}
/>
)
case 'goals':
return <GoalCreation setGoals={setGoals} onNavigate={setCurrentPage} />
case 'study-plan':
return <StudyPlan onNavigate={setCurrentPage} />
return <StudyPlan onNavigate={setCurrentPage} goal={selectedGoal || undefined} onSelectGoal={(g: Goal) => { setSelectedGoal(g); setCurrentPage('study-plan'); }} onStartLearning={(goalTitle, module) => { setLearningScreenState(prev => ({ ...prev, selectedGoalTitle: goalTitle, selectedModule: module })); setCurrentPage(module?.type === 'quiz' ? 'quiz' : 'learning'); }} />
case 'learning':
return <LearningScreen
onNavigate={setCurrentPage}
learningState={learningScreenState}
setLearningState={setLearningScreenState}
onProgressUpdated={refreshGoals}
/>
case 'quiz':
return learningScreenState.selectedGoalTitle && learningScreenState.selectedModule ? (
<QuizPage
onNavigate={setCurrentPage}
goalTitle={learningScreenState.selectedGoalTitle}
module={learningScreenState.selectedModule}
onProgressUpdated={refreshGoals}
/>
) : (
<StudyPlan onNavigate={setCurrentPage} goal={selectedGoal || undefined} onSelectGoal={(g: Goal) => { setSelectedGoal(g); setCurrentPage('study-plan'); }} onStartLearning={(goalTitle, module) => { setLearningScreenState(prev => ({ ...prev, selectedGoalTitle: goalTitle, selectedModule: module })); setCurrentPage(module?.type === 'quiz' ? 'quiz' : 'learning'); }} />
)
case 'chat':
return <AIChat />
case 'resources':
Expand All @@ -55,14 +144,18 @@ export default function Home() {
case 'settings':
return <Settings />
default:
return <Dashboard onNavigate={setCurrentPage} goals={goals} onDeleteGoal={handleDeleteGoal} />
return <Dashboard onNavigate={setCurrentPage} goals={goals} onDeleteGoal={handleDeleteGoal} refreshKey={dashboardRefreshKey} />
}
}

return (
<div className='flex h-screen bg-background'>
<Navigation currentPage={currentPage} onNavigate={setCurrentPage} />
<main className='flex-1 overflow-auto'>{renderPage()}</main>
</div>
auth.token ? (
<div className='flex h-screen bg-background'>
<Navigation currentPage={currentPage} onNavigate={(page) => { if (page === 'study-plan') setSelectedGoal(null); setCurrentPage(page); }} />
<main className='flex-1 overflow-auto'>{renderPage()}</main>
</div>
) : (
<Auth onAuthenticated={(u) => setAuth(u)} />
)
)
}
12 changes: 9 additions & 3 deletions components/pages/ai-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,23 @@ export default function AIChat() {
const endpoint = process.env.NEXT_PUBLIC_BACKEND_URL
? `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/ai-chat`
: "http://localhost:8080/api/ai-chat";
const payloadMessages = [...messages, userMsg].map((m: any) => ({ role: m.role, text: m.text }))
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: [...messages, userMsg] }),
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ messages: payloadMessages }),
})
if (!res.ok) throw new Error("Failed to get AI response")
const data = await res.json()
const reply = (data?.reply ?? "I couldn't generate a response.").toString()
const clean = reply.replace(/\$/g, "").replace(/\\[()\\[\\]]/g, "")
setMessages((prev) => [
...prev,
{ id: prev.length + 1, role: "tutor", text: reply },
{ id: prev.length + 1, role: "tutor", text: clean },
])
} catch (err) {
setMessages((prev) => [
Expand Down
69 changes: 42 additions & 27 deletions components/pages/analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,46 @@ import {
Pie,
Cell,
} from "recharts"
import { useEffect, useState } from "react"

export default function Analytics() {
const studyTimeData = [
{ day: "Mon", hours: 2.5 },
{ day: "Tue", hours: 3.2 },
{ day: "Wed", hours: 2.8 },
{ day: "Thu", hours: 3.5 },
{ day: "Fri", hours: 2.1 },
{ day: "Sat", hours: 4.0 },
{ day: "Sun", hours: 1.5 },
]
const [loading, setLoading] = useState(false)
const [studyTimeData, setStudyTimeData] = useState<Array<{ day: string; hours: number }>>([])
const [progressData, setProgressData] = useState<Array<{ week: string; progress: number }>>([])
const [contentTypeData, setContentTypeData] = useState<Array<{ name: string; value: number }>>([])
const [totalMinutesWeek, setTotalMinutesWeek] = useState(0)
const [modulesCompleted, setModulesCompleted] = useState(0)
const [modulesTotal, setModulesTotal] = useState(0)
const [averageQuizScore, setAverageQuizScore] = useState(0)
const [currentStreakDays, setCurrentStreakDays] = useState(0)

const progressData = [
{ week: "Week 1", progress: 25 },
{ week: "Week 2", progress: 42 },
{ week: "Week 3", progress: 58 },
{ week: "Week 4", progress: 72 },
]

const contentTypeData = [
{ name: "Videos", value: 45 },
{ name: "Articles", value: 30 },
{ name: "Quizzes", value: 15 },
{ name: "Notes", value: 10 },
]
useEffect(() => {
let cancelled = false
const load = async () => {
setLoading(true)
try {
const base = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8080'
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null
const res = await fetch(`${base}/api/analytics/summary`, { headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) } })
const data = await res.json().catch(() => ({}))
if (!cancelled && res.ok) {
setTotalMinutesWeek(Number(data?.totalStudyMinutesThisWeek || 0))
setModulesCompleted(Number(data?.modulesCompleted || 0))
setModulesTotal(Number(data?.modulesTotal || 0))
setAverageQuizScore(Number(data?.averageQuizScore || 0))
setCurrentStreakDays(Number(data?.currentStreakDays || 0))
setStudyTimeData(Array.isArray(data?.studyTime) ? data.studyTime : [])
setProgressData(Array.isArray(data?.progress) ? data.progress : [])
setContentTypeData(Array.isArray(data?.contentType) ? data.contentType : [])
}
} catch {
} finally {
if (!cancelled) setLoading(false)
}
}
load()
return () => { cancelled = true }
}, [])

const COLORS = ["#4f46e5", "#06b6d4", "#f97316", "#8b5cf6"]

Expand All @@ -54,22 +69,22 @@ export default function Analytics() {
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<Card className="p-6 bg-card border border-border">
<p className="text-sm text-muted-foreground mb-1">Total Study Time</p>
<p className="text-3xl font-bold text-primary">19.6 hrs</p>
<p className="text-3xl font-bold text-primary">{Math.round((totalMinutesWeek/60) * 10) / 10} hrs</p>
<p className="text-xs text-muted-foreground mt-2">This week</p>
</Card>
<Card className="p-6 bg-card border border-border">
<p className="text-sm text-muted-foreground mb-1">Modules Completed</p>
<p className="text-3xl font-bold text-secondary">24</p>
<p className="text-xs text-muted-foreground mt-2">Out of 35</p>
<p className="text-3xl font-bold text-secondary">{modulesCompleted}</p>
<p className="text-xs text-muted-foreground mt-2">Out of {modulesTotal}</p>
</Card>
<Card className="p-6 bg-card border border-border">
<p className="text-sm text-muted-foreground mb-1">Average Score</p>
<p className="text-3xl font-bold text-accent">87%</p>
<p className="text-3xl font-bold text-accent">{averageQuizScore}%</p>
<p className="text-xs text-muted-foreground mt-2">On quizzes</p>
</Card>
<Card className="p-6 bg-card border border-border">
<p className="text-sm text-muted-foreground mb-1">Current Streak</p>
<p className="text-3xl font-bold text-primary">7 days</p>
<p className="text-3xl font-bold text-primary">{currentStreakDays} days</p>
<p className="text-xs text-muted-foreground mt-2">Keep it up!</p>
</Card>
</div>
Expand Down
Loading