Skip to content
Merged
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
5 changes: 0 additions & 5 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,6 @@ mongoose.connection.on('error', (err) => {
console.error('MongoDB connection error:', err);
});

app.get('/', (req, res) => {
res.send('Server is running!')
});


//------------------ ENDPOINTS ------------------//


Expand Down
28 changes: 25 additions & 3 deletions api/routes/class-routes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from "express";
import mongoose from "mongoose";
import User from "../schemas/User.js";
import Class from '../schemas/Class.js';
import { validateInput } from "../../src/utils/backend/validate-utils.js";

Expand Down Expand Up @@ -47,6 +48,24 @@ router.get('/classes/:id', async (req, res) => {
}
})

// Get full details of students in class's roster
router.get('/class-students/:id', async (req, res) => {
try {
const { id } = req.params;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}

const studentDetails = await Class.findById(id)
.select("roster")
.populate("roster")
res.json(studentDetails.roster);
} catch (err) {
res.status(500).send(err);
}
})

// Create Class
router.post('/classes', async (req, res) => {
try {
Expand Down Expand Up @@ -157,7 +176,10 @@ router.delete('/classes/:id', async (req, res) => {
await Promise.all(
deletedClass.roster.map(studentId =>
User.findByIdAndUpdate(studentId, { $pull: { enrolledClasses: id } })
.catch(err => console.error(`Failed to update student ${studentId}:`, err)) // TODO: throw error?
.catch(err => {
console.error(`Failed to remove class from student ${studentId}'s enrolled classes:`, err)
throw err;
})
)
);

Expand All @@ -172,7 +194,7 @@ router.delete('/classes/:id', async (req, res) => {
});


/* CONVERSATION RELATED ENDPOINTS */
/* CONVERSATION SPECIFIC ENDPOINTS */

// Get Conversation classes
router.get("/conversations", async (req, res) => {
Expand Down Expand Up @@ -323,7 +345,7 @@ router.delete('/conversations/:id', async (req, res) => {
});


/* IELTS RELATED ENDPOINTS */
/* IELTS SPECIFIC ENDPOINTS */

// Get IELTS classes
router.get("/ielts", async (req, res) => {
Expand Down
61 changes: 45 additions & 16 deletions api/routes/user-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,42 @@ router.get('/user', async (req, res) => {
}
})


// Get Student's classes by ID
router.get('/students-classes/:id', async (req, res) => {
// Edit user
router.put('/user/:id', async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}

const data = await User.findOne({ _id: id }, { enrolledClasses: 1, _id: 0 });
res.json(data);
} catch (err) {
res.status(500).send(err);
}
})
const originalUser = await User.findById(id);

// Edit user
router.put('/user/:id', async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
if (!originalUser) {
return res.status(404).json({ message: 'User not found' });
}

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid ID' });
// add new email address to Clerk and delete old one
if (originalUser.email !== updates.email) {
await clerkClient.emailAddresses.createEmailAddress({
userId: originalUser.clerkId,
emailAddress: updates.email,
verified: true,
primary: true
});

await clerkClient.users
.getUser(originalUser.clerkId)
.then(async (data) => {
const userEmailData = data.emailAddresses.find(
(emailData) => emailData.emailAddress === originalUser.email
);
return userEmailData.id;
})
.then(
async (userEmailId) => await clerkClient.emailAddresses.deleteEmailAddress(userEmailId)
)
}

const updatedUser = await User.findByIdAndUpdate(
Expand Down Expand Up @@ -151,4 +162,22 @@ router.delete('/user/:id', async (req, res) => {
}
});

// Get student's classes full details
router.get('/students-classes/:id', async (req, res) => {
try {
const { id } = req.params;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}

const classDetails = await User.findById(id)
.select('enrolledClasses')
.populate('enrolledClasses')
res.json(classDetails.enrolledClasses); // return array of class objects
} catch (err) {
res.status(500).send(err);
}
})

export default router;
2 changes: 1 addition & 1 deletion api/schemas/Class.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ClassSchema = new Schema({
image: { type: String, required: true, default: "level_img_0.webp" },
link: { type: String, default: "" },
schedule: { type: [ScheduleSchema], default: [] },
roster: { type: [Schema.Types.ObjectId], default: [] },
roster: { type: [Schema.Types.ObjectId], default: [], ref: "User" },
isEnrollmentOpen: { type: Boolean, default: true }
}, { collection: 'classes' });

Expand Down
2 changes: 1 addition & 1 deletion api/schemas/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const UserSchema = new Schema({
privilege: { type: String, default: "student", enum: ["admin", "instructor", "student"] },
clerkId: { type: String, required: true },
creationDate: { type: Date, default: Date.now },
enrolledClasses: { type: [Schema.Types.ObjectId], default: [] }
enrolledClasses: { type: [Schema.Types.ObjectId], default: [], ref: "Class" }
}, { collection: 'users' });

const User = mongoose.model("User", UserSchema);
Expand Down
Binary file removed public/images/blue_mountains.png
Binary file not shown.
11 changes: 3 additions & 8 deletions src/pages/dashboards/InstructorEditClass.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { useContext, useEffect, useState } from "react";
import { UserContext } from '@/contexts/UserContext.jsx';
import { useLocation, useParams } from 'wouter';
import { useAuth } from '@clerk/clerk-react';
import { getClassById } from "@/wrappers/class-wrapper";
import { getClassById, getClassStudents } from "@/wrappers/class-wrapper";
import FormInput from '@/components/Form/FormInput'
import Button from '@/components/Button/Button';
import { updateClass } from '@/wrappers/class-wrapper.js';
import { getUser } from '@/wrappers/user-wrapper.js';
import BackButton from "@/components/Button/BackButton";
import UserItem from "@/components/UserItem";
import SkeletonUser from "@/components/Skeletons/SkeletonUser";
Expand Down Expand Up @@ -41,12 +40,8 @@ const InstructorEditClass = () => {
try {
const data = await getClassById(params.id);
setLink(data.link || '');
const students = await Promise.all(
data.roster.map(async (studentId) => {
const studentRes = await getUser(`_id=${studentId}`);
return studentRes.data
})
);
const students = await getClassStudents(data._id);
console.log(students)
setStudents(students);
setAllowRender(true);
} catch (error) {
Expand Down
14 changes: 3 additions & 11 deletions src/pages/dashboards/StudentPortal.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useContext, useEffect, useState } from 'react';
import { getClassById } from '@/wrappers/class-wrapper';
import { getStudentsClasses } from '@/wrappers/user-wrapper';
import { updateUser } from '@/wrappers/user-wrapper';
import { updateUser, getStudentsClasses } from '@/wrappers/user-wrapper';
import { UserContext } from '@/contexts/UserContext.jsx';
import { useLocation } from 'wouter';
import { useAuth } from '@clerk/clerk-react'
Expand Down Expand Up @@ -45,14 +43,8 @@ const StudentPortal = () => {
useEffect(() => {
const fetchData = async () => {
if (user) {
const response = await getStudentsClasses(user?._id);
const classes = await Promise.all(
response.enrolledClasses.map(async (classID) => {
const classResponse = await getClassById(classID);
return classResponse; // Return the class details
})
);
setClasses(classes);
const userClasses = await getStudentsClasses(user._id);
setClasses(userClasses);
setAllowRender(true);
}
};
Expand Down
14 changes: 3 additions & 11 deletions src/pages/dashboards/admin/AdminStudents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { UserContext } from '@/contexts/UserContext.jsx';
import { useLocation, Link } from 'wouter';
import { useAuth } from '@clerk/clerk-react';
import { IoPersonOutline } from "react-icons/io5";
import { getUsers, getStudentsForExport } from '@/wrappers/user-wrapper.js';
import { getClassById } from '@/wrappers/class-wrapper';
import { getUsers, getStudentsClasses, getStudentsForExport } from '@/wrappers/user-wrapper.js';
import { getLevels } from '@/wrappers/level-wrapper';
import { getStudentsClasses } from '@/wrappers/user-wrapper';
import Unauthorized from "@/pages/Unauthorized";
import Dropdown from '@/components/Dropdown/Dropdown';
import Button from '@/components/Button/Button';
Expand Down Expand Up @@ -45,16 +43,10 @@ const AdminStudents = () => {
const userData = await getUsers();
const students = userData.data.filter((user) => user.privilege === "student");

// replace enrolled class ids with full class info
const studentsWithClasses = await Promise.all(
students.map(async (student) => {
const classRef = await getStudentsClasses(student._id);
const classes = await Promise.all(
(classRef.enrolledClasses || []).map(async (classID) => {
const classData = await getClassById(classID);
return classData;
})
);

const classes = await getStudentsClasses(student._id);
return { ...student, enrolledClasses: classes };
})
);
Expand Down
22 changes: 9 additions & 13 deletions src/pages/dashboards/admin/editPages/EditClass.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import DayDropdown from '@/components/Dropdown/DayDropdown';
import UserItem from "@/components/UserItem";
import Alert from '@/components/Alert';
import { IoAdd, IoTrashBinOutline, IoPersonOutline } from "react-icons/io5";
import { updateClass, deleteClass, getClasses, getClassById } from '@/wrappers/class-wrapper';
import { getUser } from '@/wrappers/user-wrapper';
import { updateClass, deleteClass, getClassById, getClassStudents } from '@/wrappers/class-wrapper';
import Unauthorized from "@/pages/Unauthorized";
import SkeletonUser from "@/components/Skeletons/SkeletonUser";
import useDelayedSkeleton from '@/hooks/useDelayedSkeleton';
Expand Down Expand Up @@ -74,12 +73,7 @@ const EditClass = () => {
schedule: classObj.schedule
}))
}
const students = await Promise.all(
classObj.roster.map(async (studentId) => {
const studentRes = await getUser(`_id=${studentId}`);
return studentRes.data
})
);
const students = await getClassStudents(classObj._id);
setStudents(students);
setAllowRender(true);
}
Expand Down Expand Up @@ -187,11 +181,13 @@ const EditClass = () => {
<h1 className="font-extrabold mb-2">Edit Class</h1>
<h3 className="font-light text-base sm:text-lg">Edit class and student information</h3>
</div>
<div className="w-1/3">
<h2 className="mb-2">Class Preview</h2>
<ClassPreview
classObj={classData}
/>
<div className="space-y-3">
<h2>Class Preview</h2>
<div className="w-full md:w-1/3">
<ClassPreview
classObj={classData}
/>
</div>
</div>
<form onSubmit={handleEditClass} className="w-full lg:w-2/3">
<div className="grid grid-cols-3 gap-x-10 w-full mb-6">
Expand Down
24 changes: 10 additions & 14 deletions src/pages/dashboards/admin/editPages/EditConversation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import Alert from '@/components/Alert';
import SupplementaryClassPreview from "@/components/Class/SupplementaryClassPreview";
import ImagePicker from "@/components/ImagePicker";
import { levelImgs } from "@/constants/images";
import { getConversationById } from "@/wrappers/conversation-wrapper";
import { updateConversation, deleteConversation } from '@/wrappers/conversation-wrapper.js';
import { getUser } from '@/wrappers/user-wrapper';
import { getClassStudents } from '@/wrappers/class-wrapper';
import { getConversationById, updateConversation, deleteConversation } from '@/wrappers/conversation-wrapper.js';
import { IoAdd, IoTrashBinOutline, IoPersonOutline } from "react-icons/io5";
import Unauthorized from "@/pages/Unauthorized";
import SkeletonUser from "@/components/Skeletons/SkeletonUser";
Expand Down Expand Up @@ -78,12 +77,7 @@ const EditConversation = () => {
schedule: data.schedule
}))
}
const students = await Promise.all(
data.roster.map(async (studentId) => {
const studentRes = await getUser(`_id=${studentId}`);
return studentRes.data
})
);
const students = await getClassStudents(data._id);
setStudents(students);
setAllowRender(true);
} catch (error) {
Expand Down Expand Up @@ -192,11 +186,13 @@ const EditConversation = () => {
<h1 className="font-extrabold">Edit Conversation Class</h1>
<h3 className="font-light text-base sm:text-lg">Edit conversation class and student information</h3>
</div>
<div className="w-1/3 space-y-3">
<h2 className="mb-2">Conversation Class Preview</h2>
<SupplementaryClassPreview
cls={conversationData}
/>
<div className="space-y-3">
<h2>Conversation Class Preview</h2>
<div className="w-full md:w-1/3">
<SupplementaryClassPreview
cls={conversationData}
/>
</div>
<Button label="Select Image" onClick={() => setIsOpenImagePicker(true)} />
</div>
<form onSubmit={handleEditConversation} className="w-full lg:w-2/3">
Expand Down
21 changes: 9 additions & 12 deletions src/pages/dashboards/admin/editPages/EditIelts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import Alert from '@/components/Alert';
import SupplementaryClassPreview from "@/components/Class/SupplementaryClassPreview";
import ImagePicker from "@/components/ImagePicker";
import { levelImgs } from "@/constants/images";
import { getClassStudents } from '@/wrappers/class-wrapper';
import { updateIelts, deleteIelts } from '@/wrappers/ielts-wrapper.js';
import { getUser } from '@/wrappers/user-wrapper';
import { IoAdd, IoTrashBinOutline, IoPersonOutline } from "react-icons/io5";
import Unauthorized from "@/pages/Unauthorized";
import SkeletonUser from "@/components/Skeletons/SkeletonUser";
Expand Down Expand Up @@ -78,12 +78,7 @@ const EditIelts = () => {
schedule: data.schedule
}))
}
const students = await Promise.all(
data.roster.map(async (studentId) => {
const studentRes = await getUser(`_id=${studentId}`);
return studentRes.data
})
);
const students = await getClassStudents(data._id);
setStudents(students);
setAllowRender(true);
} catch (error) {
Expand Down Expand Up @@ -192,11 +187,13 @@ const EditIelts = () => {
<h2 className="font-extrabold">Edit IELTS Class</h2>
<h3 className="font-light text-base sm:text-lg">Edit IELTS class and student information</h3>
</div>
<div className="w-1/3 space-y-3">
<h3 className="mb-2">Conversation Class Preview</h3>
<SupplementaryClassPreview
cls={ieltsData}
/>
<div className="space-y-3">
<h3>IELTS Class Preview</h3>
<div className="w-full md:w-1/3">
<SupplementaryClassPreview
cls={ieltsData}
/>
</div>
<Button label="Select Image" onClick={() => setIsOpenImagePicker(true)} />
</div>
<form onSubmit={handleEditIelts} className="w-full lg:w-2/3">
Expand Down
Loading