diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c803314bf..97f939973 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,48 +2,48 @@ // https://github.com/microsoft/vscode-dev-containers/blob/main/containers/javascript-node-postgres/.devcontainer/devcontainer.json { - "name": "Node.js & PostgreSQL", - "dockerComposeFile": "docker-compose.yml", - "service": "app", - "workspaceFolder": "/workspace", - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": "true", - "username": "node", - "upgradePackages": "true" - }, - "ghcr.io/devcontainers/features/node:1": { - "version": "none" - }, - "ghcr.io/devcontainers/features/git:1": { - "version": "latest", - "ppa": "false" - } - }, + "name": "Node.js & PostgreSQL", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "node", + "upgradePackages": "true" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "none" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + } + }, - // Configure tool-specific properties. - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "ms-vsliveshare.vsliveshare" - ] - } - }, + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-vsliveshare.vsliveshare" + ] + } + }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // This can be used to network with other containers or with the host. - "forwardPorts": [3000, 3001, 5432, 5555], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // This can be used to network with other containers or with the host. + "forwardPorts": [3000, 3001, 5432, 5555], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "yarn install", + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", - // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "node", + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "node", - "postCreateCommand": "./.devcontainer/postCreateCommand.sh", - "postAttachCommand": "./codespace-instructions.sh" + "postCreateCommand": "./.devcontainer/postCreateCommand.sh", + "postAttachCommand": "./codespace-instructions.sh" } diff --git a/README.md b/README.md index 4017f8d92..a14fcc99d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ If you have used GitHub Codespaces in other projects, doing the same in freeCode Within freeCodeCamp Classroom, GitHub Codespaces is on par with Gitpod so that you can use either. -This [video](https://www.loom.com/share/37dcb9555ad642618d82619277daaa38?sid=c17189b2-5798-44c9-8b74-38749f3578e1) walks through the setup process on Github Codespaces. Note that this video was recorded on Feb 10, 2025. It is not guaranteed to be up to date with any new setup instructions added after that date. +This [video](https://www.loom.com/share/37dcb9555ad642618d82619277daaa38?sid=c17189b2-5798-44c9-8b74-38749f3578e1) walks through the setup process on Github Codespaces. Note that this video was recorded on Feb 10, 2025. It is not guaranteed to be up to date with any new setup instructions added after that date. ### Optional - GitPod Dev Environment diff --git a/__tests__/components/adminTable.test.jsx b/__tests__/components/adminTable.test.jsx index 2e0ad2483..183b1a5e1 100644 --- a/__tests__/components/adminTable.test.jsx +++ b/__tests__/components/adminTable.test.jsx @@ -2,50 +2,48 @@ import AdminTable from '../../components/adminTable'; import React from 'react'; import renderer from 'react-test-renderer'; const sampleColumns = [ - { - name: 'Name', - selector: row => row.name - }, - { - name: 'Email', - selector: row => row.userEmail - }, - { - name: 'Role', - selector: row => row.role - }, - { - name: 'Actions', - selector: row => row.adminActions - } - ]; -const sampleUsers=[ - { - id: 1, - name: "Hamzat Victor", - email: "oluwaborihamzat@gmail.com", - role: "ADMIN" - }, - { - id: 2, - name: "Alade Christopher", - email: "aladechristoph@gmail.com", - role: "TEACHER" - }, - { - id: 3, - name: "Ayomide onifade", - email: "Jangulabi@gmail.com", - role: "TEACHER" - }, -] + { + name: 'Name', + selector: row => row.name + }, + { + name: 'Email', + selector: row => row.userEmail + }, + { + name: 'Role', + selector: row => row.role + }, + { + name: 'Actions', + selector: row => row.adminActions + } +]; +const sampleUsers = [ + { + id: 1, + name: 'Hamzat Victor', + email: 'oluwaborihamzat@gmail.com', + role: 'ADMIN' + }, + { + id: 2, + name: 'Alade Christopher', + email: 'aladechristoph@gmail.com', + role: 'TEACHER' + }, + { + id: 3, + name: 'Ayomide onifade', + email: 'Jangulabi@gmail.com', + role: 'TEACHER' + } +]; describe('AdminTable', () => { it('displays 3 rows of data with expected column names: name, email, role, action', () => { const tree = renderer - .create( - - ) + .create() .toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/__tests__/components/dashtable_v2.test.jsx b/__tests__/components/dashtable_v2.test.jsx index 3563f05e8..12cb3e250 100644 --- a/__tests__/components/dashtable_v2.test.jsx +++ b/__tests__/components/dashtable_v2.test.jsx @@ -35,4 +35,4 @@ describe('GlobalDashboardTable', () => { expect(getTimeSpy).toHaveBeenCalled(); expect(container).toMatchSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/__tests__/components/layout.test.jsx b/__tests__/components/layout.test.jsx index f241c0624..676e2aec9 100644 --- a/__tests__/components/layout.test.jsx +++ b/__tests__/components/layout.test.jsx @@ -2,7 +2,6 @@ import Layout from '../../components/layout'; import React from 'react'; import renderer from 'react-test-renderer'; - describe('Layout', () => { it('displays expected children', () => { const tree = renderer diff --git a/__tests__/components/modal.test.jsx b/__tests__/components/modal.test.jsx index eb5022b86..be6a037ab 100644 --- a/__tests__/components/modal.test.jsx +++ b/__tests__/components/modal.test.jsx @@ -1,6 +1,6 @@ import Modal from '../../components/modal'; import React from 'react'; -import renderer, {act} from 'react-test-renderer'; +import renderer, { act } from 'react-test-renderer'; const sampleData = [ { @@ -30,18 +30,23 @@ const sampleData = [ } ]; -const sampleUser = "Ayomide"; -const className = 'flex cursor-pointer justify-center p-4 m-6 rounded-md hover:bg-fcc-primary-yellow shadedow-lg border-solid border-color: inherit; border-2 pl-4 pr-4 bg-[#feac32] text-black' +const sampleUser = 'Ayomide'; +const className = + 'flex cursor-pointer justify-center p-4 m-6 rounded-md hover:bg-fcc-primary-yellow shadedow-lg border-solid border-color: inherit; border-2 pl-4 pr-4 bg-[#feac32] text-black'; describe('Modal Component', () => { it('renders header correctly', () => { - const tree = renderer.create().toJSON(); + const tree = renderer + .create() + .toJSON(); expect(tree).toMatchSnapshot(); }); - it('renders whole form after header clicked', ()=> { - const testRenderer = renderer.create(); + it('renders whole form after header clicked', () => { + const testRenderer = renderer.create( + + ); const testInstance = testRenderer.root; - const header = testInstance.findByProps({className}) + const header = testInstance.findByProps({ className }); act(() => { header.props.onClick(); }); @@ -49,6 +54,3 @@ describe('Modal Component', () => { expect(tree).toMatchSnapshot(); }); }); - - - diff --git a/__tests__/components/updateUserForm.test.jsx b/__tests__/components/updateUserForm.test.jsx index 11dcef271..f82e19d53 100644 --- a/__tests__/components/updateUserForm.test.jsx +++ b/__tests__/components/updateUserForm.test.jsx @@ -2,47 +2,44 @@ import UpdateUserForm from '../../components/updateUserForm'; import React from 'react'; import renderer from 'react-test-renderer'; -const sampleUsers=[ - { - id: '5f33071498eb2472b87ddee4', - name: "Hamzat Victor", - email: "oluwaborihamzat@gmail.com", - role: "ADMIN" - }, - { - id: '72245f33071498ebb87ccee4', - name: "Alade Christopher", - email: "aladechristoph@gmail.com", - role: "TEACHER" - }, - { - id: '98eb72245f330714b87ccee4', - name: "Ayomide onifade", - email: "Jangulabi@gmail.com", - role: "NONE" - }, -] - +const sampleUsers = [ + { + id: '5f33071498eb2472b87ddee4', + name: 'Hamzat Victor', + email: 'oluwaborihamzat@gmail.com', + role: 'ADMIN' + }, + { + id: '72245f33071498ebb87ccee4', + name: 'Alade Christopher', + email: 'aladechristoph@gmail.com', + role: 'TEACHER' + }, + { + id: '98eb72245f330714b87ccee4', + name: 'Ayomide onifade', + email: 'Jangulabi@gmail.com', + role: 'NONE' + } +]; describe('updateUserForm', () => { - it('shows a form for user to update details', () => { const tree = renderer - .create() + .create() .toJSON(); expect(tree).toMatchSnapshot(); }); it(`doesn't show other roles if user role is "ADMIN"`, () => { const tree = renderer - .create() + .create() .toJSON(); expect(tree).toMatchSnapshot(); }); it(`shows all available roles if user role is not "ADMIN"`, () => { const tree = renderer - .create() + .create() .toJSON(); expect(tree).toMatchSnapshot(); }); - }); diff --git a/components/ClassInviteTable.js b/components/ClassInviteTable.js index f1965fc80..a9f220529 100644 --- a/components/ClassInviteTable.js +++ b/components/ClassInviteTable.js @@ -29,12 +29,11 @@ export default function ClassInviteTable({ ); const ref = useRef(); - const userCurrentDomain = process.env.NEXTAUTH_URL; + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; const copy = async () => { - //Add the full URL to send to student await navigator.clipboard.writeText( - `${userCurrentDomain}/join/` + currentClass.classroomId + `${baseUrl}/join/${currentClass.classroomId}` ); toast('Class code successfully copied', { diff --git a/components/dashtabs.js b/components/dashtabs.js index 6b9f4d9ae..15d5b28a8 100644 --- a/components/dashtabs.js +++ b/components/dashtabs.js @@ -1,68 +1,63 @@ -import DashTable from './dashtable'; -import { Tabs, TabList, Tab, TabPanel } from 'react-tabs'; -import reactTabStyles from './dashTabs.module.css'; -import { useState } from 'react/cjs/react.development'; +// components/dashtabs.js +import React from 'react'; +import Link from 'next/link'; -export default function DashTabs(props) { - const [tabIndex, setTabIndex] = useState(0); - // This sets our selected tab to our first index of certification module names - const [tabIndexName, setTabIndexName] = useState(props.certificationNames[0]); - // Here we are copying the columns array (which is now immutable) in order to be able to add the Student Name column to it - var columnNames = [...props.columns]; - const presetColumns = [ - { - name: 'Student Name', - selector: row => row['student-name'], - dashedName: 'student-name' - }, - { - name: 'Student Activity', - selector: row => row['student-activity'], - dashedName: 'student-activity' - } - ]; - let columns = columnNames.map(x => { - let finalColumns = presetColumns.concat(x); - return finalColumns; - }); +export default function DashTabs({ columns, certificationNames, students }) { + return ( +
+

