diff --git a/backend/.gitignore b/backend/.gitignore index 40b878d..f566749 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1 +1,17 @@ -node_modules/ \ No newline at end of file +# dependencies +node_modules/ + +# environment variables +.env +.env.local +.env.development +.env.production + +# system files +.DS_Store +Thumbs.db + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/backend/attendance/attendanceDB.js b/backend/attendance/attendanceDB.js new file mode 100644 index 0000000..6645e1b --- /dev/null +++ b/backend/attendance/attendanceDB.js @@ -0,0 +1,88 @@ +class AttendanceDB { + constructor() { + this.attendance = []; + this.nextId = 1; + } + + async save(classId, date, entries) { + const dateStr = new Date(date).toDateString(); + const existingIndex = this.attendance.findIndex( + (record) => record.classId === classId && record.date === dateStr + ); + + const attendanceRecord = { + id: + existingIndex >= 0 ? this.attendance[existingIndex].id : this.nextId++, + classId, + date: dateStr, + entries, + createdAt: + existingIndex >= 0 + ? this.attendance[existingIndex].createdAt + : new Date(), + updatedAt: new Date(), + }; + + if (existingIndex >= 0) { + this.attendance[existingIndex] = attendanceRecord; + return { message: 'Attendance updated', attendance: attendanceRecord }; + } else { + this.attendance.push(attendanceRecord); + return { message: 'Attendance created', attendance: attendanceRecord }; + } + } + + // Find attendance by class and date + async findByClassAndDate(classId, date) { + const dateStr = new Date(date).toDateString(); + return this.attendance.find( + (record) => record.classId === classId && record.date === dateStr + ); + } + + // Find all attendance for a student + async findByStudent(studentId) { + const studentRecords = []; + + this.attendance.forEach((record) => { + const studentEntry = record.entries.find( + (entry) => entry.studentId === studentId + ); + if (studentEntry) { + studentRecords.push({ + classId: record.classId, + date: record.date, + status: studentEntry.status, + recordId: record.id, + }); + } + }); + + return studentRecords; + } + + // Get all attendance records + async findAll() { + return this.attendance; + } + + // Get attendance summary for a class + async getClassSummary(classId) { + const classRecords = this.attendance.filter( + (record) => record.classId === classId + ); + return classRecords.map((record) => ({ + date: record.date, + totalStudents: record.entries.length, + presentCount: record.entries.filter((entry) => entry.status === 'present') + .length, + absentCount: record.entries.filter((entry) => entry.status === 'absent') + .length, + entries: record.entries, + })); + } +} + +const attendanceDB = new AttendanceDB(); + +export default attendanceDB; diff --git a/backend/attendance/attendanceRoutes.js b/backend/attendance/attendanceRoutes.js new file mode 100644 index 0000000..e001e26 --- /dev/null +++ b/backend/attendance/attendanceRoutes.js @@ -0,0 +1,100 @@ +import express from 'express'; +import Attendance from '../models/attendance.js'; + +const router = express.Router(); + +router.post('/', async (req, res) => { + try { + console.log('Received attendance data:', req.body); + const { classId, date, entries } = req.body; + + if (!classId || !date || !entries) { + console.log('Missing required fields'); + return res + .status(400) + .json({ error: 'Missing required fields: classId, date, entries' }); + } + + const existing = await Attendance.findOne({ classId, date }); + + if (existing) { + console.log('Updating existing attendance record'); + existing.entries = entries; + await existing.save(); + console.log('Attendance updated successfully'); + return res.json({ message: 'Attendance updated', attendance: existing }); + } + + console.log('Creating new attendance record'); + const newAttendance = new Attendance({ classId, date, entries }); + await newAttendance.save(); + console.log('Attendance saved successfully:', newAttendance); + res.status(201).json(newAttendance); + } catch (error) { + console.error('Error saving attendance:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/debug/all', async (req, res) => { + try { + console.log('Fetching ALL attendance records for debugging'); + const allRecords = await Attendance.find({}).limit(50); + console.log('Total records in database:', allRecords.length); + + const summary = allRecords.map((r) => ({ + classId: r.classId, + date: r.date, + studentIds: r.entries.map((e) => e.studentId), + entriesCount: r.entries.length, + })); + + res.json({ + totalRecords: allRecords.length, + records: summary, + fullRecords: allRecords, + }); + } catch (error) { + console.error('Error fetching all records:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/student/:studentId', async (req, res) => { + try { + const { studentId } = req.params; + console.log('Attendance for student:', studentId); + + const records = await Attendance.find({ 'entries.studentId': studentId }); + console.log('Found', records.length, 'attendance records'); + + if (records.length > 0) { + console.log('Sample record:', JSON.stringify(records[0], null, 2)); + } + + const formatted = records.map((r) => ({ + classId: r.classId, + date: r.date, + status: + r.entries.find((e) => e.studentId === studentId)?.status || 'absent', + })); + + console.log('Sending formatted records:', formatted); + res.json(formatted); + } catch (error) { + console.error('Error fetching student attendance:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/:classId/:date', async (req, res) => { + try { + const { classId, date } = req.params; + const attendance = await Attendance.findOne({ classId, date }); + res.json(attendance || { message: 'No record found' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +export default router; diff --git a/backend/index.js b/backend/index.js index 0352d61..0298dbf 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,7 +1,47 @@ -const express = require('express') -const app = express() -const port = 8000 +import express from 'express'; +import mongoose from 'mongoose'; +import cors from 'cors'; +import dotenv from 'dotenv'; +import attendanceRoutes from './attendance/attendanceRoutes.js'; +import classRoutes from './routes/classes.js'; +import quizRoutes from './routes/quiz.js'; +import noteRoutes from './routes/notes.js'; +import announcementRoutes from './routes/announcements.js'; + +dotenv.config(); + +const app = express(); +const port = 8000; + +app.use(cors()); +app.use(express.json()); + +const MONGODB_URI = process.env.MONGODB_URI; + +if (!process.env.MONGODB_URI) { + console.warn( + ' Warning: MONGODB_URI not found in .env file, using fallback' + ); + +} + +console.log('Attempting to connect to MongoDB...'); +mongoose + .connect(MONGODB_URI) + .then(() => { + console.log('Connected to MongoDB successfully'); + console.log('Database:', mongoose.connection.name); + }) + .catch((err) => { + console.error('MongoDB connection error:', err.message); + }); + +app.use('/api/attendance', attendanceRoutes); +app.use('/api/classes', classRoutes); +app.use('/api/quiz', quizRoutes); +app.use('/api/notes', noteRoutes); +app.use('/api/announcements', announcementRoutes); app.listen(port, () => { - console.log(`Example app listening on port ${port}`) -}) + console.log(`Server running on port ${port}`); +}); diff --git a/backend/models/Announcement.js b/backend/models/Announcement.js new file mode 100644 index 0000000..ef080cb --- /dev/null +++ b/backend/models/Announcement.js @@ -0,0 +1,34 @@ +import mongoose from 'mongoose'; + +const announcementSchema = new mongoose.Schema({ + classId: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + trim: true, + }, + content: { + type: String, + required: true, + }, + professor: { + type: String, + required: true, + }, + priority: { + type: String, + enum: ['low', 'medium', 'high'], + default: 'medium', + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +announcementSchema.index({ classId: 1, createdAt: -1 }); + +export default mongoose.model('Announcement', announcementSchema); diff --git a/backend/models/Class.js b/backend/models/Class.js new file mode 100644 index 0000000..38a6bb0 --- /dev/null +++ b/backend/models/Class.js @@ -0,0 +1,35 @@ +import mongoose from 'mongoose'; + +const classSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true, + }, + code: { + type: String, + required: true, + unique: true, + uppercase: true, + length: 6, + }, + professorId: { + type: String, + required: true, + }, + professorName: { + type: String, + required: true, + }, + students: [{ + type: String, + }], + createdAt: { + type: Date, + default: Date.now, + }, +}); + +classSchema.index({ professorId: 1 }); + +export default mongoose.model('Class', classSchema); diff --git a/backend/models/Note.js b/backend/models/Note.js new file mode 100644 index 0000000..f6e8948 --- /dev/null +++ b/backend/models/Note.js @@ -0,0 +1,33 @@ +import mongoose from 'mongoose'; + +const noteSchema = new mongoose.Schema({ + classId: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + trim: true, + }, + content: { + type: String, + required: true, + }, + professor: { + type: String, + required: true, + }, + fileUrl: { + type: String, + default: null, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +noteSchema.index({ classId: 1, createdAt: -1 }); + +export default mongoose.model('Note', noteSchema); diff --git a/backend/models/Quiz.js b/backend/models/Quiz.js new file mode 100644 index 0000000..b205e55 --- /dev/null +++ b/backend/models/Quiz.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose'; + +const questionSchema = new mongoose.Schema({ + question: { type: String, required: true }, + options: { type: [String], required: true }, + correctAnswer: { type: Number, required: true }, +}); + +const quizSchema = new mongoose.Schema({ + classId: { type: String, required: true }, + title: { type: String, required: true }, + questions: [questionSchema], + professor: { type: String, default: "Mr. Sharma" }, + createdAt: { type: Date, default: Date.now }, +}); + +export default mongoose.model("Quiz", quizSchema); diff --git a/backend/models/attendance.js b/backend/models/attendance.js new file mode 100644 index 0000000..0629d00 --- /dev/null +++ b/backend/models/attendance.js @@ -0,0 +1,14 @@ +import mongoose from 'mongoose'; + +const attendanceEntrySchema = new mongoose.Schema({ + studentId: { type: String, required: true }, + status: { type: String, enum: ['present', 'absent'], required: true }, +}); + +const attendanceSchema = new mongoose.Schema({ + classId: { type: String, required: true }, + date: { type: Date, required: true }, + entries: [attendanceEntrySchema], +}); + +export default mongoose.model('Attendance', attendanceSchema); diff --git a/backend/package-lock.json b/backend/package-lock.json index 804f93b..9bc481f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,7 +9,34 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^5.1.0" + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "mongoose": "^8.19.2" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" } }, "node_modules/accepts": { @@ -45,6 +72,15 @@ "node": ">=18" } }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -122,6 +158,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -148,6 +197,18 @@ "node": ">= 0.8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -439,6 +500,15 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -457,6 +527,12 @@ "node": ">= 0.8" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -490,6 +566,105 @@ "node": ">= 0.6" } }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.19.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.2.tgz", + "integrity": "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -505,6 +680,15 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -570,6 +754,15 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -782,6 +975,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -800,6 +1008,18 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -832,6 +1052,28 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 0e7cfbe..8231a87 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,13 +3,19 @@ "version": "1.0.0", "description": "", "main": "index.js", + "type": "module", "scripts": { + "start": "node index.js", + "dev": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "express": "^5.1.0" + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "mongoose": "^8.19.2" } } diff --git a/backend/routes/announcements.js b/backend/routes/announcements.js new file mode 100644 index 0000000..8f239c5 --- /dev/null +++ b/backend/routes/announcements.js @@ -0,0 +1,72 @@ +import express from 'express'; +import Announcement from '../models/Announcement.js'; + +const router = express.Router(); + +// Create a new announcement +router.post('/', async (req, res) => { + try { + const { classId, title, content, professor, priority } = req.body; + + if (!classId || !title || !content || !professor) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + const newAnnouncement = new Announcement({ + classId, + title, + content, + professor, + priority: priority || 'medium', + }); + + await newAnnouncement.save(); + + res.status(201).json({ + success: true, + message: 'Announcement created successfully', + announcement: newAnnouncement, + }); + } catch (error) { + console.error('Error creating announcement:', error); + res.status(500).json({ error: 'Failed to create announcement' }); + } +}); + +// Get all announcements for a class +router.get('/:classId', async (req, res) => { + try { + const { classId } = req.params; + const announcements = await Announcement.find({ classId }).sort({ createdAt: -1 }); + + res.json({ + success: true, + announcements, + }); + } catch (error) { + console.error('Error fetching announcements:', error); + res.status(500).json({ error: 'Failed to fetch announcements' }); + } +}); + +// Delete an announcement +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + const deletedAnnouncement = await Announcement.findByIdAndDelete(id); + + if (!deletedAnnouncement) { + return res.status(404).json({ error: 'Announcement not found' }); + } + + res.json({ + success: true, + message: 'Announcement deleted successfully', + }); + } catch (error) { + console.error('Error deleting announcement:', error); + res.status(500).json({ error: 'Failed to delete announcement' }); + } +}); + +export default router; diff --git a/backend/routes/classes.js b/backend/routes/classes.js new file mode 100644 index 0000000..0bf077c --- /dev/null +++ b/backend/routes/classes.js @@ -0,0 +1,158 @@ +import express from 'express'; +import Class from '../models/Class.js'; + +const router = express.Router(); + +function generateClassCode() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let code = ''; + for (let i = 0; i < 6; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; +} + +router.post('/create', async (req, res) => { + try { + const { name, professorId, professorName } = req.body; + + if (!name || !professorId || !professorName) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + let code = generateClassCode(); + let codeExists = await Class.findOne({ code }); + + while (codeExists) { + code = generateClassCode(); + codeExists = await Class.findOne({ code }); + } + + const newClass = new Class({ + name, + code, + professorId, + professorName, + students: [], + }); + + await newClass.save(); + + res.status(201).json({ + success: true, + class: newClass, + }); + } catch (error) { + console.error('Error creating class:', error); + res.status(500).json({ error: 'Failed to create class' }); + } +}); + +router.get('/professor/:professorId', async (req, res) => { + try { + const { professorId } = req.params; + const classes = await Class.find({ professorId }).sort({ createdAt: -1 }); + + res.json({ + success: true, + classes, + }); + } catch (error) { + console.error('Error fetching professor classes:', error); + res.status(500).json({ error: 'Failed to fetch classes' }); + } +}); + +router.get('/student/:studentId', async (req, res) => { + try { + const { studentId } = req.params; + const classes = await Class.find({ students: studentId }).sort({ createdAt: -1 }); + + res.json({ + success: true, + classes, + }); + } catch (error) { + console.error('Error fetching student classes:', error); + res.status(500).json({ error: 'Failed to fetch classes' }); + } +}); + +router.get('/code/:code', async (req, res) => { + try { + const { code } = req.params; + const classData = await Class.findOne({ code: code.toUpperCase() }); + + if (!classData) { + return res.status(404).json({ error: 'Class not found' }); + } + + res.json({ + success: true, + class: classData, + }); + } catch (error) { + console.error('Error fetching class by code:', error); + res.status(500).json({ error: 'Failed to fetch class' }); + } +}); + +router.post('/join', async (req, res) => { + try { + const { code, studentId } = req.body; + + if (!code || !studentId) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + const classData = await Class.findOne({ code: code.toUpperCase() }); + + if (!classData) { + return res.status(404).json({ error: 'Class not found' }); + } + + if (classData.students.includes(studentId)) { + return res.status(400).json({ error: 'Already enrolled in this class' }); + } + + classData.students.push(studentId); + await classData.save(); + + res.json({ + success: true, + class: classData, + }); + } catch (error) { + console.error('Error joining class:', error); + res.status(500).json({ error: 'Failed to join class' }); + } +}); + +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + + + // Validate MongoDB ObjectId format + if (!id.match(/^[0-9a-fA-F]{24}$/)) { + return res.status(400).json({ error: 'Invalid class ID format' }); + } + + const classData = await Class.findById(id); + + + if (!classData) { + return res.status(404).json({ error: 'Class not found' }); + } + + res.json({ + success: true, + class: classData, + }); + } catch (error) { + console.error('Error fetching class:', error); + res.status(500).json({ error: 'Failed to fetch class', details: error.message }); + } +}); + +export default router; diff --git a/backend/routes/notes.js b/backend/routes/notes.js new file mode 100644 index 0000000..8011f26 --- /dev/null +++ b/backend/routes/notes.js @@ -0,0 +1,72 @@ +import express from 'express'; +import Note from '../models/Note.js'; + +const router = express.Router(); + +// Create a new note +router.post('/', async (req, res) => { + try { + const { classId, title, content, professor, fileUrl } = req.body; + + if (!classId || !title || !content || !professor) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + const newNote = new Note({ + classId, + title, + content, + professor, + fileUrl: fileUrl || null, + }); + + await newNote.save(); + + res.status(201).json({ + success: true, + message: 'Note created successfully', + note: newNote, + }); + } catch (error) { + console.error('Error creating note:', error); + res.status(500).json({ error: 'Failed to create note' }); + } +}); + +// Get all notes for a class +router.get('/:classId', async (req, res) => { + try { + const { classId } = req.params; + const notes = await Note.find({ classId }).sort({ createdAt: -1 }); + + res.json({ + success: true, + notes, + }); + } catch (error) { + console.error('Error fetching notes:', error); + res.status(500).json({ error: 'Failed to fetch notes' }); + } +}); + +// Delete a note +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + const deletedNote = await Note.findByIdAndDelete(id); + + if (!deletedNote) { + return res.status(404).json({ error: 'Note not found' }); + } + + res.json({ + success: true, + message: 'Note deleted successfully', + }); + } catch (error) { + console.error('Error deleting note:', error); + res.status(500).json({ error: 'Failed to delete note' }); + } +}); + +export default router; diff --git a/backend/routes/quiz.js b/backend/routes/quiz.js new file mode 100644 index 0000000..47b36da --- /dev/null +++ b/backend/routes/quiz.js @@ -0,0 +1,51 @@ +import express from 'express'; +import Quiz from '../models/Quiz.js'; + +const router = express.Router(); + +// ➕ Create new quiz +router.post("/", async (req, res) => { + try { + const { classId, title, questions, professor } = req.body; + + if (!classId || !title || !questions) { + return res.status(400).json({ message: "Missing required fields" }); + } + + if (!Array.isArray(questions) || questions.length === 0) { + return res.status(400).json({ message: "Questions must be a non-empty array" }); + } + + const newQuiz = new Quiz({ + classId, + title, + questions, + professor: professor || "Mr. Sharma", + }); + + await newQuiz.save(); + console.log(`✅ Quiz created successfully: ${newQuiz._id} for class ${classId}`); + res.status(201).json({ message: "Quiz created successfully", quiz: newQuiz }); + } catch (error) { + console.error("Error creating quiz:", error); + res.status(500).json({ message: "Server error", error: error.message }); + } +}); + +// Get all quizzes for a class +router.get("/:classId", async (req, res) => { + try { + const { classId } = req.params; + console.log(`📚 Fetching quizzes for class: ${classId}`); + + const quizzes = await Quiz.find({ classId: classId }).sort({ createdAt: -1 }); + console.log(`✅ Found ${quizzes.length} quizzes for class ${classId}`); + + res.json(quizzes); + } catch (error) { + console.error("Error fetching quizzes:", error); + res.status(500).json({ message: "Server error", error: error.message }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..3827080 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,28 @@ +const express = require("express"); +const mongoose = require("mongoose"); +const cors = require("cors"); + +require("dotenv").config(); + +const quizRoutes = require("./routes/quiz"); + +const app = express(); +app.use(cors({ + origin: "http://localhost:3000", + credentials: true +})); +app.use(express.json()); + +// MongoDB connection +mongoose.connect(process.env.MONGODB_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, +}) +.then(() => console.log(" Connected to MongoDB")) +.catch((err) => console.error(" MongoDB connection error:", err)); + +// Routes +app.use("/api/quiz", quizRoutes); + +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => console.log(` Server running on port ${PORT}`)); diff --git a/frontend/app/api/auth/[...nextauth]/route.js b/frontend/app/api/auth/[...nextauth]/route.js new file mode 100644 index 0000000..e0b2d2d --- /dev/null +++ b/frontend/app/api/auth/[...nextauth]/route.js @@ -0,0 +1,65 @@ +import NextAuth from "next-auth" +import GoogleProvider from "next-auth/providers/google" +import { MongoDBAdapter } from "@auth/mongodb-adapter" +import clientPromise from "@/lib/mongodb" + +export const authOptions = { + adapter: MongoDBAdapter(clientPromise), + secret: process.env.NEXTAUTH_SECRET, + + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID ?? "", + clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "", + }), + ], + callbacks: { + async jwt({ token, account, user }) { + if (account) token.accessToken = account.access_token + + if (user) { + token.id = user.id + token.email = user.email ?? token.email ?? null + if (typeof user.role !== "undefined") { + token.role = user.role + token.roleResolved = true + } + } + + if (typeof token.role !== "undefined" && typeof token.roleResolved === "undefined") { + token.roleResolved = true + } + + if (!token.roleResolved && (token.email || user?.email)) { + try { + const client = await clientPromise + const db = client.db() + const existingUser = await db + .collection("users") + .findOne({ email: token.email ?? user?.email }, { projection: { role: 1 } }) + + token.role = existingUser?.role ?? null + token.roleResolved = true + } catch (err) { + console.error("jwt role resolution error", err) + token.roleResolved = false + } + } + + if (typeof token.role === "undefined") token.role = null + + return token + }, + + async session({ session, token }) { + session.accessToken = token?.accessToken ?? null + session.user = session.user ?? {} + session.user.id = token?.id ?? token?.sub ?? null + session.user.role = token?.role ?? session.user.role ?? null + return session + }, + }, +} + +const handler = NextAuth(authOptions) +export { handler as GET, handler as POST } diff --git a/frontend/app/api/auth/login/route.js b/frontend/app/api/auth/login/route.js deleted file mode 100644 index db2c843..0000000 --- a/frontend/app/api/auth/login/route.js +++ /dev/null @@ -1,44 +0,0 @@ -import { NextRequest, NextResponse } from "next/server" - -// Mock user database (in production, use a real database) -const mockUsers = [ - { - id: "1", - name: "John Professor", - email: "professor@example.com", - password: "password123", - role: "professor", - }, - { - id: "student2", - name: "Jane Student", - email: "student@example.com", - password: "password123", - role: "student", - }, -] - -export async function POST(request) { - try { - const { email, password } = await request.json() - - if (!email || !password) { - return NextResponse.json({ message: "Email and password required" }, { status: 400 }) - } - - const user = mockUsers.find((u) => u.email === email && u.password === password) - - if (!user) { - return NextResponse.json({ message: "Invalid email or password" }, { status: 401 }) - } - - const { password: _, ...userWithoutPassword } = user - - return NextResponse.json({ - user: userWithoutPassword, - token: `token_${user.id}`, - }) - } catch (error) { - return NextResponse.json({ message: "Internal server error" }, { status: 500 }) - } -} diff --git a/frontend/app/api/auth/signup/route.js b/frontend/app/api/auth/signup/route.js deleted file mode 100644 index c71749c..0000000 --- a/frontend/app/api/auth/signup/route.js +++ /dev/null @@ -1,42 +0,0 @@ -import { NextRequest, NextResponse } from "next/server" - -// In-memory storage (in production, use a database) -const users = [] - -export async function POST(request) { - try { - const { name, email, password, role } = await request.json() - - // Validate input - if (!name || !email || !password || !role) { - return NextResponse.json({ message: "Missing required fields" }, { status: 400 }) - } - - // Check if user already exists - if (users.some((u) => u.email === email)) { - return NextResponse.json({ message: "Email already registered" }, { status: 400 }) - } - - // Create new user - const user = { - id: Date.now().toString(), - name, - email, - password, // In production, hash this! - role, - createdAt: new Date(), - } - - users.push(user) - - // Return user data (without password) - const { password: _, ...userWithoutPassword } = user - - return NextResponse.json({ - user: userWithoutPassword, - token: `token_${user.id}`, - }) - } catch (error) { - return NextResponse.json({ message: "Internal server error" }, { status: 500 }) - } -} diff --git a/frontend/app/api/classes/[id]/route.js b/frontend/app/api/classes/[id]/route.js new file mode 100644 index 0000000..297288f --- /dev/null +++ b/frontend/app/api/classes/[id]/route.js @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + +export async function GET(request, { params }) { + try { + const { id } = await params; + + console.log('Fetching class with ID:', id); + + const response = await fetch(`${API_URL}/api/classes/${id}`); + const data = await response.json(); + + console.log('Backend response:', { ok: response.ok, status: response.status, data }); + + if (!response.ok) { + return NextResponse.json( + { error: data.error || 'Failed to fetch class' }, + { status: response.status } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching class:', error); + return NextResponse.json( + { error: error.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/frontend/app/api/classes/join/route.js b/frontend/app/api/classes/join/route.js new file mode 100644 index 0000000..9d16a8e --- /dev/null +++ b/frontend/app/api/classes/join/route.js @@ -0,0 +1,42 @@ +import { NextResponse } from 'next/server'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + +export async function POST(request) { + try { + const body = await request.json(); + const { code, studentId } = body; + + if (!code || !studentId) { + return NextResponse.json( + { error: 'Missing required fields' }, + { status: 400 } + ); + } + + const response = await fetch(`${API_URL}/api/classes/join`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ code, studentId }), + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json( + { error: data.error || 'Failed to join class' }, + { status: response.status } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Error joining class:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/frontend/app/api/classes/route.js b/frontend/app/api/classes/route.js new file mode 100644 index 0000000..c02fc81 --- /dev/null +++ b/frontend/app/api/classes/route.js @@ -0,0 +1,81 @@ +import { NextResponse } from 'next/server'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + +export async function POST(request) { + try { + const body = await request.json(); + const { name, professorId, professorName } = body; + + if (!name || !professorId || !professorName) { + return NextResponse.json( + { error: 'Missing required fields' }, + { status: 400 } + ); + } + + const response = await fetch(`${API_URL}/api/classes/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name, professorId, professorName }), + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json( + { error: data.error || 'Failed to create class' }, + { status: response.status } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Error creating class:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +export async function GET(request) { + try { + const { searchParams } = new URL(request.url); + const professorId = searchParams.get('professorId'); + const studentId = searchParams.get('studentId'); + + let url = `${API_URL}/api/classes`; + + if (professorId) { + url = `${API_URL}/api/classes/professor/${professorId}`; + } else if (studentId) { + url = `${API_URL}/api/classes/student/${studentId}`; + } else { + return NextResponse.json( + { error: 'Missing professorId or studentId parameter' }, + { status: 400 } + ); + } + + const response = await fetch(url); + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json( + { error: data.error || 'Failed to fetch classes' }, + { status: response.status } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching classes:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/frontend/app/api/user/set-role/route.js b/frontend/app/api/user/set-role/route.js new file mode 100644 index 0000000..4945b5f --- /dev/null +++ b/frontend/app/api/user/set-role/route.js @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server" +import clientPromise from "@/lib/mongodb" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" + +const VALID_ROLES = new Set(["professor", "student"]) + +export async function POST(req) { + try { + const body = await req.json() + const incomingRole = typeof body?.role === "string" ? body.role.trim().toLowerCase() : "" + + if (!incomingRole) { + return NextResponse.json({ error: "role is required" }, { status: 400 }) + } + + if (!VALID_ROLES.has(incomingRole)) { + return NextResponse.json({ error: "invalid role" }, { status: 400 }) + } + + const session = await getServerSession(authOptions) + if (!session || !session.user || !session.user.email) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const client = await clientPromise + const db = client.db() + + const users = db.collection("users") + + const updateResult = await users.updateOne({ email: session.user.email }, { $set: { role: incomingRole } }) + + if (updateResult.matchedCount === 0) { + return NextResponse.json({ error: "user not found" }, { status: 404 }) + } + + const updatedUser = await users.findOne( + { email: session.user.email }, + { projection: { role: 1, email: 1, name: 1 } } + ) + + return NextResponse.json({ + ok: true, + updated: updateResult.modifiedCount > 0, + user: updatedUser, + }) + } catch (err) { + console.error("set-role error", err) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/frontend/app/auth/choose-role/page.jsx b/frontend/app/auth/choose-role/page.jsx new file mode 100644 index 0000000..c87ee46 --- /dev/null +++ b/frontend/app/auth/choose-role/page.jsx @@ -0,0 +1,98 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { useSession } from "next-auth/react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { useAuth } from "@/lib/auth-context" + +export default function ChooseRolePage() { + const { data: session, status, update } = useSession() + const { user, isLoading, needsRoleSelection, updateRole } = useAuth() + const router = useRouter() + const [error, setError] = useState(null) + const [submittingRole, setSubmittingRole] = useState(null) + + useEffect(() => { + if (status === "loading" || isLoading) return + + if (status === "unauthenticated" || !session?.user) { + router.replace("/auth/login") + return + } + + if (!needsRoleSelection && user?.role) { + router.replace(user.role === "professor" ? "/professor" : "/student") + } + }, [status, isLoading, needsRoleSelection, user, router, session]) + + const handleChoose = async (role) => { + if (submittingRole) return + setError(null) + setSubmittingRole(role) + + try { + const res = await fetch("/api/user/set-role", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ role }), + credentials: "include", + }) + + const data = await res.json().catch(() => ({})) + + if (!res.ok || !data?.ok) { + const message = data?.error ?? "Unable to save role. Please try again." + console.error("Failed to save role", message) + setError(message) + return + } + + await update({ role }) + updateRole(role) + + router.replace(role === "professor" ? "/professor" : "/student") + } catch (e) { + console.error(e) + setError("Something went wrong. Please try again.") + } finally { + setSubmittingRole(null) + } + } + + if (status === "loading" || isLoading) return null + + return ( +
+ +