Joined Students

- // This function sets the tab name which later gives our selected tab selected styling - function determineItemStyle(x) { - setTabIndexName(x); - } + + + + + + + + + + + {students.length === 0 ? ( + + + + ) : ( + students.map(s => ( + + + + + + + )) + )} + +
EmailActivityProgressDetails
+ No students joined yet +
{s.email} + {/* placeholder until you track student activity */}0 + + {/* placeholder until you track real progress */} + 0/100 + + + View + +
- return ( - <> - setTabIndex(index)}> - - {props.certificationNames.map((x, index) => ( - determineItemStyle(x)} - className={ - x == tabIndexName - ? reactTabStyles.react_tabs__tab__selected - : reactTabStyles.react_tabs__tab - } - key={index} - > - {x} - + {/* ✅ Example dashboard summary below */} +
+

Class Certifications

+
    + {certificationNames.map((name, i) => ( +
  • + {name} ({columns[i]?.length || 0} challenges) +
  • ))} - - {/* - Here, we are mapping the columns array that holds our challenge names. These names are the columns of their own respective tables. - We do not want to send unnecessary column names to our table so we are splitting them here by certification using - Tabs and Tablist to ensure they are sent to their respective Tab (Certification) - */} - {columns.map(certification => ( - - - - ))} - - +
+
+
); } diff --git a/components/modal.js b/components/modal.js index 3925c0cc7..042798840 100644 --- a/components/modal.js +++ b/components/modal.js @@ -39,7 +39,7 @@ export default function Modal({ }); if (response.ok) { - let jsonRes = await response.json() + let jsonRes = await response.json(); let newClassroom = { classroomName: jsonRes.classroomName, description: jsonRes.description, diff --git a/components/navbar.js b/components/navbar.js index cb7bdecf4..822c515ec 100644 --- a/components/navbar.js +++ b/components/navbar.js @@ -1,34 +1,131 @@ +// components/navbar.js import Image from 'next/image'; import Link from 'next/link'; import React from 'react'; import AuthButton from '../components/authButton'; +import { useSession } from 'next-auth/react'; export default function Navbar({ children }) { + const { data: session } = useSession(); + const userRole = session?.user?.role?.toLowerCase(); // "teacher" | "student" | "admin" | "none" + return (
+ {/* LEFT SPACER */}
- - - FreeCodecamp Logo - + + {/* LOGO */} + + FreeCodeCamp Logo -
+ + {/* RIGHT SIDE */} +
+ {/* CHILDREN ITEMS */} {React.Children.toArray(children).map(child => (
{child}
))} + + {/* ⭐ MENTOR–MENTEE DROPDOWN */} + {(userRole === 'teacher' || userRole === 'student') && ( +
+
+ + + {/* FIXED DROPDOWN — stays open while cursor is inside */} +
+ {/* Teacher: Mentor Setup */} + {userRole === 'teacher' && ( + + ⭐ Mentor Setup + + )} + + {/* Student: Request Mentor */} + {userRole === 'student' && ( + + 🎒 Request Mentor + + )} + + {/* Teacher: Mentorship Dashboard */} + {userRole === 'teacher' && ( + + 📊 Mentorship Dashboard + + )} +
+
+
+ )} + + {/* ADMIN PANEL */} + {userRole === 'admin' && ( +
+ + 🔐 Admin Panel + +
+ )} + + {/* TEACHER DASHBOARD */} + {userRole === 'teacher' && ( +
+ + 📘 Teacher + +
+ )} + + {/* STUDENT DASHBOARD */} + {userRole === 'student' && ( +
+ + 🎒 Student + +
+ )} + + {/* SIGN-IN / SIGN-OUT BUTTON */}
- +
diff --git a/db-1758806125541.json b/db-1758806125541.json new file mode 100644 index 000000000..8aa3dd2e7 --- /dev/null +++ b/db-1758806125541.json @@ -0,0 +1,110 @@ +{ + "data": [ + { + "email": "student[A]@gmail.com", + "certifications": [ + { + "2022/responsive-web-design": { + "blocks": [ + { + "learn-basic-css-by-building-a-cafe-menu": { + "completedChallenges": [ + { + "id": "5f33071498eb2472b87ddee4", + "challengeName": "Step 1", + "completedDate": 1475094716730, + "files": [] + }, + { + "id": "5f3313e74582ad9d063e3a38", + "challengeName": "Step 2", + "completedDate": 1537207306322, + "files": [] + } + ] + } + } + ] + } + }, + { + "quality-assurance": { + "blocks": [ + { + "advanced-node-and-express": { + "completedChallenges": [ + { + "id": "5895f700f9fc0f352b528e63", + "challengeName": "Set up a Template Engine", + "completedDate": 98448684, + "files": [] + }, + { + "id": "5895f70df9fc0f352b528e6a", + "challengeName": "Create New Middleware", + "completedDate": 98448643284, + "files": [] + } + ] + } + }, + { + "quality-assurance-and-testing-with-chai": { + "completedChallenges": [ + { + "id": "587d824a367417b2b2512c46", + "challengeName": "Learn How JavaScript Assertions Work", + "completedDate": 47664591, + "files": [] + } + ] + } + } + ] + } + } + ] + }, + { + "email": "student[B]@gmail.com", + "certifications": [ + { + "quality-assurance": { + "blocks": [ + { + "advanced-node-and-express": { + "completedChallenges": [ + { + "id": "5895f700f9fc0f352b528e63", + "challengeName": "Set up a Template Engine", + "completedDate": 98448684, + "files": [] + }, + { + "id": "5895f70df9fc0f352b528e6a", + "challengeName": "Create New Middleware", + "completedDate": 98448643284, + "files": [] + } + ] + } + }, + { + "quality-assurance-and-testing-with-chai": { + "completedChallenges": [ + { + "id": "587d824a367417b2b2512c46", + "challengeName": "Learn How JavaScript Assertions Work", + "completedDate": 47664591, + "files": [] + } + ] + } + } + ] + } + } + ] + } + ] +} diff --git a/db-1762957641048.json b/db-1762957641048.json new file mode 100644 index 000000000..8aa3dd2e7 --- /dev/null +++ b/db-1762957641048.json @@ -0,0 +1,110 @@ +{ + "data": [ + { + "email": "student[A]@gmail.com", + "certifications": [ + { + "2022/responsive-web-design": { + "blocks": [ + { + "learn-basic-css-by-building-a-cafe-menu": { + "completedChallenges": [ + { + "id": "5f33071498eb2472b87ddee4", + "challengeName": "Step 1", + "completedDate": 1475094716730, + "files": [] + }, + { + "id": "5f3313e74582ad9d063e3a38", + "challengeName": "Step 2", + "completedDate": 1537207306322, + "files": [] + } + ] + } + } + ] + } + }, + { + "quality-assurance": { + "blocks": [ + { + "advanced-node-and-express": { + "completedChallenges": [ + { + "id": "5895f700f9fc0f352b528e63", + "challengeName": "Set up a Template Engine", + "completedDate": 98448684, + "files": [] + }, + { + "id": "5895f70df9fc0f352b528e6a", + "challengeName": "Create New Middleware", + "completedDate": 98448643284, + "files": [] + } + ] + } + }, + { + "quality-assurance-and-testing-with-chai": { + "completedChallenges": [ + { + "id": "587d824a367417b2b2512c46", + "challengeName": "Learn How JavaScript Assertions Work", + "completedDate": 47664591, + "files": [] + } + ] + } + } + ] + } + } + ] + }, + { + "email": "student[B]@gmail.com", + "certifications": [ + { + "quality-assurance": { + "blocks": [ + { + "advanced-node-and-express": { + "completedChallenges": [ + { + "id": "5895f700f9fc0f352b528e63", + "challengeName": "Set up a Template Engine", + "completedDate": 98448684, + "files": [] + }, + { + "id": "5895f70df9fc0f352b528e6a", + "challengeName": "Create New Middleware", + "completedDate": 98448643284, + "files": [] + } + ] + } + }, + { + "quality-assurance-and-testing-with-chai": { + "completedChallenges": [ + { + "id": "587d824a367417b2b2512c46", + "challengeName": "Learn How JavaScript Assertions Work", + "completedDate": 47664591, + "files": [] + } + ] + } + } + ] + } + } + ] + } + ] +} diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 000000000..90a305715 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,34 @@ +// lib/auth.js +import { PrismaAdapter } from '@next-auth/prisma-adapter'; +import prisma from '../prisma/prisma'; +import { getServerSession } from 'next-auth/next'; // server-only +import GitHubProvider from 'next-auth/providers/github'; + +export const authOptions = { + adapter: PrismaAdapter(prisma), + providers: [ + GitHubProvider({ + clientId: process.env.GITHUB_ID, + clientSecret: process.env.GITHUB_SECRET + }) + ], + session: { strategy: 'jwt' }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + token.role = user.role?.toLowerCase(); + } + return token; + }, + async session({ session, token }) { + session.user.id = token.id; + session.user.role = token.role; + return session; + } + } +}; + +export function getServerAuthSession(req, res) { + return getServerSession(req, res, authOptions); +} diff --git a/pages/_app.js b/pages/_app.js index 18866dd7f..70f7b5d76 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,7 +1,7 @@ -import '../styles/globals.css'; import { SessionProvider } from 'next-auth/react'; +import '../styles/globals.css'; -export default function MyApp({ +export default function App({ Component, pageProps: { session, ...pageProps } }) { diff --git a/pages/access-denied.js b/pages/access-denied.js new file mode 100644 index 000000000..1cc6a14bc --- /dev/null +++ b/pages/access-denied.js @@ -0,0 +1,8 @@ +export default function AccessDenied() { + return ( +
+

🚫 Access Denied

+

You do not have permission to view this page.

+
+ ); +} diff --git a/pages/admin/index.js b/pages/admin/index.js index ac5e996ca..a167e870f 100644 --- a/pages/admin/index.js +++ b/pages/admin/index.js @@ -1,33 +1,16 @@ import Head from 'next/head'; import styles from '../../styles/Home.module.css'; import Navbar from '../../components/navbar'; -import Link from 'next/link'; -import { getSession } from 'next-auth/react'; + import prisma from '../../prisma/prisma'; import dynamic from 'next/dynamic'; -import redirectUser from '../../util/redirectUser.js'; - -export async function getServerSideProps(ctx) { - const userSession = await getSession(ctx); - if (!userSession) { - return redirectUser('/error'); - } - - const user = await prisma.User.findUnique({ - where: { - email: userSession['user']['email'] - }, - select: { - email: true, - role: true - } - }); +import { requireRole } from '../../util/protectRoute'; - if (user.role != 'ADMIN') { - return redirectUser('/error'); - } +export async function getServerSideProps(context) { + const sessionCheck = await requireRole(context, ['admin']); + if (sessionCheck.redirect) return sessionCheck; - const users = await prisma.User.findMany({ + const users = await prisma.user.findMany({ select: { id: true, name: true, @@ -35,59 +18,106 @@ export async function getServerSideProps(ctx) { role: true } }); + return { props: { - userSession, - users: users + users, + session: sessionCheck.props.session } }; } -export default function Home(props) { +export default function AdminPage({ users }) { const AdminTable = dynamic(() => import('../../components/adminTable'), { ssr: false }); - const columns = [ - { - name: 'Name', - selector: row => row.name - }, - { - name: 'Email', - selector: row => row.userEmail - }, - { - name: 'Role', - selector: row => row.role - }, - { - name: 'Actions', - selector: row => row.adminActions + + async function updateRole(email, role) { + const res = await fetch('/api/admin/updateRole', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, role: role.toLowerCase() }) + }); + + if (res.ok) { + alert(`Updated ${email} to ${role}`); + window.location.reload(); + } else { + alert('Error updating role'); } + } + + async function deleteUser(email) { + if (!confirm(`Delete ${email}?`)) return; + + const res = await fetch('/api/admin/deleteUser', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + + if (res.ok) { + alert(`Deleted ${email}`); + window.location.reload(); + } else { + alert('Error deleting user'); + } + } + + const usersWithActions = users.map(u => ({ + name: u.name, + email: u.email, + role: u.role, + adminActions: ( +
+ + + + +
+ ) + })); + + const columns = [ + { name: 'Name', selector: r => r.name }, + { name: 'Email', selector: r => r.email }, + { name: 'Role', selector: r => r.role }, + { name: 'Actions', selector: r => r.adminActions } ]; + return ( - <> -
- - Create Next App - - - - -
- Classes -
-
- Menu -
-
-
-

- Admin -

-
- +
+ + Admin Dashboard + + + + +
+

Admin

- + + +
); } diff --git a/pages/api/admin/deleteUser.js b/pages/api/admin/deleteUser.js new file mode 100644 index 000000000..6e6336dd8 --- /dev/null +++ b/pages/api/admin/deleteUser.js @@ -0,0 +1,30 @@ +import { getSession } from 'next-auth/react'; +import prisma from '../../../prisma/prisma'; + +export default async function handler(req, res) { + if (req.method !== 'DELETE') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const session = await getSession({ req }); + if (!session) return res.status(401).json({ error: 'Not authenticated' }); + + const currentUser = await prisma.user.findUnique({ + where: { email: session.user.email } + }); + + if (!currentUser || currentUser.role !== 'ADMIN') { + return res.status(403).json({ error: 'Not authorized' }); + } + + const { email } = req.body; + if (!email) return res.status(400).json({ error: 'Missing email' }); + + try { + await prisma.user.delete({ where: { email } }); + return res.status(200).json({ message: '✅ User deleted successfully' }); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Failed to delete user' }); + } +} diff --git a/pages/api/admin/updateRole.js b/pages/api/admin/updateRole.js new file mode 100644 index 000000000..4f817fc80 --- /dev/null +++ b/pages/api/admin/updateRole.js @@ -0,0 +1,25 @@ +import prisma from '../../../prisma/prisma'; + +export default async function handler(req, res) { + if (req.method !== 'PUT') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { email, role } = req.body; + + if (!email || !role) { + return res.status(400).json({ error: 'Email and role are required' }); + } + + const updatedUser = await prisma.User.update({ + where: { email }, + data: { role } + }); + + res.status(200).json(updatedUser); + } catch (error) { + console.error('Error updating role:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index 4895ac83c..f7ed53638 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -1,54 +1,52 @@ import NextAuth from 'next-auth'; -import Auth0Provider from 'next-auth/providers/auth0'; -import GithubProvider from 'next-auth/providers/github'; +import GitHubProvider from 'next-auth/providers/github'; import { PrismaAdapter } from '@next-auth/prisma-adapter'; import prisma from '../../../prisma/prisma'; export const authOptions = { - site: process.env.NEXTAUTH_URL, - - // Configure one or more authentication providers adapter: PrismaAdapter(prisma), + providers: [ - Auth0Provider({ - clientId: process.env.AUTH0_CLIENT_ID, - clientSecret: process.env.AUTH0_CLIENT_SECRET, - issuer: process.env.AUTH0_ISSUER, - // Enable dangerous account linking in dev environment - ...(process.env.DANGEROUS_ACCOUNT_LINKING_ENABLED == 'true' - ? { allowDangerousEmailAccountLinking: true } - : {}) + GitHubProvider({ + clientId: process.env.GITHUB_ID, + clientSecret: process.env.GITHUB_SECRET }) - // ...add more providers here ], + + session: { strategy: 'jwt' }, + callbacks: { - async redirect({ url, baseUrl }) { - // Allows relative callback URLs - if (url.startsWith('/')) return `${baseUrl}${url}`; - // Allows callback URLs on the same origin - else if (new URL(url).origin === baseUrl) return url; - return baseUrl; - } - } -}; + // Store user.id and role in JWT + async jwt({ token, user }) { + // On first login, store user.id + if (user) { + token.id = user.id; + } -if (process.env.GITHUB_OAUTH_PROVIDER_ENABLED == 'true') { - authOptions.providers.push( - GithubProvider({ - clientId: process.env.GITHUB_ID, - clientSecret: process.env.GITHUB_SECRET, - // Enable dangerous account linking in dev environment - ...(process.env.DANGEROUS_ACCOUNT_LINKING_ENABLED == 'true' - ? { allowDangerousEmailAccountLinking: true } - : {}) - }) - ); -} + // Always fetch role from DB + if (token.id) { + const dbUser = await prisma.user.findUnique({ + where: { id: token.id } + }); -export default NextAuth(authOptions); + // DO NOT UPPERCASE (this was your bug) + token.role = dbUser?.role || 'none'; + } -/* Test Cases - Auth0 Google/GitHub -> GitHub - GitHub -> Auth0 Google/GitHub + return token; + }, + + // Make role available in session + async session({ session, token }) { + if (session.user) { + session.user.id = token.id; + session.user.role = token.role; // student / mentor + } + return session; + } + }, - Tested on Incognito tab of Microsoft Edge, Brave, Safari, Chrome, FireFox*/ + secret: process.env.NEXTAUTH_SECRET +}; + +export default NextAuth(authOptions); diff --git a/pages/api/classes/join.js b/pages/api/classes/join.js new file mode 100644 index 000000000..8880606d5 --- /dev/null +++ b/pages/api/classes/join.js @@ -0,0 +1,31 @@ +import { getSession } from 'next-auth/react'; +import prisma from '../../../prisma/prisma'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const session = await getSession({ req }); + if (!session) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { classroomId } = req.body; + + try { + await prisma.classroom.update({ + where: { classroomId }, + data: { + students: { + connect: { email: session.user.email } // ✅ attach logged in student + } + } + }); + + return res.json({ message: 'Joined classroom successfully' }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +} diff --git a/pages/api/create_class_teacher.js b/pages/api/create_class_teacher.js index 08100564f..cb151ef7c 100644 --- a/pages/api/create_class_teacher.js +++ b/pages/api/create_class_teacher.js @@ -1,53 +1,39 @@ +// pages/api/create_class_teacher.js import prisma from '../../prisma/prisma'; -import { getServerSession } from 'next-auth'; -import { authOptions } from './auth/[...nextauth]'; +import { getServerAuthSession } from '../../lib/auth'; -export default async function handle(req, res) { - //unstable_getServerSession is recommended here: https://next-auth.js.org/configuration/nextjs - const session = await getServerSession(req, res, authOptions); - let user; +export default async function handler(req, res) { + console.log('create_class_teacher: method', req.method); + if (req.method !== 'POST') + return res.status(405).json({ error: 'Method not allowed' }); - if (!req.method == 'POST') { - return res.status(405).end(); - } + const session = await getServerAuthSession(req, res); + console.log('create_class_teacher: session:', session?.user); + + if (!session || !session.user) + return res.status(401).json({ error: 'Not authenticated' }); + + const role = String(session.user.role || '').toLowerCase(); + if (role !== 'teacher' && role !== 'admin') + return res.status(403).json({ error: 'Forbidden' }); - if (!session) { - return res.status(403).end(); + const { classroomName, description, classroomTeacherId } = req.body || {}; + if (!classroomName || classroomTeacherId === undefined) { + return res.status(400).json({ error: 'Missing fields' }); } try { - user = await prisma.user.findUniqueOrThrow({ - where: { - email: session.user.email - }, - select: { - role: true, - id: true + const created = await prisma.classroom.create({ + data: { + classroomName, + description: description || '', + classroomTeacherId } }); - } catch { - return res.status(403).end(); - } - - //checks whether user is teacher/admin - if (user.role !== 'TEACHER' && user.role !== 'ADMIN') { - return res.status(403).end(); + console.log('create_class_teacher: created:', created); + return res.status(201).json(created); + } catch (err) { + console.error('create_class_teacher error:', err); + return res.status(500).json({ error: 'Failed to create' }); } - - const data = req.body; - - //makes sure teacher is only creating class for themselves - if (user.role === 'TEACHER' && user.id !== data['classroomTeacherId']) { - return res.status(403).end(); - } - - const createClassInDB = await prisma.classroom.create({ - data: { - classroomName: data['classroomName'], - description: data['description'], - classroomTeacherId: data['classroomTeacherId'], - fccCertifications: data['fccCertifications'] - } - }); - return res.json(createClassInDB); } diff --git a/pages/api/deleteclass.js b/pages/api/deleteclass.js index 2fb66a07f..9a67a7b1b 100644 --- a/pages/api/deleteclass.js +++ b/pages/api/deleteclass.js @@ -1,61 +1,42 @@ +// pages/api/deleteclass.js import prisma from '../../prisma/prisma'; -import { unstable_getServerSession } from 'next-auth'; -import { authOptions } from './auth/[...nextauth]'; +import { getServerAuthSession } from '../../lib/auth'; -export default async function handle(req, res) { - //unstable_getServerSession is recommended here: https://next-auth.js.org/configuration/nextjs - const session = await unstable_getServerSession(req, res, authOptions); - let user, classroom; +export default async function handler(req, res) { + if (req.method !== 'DELETE') + return res.status(405).json({ error: 'Method not allowed' }); - if (!req.method == 'DELETE') { - return res.status(405).end(); - } - - if (!session) { - return res.status(403).end(); - } - - try { - user = await prisma.user.findUniqueOrThrow({ - where: { - email: session.user.email - }, - select: { - role: true, - id: true - } - }); - } catch { - return res.status(403).end(); - } + const session = await getServerAuthSession(req, res); + if (!session?.user) + return res.status(401).json({ error: 'Not authenticated' }); - //checks whether user is teacher/admin - if (user.role !== 'TEACHER' && user.role !== 'ADMIN') { - return res.status(403).end(); - } + const role = String(session.user.role || '').toLowerCase(); + if (role !== 'teacher' && role !== 'admin') + return res.status(403).json({ error: 'Forbidden' }); - const data = req.body; + const classroomId = req.body?.classroomId; + if (!classroomId) + return res.status(400).json({ error: 'Missing classroomId' }); try { - classroom = await prisma.classroom.findUniqueOrThrow({ - where: { - classroomId: data - } + const classRow = await prisma.classroom.findUnique({ + where: { classroomId }, + select: { classroomTeacherId: true } }); - } catch { - return res.status(400).end(); - } - - //makes sure teacher can only delete their own class - if (user.role === 'TEACHER' && user.id !== classroom.classroomTeacherId) { - return res.status(403).end(); + if (!classRow) return res.status(404).json({ error: 'Class not found' }); + + if ( + role === 'teacher' && + String(classRow.classroomTeacherId) !== String(session.user.id) + ) + return res + .status(403) + .json({ error: "Cannot delete class you don't own" }); + + await prisma.classroom.delete({ where: { classroomId } }); + return res.status(200).json({ success: true }); + } catch (err) { + console.error('deleteclass error:', err); + return res.status(500).json({ error: 'Failed to delete class' }); } - - await prisma.classroom.delete({ - where: { - classroomId: data - } - }); - - return res.status(200).end(); } diff --git a/pages/api/editclass.js b/pages/api/editclass.js index d0808bc9f..174d642a6 100644 --- a/pages/api/editclass.js +++ b/pages/api/editclass.js @@ -1,59 +1,49 @@ +// pages/api/editclass.js import prisma from '../../prisma/prisma'; -import { unstable_getServerSession } from 'next-auth'; -import { authOptions } from './auth/[...nextauth]'; +import { getServerAuthSession } from '../../lib/auth'; -export default async function handle(req, res) { - // unstable_getServerSession is recommended here: https://next-auth.js.org/configuration/nextjs - const session = await unstable_getServerSession(req, res, authOptions); - const data = req.body; - let user; +export default async function handler(req, res) { + if (req.method !== 'PATCH') + return res.status(405).json({ error: 'Method not allowed' }); - if (!req.method == 'PUT') { - res.status(405).end(); - } + const session = await getServerAuthSession(req, res); + if (!session || !session.user) + return res.status(401).json({ error: 'Not authenticated' }); - if (!session) { - res.status(403).end(); - } + const role = String(session.user.role || '').toLowerCase(); + if (role !== 'teacher' && role !== 'admin') + return res.status(403).json({ error: 'Forbidden' }); + + const { classroomId, classroomName, description } = req.body || {}; + if (!classroomId) + return res.status(400).json({ error: 'Missing classroomId' }); try { - user = await prisma.user.findUniqueOrThrow({ - where: { - email: session.user.email - }, - select: { - role: true - } + const classRow = await prisma.classroom.findUnique({ + where: { classroomId }, + select: { classroomTeacherId: true } }); - } catch { - return res.status(403).end(); - } - if (user.role !== 'TEACHER') { - return res.status(403).end(); - } + if (!classRow) return res.status(404).json({ error: 'Class not found' }); - if (data.fccCertifications.length === 0) { - data.fccCertifications = undefined; - } + if ( + role === 'teacher' && + String(classRow.classroomTeacherId) !== String(session.user.id) + ) { + return res.status(403).json({ error: "Cannot edit class you don't own" }); + } - if ( - data.className === undefined && - data.description === undefined && - data.fccCertifications === undefined - ) { - return res.status(304).end(); - } + const updated = await prisma.classroom.update({ + where: { classroomId }, + data: { + classroomName, + description + } + }); - const editClassInDB = await prisma.classroom.update({ - where: { - classroomId: data.classroomId - }, - data: { - classroomName: data.className, - description: data.description, - fccCertifications: data.fccCertifications - } - }); - return res.json(editClassInDB); + return res.status(200).json(updated); + } catch (err) { + console.error('editclass error:', err); + return res.status(500).json({ error: 'Failed to update class' }); + } } diff --git a/pages/api/getclass.js b/pages/api/getclass.js new file mode 100644 index 000000000..a1fc3269e --- /dev/null +++ b/pages/api/getclass.js @@ -0,0 +1,19 @@ +// pages/api/get_class.js +import prisma from '../../prisma/prisma'; + +export default async function handler(req, res) { + const { classroomId } = req.query || {}; + if (!classroomId) + return res.status(400).json({ error: 'Missing classroomId' }); + + try { + const classroom = await prisma.classroom.findUnique({ + where: { classroomId: Number(classroomId) } + }); + if (!classroom) return res.status(404).json({ error: 'Not found' }); + return res.status(200).json(classroom); + } catch (err) { + console.error('get_class error:', err); + return res.status(500).json({ error: 'Failed to fetch' }); + } +} diff --git a/pages/api/invite_student_by_email.js b/pages/api/invite_student_by_email.js new file mode 100644 index 000000000..a9bd05f07 --- /dev/null +++ b/pages/api/invite_student_by_email.js @@ -0,0 +1,54 @@ +// pages/api/invite_student_by_email.js +import prisma from '../../prisma/prisma'; +import { getServerAuthSession } from '../../lib/auth'; + +export default async function handler(req, res) { + if (req.method !== 'POST') + return res.status(405).json({ error: 'Method not allowed' }); + + const session = await getServerAuthSession(req, res); + if (!session?.user?.email) + return res.status(401).json({ error: 'Not authenticated' }); + + const { classroomId, email } = req.body || {}; + if (!classroomId || !email) + return res.status(400).json({ error: 'Missing classroomId or email' }); + + let user; + try { + user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + select: { id: true, role: true } + }); + } catch { + return res.status(403).json({ error: 'User not found' }); + } + + const role = String(user.role || '').toLowerCase(); + + let classroom; + try { + classroom = await prisma.classroom.findUniqueOrThrow({ + where: { classroomId: Number(classroomId) } + }); + } catch { + return res.status(404).json({ error: 'Class not found' }); + } + + if ( + role === 'teacher' && + Number(user.id) !== Number(classroom.classroomTeacherId) + ) { + return res + .status(403) + .json({ error: 'Forbidden: cannot invite to class you do not own' }); + } + + try { + // Minimal: return success (implement email/invite DB as needed) + return res.status(200).json({ message: `Invite stub sent to ${email}` }); + } catch (err) { + console.error('Invite error', err); + return res.status(500).json({ error: 'Failed to create invite' }); + } +} diff --git a/pages/api/join.js b/pages/api/join.js new file mode 100644 index 000000000..aafcbcf23 --- /dev/null +++ b/pages/api/join.js @@ -0,0 +1,37 @@ +// pages/api/join.js +import prisma from '../../prisma/prisma'; +import { getServerAuthSession } from '../../lib/auth'; + +export default async function handler(req, res) { + if (req.method !== 'POST') + return res.status(405).json({ error: 'Method not allowed' }); + + const session = await getServerAuthSession(req, res); + if (!session?.user) + return res.status(401).json({ error: 'Not authenticated' }); + + const { classroomId } = req.body || {}; + if (!classroomId) + return res.status(400).json({ error: 'Missing classroomId' }); + + try { + const classRow = await prisma.classroom.findUnique({ + where: { classroomId } + }); + if (!classRow) return res.status(404).json({ error: 'Class not found' }); + + // This requires Classroom.students relation in Prisma schema + await prisma.classroom.update({ + where: { classroomId }, + data: { students: { connect: { id: session.user.id } } } + }); + + return res.status(200).json({ message: 'Joined class' }); + } catch (err) { + console.error('join error:', err); + return res.status(500).json({ + error: + 'Failed to join. Ensure Classroom.students relation exists in Prisma schema.' + }); + } +} diff --git a/pages/api/join_class.js b/pages/api/join_class.js new file mode 100644 index 000000000..5504c8574 --- /dev/null +++ b/pages/api/join_class.js @@ -0,0 +1,48 @@ +// pages/api/join_class.js +import prisma from '../../prisma/prisma'; +import { getServerAuthSession } from '../../lib/auth'; + +export default async function handler(req, res) { + if (req.method !== 'POST') + return res.status(405).json({ error: 'Method not allowed' }); + + const session = await getServerAuthSession(req, res); + if (!session?.user?.email) + return res.status(401).json({ error: 'Not authenticated' }); + + const { classroomId } = req.body || {}; + if (!classroomId) + return res.status(400).json({ error: 'Missing classroomId' }); + + let user; + try { + user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + select: { id: true, role: true } + }); + } catch { + return res.status(403).json({ error: 'User not found' }); + } + + const role = String(user.role || '').toLowerCase(); + if (role !== 'student' && role !== 'admin') + return res.status(403).json({ error: 'Only students can join classes' }); + + try { + const updated = await prisma.classroom.update({ + where: { classroomId: Number(classroomId) }, + data: { + // assumes relation field `students` exists in schema + students: { + connect: { id: Number(user.id) } + } + } + }); + return res.status(200).json({ message: 'Joined', classroom: updated }); + } catch (err) { + console.error('Join class error:', err); + return res + .status(500) + .json({ error: 'Failed to join (maybe relation missing)' }); + } +} diff --git a/pages/api/mentee/request.js b/pages/api/mentee/request.js new file mode 100644 index 000000000..b67837567 --- /dev/null +++ b/pages/api/mentee/request.js @@ -0,0 +1,49 @@ +import prisma from '../../../prisma/prisma'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '../auth/[...nextauth]'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const session = await getServerSession(req, res, authOptions); + + if (!session || !session.user?.id) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + // IMPORTANT + if (session.user.role !== 'student') { + return res + .status(403) + .json({ error: 'Only students can request mentors' }); + } + + const userId = session.user.id; + const { subjects } = req.body; + + if (!Array.isArray(subjects) || subjects.length === 0) { + return res + .status(400) + .json({ error: 'At least one subject is required' }); + } + + const menteeReq = await prisma.menteeRequest.create({ + data: { + userId, + subjects, // <<-- FIXED + status: 'pending' + } + }); + + return res.status(200).json({ + message: 'Request submitted successfully', + data: menteeReq + }); + } catch (err) { + console.error('MENTEE REQUEST ERROR:', err); + return res.status(500).json({ error: 'Server error' }); + } +} diff --git a/pages/api/mentor/setup.js b/pages/api/mentor/setup.js new file mode 100644 index 000000000..15bffc789 --- /dev/null +++ b/pages/api/mentor/setup.js @@ -0,0 +1,52 @@ +// pages/api/mentor/setup.js +import prisma from '../../../prisma/prisma'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '../auth/[...nextauth]'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const session = await getServerSession(req, res, authOptions); + + if (!session || !session.user?.id) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const userId = session.user.id; + const { subjects, priorities, about } = req.body; + + if (!Array.isArray(subjects) || subjects.length === 0) { + return res + .status(400) + .json({ error: 'At least one subject is required' }); + } + + // Convert priorities object → JSON string + const prioritiesJson = JSON.stringify(priorities || {}); + + const mentor = await prisma.mentorProfile.upsert({ + where: { userId }, + update: { + subjects, + subjectPriorities: prioritiesJson, + about: about || '', + available: true + }, + create: { + userId, + subjects, + subjectPriorities: prioritiesJson, + about: about || '', + available: true + } + }); + + return res.status(200).json(mentor); + } catch (error) { + console.error('mentor setup API error:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/pages/api/mentorship/assign.js b/pages/api/mentorship/assign.js new file mode 100644 index 000000000..94ef5fc03 --- /dev/null +++ b/pages/api/mentorship/assign.js @@ -0,0 +1,83 @@ +import prisma from '../../../prisma/prisma'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { menteeId } = req.body; + + if (!menteeId) { + return res.status(400).json({ error: 'menteeId is required' }); + } + + // 1️⃣ Fetch mentee request + const mentee = await prisma.menteeRequest.findUnique({ + where: { id: menteeId } + }); + + if (!mentee) { + return res.status(404).json({ error: 'Mentee request not found' }); + } + + const menteeSubject = mentee.goal.toLowerCase(); + + // 2️⃣ Fetch all available mentors + const mentors = await prisma.mentorProfile.findMany({ + where: { available: true } + }); + + if (!mentors.length) { + return res.status(400).json({ error: 'No mentors available' }); + } + + // 3️⃣ Priority Matching + let bestMatch = null; + let highestPriority = -1; + + for (const mentor of mentors) { + const subjects = JSON.parse(mentor.skills); + + for (const subj of subjects) { + if (subj.subject.toLowerCase() === menteeSubject) { + if (subj.priority > highestPriority) { + highestPriority = subj.priority; + bestMatch = mentor; + } + } + } + } + + // 4️⃣ If no mentor teaches that subject + if (!bestMatch) { + return res.status(400).json({ + error: `No mentor teaches the subject ${menteeSubject}` + }); + } + + // 5️⃣ Create the match + const match = await prisma.mentorMenteePair.create({ + data: { + mentorId: bestMatch.id, + menteeId + } + }); + + // 6️⃣ Mark mentee as matched + await prisma.menteeRequest.update({ + where: { id: menteeId }, + data: { status: 'matched' } + }); + + return res.status(200).json({ + message: 'Match successful', + mentor: bestMatch, + mentee, + match + }); + } catch (error) { + console.error('assign error:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/pages/api/mentorship/match.js b/pages/api/mentorship/match.js new file mode 100644 index 000000000..3ba22d333 --- /dev/null +++ b/pages/api/mentorship/match.js @@ -0,0 +1,106 @@ +// pages/api/mentorship/match.js +import prisma from '../../../prisma/prisma'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // 1️⃣ Get all pending mentees + const mentees = await prisma.menteeRequest.findMany({ + where: { status: 'pending' }, + orderBy: { id: 'asc' } + }); + + if (mentees.length === 0) { + return res.status(200).json({ + message: 'No pending mentees', + createdPairsCount: 0 + }); + } + + // 2️⃣ Get all mentors + const mentors = await prisma.mentorProfile.findMany({ + orderBy: { id: 'asc' } + }); + + let createdPairsCount = 0; + + // 3️⃣ Multi-subject matching + for (const mentee of mentees) { + const menteeSubjects = mentee.subjects || []; + if (menteeSubjects.length === 0) continue; + + for (const subject of menteeSubjects) { + const lowerSubject = subject.toLowerCase(); + + let bestMentor = null; + let bestPriority = -1; + + for (const mentor of mentors) { + const mentorSubjects = mentor.subjects || []; + + // mentor must teach this subject + if ( + !mentorSubjects.map(s => s.toLowerCase()).includes(lowerSubject) + ) { + continue; + } + + // parse subjectPriorities JSON + let priorityMap = {}; + try { + priorityMap = mentor.subjectPriorities + ? JSON.parse(mentor.subjectPriorities) + : {}; + } catch { + priorityMap = {}; + } + + const p = Number(priorityMap[subject] ?? 1); + + // pick highest priority mentor + if (bestMentor === null || p > bestPriority) { + bestMentor = mentor; + bestPriority = p; + } + } + + if (!bestMentor) continue; + + // 4️⃣ avoid duplicate mentor–mentee pair + const exists = await prisma.mentorMenteePair.findFirst({ + where: { + mentorId: bestMentor.id, + menteeId: mentee.id + } + }); + + if (!exists) { + await prisma.mentorMenteePair.create({ + data: { + mentorId: bestMentor.id, + menteeId: mentee.id + } + }); + createdPairsCount++; + } + } + + // 5️⃣ mark mentee as matched after processing all subjects + await prisma.menteeRequest.update({ + where: { id: mentee.id }, + data: { status: 'matched' } + }); + } + + return res.status(200).json({ + message: 'Multi-subject matching completed', + createdPairsCount + }); + } catch (error) { + console.error('match error:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/pages/api/mentorship/overview.js b/pages/api/mentorship/overview.js new file mode 100644 index 000000000..39f26057d --- /dev/null +++ b/pages/api/mentorship/overview.js @@ -0,0 +1,73 @@ +// pages/api/mentorship/overview.js +import prisma from '../../../prisma/prisma'; + +export default async function handler(req, res) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // 1️⃣ Mentors + user + pairs + const mentors = await prisma.mentorProfile.findMany({ + include: { + user: true, + pairs: { + include: { + mentee: { include: { user: true } } + } + } + }, + orderBy: { id: 'asc' } // stable order for tie-breaking + }); + + const formattedMentors = mentors.map(m => { + const subjects = m.subjects || []; + + let priorities = {}; + try { + priorities = m.subjectPriorities ? JSON.parse(m.subjectPriorities) : {}; + } catch { + priorities = {}; + } + + return { + id: m.id, + user: m.user, + subjects, + subjectPriorities: priorities, // OBJECT, not string + pairs: m.pairs || [] + }; + }); + + // 2️⃣ Mentees + user + const mentees = await prisma.menteeRequest.findMany({ + include: { user: true }, + orderBy: { id: 'asc' } + }); + + const formattedMentees = mentees.map(m => ({ + id: m.id, + user: m.user, + subjects: m.goal ? [m.goal] : [], + status: m.status + })); + + // 3️⃣ All current matches + const pairs = await prisma.mentorMenteePair.findMany({ + include: { + mentor: { include: { user: true } }, + mentee: { include: { user: true } } + }, + orderBy: { id: 'asc' } + }); + + return res.status(200).json({ + mentors: formattedMentors, + mentees: formattedMentees, + pairs + }); + } catch (error) { + console.error('overview error:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/pages/api/mentorship/reset.js b/pages/api/mentorship/reset.js new file mode 100644 index 000000000..23115cadd --- /dev/null +++ b/pages/api/mentorship/reset.js @@ -0,0 +1,25 @@ +import prisma from '../../../prisma/prisma'; + +export default async function handler(req, res) { + if (req.method !== 'GET' && req.method !== 'POST') { + return res.status(405).json({ + error: 'Use GET or POST /api/mentorship/reset' + }); + } + + try { + // Delete mentor-mentee matchmaking data ONLY + await prisma.mentorMenteePair.deleteMany().catch(() => {}); + await prisma.menteeRequest.deleteMany().catch(() => {}); + await prisma.mentorRequest?.deleteMany?.().catch(() => {}); + await prisma.mentorProfile?.deleteMany?.().catch(() => {}); + + return res.status(200).json({ + message: + 'Mentorship data cleared (requests, profiles, pairs). Login users are SAFE.' + }); + } catch (error) { + console.error('RESET ERROR:', error); + return res.status(500).json({ error: error.message }); + } +} diff --git a/pages/api/ping.js b/pages/api/ping.js new file mode 100644 index 000000000..8394f6c70 --- /dev/null +++ b/pages/api/ping.js @@ -0,0 +1,4 @@ +// pages/api/ping.js +export default function handler(req, res) { + res.status(200).json({ ok: true, path: '/api/ping' }); +} diff --git a/pages/api/student_email_join.js b/pages/api/student_email_join.js index 738b4fe20..3bc7f63f9 100644 --- a/pages/api/student_email_join.js +++ b/pages/api/student_email_join.js @@ -1,63 +1,94 @@ +// pages/api/student_email_join.js import prisma from '../../prisma/prisma'; -import { unstable_getServerSession } from 'next-auth'; -import { authOptions } from './auth/[...nextauth]'; +import { getServerAuthSession } from '../../lib/auth'; -export default async function handle(req, res) { - // unstable_getServerSession is recommended here: https://next-auth.js.org/configuration/nextjs - const session = await unstable_getServerSession(req, res, authOptions); +export default async function handler(req, res) { + console.log('student_email_join: incoming method', req.method); + if (req.method !== 'POST') + return res.status(405).json({ error: 'Method not allowed' }); - if (!req.method == 'PUT') { - res.status(405).end(); - } + try { + const session = await getServerAuthSession(req, res); + console.log( + 'student_email_join: session user:', + session?.user?.email, + session?.user?.id, + session?.user?.role + ); - if (!session) { - res.status(403).end(); - } + if (!session || !session.user) { + console.log('student_email_join: no session -> 401'); + return res.status(401).json({ error: 'Not authenticated' }); + } - const body = req.body; - // Grab user info here - const userInfo = await prisma.user.findUnique({ - where: { - email: session.user.email - }, - select: { - id: true + const role = String(session.user.role || '').toLowerCase(); + if (role !== 'teacher' && role !== 'admin') { + console.log('student_email_join: wrong role:', role); + return res.status(403).json({ error: 'Forbidden' }); } - }); - // Grab class info here - const checkClass = await prisma.classroom.findUniqueOrThrow({ - where: { - classroomId: body.join[0] - }, - select: { - fccUserIds: true + + const { classroomId, email } = req.body || {}; + console.log('student_email_join: payload:', { classroomId, email }); + + if (!classroomId) { + console.log('student_email_join: missing classroomId'); + return res.status(400).json({ error: 'Missing classroomId' }); } - }); - const existsInClassroom = checkClass.fccUserIds.includes(userInfo.id); - if (existsInClassroom) { - res.status(409).end(); - } - // TODO: Once we allow multiple teachers inside of a classroom, make sure that the teachers - // are placed inside of the teacher array rather than as a regular student - else if (userInfo.role === 'NONE') { - // This runs only when a new user attempts to join a classroom. - await prisma.user.update({ - where: { - email: session.user.email - }, - data: { - role: 'STUDENT' - } + + const classRow = await prisma.classroom.findUnique({ + where: { classroomId } }); - } - // Update calssroom with user id - await prisma.classroom.update({ - where: { - classroomId: body.join[0] - }, - data: { - fccUserIds: { push: userInfo.id } + if (!classRow) { + console.log('student_email_join: class not found:', classroomId); + return res.status(404).json({ error: 'Class not found' }); + } + + if ( + role === 'teacher' && + String(classRow.classroomTeacherId) !== String(session.user.id) + ) { + console.log('student_email_join: teacher does not own class', { + owner: classRow.classroomTeacherId, + user: session.user.id + }); + return res + .status(403) + .json({ error: "Cannot invite to class you don't own" }); } - }); - res.status(200).end(); + + // Build join link (absolute) + const base = + process.env.NEXTAUTH_URL?.replace(/\/$/, '') || + `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}`; + const joinLink = `${base}/join?classroomId=${encodeURIComponent( + classRow.classroomId + )}`; + + // Optionally: if you want to create an Invite DB row (skip if no model) + try { + const invite = await prisma.invite.create({ + data: { + classroomId: classRow.classroomId, + email: email || null, + invitedById: String(session.user.id) + } + }); + console.log('student_email_join: created invite row id:', invite.id); + return res + .status(201) + .json({ message: 'Invite created', joinLink, inviteId: invite.id }); + } catch (inviteErr) { + // If Invite model doesn't exist, just return the link + console.log( + 'student_email_join: no invite model or create failed, returning link', + inviteErr?.message || inviteErr + ); + return res + .status(200) + .json({ message: 'Invite link created (no DB invite)', joinLink }); + } + } catch (err) { + console.error('student_email_join: unexpected error:', err); + return res.status(500).json({ error: 'Server error during invite' }); + } } diff --git a/pages/dashboard/[id].js b/pages/dashboard/[id].js index b4f2e4c4a..5f6893069 100644 --- a/pages/dashboard/[id].js +++ b/pages/dashboard/[id].js @@ -1,106 +1,83 @@ +// pages/dashboard/[id].js import Head from 'next/head'; import Layout from '../../components/layout'; import Link from 'next/link'; import Navbar from '../../components/navbar'; import prisma from '../../prisma/prisma'; -import DashTabs from '../../components/dashtabs'; import { getSession } from 'next-auth/react'; -import { - createDashboardObject, - fetchStudentData, - getDashedNamesURLs, - getNonDashedNamesURLs, - getSuperBlockJsons -} from '../../util/api_proccesor'; -import redirectUser from '../../util/redirectUser.js'; +import { fetchStudentData } from '../../util/api_proccesor'; +import redirectUser from '../../util/redirectUser'; export async function getServerSideProps(context) { - //making sure User is the teacher of this classsroom's dashboard const userSession = await getSession(context); if (!userSession) { return redirectUser('/error'); } + + // ✅ get teacher by email const userEmail = await prisma.User.findMany({ - where: { - email: userSession['user']['email'] - } + where: { email: userSession.user.email } }); + // ✅ classroom teacher ID const classroomTeacherId = await prisma.classroom.findUnique({ - where: { - classroomId: context.params.id - }, - select: { - classroomTeacherId: true - } + where: { classroomId: context.params.id }, + select: { classroomTeacherId: true } }); - if (userEmail[0].id !== classroomTeacherId['classroomTeacherId']) { + // ✅ verify teacher + if ( + !userEmail.length || + userEmail[0].id !== classroomTeacherId.classroomTeacherId + ) { return redirectUser('/classes'); } - const certificationNumbers = await prisma.classroom.findUnique({ - where: { - classroomId: context.params.id - }, - select: { - fccCertifications: true - } - }); - let superblockURLS = await getDashedNamesURLs( - certificationNumbers.fccCertifications - ); - let nonDashedNames = await getNonDashedNamesURLs( - certificationNumbers.fccCertifications - ); - - let superBlockJsons = await getSuperBlockJsons(superblockURLS); - let dashboardObjs = createDashboardObject(superBlockJsons); - - let currStudentData = await fetchStudentData(); + // ✅ fetch enrolled students + const currStudentData = await fetchStudentData(context.params.id); return { props: { userSession, - columns: dashboardObjs, - certificationNames: nonDashedNames, data: currStudentData } }; } -export default function Home({ - userSession, - columns, - certificationNames, - data -}) { - let tabNames = certificationNames; - let columnNames = columns; - let studentData = data; - +export default function DashboardPage({ userSession, data }) { return ( - Create Next App - - + Classroom Dashboard + + {userSession && ( <> -
- Classes +
+ Classes
-
- Menu +
+ Menu
- + + {/* ✅ Simple student list */} +
+

Enrolled Students

+ {data.length > 0 ? ( +
    + {data.map(s => ( +
  • + {s.name} ({s.email}) +
  • + ))} +
+ ) : ( +

No students enrolled in this class.

+ )} +
)} diff --git a/pages/dashboard/v2/[id].js b/pages/dashboard/v2/[id].js index 706492318..0a84cf90c 100644 --- a/pages/dashboard/v2/[id].js +++ b/pages/dashboard/v2/[id].js @@ -1,47 +1,34 @@ +// pages/dashboard/v2/[id].js import Head from 'next/head'; import Layout from '../../../components/layout'; import Link from 'next/link'; -import prisma from '../../../prisma/prisma'; import Navbar from '../../../components/navbar'; +import prisma from '../../../prisma/prisma'; import { getSession } from 'next-auth/react'; -import GlobalDashboardTable from '../../../components/dashtable_v2'; -import React from 'react'; -import { - createSuperblockDashboardObject, - getTotalChallengesForSuperblocks, - getDashedNamesURLs, - getSuperBlockJsons, - fetchStudentData, - checkIfStudentHasProgressDataForSuperblocksSelectedByTeacher -} from '../../../util/api_proccesor'; -import redirectUser from '../../../util/redirectUser.js'; +import { fetchStudentData } from '../../../util/api_proccesor'; +import redirectUser from '../../../util/redirectUser'; export async function getServerSideProps(context) { - //making sure User is the teacher of this classsroom's dashboard const userSession = await getSession(context); if (!userSession) { return redirectUser('/error'); } + // ✅ get teacher by email const userEmail = await prisma.User.findMany({ - where: { - email: userSession['user']['email'] - } + where: { email: userSession.user.email } }); + // ✅ classroom teacher ID const classroomTeacherId = await prisma.classroom.findUnique({ - where: { - classroomId: context.params.id - }, - select: { - classroomTeacherId: true - } + where: { classroomId: context.params.id }, + select: { classroomTeacherId: true } }); + // ✅ verify teacher if ( - classroomTeacherId == null || - userEmail[0].id == null || - userEmail[0].id !== classroomTeacherId['classroomTeacherId'] + !userEmail.length || + userEmail[0].id !== classroomTeacherId.classroomTeacherId ) { return redirectUser('/classes'); } @@ -96,44 +83,45 @@ export async function getServerSideProps(context) { return { props: { userSession, - classroomId: context.params.id, - studentData, - totalChallenges: totalChallenges, - studentsAreEnrolledInSuperblocks + data: currStudentData } }; } -export default function Home({ - userSession, - classroomId, - totalChallenges, - studentData, - studentsAreEnrolledInSuperblocks -}) { +export default function DashboardPage({ userSession, data }) { return ( - Create Next App - - + Classroom Dashboard + + {userSession && ( <> -
- Classes +
+ Classes
-
- Menu +
+ Menu
- + + {/* ✅ Simple student list */} +
+

Enrolled Students

+ {data.length > 0 ? ( +
    + {data.map(s => ( +
  • + {s.name} ({s.email}) +
  • + ))} +
+ ) : ( +

No students enrolled in this class.

+ )} +
)} diff --git a/pages/join.js b/pages/join.js new file mode 100644 index 000000000..e92f87977 --- /dev/null +++ b/pages/join.js @@ -0,0 +1,76 @@ +// pages/join.js — Auto-join page (ESLint compliant, no logic changes) +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { signIn, useSession } from 'next-auth/react'; + +export default function JoinPage() { + const router = useRouter(); + const { classroomId } = router.query; + const { status } = useSession(); // removed unused "session" + const [statusText, setStatusText] = useState('Waiting...'); + + // Auto-join when router and classroomId are ready + useEffect(() => { + if (!router.isReady) return; + + if (!classroomId) { + setStatusText('Missing classroomId in URL.'); + return; + } + + // Not signed in → redirect to sign in + if (status === 'unauthenticated') { + const callback = `${window.location.pathname}${window.location.search}`; + signIn(undefined, { callbackUrl: callback }); + return; + } + + if (status === 'loading') { + setStatusText('Checking sign-in...'); + return; + } + + // Signed in → try joining + if (status === 'authenticated') { + (async () => { + setStatusText('Joining class...'); + try { + const res = await fetch('/api/join', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ classroomId }) + }); + + const json = await res.json().catch(() => ({})); + + if (!res.ok) { + setStatusText('Join failed: ' + (json.error || res.status)); + return; + } + + setStatusText('Joined! Redirecting to your student dashboard...'); + + // small pause before redirect + setTimeout(() => router.push('/student'), 800); + } catch (err) { + console.error('Join error:', err); + setStatusText('Network error while joining.'); + } + })(); + } + }, [router, router.isReady, classroomId, status]); // added router to deps + + return ( +
+

Join Class

+

Classroom ID: {String(classroomId || '')}

+
Status: {statusText}
+ +
+ If nothing happens, ensure you're signed in as a student and + refresh. + {/* Fixed unescaped apostrophe */} +
+
+ ); +} diff --git a/pages/join/[...joinCode].js b/pages/join/[...joinCode].js index f26094e6e..9b1563d50 100644 --- a/pages/join/[...joinCode].js +++ b/pages/join/[...joinCode].js @@ -1,123 +1,70 @@ -import Head from 'next/head'; -import Navbar from '../../components/navbar'; -import { useState } from 'react'; +// pages/join/[...joinCode].js import { useRouter } from 'next/router'; import { getSession } from 'next-auth/react'; -import AuthButton from '../../components/authButton'; -import DisplayNotification from './displayNotification'; -import { ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; +import { useEffect, useState } from 'react'; export async function getServerSideProps(ctx) { - const userSession = await getSession(ctx); + const session = await getSession(ctx); // renamed to avoid 'unused variable' warning + + if (!session) { + // redirect if not logged in + return { + redirect: { + destination: '/error', + permanent: false + } + }; + } + return { props: { - userSession: userSession + userSession: session } }; } -export default function JoinWithCode({ userSession }) { - const [formData] = useState({}); + +export default function JoinWithCode() { const router = useRouter(); const { joinCode } = router.query; + const [status, setStatus] = useState('Joining...'); + + useEffect(() => { + if (!joinCode) return; - const classroomRequest = async event => { - event.preventDefault(); - formData.join = joinCode; - try { - const res = await fetch(`/api/student_email_join`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - if (res.status === 409) { - DisplayNotification('Error', 'You have already joined this classroom.'); - } else { - DisplayNotification( - 'Success', - 'Congrats! You are now enrolled in this class.' - ); + async function joinClassroom() { + try { + const res = await fetch('/api/student_email_join', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + classroomId: joinCode // invite code is classroomId + }) + }); + + const data = await res.json(); + + if (res.ok) { + setStatus('✅ You have successfully joined the classroom!'); + // redirect student to dashboard after 2s + setTimeout(() => { + router.push(`/dashboard/${joinCode}`); + }, 2000); + } else { + setStatus(`❌ Error: ${data.error}`); + } + } catch (err) { + console.error('Join error:', err); + setStatus('❌ Something went wrong while joining.'); } - } catch (error) { - DisplayNotification( - 'Error', - 'Sorry, there was an error on our end. Please try again later.' - ); - console.log(error); } - }; + + joinClassroom(); + }, [joinCode, router]); return ( - <> -
- -
-
- - Create Next App - - - - - {userSession ? ( - <> -
-
-
-

- Register for Classroom -

-
-
-
- -
-
-
-
- - ) : ( - <> -
-
-
-

- Sign In with FreeCodeCamp -

-
-
- -
-
-
- - )} -
- +
+

Classroom Join

+

{status}

+
); } diff --git a/pages/join/displayNotification.js b/pages/join/displayNotification.js new file mode 100644 index 000000000..fe4ed3548 --- /dev/null +++ b/pages/join/displayNotification.js @@ -0,0 +1,11 @@ +import { toast } from 'react-toastify'; + +export default function DisplayNotification(type, message) { + if (type === 'success') { + toast.success(message); + } else if (type === 'error') { + toast.error(message); + } else { + toast(message); + } +} diff --git a/pages/mentee/request.js b/pages/mentee/request.js new file mode 100644 index 000000000..ccb26a1a8 --- /dev/null +++ b/pages/mentee/request.js @@ -0,0 +1,131 @@ +// pages/mentee/request.js + +import { useState } from 'react'; + +const SUBJECTS = [ + 'DSA', + 'Web Development', + 'Python', + 'DBMS', + 'Operating Systems' +]; + +export default function MenteeRequest() { + const [subjects, setSubjects] = useState([]); + const [message, setMessage] = useState(''); + + function toggleSubject(subj) { + setSubjects(prev => + prev.includes(subj) ? prev.filter(s => s !== subj) : [...prev, subj] + ); + } + + async function submit(e) { + e.preventDefault(); + + if (subjects.length === 0) { + setMessage('❌ Please select at least one subject'); + return; + } + + setMessage('Submitting...'); + + const res = await fetch('/api/mentee/request', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ subjects }) + }); + + const data = await res.json(); + + if (res.ok) { + setMessage('✔ Mentorship request submitted successfully!'); + setSubjects([]); // clear + } else { + setMessage(`❌ ${data.error}`); + } + } + + return ( +
+