Welcome{session?.user?.name ? `, ${session.user.name}` : ""}

+

Choose how you'd like to continue

+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+
+ ) +} diff --git a/frontend/app/auth/login/page.jsx b/frontend/app/auth/login/page.jsx index 155bca6..5eb4587 100644 --- a/frontend/app/auth/login/page.jsx +++ b/frontend/app/auth/login/page.jsx @@ -1,118 +1,62 @@ "use client" -import React from "react" - -import { useState } from "react" +import React, { useEffect, useState } from "react" import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" -import { Input } from "@/components/ui/input" +import { signIn, useSession } from "next-auth/react" +import { useAuth } from "@/lib/auth-context" export default function LoginPage() { const router = useRouter() - const [formData, setFormData] = useState({ - email: "", - password: "", - }) - const [error, setError] = useState("") - const [loading, setLoading] = useState(false) - - const handleChange = (e) => { - const { name, value } = e.target - setFormData((prev) => ({ ...prev, [name]: value })) - } + const { status } = useSession() + const { user, isLoading, needsRoleSelection } = useAuth() + const [isSigningIn, setIsSigningIn] = useState(false) - const handleSubmit = async (e) => { - e.preventDefault() - setError("") + useEffect(() => { + if (status === "loading" || isLoading) return + if (status !== "authenticated" || !user) return - if (!formData.email || !formData.password) { - setError("Please fill in all fields") + if (needsRoleSelection || !user.role) { + router.replace("/auth/choose-role") return } - setLoading(true) + router.replace(user.role === "professor" ? "/professor" : "/student") + }, [status, isLoading, user, needsRoleSelection, router]) + const handleGoogleSignIn = async () => { + if (isSigningIn) return + setIsSigningIn(true) try { - const response = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }) - - const data = await response.json() - - if (!response.ok) { - setError(data.message || "Login failed") - return - } - - localStorage.setItem("user", JSON.stringify(data.user)) - localStorage.setItem("token", data.token) - - if (data.user?.role === "professor") { - router.push("/professor") - } else if (data.user?.role === "student") { - router.push("/student") - } else { - console.log("Unknown role:", data.user?.role) - setError("Invalid user role") - } + await signIn("google", { callbackUrl: `${window.location.origin}/auth/choose-role` }) } catch (err) { - console.log("Login error:", err) - setError("An error occurred. Please try again.") + console.error("Google sign-in failed", err) } finally { - setLoading(false) + setIsSigningIn(false) } } return ( -
- +
+

Sign In

-

Welcome back to EduConnect

- -
-
- - -
- -
- - -
- - {error &&
{error}
} +

+ Welcome to EduConnect +

+
- +

- Don't have an account?{" "} - - Sign Up - + New here? Just sign in with your Google account — we’ll set things up!

diff --git a/frontend/app/auth/signup/page.jsx b/frontend/app/auth/signup/page.jsx index 8168646..48039dc 100644 --- a/frontend/app/auth/signup/page.jsx +++ b/frontend/app/auth/signup/page.jsx @@ -1,170 +1,41 @@ "use client" import React from "react" - -import { useState, useEffect } from "react" +import { useEffect } from "react" import { useRouter, useSearchParams } from "next/navigation" import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" -import { Input } from "@/components/ui/input" +import { signIn } from "next-auth/react" export default function SignUpPage() { const router = useRouter() const searchParams = useSearchParams() const role = searchParams.get("role") - const [formData, setFormData] = useState({ - name: "", - email: "", - password: "", - confirmPassword: "", - }) - const [error, setError] = useState("") - const [loading, setLoading] = useState(false) - useEffect(() => { if (!role) { router.push("/") } }, [role, router]) - const handleChange = (e) => { - const { name, value } = e.target - setFormData((prev) => ({ ...prev, [name]: value })) - } - - const handleSubmit = async (e) => { - e.preventDefault() - setError("") - - if (!formData.name || !formData.email || !formData.password) { - setError("Please fill in all fields") - return - } - - if (formData.password !== formData.confirmPassword) { - setError("Passwords do not match") - return - } - - if (formData.password.length < 6) { - setError("Password must be at least 6 characters") - return - } - - setLoading(true) - - try { - const response = await fetch("/api/auth/signup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: formData.name, - email: formData.email, - password: formData.password, - role: role, - }), - }) - - const data = await response.json() - - if (!response.ok) { - setError(data.message || "Sign up failed") - return - } - - // Store user data in localStorage - localStorage.setItem("user", JSON.stringify(data.user)) - localStorage.setItem("token", data.token) + if (!role) return null - // Redirect based on role - if (role === "professor") { - router.push("/professor") - } else { - router.push("/student") - } - } catch (err) { - setError("An error occurred. Please try again.") - } finally { - setLoading(false) - } + const handleGoogleSignIn = async () => { + await signIn("google", { callbackUrl: `${window.location.origin}/auth/choose-role` }) } - if (!role) return null - return (
- -