Request a Mentor

+ +
+

Select Subjects

+ +
+ {SUBJECTS.map(sub => ( + + ))} +
+ + + + {message &&

{message}

} +
+
+ ); +} + +const styles = { + page: { + padding: '2rem', + minHeight: '100vh', + background: '#f4f4f9', + fontFamily: 'Arial' + }, + title: { + textAlign: 'center', + fontSize: '2rem', + marginBottom: '1rem' + }, + card: { + background: '#fff', + padding: '1.5rem', + maxWidth: '600px', + margin: 'auto', + borderRadius: '10px', + boxShadow: '0 2px 10px #0002' + }, + list: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: '12px', + marginTop: '15px' + }, + item: { + display: 'flex', + alignItems: 'center', + gap: '10px', + padding: '8px', + background: '#f8f9ff', + borderRadius: '6px', + border: '1px solid #e0e0e0' + }, + button: { + width: '100%', + padding: '12px', + marginTop: '20px', + background: '#16a34a', + color: 'white', + border: 'none', + borderRadius: '6px', + fontSize: '16px', + cursor: 'pointer' + }, + message: { + marginTop: '1rem', + textAlign: 'center', + fontWeight: 'bold', + color: '#4f46e5' + } +}; diff --git a/pages/mentor-dashboard.js b/pages/mentor-dashboard.js new file mode 100644 index 000000000..287a205f0 --- /dev/null +++ b/pages/mentor-dashboard.js @@ -0,0 +1,93 @@ +import { useState, useEffect } from 'react'; + +export default function MentorDashboard() { + const [user, setUser] = useState(null); + const [role, setRole] = useState(''); + const [matches, setMatches] = useState([]); + + useEffect(() => { + // ✅ Fetch logged-in user info from your auth session (mocked for now) + fetch('/api/auth/session') + .then(res => res.json()) + .then(data => { + setUser(data?.user); + setRole(data?.user?.role); // teacher or student + }); + }, []); + + const registerAsMentor = async skills => { + await fetch('/api/mentor/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId: user.id, skills }) + }); + alert('Mentor registered!'); + }; + + const registerAsMentee = async interests => { + await fetch('/api/mentee/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId: user.id, interests }) + }); + alert('Mentee registered!'); + }; + + const createMatch = async () => { + const res = await fetch('/api/matches/create', { method: 'POST' }); + const data = await res.json(); + setMatches([...matches, data]); + }; + + return ( +
+

Mentor–Mentee Dashboard

+ + {!user &&

Loading user info...

} + + {role === 'teacher' && ( +
+

Register as Mentor

+ + +
+ )} + + {role === 'student' && ( +
+

Register as Mentee

+ + +
+ )} + + {role === 'teacher' && ( +
+

Create Random Match

+ +
+ )} + +
+

Matches

+ {matches.length === 0 ? ( +

No matches yet

+ ) : ( +
{JSON.stringify(matches, null, 2)}
+ )} +
+
+ ); +} diff --git a/pages/mentor/setup.js b/pages/mentor/setup.js new file mode 100644 index 000000000..8323b9cc4 --- /dev/null +++ b/pages/mentor/setup.js @@ -0,0 +1,132 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; + +const SUBJECTS = [ + 'DSA', + 'Web Development', + 'Python', + 'DBMS', + 'Operating Systems' +]; + +export default function MentorSetup() { + const [subjects, setSubjects] = useState([]); + const [priorities, setPriorities] = useState({}); + const [about, setAbout] = useState(''); + const [msg, setMsg] = useState(''); + + const router = useRouter(); + + function toggle(sub) { + setSubjects(prev => + prev.includes(sub) ? prev.filter(x => x !== sub) : [...prev, sub] + ); + } + + const setPriority = (subj, val) => { + setPriorities(p => ({ ...p, [subj]: Number(val) })); + }; + + async function submit(e) { + e.preventDefault(); + setMsg('Saving...'); + + const res = await fetch('/api/mentor/setup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ subjects, priorities, about }) + }); + + const data = await res.json(); + + setMsg(res.ok ? 'Mentor profile saved!' : data.error); + + if (res.ok) setTimeout(() => router.push('/mentorship/dashboard'), 1000); + } + + return ( +
+

Mentor Profile Setup

+ +
+

Select Subjects You Want to Teach:

+
+ {SUBJECTS.map(sub => ( +
+ toggle(sub)} + /> + {sub} + + {subjects.includes(sub) && ( + setPriority(sub, e.target.value)} + /> + )} +
+ ))} +
+ +

About You

+