- Sign Up as {role === "professor" ? "Professor" : "Student"} -

-

Create your account to get started

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- - {error &&
{error}
} + +

Sign Up as {role === "professor" ? "Professor" : "Student"}

+

Use Google to create and sign in to your account

- - +

- Already have an account?{" "} - - Sign In - + Already have an account? Sign In

diff --git a/frontend/app/globals.css b/frontend/app/globals.css index dc98be7..50e7e49 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,12 +1,18 @@ @import "tailwindcss"; @import "tw-animate-css"; +@font-face { + font-family: "AssociateSansBold"; + src: url("../public/assets/fonts/AssociateSansBold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} @custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); + --font-sans: "AssociateSansBold"; --font-mono: var(--font-geist-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); diff --git a/frontend/app/layout.js b/frontend/app/layout.js index 7371b08..0245361 100644 --- a/frontend/app/layout.js +++ b/frontend/app/layout.js @@ -1,6 +1,6 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import { AuthProvider } from "@/lib/auth-context" +import Providers from "@/components/providers" const geistSans = Geist({ variable: "--font-geist-sans", @@ -23,7 +23,7 @@ export default function RootLayout({ children }) { - {children} + {children} ); diff --git a/frontend/app/page.js b/frontend/app/page.js index 019381c..41baa0f 100644 --- a/frontend/app/page.js +++ b/frontend/app/page.js @@ -1,109 +1,32 @@ "use client" -import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" -import { Card } from "@/components/ui/card" -import { useAuth } from "@/lib/auth-context" export default function LandingPage() { const router = useRouter() - const { user, isLoading } = useAuth() - const [selectedRole, setSelectedRole] = useState(null) - useEffect(() => { - if (!isLoading && user) { - if (user.role === "professor") { - router.push("/professor") - } else { - router.push("/student") - } - } - }, [user, isLoading, router]) - - if (isLoading) { - return ( -
-
-
Loading...
-
-
- ) - } - - const handleRoleSelect = () => { - setSelectedRole(role) + const handleStudentLogin = () => { + router.push("/student") } - const handleSignUp = () => { - if (selectedRole === "professor") { - router.push("/auth/signup?role=professor") - } else if (selectedRole === "student") { - router.push("/auth/signup?role=student") - } + const handleProfessorLogin = () => { + router.push("/professor") } return ( -
-
- {/* Header */} -
-

EduConnect

-

Learning made simple for everyone

-
- - {/* Role Selection */} -
- {/* Student Card */} - handleRoleSelect("student")} - > -
-
👨‍🎓
-

I'm a Student

-

Join classes, access notes, and complete assignments

-
{selectedRole === "student" && "✓ Selected"}
-
-
- - {/* Professor Card */} - handleRoleSelect("professor")} - > -
-
👨‍🏫
-

I'm a Professor

-

Create classes, manage students, and share resources

-
{selectedRole === "professor" && "✓ Selected"}
-
-
-
- - {/* Sign Up Button */} -
- -
- - {/* Footer */} -
-

- Already have an account?{" "} - - Sign In - -

+
+
+
+

WELCOME

+
+ +
diff --git a/frontend/app/professor/class/[id]/page.jsx b/frontend/app/professor/class/[id]/page.jsx index 17ecece..82a1194 100644 --- a/frontend/app/professor/class/[id]/page.jsx +++ b/frontend/app/professor/class/[id]/page.jsx @@ -2,60 +2,94 @@ import { useEffect, useState } from "react" import { useRouter, useParams } from "next/navigation" -import { useAuth } from "@/lib/auth-context" -import { dummyClasses } from "@/lib/dummy-data" +import { classAPI } from "@/lib/api/classes" import { Button } from "@/components/ui/button" import ProfessorNav from "@/components/professor-nav" import ClassTabs from "@/components/professor/class-tabs" +import { useAuth } from "@/lib/auth-context" export default function ProfessorClassPage() { const router = useRouter() const params = useParams() - const { user, isLoading } = useAuth() const classId = params.id const [classData, setClassData] = useState(null) const [activeTab, setActiveTab] = useState("overview") + const [loading, setLoading] = useState(true) + const { user, isLoading, needsRoleSelection } = useAuth() useEffect(() => { - if (!isLoading && (!user || user.role !== "professor")) { - router.push("/auth/login") + if (isLoading) return + + if (!user) { + router.replace("/auth/login") + return + } + + if (needsRoleSelection || !user.role) { + router.replace("/auth/choose-role") return } - if (classId) { - const cls = dummyClasses.find((c) => c.id === classId) - if (cls && cls.professorId === user?.id) { - setClassData(cls) - } else { - router.push("/professor") + + if (user.role !== "professor") { + router.replace(user.role === "student" ? "/student" : "/") + } + }, [user, isLoading, needsRoleSelection, router]) + + useEffect(() => { + async function fetchClass() { + if (!classId || !user || user.role !== "professor") return + + try { + setLoading(true) + const cls = await classAPI.getById(classId) + + const isOwner = cls && (cls.professorId === user.id || cls.professorId === user.email) + + if (isOwner) { + setClassData(cls) + } else { + router.replace("/professor") + } + } catch (error) { + console.error("Error fetching class:", error) + router.replace("/professor") + } finally { + setLoading(false) } } - }, [classId, user, isLoading, router]) - if (isLoading || !classData) { + fetchClass() + }, [classId, user, router]) + + if (isLoading || needsRoleSelection || !user || user.role !== "professor") { + return
Loading...
+ } + + if (loading || !classData) { return
Loading...
} return ( -
- +
+

{classData.name}

- Class Code: {classData.code} + Class Code: {classData.code}

- +
) diff --git a/frontend/app/professor/page.jsx b/frontend/app/professor/page.jsx index b092860..9be79d7 100644 --- a/frontend/app/professor/page.jsx +++ b/frontend/app/professor/page.jsx @@ -1,51 +1,62 @@ "use client" import { useEffect, useState } from "react" -import { useRouter } from "next/navigation" -import { useAuth } from "@/lib/auth-context" import { Button } from "@/components/ui/button" import ProfessorNav from "@/components/professor-nav" import ClassList from "@/components/professor/class-list" import CreateClassModal from "@/components/professor/create-class-modal" +import { useAuth } from "@/lib/auth-context" +import { useRouter } from "next/navigation" export default function ProfessorDashboard() { const router = useRouter() - const { user, isLoading } = useAuth() + const { user, isLoading, needsRoleSelection } = useAuth() const [showCreateModal, setShowCreateModal] = useState(false) const [refreshKey, setRefreshKey] = useState(0) useEffect(() => { - if (!isLoading && (!user || user.role !== "professor")) { - router.push("/auth/login") + if (isLoading) return + + if (!user) { + router.replace("/auth/login") + return } - }, [user, isLoading, router]) - if (isLoading) { - return
Loading...
- } + if (needsRoleSelection || !user.role) { + router.replace("/auth/choose-role") + return + } - if (!user || user.role !== "professor") { - return null - } + if (user.role !== "professor") { + router.replace(user.role === "student" ? "/student" : "/") + } + }, [user, isLoading, needsRoleSelection, router]) const handleClassCreated = () => { setShowCreateModal(false) setRefreshKey((prev) => prev + 1) } + if (isLoading || !user || user.role !== "professor" || needsRoleSelection) { + return
Loading...
+ } + return ( -
+
-

My Classes

+

My Classes

Manage your classes and students

+

Signed in as {user?.name ?? user?.email}

+
+
+
-
diff --git a/frontend/app/student/class/[id]/page.jsx b/frontend/app/student/class/[id]/page.jsx index dc2e3f6..bf85590 100644 --- a/frontend/app/student/class/[id]/page.jsx +++ b/frontend/app/student/class/[id]/page.jsx @@ -2,51 +2,87 @@ import { useEffect, useState } from "react" import { useRouter, useParams } from "next/navigation" -import { useAuth } from "@/lib/auth-context" -import { dummyClasses } from "@/lib/dummy-data" +import { classAPI } from "@/lib/api/classes" import { Button } from "@/components/ui/button" import StudentNav from "@/components/student-nav" import StudentClassTabs from "@/components/student/class-tabs" +import { useAuth } from "@/lib/auth-context" export default function StudentClassPage() { const router = useRouter() const params = useParams() - const { user, isLoading } = useAuth() const classId = params.id const [classData, setClassData] = useState(null) const [activeTab, setActiveTab] = useState("overview") + const [loading, setLoading] = useState(true) + const { user, isLoading, needsRoleSelection } = useAuth() useEffect(() => { - if (!isLoading && (!user || user.role !== "student")) { - router.push("/auth/login") + if (isLoading) return + + if (!user) { + router.replace("/auth/login") + return + } + + if (needsRoleSelection || !user.role) { + router.replace("/auth/choose-role") return } - if (classId) { - const cls = dummyClasses.find((c) => c.id === classId) - if (cls && cls.students.includes(user?.id || "")) { - setClassData(cls) - } else { - router.push("/student") + if (user.role !== "student") { + router.replace(user.role === "professor" ? "/professor" : "/") + } + }, [user, isLoading, needsRoleSelection, router]) + + useEffect(() => { + async function fetchClass() { + if (!classId || !user || user.role !== "student") return + + try { + setLoading(true) + const cls = await classAPI.getById(classId) + + const studentIdentifiers = [user.id, user.email].filter(Boolean) + const belongsToStudent = + cls && cls.students && studentIdentifiers.some((identifier) => cls.students.includes(identifier)) + + if (belongsToStudent) { + setClassData(cls) + } else { + router.replace("/student") + } + } catch (error) { + console.error("Error fetching class:", error) + router.replace("/student") + } finally { + setLoading(false) } } - }, [classId, user, isLoading, router]) - if (isLoading || !classData) { + fetchClass() + }, [classId, user, router]) + + if (isLoading || needsRoleSelection || !user || user.role !== "student") { return
Loading...
} + if (loading || !classData) { + return
Loading class...
+ } + + return ( -
- +
+
@@ -54,7 +90,7 @@ export default function StudentClassPage() {

Professor: {classData.professorName}

- +
) diff --git a/frontend/app/student/page.jsx b/frontend/app/student/page.jsx index 2b2f382..a7bced2 100644 --- a/frontend/app/student/page.jsx +++ b/frontend/app/student/page.jsx @@ -1,40 +1,48 @@ "use client" import { useEffect, useState } from "react" -import { useRouter } from "next/navigation" -import { useAuth } from "@/lib/auth-context" import { Button } from "@/components/ui/button" import StudentNav from "@/components/student-nav" import StudentClassList from "@/components/student/class-list" import JoinClassModal from "@/components/student/join-class-modal" +import { useAuth } from "@/lib/auth-context" +import { useRouter } from "next/navigation" export default function StudentDashboard() { const router = useRouter() - const { user, isLoading } = useAuth() + const { user, isLoading, needsRoleSelection } = useAuth() const [showJoinModal, setShowJoinModal] = useState(false) const [refreshKey, setRefreshKey] = useState(0) useEffect(() => { - if (!isLoading && (!user || user.role !== "student")) { - router.push("/auth/login") + if (isLoading) return + + if (!user) { + router.replace("/auth/login") + return } - }, [user, isLoading, router]) - if (isLoading) { - return
Loading...
- } + if (needsRoleSelection || !user.role) { + router.replace("/auth/choose-role") + return + } - if (!user || user.role !== "student") { - return null - } + if (user.role !== "student") { + router.replace(user.role === "professor" ? "/professor" : "/") + } + }, [user, isLoading, needsRoleSelection, router]) const handleClassJoined = () => { setShowJoinModal(false) setRefreshKey((prev) => prev + 1) } + if (isLoading || !user || user.role !== "student" || needsRoleSelection) { + return
Loading...
+ } + return ( -
+
@@ -42,20 +50,19 @@ export default function StudentDashboard() {

My Classes

Access your classes and materials

+

Signed in as {user?.name ?? user?.email}

+
+
+
-
{showJoinModal && ( - setShowJoinModal(false)} - onClassJoined={handleClassJoined} - studentId={user.id} - /> + setShowJoinModal(false)} onClassJoined={handleClassJoined} studentId={user.id} /> )}
diff --git a/frontend/clear-test-classes.js b/frontend/clear-test-classes.js new file mode 100644 index 0000000..93e8f9d --- /dev/null +++ b/frontend/clear-test-classes.js @@ -0,0 +1,24 @@ +const storage = { + loadClassesFromStorage() { + if (typeof window === 'undefined') return []; + const stored = localStorage.getItem('educonnect_classes'); + return stored ? JSON.parse(stored) : []; + }, + saveClassesToStorage(classes) { + if (typeof window === 'undefined') return; + localStorage.setItem('educonnect_classes', JSON.stringify(classes)); + } +}; + +let classes = storage.loadClassesFromStorage(); +console.log('Current classes:', classes); + +const filteredClasses = classes.filter(cls => { + const name = cls.name?.toLowerCase() || ''; + return !name.includes('aii') && !name.includes('dfg') && name !== 'test'; +}); + +console.log('Filtered classes:', filteredClasses); + +storage.saveClassesToStorage(filteredClasses); +console.log('Test classes removed!'); diff --git a/frontend/components/professor-nav.jsx b/frontend/components/professor-nav.jsx index 28d64b1..3ca7278 100644 --- a/frontend/components/professor-nav.jsx +++ b/frontend/components/professor-nav.jsx @@ -1,29 +1,28 @@ "use client" import { useAuth } from "@/lib/auth-context" -import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" -export default function ProfessorNav() { +export default function ProfessorNav({ professor }) { const { user, logout } = useAuth() - const router = useRouter() - const handleLogout = () => { - logout() - router.push("/") + const handleLogout = async () => { + await logout({ callbackUrl: "/" }) } + const display = professor ?? user + return ( -