From df77b9d8550ccb4884683f6407cd16d823c01df7 Mon Sep 17 00:00:00 2001 From: FLAME Date: Sun, 16 Feb 2025 20:57:33 -0500 Subject: [PATCH 01/14] Set up Github Workflow to auto-format unformatted code using Prettier --- .github/workflow/workflow.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflow/workflow.yml diff --git a/.github/workflow/workflow.yml b/.github/workflow/workflow.yml new file mode 100644 index 00000000..0cf799e2 --- /dev/null +++ b/.github/workflow/workflow.yml @@ -0,0 +1,23 @@ +name: Format the code + +on: + push: + pull_request: + +jobs: + format: + runs-on: ubuntu-latest + name: Format Files + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "20" + - name: Prettier + run: npx prettier --write **/*.{js,jsx,ts,tsx,md} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: stefanzweifel/git-auto-commit-action@v4 + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + with: + commit_message: "Auto-formatted the code using Prettier" From baac0461fc2630a86166cecf135303d2a0022fe7 Mon Sep 17 00:00:00 2001 From: FLAME Date: Sun, 16 Feb 2025 21:01:52 -0500 Subject: [PATCH 02/14] Fix typo --- .github/{workflow => workflows}/workflow.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflow => workflows}/workflow.yml (100%) diff --git a/.github/workflow/workflow.yml b/.github/workflows/workflow.yml similarity index 100% rename from .github/workflow/workflow.yml rename to .github/workflows/workflow.yml From 1ce1d4329249ce9695dd7d9375f806e8c0dddc7e Mon Sep 17 00:00:00 2001 From: FLAME Date: Sun, 16 Feb 2025 21:02:43 -0500 Subject: [PATCH 03/14] Update workflow.yml --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 0cf799e2..a80b7318 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -14,7 +14,7 @@ jobs: with: node-version: "20" - name: Prettier - run: npx prettier --write **/*.{js,jsx,ts,tsx,md} + run: npx prettier --write **/*.{js,ts,tsx,md} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: stefanzweifel/git-auto-commit-action@v4 From 148d41999e1a1ae1bf38cf7deb8d7c7cf55038dc Mon Sep 17 00:00:00 2001 From: Austin-X Date: Mon, 17 Feb 2025 02:03:01 +0000 Subject: [PATCH 04/14] Auto-formatted the code using Prettier --- README.md | 1 - course-matrix/backend/src/config/config.ts | 16 +- .../backend/src/config/swaggerOptions.ts | 24 +- .../backend/src/controllers/authentication.ts | 57 ++- .../src/controllers/coursesController.ts | 154 +++--- .../src/controllers/departmentsController.ts | 16 +- .../src/controllers/offeringsController.ts | 39 +- .../backend/src/controllers/userController.ts | 81 +-- course-matrix/backend/src/db/setupDb.ts | 18 +- course-matrix/backend/src/index.ts | 103 ++-- .../backend/src/middleware/asyncHandler.ts | 20 +- .../backend/src/middleware/authHandler.ts | 52 +- .../backend/src/middleware/errorHandler.ts | 60 +-- .../backend/src/routes/authRouter.ts | 16 +- .../backend/src/routes/courseRouter.ts | 2 +- .../backend/src/routes/userRouter.ts | 16 +- course-matrix/backend/src/utils/utils.ts | 26 +- course-matrix/frontend/README.md | 12 +- course-matrix/frontend/eslint.config.js | 24 +- course-matrix/frontend/postcss.config.js | 2 +- course-matrix/frontend/src/App.tsx | 34 +- .../frontend/src/api/authApiSlice.ts | 86 ++-- .../frontend/src/api/baseApiSlice.ts | 20 +- course-matrix/frontend/src/api/config.ts | 10 +- .../frontend/src/api/coursesApiSlice.ts | 36 +- .../frontend/src/api/departmentsApiSlice.ts | 34 +- .../frontend/src/api/offeringsApiSlice.ts | 36 +- .../frontend/src/app/dashboard/page.tsx | 10 +- .../frontend/src/components/UserMenu.tsx | 238 ++++----- .../frontend/src/components/app-sidebar.tsx | 32 +- .../frontend/src/components/auth-route.tsx | 26 +- .../frontend/src/components/logo.tsx | 34 +- .../src/components/password-input.tsx | 30 +- .../frontend/src/components/period-select.tsx | 119 +++-- .../frontend/src/components/search-form.tsx | 8 +- .../src/components/time-picker-hr.tsx | 10 +- .../src/components/time-picker-input.tsx | 34 +- .../frontend/src/components/ui/accordion.tsx | 26 +- .../frontend/src/components/ui/avatar.tsx | 24 +- .../frontend/src/components/ui/breadcrumb.tsx | 48 +- .../frontend/src/components/ui/button.tsx | 26 +- .../frontend/src/components/ui/card.tsx | 41 +- .../frontend/src/components/ui/checkbox.tsx | 16 +- .../frontend/src/components/ui/dialog.tsx | 52 +- .../src/components/ui/dropdown-menu.tsx | 80 +-- .../frontend/src/components/ui/form.tsx | 101 ++-- .../frontend/src/components/ui/hover-card.tsx | 18 +- .../frontend/src/components/ui/input.tsx | 16 +- .../frontend/src/components/ui/label.tsx | 18 +- .../frontend/src/components/ui/select.tsx | 56 +-- .../frontend/src/components/ui/separator.tsx | 18 +- .../frontend/src/components/ui/sheet.tsx | 56 +-- .../frontend/src/components/ui/sidebar.tsx | 354 ++++++------- .../frontend/src/components/ui/skeleton.tsx | 6 +- .../frontend/src/components/ui/spinner.tsx | 31 +- .../frontend/src/components/ui/table.tsx | 44 +- .../frontend/src/components/ui/tooltip.tsx | 20 +- .../src/components/version-switcher.tsx | 16 +- .../frontend/src/hooks/use-mobile.tsx | 24 +- course-matrix/frontend/src/lib/utils.ts | 8 +- course-matrix/frontend/src/main.tsx | 18 +- .../frontend/src/models/filter-form.ts | 30 +- .../frontend/src/models/login-form.ts | 10 +- course-matrix/frontend/src/models/models.ts | 100 ++-- .../frontend/src/models/signup-form.ts | 49 +- .../frontend/src/models/timetable-form.ts | 282 +++++++---- .../src/pages/Dashboard/Dashboard.tsx | 134 ++--- .../src/pages/Loading/LoadingPage.tsx | 14 +- .../frontend/src/pages/Login/LoginPage.tsx | 204 ++++---- .../frontend/src/pages/Signup/SignUpPage.tsx | 228 +++++---- .../src/pages/Signup/SignupSuccessfulPage.tsx | 41 +- .../pages/TimetableBuilder/CourseSearch.tsx | 181 ++++--- .../TimetableBuilder/CreateCustomSetting.tsx | 283 ++++++----- .../TimetableBuilder/OfferingContent.tsx | 105 ++-- .../pages/TimetableBuilder/SearchFilters.tsx | 116 +++-- .../TimetableBuilder/TimetableBuilder.tsx | 464 +++++++++++------- .../pages/TimetableBuilder/mockSearchData.ts | 27 +- .../frontend/src/stores/authslice.ts | 44 +- course-matrix/frontend/src/stores/store.ts | 29 +- .../src/utils/convert-breadth-requirement.ts | 15 +- .../frontend/src/utils/format-date-time.ts | 2 +- .../frontend/src/utils/time-picker-utils.tsx | 57 ++- .../frontend/src/utils/type-utils.ts | 3 +- course-matrix/frontend/src/utils/typeutils.ts | 3 +- .../frontend/src/utils/useClickOutside.tsx | 16 +- .../frontend/src/utils/useDebounce.ts | 14 +- course-matrix/frontend/tailwind.config.js | 171 ++++--- course-matrix/frontend/vite.config.ts | 12 +- doc/sprint0/product.md | 24 + doc/sprint0/product_backlog.md | 365 +++++++------- doc/sprint0/team.md | 15 +- doc/sprint0/ux_ui_mockups.md | 2 +- doc/sprint1/RPM.md | 19 +- doc/sprint1/iteration-01.plan.md | 99 ++-- doc/sprint1/sprint-01-review.md | 64 +-- 95 files changed, 3236 insertions(+), 2639 deletions(-) diff --git a/README.md b/README.md index c9d27f0c..e4a24cce 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ The `DATABASE_URL` variable should contain your Supabase project url and the `DA VITE_SERVER_URL="http://localhost:8081" ``` - ### Running the Application To run the application locally: diff --git a/course-matrix/backend/src/config/config.ts b/course-matrix/backend/src/config/config.ts index 93421031..aae9e79d 100644 --- a/course-matrix/backend/src/config/config.ts +++ b/course-matrix/backend/src/config/config.ts @@ -3,13 +3,13 @@ import { config } from "dotenv"; const configFile = `./.env`; config({ path: configFile }); -const {PORT, NODE_ENV, CLIENT_APP_URL, DATABASE_URL, DATABASE_KEY } = - process.env; +const { PORT, NODE_ENV, CLIENT_APP_URL, DATABASE_URL, DATABASE_KEY } = + process.env; export default { - PORT, - env: NODE_ENV, - CLIENT_APP_URL, - DATABASE_URL, - DATABASE_KEY, -}; \ No newline at end of file + PORT, + env: NODE_ENV, + CLIENT_APP_URL, + DATABASE_URL, + DATABASE_KEY, +}; diff --git a/course-matrix/backend/src/config/swaggerOptions.ts b/course-matrix/backend/src/config/swaggerOptions.ts index e0d23de0..5a6c1a1e 100644 --- a/course-matrix/backend/src/config/swaggerOptions.ts +++ b/course-matrix/backend/src/config/swaggerOptions.ts @@ -1,16 +1,16 @@ export const swaggerOptions = { swaggerDefinition: { - openapi: '3.0.0', - info: { - title: 'Application API', - description: 'Application API Information', - version: "v1" + openapi: "3.0.0", + info: { + title: "Application API", + description: "Application API Information", + version: "v1", + }, + servers: [ + { + url: "http://localhost:8081", }, - servers: [ - { - url: "http://localhost:8081" - } - ], + ], }, - apis: ['./src/routes/*.ts'] -} \ No newline at end of file + apis: ["./src/routes/*.ts"], +}; diff --git a/course-matrix/backend/src/controllers/authentication.ts b/course-matrix/backend/src/controllers/authentication.ts index e35df5cb..b683b9a8 100644 --- a/course-matrix/backend/src/controllers/authentication.ts +++ b/course-matrix/backend/src/controllers/authentication.ts @@ -1,37 +1,36 @@ -import {Request, Response} from 'express'; +import { Request, Response } from "express"; -import {supabase} from '../db/setupDb'; -import asyncHandler from '../middleware/asyncHandler'; +import { supabase } from "../db/setupDb"; +import asyncHandler from "../middleware/asyncHandler"; -export const handleAuthCode = - asyncHandler(async (req: Request, res: Response) => { - try { - const token_hash = req.query.token_hash as string; - const type = req.query.type as string; - const next = (req.query.next as string) ?? '/'; - - if (!token_hash || !type) { - return res.status(400).json({error: 'Missing token or type'}); - } - - if (token_hash && type) { - const {data, error} = await supabase.auth.verifyOtp({ - type: 'email', - token_hash, - }); +export const handleAuthCode = asyncHandler( + async (req: Request, res: Response) => { + try { + const token_hash = req.query.token_hash as string; + const type = req.query.type as string; + const next = (req.query.next as string) ?? "/"; + if (!token_hash || !type) { + return res.status(400).json({ error: "Missing token or type" }); + } + if (token_hash && type) { + const { data, error } = await supabase.auth.verifyOtp({ + type: "email", + token_hash, + }); - if (!error) { - console.log('Authentication successful'); + if (!error) { + console.log("Authentication successful"); - return res.redirect(303, decodeURIComponent(next)); - } + return res.redirect(303, decodeURIComponent(next)); } - // Redirect to an error page if verification fails or parameters are - // missing - return res.redirect(303, '/auth/auth-code-error'); - } catch (error) { - return res.status(500).json({error: 'Internal Server Error'}); } - }); \ No newline at end of file + // Redirect to an error page if verification fails or parameters are + // missing + return res.redirect(303, "/auth/auth-code-error"); + } catch (error) { + return res.status(500).json({ error: "Internal Server Error" }); + } + }, +); diff --git a/course-matrix/backend/src/controllers/coursesController.ts b/course-matrix/backend/src/controllers/coursesController.ts index 5b5e84d7..5ea47f30 100644 --- a/course-matrix/backend/src/controllers/coursesController.ts +++ b/course-matrix/backend/src/controllers/coursesController.ts @@ -2,81 +2,95 @@ import { Request, Response } from "express"; import asyncHandler from "../middleware/asyncHandler"; import { supabaseCourseClient } from "../db/setupDb"; -const DEFAULT_COURSE_LIMIT = 1000 +const DEFAULT_COURSE_LIMIT = 1000; export default { - getCourses: asyncHandler(async (req: Request, res: Response) => { - try { - // Get the query parameters - const { limit, search, semester, breadthRequirement, creditWeight, department, yearLevel } = req.query; + getCourses: asyncHandler(async (req: Request, res: Response) => { + try { + // Get the query parameters + const { + limit, + search, + semester, + breadthRequirement, + creditWeight, + department, + yearLevel, + } = req.query; - // Query the courses, offerings tables from the database - let coursesQuery = supabaseCourseClient - .from("courses") - .select() - .limit(Number(limit || DEFAULT_COURSE_LIMIT)); - - if ((search as string)?.trim()) { - coursesQuery = coursesQuery.or(`code.ilike.%${search}%,name.ilike.%${search}%`); - } - let offeringsQuery = supabaseCourseClient.from("offerings").select(); + // Query the courses, offerings tables from the database + let coursesQuery = supabaseCourseClient + .from("courses") + .select() + .limit(Number(limit || DEFAULT_COURSE_LIMIT)); - // Get the data and errors from the queries - const { data: coursesData, error: coursesError } = await coursesQuery; - const { data: offeringsData, error: offeringsError } = await offeringsQuery; + if ((search as string)?.trim()) { + coursesQuery = coursesQuery.or( + `code.ilike.%${search}%,name.ilike.%${search}%`, + ); + } + let offeringsQuery = supabaseCourseClient.from("offerings").select(); - // Set the courses and offerings data - const courses = coursesData || []; - const offerings = offeringsData || []; + // Get the data and errors from the queries + const { data: coursesData, error: coursesError } = await coursesQuery; + const { data: offeringsData, error: offeringsError } = + await offeringsQuery; - // Create a map of course codes to semesters. - const courseCodesToSemestersMap: { [key: string]: string[] } = {}; - offerings.forEach((offering) => { - const courseCode = offering.code; - const semester = offering.offering; - if (courseCodesToSemestersMap[courseCode]) { - courseCodesToSemestersMap[courseCode].push(semester); - } else { - courseCodesToSemestersMap[courseCode] = [semester]; - } - }); - - // Filter the courses based on the breadth requirement, credit weight, semester, department, and year level - let filteredCourses = courses; - if (breadthRequirement) { - filteredCourses = filteredCourses.filter((course) => { - return course.breadth_requirement === breadthRequirement; - }); - } - if (creditWeight) { - filteredCourses = filteredCourses.filter((course) => { - const courseCreditWeight = - course.code[course.code.length - 2] === "H" ? 0.5 : 1; - return courseCreditWeight === Number(creditWeight); - }); - } - if (semester) { - filteredCourses = filteredCourses.filter((course) => { - return courseCodesToSemestersMap[course.code]?.includes(semester as string); - }); - } - if (department) { - filteredCourses = filteredCourses.filter((course) => { - const courseDepartment = course.code.substring(0, 3); - return courseDepartment === department; - }); - } - if (yearLevel) { - filteredCourses = filteredCourses.filter((course) => { - const courseYearLevel = course.code.charCodeAt(3) - 'A'.charCodeAt(0) + 1; - return courseYearLevel === Number(yearLevel); - }); - } + // Set the courses and offerings data + const courses = coursesData || []; + const offerings = offeringsData || []; - // Return the filtered courses - return res.status(200).send(filteredCourses); - } catch (err) { - return res.status(500).send({ err }); - } - }), + // Create a map of course codes to semesters. + const courseCodesToSemestersMap: { [key: string]: string[] } = {}; + offerings.forEach((offering) => { + const courseCode = offering.code; + const semester = offering.offering; + if (courseCodesToSemestersMap[courseCode]) { + courseCodesToSemestersMap[courseCode].push(semester); + } else { + courseCodesToSemestersMap[courseCode] = [semester]; + } + }); + + // Filter the courses based on the breadth requirement, credit weight, semester, department, and year level + let filteredCourses = courses; + if (breadthRequirement) { + filteredCourses = filteredCourses.filter((course) => { + return course.breadth_requirement === breadthRequirement; + }); + } + if (creditWeight) { + filteredCourses = filteredCourses.filter((course) => { + const courseCreditWeight = + course.code[course.code.length - 2] === "H" ? 0.5 : 1; + return courseCreditWeight === Number(creditWeight); + }); + } + if (semester) { + filteredCourses = filteredCourses.filter((course) => { + return courseCodesToSemestersMap[course.code]?.includes( + semester as string, + ); + }); + } + if (department) { + filteredCourses = filteredCourses.filter((course) => { + const courseDepartment = course.code.substring(0, 3); + return courseDepartment === department; + }); + } + if (yearLevel) { + filteredCourses = filteredCourses.filter((course) => { + const courseYearLevel = + course.code.charCodeAt(3) - "A".charCodeAt(0) + 1; + return courseYearLevel === Number(yearLevel); + }); + } + + // Return the filtered courses + return res.status(200).send(filteredCourses); + } catch (err) { + return res.status(500).send({ err }); + } + }), }; diff --git a/course-matrix/backend/src/controllers/departmentsController.ts b/course-matrix/backend/src/controllers/departmentsController.ts index c727c177..a945dbd6 100644 --- a/course-matrix/backend/src/controllers/departmentsController.ts +++ b/course-matrix/backend/src/controllers/departmentsController.ts @@ -1,17 +1,17 @@ -import {Request, Response} from 'express'; +import { Request, Response } from "express"; -import {supabaseCourseClient} from '../db/setupDb'; -import asyncHandler from '../middleware/asyncHandler'; +import { supabaseCourseClient } from "../db/setupDb"; +import asyncHandler from "../middleware/asyncHandler"; export default { getDepartments: asyncHandler(async (req: Request, res: Response) => { try { // Query the departments table from the database - let departmentsQuery = supabaseCourseClient.from('departments').select(); + let departmentsQuery = supabaseCourseClient.from("departments").select(); // Get the data and errors from the query - const {data: departmentsData, error: departmentsError} = - await departmentsQuery; + const { data: departmentsData, error: departmentsError } = + await departmentsQuery; // Set the departments data const departments = departmentsData || []; @@ -20,7 +20,7 @@ export default { res.status(200).json(departments); } catch (error) { console.error(error); - res.status(500).json({message: 'Internal Server Error'}); + res.status(500).json({ message: "Internal Server Error" }); } }), -} \ No newline at end of file +}; diff --git a/course-matrix/backend/src/controllers/offeringsController.ts b/course-matrix/backend/src/controllers/offeringsController.ts index 532a4fa3..047cbb8c 100644 --- a/course-matrix/backend/src/controllers/offeringsController.ts +++ b/course-matrix/backend/src/controllers/offeringsController.ts @@ -3,25 +3,26 @@ import asyncHandler from "../middleware/asyncHandler"; import { supabaseCourseClient } from "../db/setupDb"; export default { - getOfferings: asyncHandler(async (req: Request, res: Response) => { - try { - const { course_code, semester } = req.query; - - let offeringsQuery = supabaseCourseClient - .from("offerings") - .select() - .eq("code", course_code) - .eq("offering", semester); + getOfferings: asyncHandler(async (req: Request, res: Response) => { + try { + const { course_code, semester } = req.query; - // Get the data and errors from the query - const { data: offeringsData, error: offeringsError } = await offeringsQuery; + let offeringsQuery = supabaseCourseClient + .from("offerings") + .select() + .eq("code", course_code) + .eq("offering", semester); - const offerings = offeringsData || []; + // Get the data and errors from the query + const { data: offeringsData, error: offeringsError } = + await offeringsQuery; - res.status(200).json(offerings); - } catch (error) { - console.error(error); - res.status(500).json({ message: "Internal Server Error" }); - } - }), -} \ No newline at end of file + const offerings = offeringsData || []; + + res.status(200).json(offerings); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Internal Server Error" }); + } + }), +}; diff --git a/course-matrix/backend/src/controllers/userController.ts b/course-matrix/backend/src/controllers/userController.ts index 2a4f4295..58d9d21c 100644 --- a/course-matrix/backend/src/controllers/userController.ts +++ b/course-matrix/backend/src/controllers/userController.ts @@ -1,35 +1,35 @@ -import cookieParser from 'cookie-parser'; -import {CookieOptions, Request, Response} from 'express'; +import cookieParser from "cookie-parser"; +import { CookieOptions, Request, Response } from "express"; -import config from '../config/config'; -import {supabase} from '../db/setupDb'; -import asyncHandler from '../middleware/asyncHandler'; +import config from "../config/config"; +import { supabase } from "../db/setupDb"; +import asyncHandler from "../middleware/asyncHandler"; const COOKIE_OPTIONS: CookieOptions = { - httpOnly: true, // Prevents JavaScript access (XSS protection) - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', + httpOnly: true, // Prevents JavaScript access (XSS protection) + secure: process.env.NODE_ENV === "production", + sameSite: "strict", maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days }; export const signUp = asyncHandler(async (req: Request, res: Response) => { try { - const {email, password} = req.body; + const { email, password } = req.body; // calling supabase for user registeration - const {data, error} = await supabase.auth.signUp({ + const { data, error } = await supabase.auth.signUp({ email, password, - options: {emailRedirectTo: `${config.CLIENT_APP_URL}/signup-success`}, + options: { emailRedirectTo: `${config.CLIENT_APP_URL}/signup-success` }, }); if (data.user?.identities?.length === 0) { - console.error('User already exists', error); + console.error("User already exists", error); return res.status(400).json({ error: { - message: 'User already exists', - status: 400 - } + message: "User already exists", + status: 400, + }, }); } @@ -37,67 +37,72 @@ export const signUp = asyncHandler(async (req: Request, res: Response) => { return res.status(400).json(error); } - res.status(201).json( - {message: 'User registered successfully!', user: data.user}); - + res + .status(201) + .json({ message: "User registered successfully!", user: data.user }); } catch (error) { - res.status(500).json({message: 'Internal Server Error'}); + res.status(500).json({ message: "Internal Server Error" }); } }); - export const login = asyncHandler(async (req: Request, res: Response) => { - const {email, password} = req.body; + const { email, password } = req.body; try { // user sign in via supabase - const {data, error} = - await supabase.auth.signInWithPassword({email, password}); + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); if (error) { return res.status(401).json(error); } - res.cookie('refresh_token', data.session?.refresh_token, COOKIE_OPTIONS); + res.cookie("refresh_token", data.session?.refresh_token, COOKIE_OPTIONS); res.status(200).json({ - message: 'Login Success!', + message: "Login Success!", access_token: data.session?.access_token, - user: data.user + user: data.user, }); } catch (error) { - res.status(500).json({message: 'Internal Server Error'}); + res.status(500).json({ message: "Internal Server Error" }); } }); export const logout = asyncHandler(async (req: Request, res: Response) => { try { - const {error} = await supabase.auth.signOut(); + const { error } = await supabase.auth.signOut(); if (error) { return res.status(400).json(error); } - res.clearCookie('refresh_token', COOKIE_OPTIONS); + res.clearCookie("refresh_token", COOKIE_OPTIONS); - res.status(200).json({message: 'User logged out'}); + res.status(200).json({ message: "User logged out" }); } catch (error) { - res.status(500).json({message: 'Internal Server Error'}); + res.status(500).json({ message: "Internal Server Error" }); } }); export const session = asyncHandler(async (req: Request, res: Response) => { try { const refresh_token = req.cookies.refresh_token; - if (!refresh_token) return res.status(401).json({error: 'Not Authorized'}); + if (!refresh_token) + return res.status(401).json({ error: "Not Authorized" }); - const {data, error} = await supabase.auth.refreshSession({refresh_token}); + const { data, error } = await supabase.auth.refreshSession({ + refresh_token, + }); - if (error) return res.status(401).json({error: error.message}); + if (error) return res.status(401).json({ error: error.message }); - res.status(200).json( - {message: 'User fetched successfully', user: data.user}); + res + .status(200) + .json({ message: "User fetched successfully", user: data.user }); } catch (error: any) { - console.error('Session Error:', error); - res.status(500).json({error: error.message}); + console.error("Session Error:", error); + res.status(500).json({ error: error.message }); } }); diff --git a/course-matrix/backend/src/db/setupDb.ts b/course-matrix/backend/src/db/setupDb.ts index c8fb9032..07bdb9ab 100644 --- a/course-matrix/backend/src/db/setupDb.ts +++ b/course-matrix/backend/src/db/setupDb.ts @@ -1,10 +1,16 @@ -import {createClient} from '@supabase/supabase-js' +import { createClient } from "@supabase/supabase-js"; -import config from '../config/config'; +import config from "../config/config"; -export const supabase = - createClient(config.DATABASE_URL!, config.DATABASE_KEY!); +export const supabase = createClient( + config.DATABASE_URL!, + config.DATABASE_KEY!, +); -export const supabaseCourseClient = createClient(config.DATABASE_URL!, config.DATABASE_KEY!, {db: {schema: 'course'}}); +export const supabaseCourseClient = createClient( + config.DATABASE_URL!, + config.DATABASE_KEY!, + { db: { schema: "course" } }, +); -console.log('Connected to Supabase Client!') +console.log("Connected to Supabase Client!"); diff --git a/course-matrix/backend/src/index.ts b/course-matrix/backend/src/index.ts index e401eec9..d5b34129 100644 --- a/course-matrix/backend/src/index.ts +++ b/course-matrix/backend/src/index.ts @@ -1,74 +1,85 @@ -import cookieParser from 'cookie-parser'; -import cors from 'cors'; -import express, {Express} from 'express'; -import {Server} from 'http'; -import swaggerjsdoc from 'swagger-jsdoc'; -import swaggerUi from 'swagger-ui-express'; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import express, { Express } from "express"; +import { Server } from "http"; +import swaggerjsdoc from "swagger-jsdoc"; +import swaggerUi from "swagger-ui-express"; -import config from './config/config'; -import {swaggerOptions} from './config/swaggerOptions'; -import {supabase} from './db/setupDb'; -import asyncHandler from './middleware/asyncHandler'; -import {errorConverter, errorHandler} from './middleware/errorHandler'; -import {authRouter} from './routes/authRouter'; -import {coursesRouter, departmentsRouter, offeringsRouter} from './routes/courseRouter'; +import config from "./config/config"; +import { swaggerOptions } from "./config/swaggerOptions"; +import { supabase } from "./db/setupDb"; +import asyncHandler from "./middleware/asyncHandler"; +import { errorConverter, errorHandler } from "./middleware/errorHandler"; +import { authRouter } from "./routes/authRouter"; +import { + coursesRouter, + departmentsRouter, + offeringsRouter, +} from "./routes/courseRouter"; const app: Express = express(); -const HOST = 'localhost'; +const HOST = "localhost"; let server: Server; const swaggerDocs = swaggerjsdoc(swaggerOptions); -app.use(cors({origin: config.CLIENT_APP_URL, credentials: true})); +app.use(cors({ origin: config.CLIENT_APP_URL, credentials: true })); app.use(express.json()); app.use(cookieParser()); -app.use(express.urlencoded({extended: true})); +app.use(express.urlencoded({ extended: true })); app.use(errorConverter); app.use(errorHandler); -app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs)); +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs)); // Routes -app.use('/auth', authRouter); +app.use("/auth", authRouter); -app.use('/api/courses', coursesRouter); -app.use('/api/departments', departmentsRouter); -app.use('/api/offerings', offeringsRouter); +app.use("/api/courses", coursesRouter); +app.use("/api/departments", departmentsRouter); +app.use("/api/offerings", offeringsRouter); -app.get('/', asyncHandler(async (_, response) => response.json({ - info: 'Testing course matrix backend server' - }))); +app.get( + "/", + asyncHandler(async (_, response) => + response.json({ + info: "Testing course matrix backend server", + }), + ), +); // Test get data from db -app.get('/post', asyncHandler(async (_, res) => { - try { - const {data, error} = await supabase.from('posts').select(); - console.log('Got posts', data); - return res.status(200).send(data); - } catch (err) { - return res.status(500).send({err}) - } - })) +app.get( + "/post", + asyncHandler(async (_, res) => { + try { + const { data, error } = await supabase.from("posts").select(); + console.log("Got posts", data); + return res.status(200).send(data); + } catch (err) { + return res.status(500).send({ err }); + } + }), +); server = app.listen(config.PORT, () => { console.log(`Server is running at http://${HOST}:${config.PORT}`); }); // graceful shutdown -const exitHandler = - () => { - if (server) { - server.close(() => { - console.info('Server closed'); - process.exit(1); - }) - } else { - process.exit(1); - } - } +const exitHandler = () => { + if (server) { + server.close(() => { + console.info("Server closed"); + process.exit(1); + }); + } else { + process.exit(1); + } +}; const unexpectedErrorHandler = (error: unknown) => { console.error(error); exitHandler(); }; -process.on('uncaughtException', unexpectedErrorHandler); -process.on('unhandledRejection', unexpectedErrorHandler); +process.on("uncaughtException", unexpectedErrorHandler); +process.on("unhandledRejection", unexpectedErrorHandler); diff --git a/course-matrix/backend/src/middleware/asyncHandler.ts b/course-matrix/backend/src/middleware/asyncHandler.ts index 4796c23a..cb8f7ce0 100644 --- a/course-matrix/backend/src/middleware/asyncHandler.ts +++ b/course-matrix/backend/src/middleware/asyncHandler.ts @@ -1,13 +1,17 @@ -import { Request, Response, NextFunction, RequestHandler } from 'express'; +import { Request, Response, NextFunction, RequestHandler } from "express"; -type AsyncFunction = (req: Request, res: Response, next: NextFunction) => Promise; +type AsyncFunction = ( + req: Request, + res: Response, + next: NextFunction, +) => Promise; const asyncHandler = (fn: AsyncFunction): RequestHandler => { - return (req: Request, res: Response, next: NextFunction): void => { - Promise.resolve(fn(req, res, next)).catch((error: Error) => { - res.status(500).json({ message: error.message }); - }); - }; + return (req: Request, res: Response, next: NextFunction): void => { + Promise.resolve(fn(req, res, next)).catch((error: Error) => { + res.status(500).json({ message: error.message }); + }); + }; }; -export default asyncHandler; \ No newline at end of file +export default asyncHandler; diff --git a/course-matrix/backend/src/middleware/authHandler.ts b/course-matrix/backend/src/middleware/authHandler.ts index 76f99ec1..e7ab9d6d 100644 --- a/course-matrix/backend/src/middleware/authHandler.ts +++ b/course-matrix/backend/src/middleware/authHandler.ts @@ -12,31 +12,33 @@ interface AuthenticatedRequest extends Request { session?: Session; } -export const authHandler = asyncHandler( async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - // Check refresh token in cookies to determine if authorized - const refreshToken = req.cookies.refresh_token; - - if (!refreshToken) { - return res.status(401).json({ message: 'No refresh token found' }); - } - - try { - // Verify the session - const { data, error } = await supabase.auth.refreshSession({ - refresh_token: refreshToken - }); - - if (error) { - res.clearCookie('refresh_token'); - return res.status(401).json({ message: 'Invalid or expired session' }); +export const authHandler = asyncHandler( + async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + // Check refresh token in cookies to determine if authorized + const refreshToken = req.cookies.refresh_token; + + if (!refreshToken) { + return res.status(401).json({ message: "No refresh token found" }); } - // Attach user and session to request for route handlers to access later - (req as AuthenticatedRequest).user = data?.user ?? undefined; - (req as AuthenticatedRequest).session = data?.session ?? undefined; + try { + // Verify the session + const { data, error } = await supabase.auth.refreshSession({ + refresh_token: refreshToken, + }); + + if (error) { + res.clearCookie("refresh_token"); + return res.status(401).json({ message: "Invalid or expired session" }); + } - next(); - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); - } -}) \ No newline at end of file + // Attach user and session to request for route handlers to access later + (req as AuthenticatedRequest).user = data?.user ?? undefined; + (req as AuthenticatedRequest).session = data?.session ?? undefined; + + next(); + } catch (error) { + res.status(500).json({ message: "Internal Server Error" }); + } + }, +); diff --git a/course-matrix/backend/src/middleware/errorHandler.ts b/course-matrix/backend/src/middleware/errorHandler.ts index ffe94d5c..99d44284 100644 --- a/course-matrix/backend/src/middleware/errorHandler.ts +++ b/course-matrix/backend/src/middleware/errorHandler.ts @@ -2,40 +2,40 @@ import { ErrorRequestHandler } from "express"; import ApiError from "../utils/utils"; export const errorConverter: ErrorRequestHandler = (err, req, res, next) => { - let error = err; - if (!(error instanceof ApiError)) { - const statusCode = - error.statusCode || - (error instanceof Error - ? 400 // Bad Request - : 500); // Internal Server Error - const message = - error.message || - (statusCode === 400 ? "Bad Request" : "Internal Server Error"); - error = new ApiError(statusCode, message, false, err.stack.toString()); - } - next(error); + let error = err; + if (!(error instanceof ApiError)) { + const statusCode = + error.statusCode || + (error instanceof Error + ? 400 // Bad Request + : 500); // Internal Server Error + const message = + error.message || + (statusCode === 400 ? "Bad Request" : "Internal Server Error"); + error = new ApiError(statusCode, message, false, err.stack.toString()); + } + next(error); }; export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { - let { statusCode, message } = err; - if (process.env.NODE_ENV === "production" && !err.isOperational) { - statusCode = 500; // Internal Server Error - message = "Internal Server Error"; - } + let { statusCode, message } = err; + if (process.env.NODE_ENV === "production" && !err.isOperational) { + statusCode = 500; // Internal Server Error + message = "Internal Server Error"; + } - res.locals.errorMessage = err.message; + res.locals.errorMessage = err.message; - const response = { - code: statusCode, - message, - ...(process.env.NODE_ENV === "development" && { stack: err.stack }), - }; + const response = { + code: statusCode, + message, + ...(process.env.NODE_ENV === "development" && { stack: err.stack }), + }; - if (process.env.NODE_ENV === "development") { - console.error(err); - } + if (process.env.NODE_ENV === "development") { + console.error(err); + } - res.status(statusCode).json(response); - next(); -}; \ No newline at end of file + res.status(statusCode).json(response); + next(); +}; diff --git a/course-matrix/backend/src/routes/authRouter.ts b/course-matrix/backend/src/routes/authRouter.ts index 1b78b681..c24d2281 100644 --- a/course-matrix/backend/src/routes/authRouter.ts +++ b/course-matrix/backend/src/routes/authRouter.ts @@ -1,12 +1,12 @@ -import express from 'express'; +import express from "express"; -import {handleAuthCode} from '../controllers/authentication' -import {login, logout, session, signUp} from '../controllers/userController' +import { handleAuthCode } from "../controllers/authentication"; +import { login, logout, session, signUp } from "../controllers/userController"; export const authRouter = express.Router(); -authRouter.post('/signup', signUp); -authRouter.post('/login', login); -authRouter.post('/logout', logout); -authRouter.get('/confirm', handleAuthCode); -authRouter.get('/session', session); \ No newline at end of file +authRouter.post("/signup", signUp); +authRouter.post("/login", login); +authRouter.post("/logout", logout); +authRouter.get("/confirm", handleAuthCode); +authRouter.get("/session", session); diff --git a/course-matrix/backend/src/routes/courseRouter.ts b/course-matrix/backend/src/routes/courseRouter.ts index 7eca424c..fdacfc36 100644 --- a/course-matrix/backend/src/routes/courseRouter.ts +++ b/course-matrix/backend/src/routes/courseRouter.ts @@ -10,4 +10,4 @@ export const offeringsRouter = express.Router(); coursesRouter.get("/", authHandler, coursesController.getCourses); departmentsRouter.get("/", authHandler, departmentsController.getDepartments); -offeringsRouter.get("/", authHandler, offeringsController.getOfferings) \ No newline at end of file +offeringsRouter.get("/", authHandler, offeringsController.getOfferings); diff --git a/course-matrix/backend/src/routes/userRouter.ts b/course-matrix/backend/src/routes/userRouter.ts index bff73c35..43b8b571 100644 --- a/course-matrix/backend/src/routes/userRouter.ts +++ b/course-matrix/backend/src/routes/userRouter.ts @@ -1,12 +1,12 @@ -import express from 'express'; +import express from "express"; -import {handleAuthCode} from '../controllers/authentication' -import {login, logout, session, signUp} from '../controllers/userController' +import { handleAuthCode } from "../controllers/authentication"; +import { login, logout, session, signUp } from "../controllers/userController"; export const usersRouter = express.Router(); -usersRouter.post('/signup', signUp); -usersRouter.post('/login', login); -usersRouter.post('/logout', logout); -usersRouter.get('/confirm', handleAuthCode); -usersRouter.get('/session', session); \ No newline at end of file +usersRouter.post("/signup", signUp); +usersRouter.post("/login", login); +usersRouter.post("/logout", logout); +usersRouter.get("/confirm", handleAuthCode); +usersRouter.get("/session", session); diff --git a/course-matrix/backend/src/utils/utils.ts b/course-matrix/backend/src/utils/utils.ts index 2e7722bc..11e0c079 100644 --- a/course-matrix/backend/src/utils/utils.ts +++ b/course-matrix/backend/src/utils/utils.ts @@ -3,20 +3,20 @@ class ApiError extends Error { isOperational: boolean; constructor( - statusCode: number, - message: string | undefined, - isOperational = true, - stack = "" + statusCode: number, + message: string | undefined, + isOperational = true, + stack = "", ) { - super(message); - this.statusCode = statusCode; - this.isOperational = isOperational; - if (stack) { - this.stack = stack; - } else { - Error.captureStackTrace(this, this.constructor); - } + super(message); + this.statusCode = statusCode; + this.isOperational = isOperational; + if (stack) { + this.stack = stack; + } else { + Error.captureStackTrace(this, this.constructor); + } } } -export default ApiError \ No newline at end of file +export default ApiError; diff --git a/course-matrix/frontend/README.md b/course-matrix/frontend/README.md index 74872fd4..780c92d8 100644 --- a/course-matrix/frontend/README.md +++ b/course-matrix/frontend/README.md @@ -18,11 +18,11 @@ export default tseslint.config({ languageOptions: { // other options... parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, }, -}) +}); ``` - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` @@ -31,11 +31,11 @@ export default tseslint.config({ ```js // eslint.config.js -import react from 'eslint-plugin-react' +import react from "eslint-plugin-react"; export default tseslint.config({ // Set the react version - settings: { react: { version: '18.3' } }, + settings: { react: { version: "18.3" } }, plugins: { // Add the react plugin react, @@ -44,7 +44,7 @@ export default tseslint.config({ // other rules... // Enable its recommended rules ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, + ...react.configs["jsx-runtime"].rules, }, -}) +}); ``` diff --git a/course-matrix/frontend/eslint.config.js b/course-matrix/frontend/eslint.config.js index 092408a9..79a552ea 100644 --- a/course-matrix/frontend/eslint.config.js +++ b/course-matrix/frontend/eslint.config.js @@ -1,28 +1,28 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], }, }, -) +); diff --git a/course-matrix/frontend/postcss.config.js b/course-matrix/frontend/postcss.config.js index e99ebc2c..2aa7205d 100644 --- a/course-matrix/frontend/postcss.config.js +++ b/course-matrix/frontend/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} \ No newline at end of file +}; diff --git a/course-matrix/frontend/src/App.tsx b/course-matrix/frontend/src/App.tsx index 018a8900..b5fe7fb6 100644 --- a/course-matrix/frontend/src/App.tsx +++ b/course-matrix/frontend/src/App.tsx @@ -1,25 +1,27 @@ -import './App.css' -import { Navigate, Route, Routes } from 'react-router-dom' -import LoginPage from './pages/Login/LoginPage' -import Dashboard from './pages/Dashboard/Dashboard' -import SignupPage from './pages/Signup/SignUpPage' -import AuthRoute from './components/auth-route' -import SignupSuccessfulPage from './pages/Signup/SignupSuccessfulPage' +import "./App.css"; +import { Navigate, Route, Routes } from "react-router-dom"; +import LoginPage from "./pages/Login/LoginPage"; +import Dashboard from "./pages/Dashboard/Dashboard"; +import SignupPage from "./pages/Signup/SignUpPage"; +import AuthRoute from "./components/auth-route"; +import SignupSuccessfulPage from "./pages/Signup/SignupSuccessfulPage"; function App() { - return (
- }/> - }/> - }/> - } /> - } /> - }/> + } /> + } /> + } /> + } /> + } /> + } + />
- ) + ); } -export default App +export default App; diff --git a/course-matrix/frontend/src/api/authApiSlice.ts b/course-matrix/frontend/src/api/authApiSlice.ts index 8655ee74..e028cb4a 100644 --- a/course-matrix/frontend/src/api/authApiSlice.ts +++ b/course-matrix/frontend/src/api/authApiSlice.ts @@ -1,57 +1,57 @@ -import {apiSlice} from './baseApiSlice' -import {AUTH_URL} from "./config" +import { apiSlice } from "./baseApiSlice"; +import { AUTH_URL } from "./config"; // Endpoints for /api/auth export const authApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ - login: builder.mutation({ - query: (data) => ({ - url: `${AUTH_URL}/login`, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - }, - body: data, - credentials: 'include', - }), + login: builder.mutation({ + query: (data) => ({ + url: `${AUTH_URL}/login`, + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + body: data, + credentials: "include", }), - logout: builder.mutation({ - query: () => ({ - url: `${AUTH_URL}/logout`, - method: 'POST', - credentials: 'include', - }), + }), + logout: builder.mutation({ + query: () => ({ + url: `${AUTH_URL}/logout`, + method: "POST", + credentials: "include", }), - signup: builder.mutation({ - query: (data) => ({ - url: `${AUTH_URL}/signup`, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - }, - body: data, - credentials: 'include', - }), + }), + signup: builder.mutation({ + query: (data) => ({ + url: `${AUTH_URL}/signup`, + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + body: data, + credentials: "include", }), - getSession: builder.query({ - query: () => ({ - url: `${AUTH_URL}/session`, - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - }, - credentials: 'include', - }), + }), + getSession: builder.query({ + query: () => ({ + url: `${AUTH_URL}/session`, + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + credentials: "include", }), - }) -}) + }), + }), +}); export const { useLoginMutation, useLogoutMutation, useSignupMutation, useGetSessionQuery, -} = authApiSlice; \ No newline at end of file +} = authApiSlice; diff --git a/course-matrix/frontend/src/api/baseApiSlice.ts b/course-matrix/frontend/src/api/baseApiSlice.ts index 73aa1879..c9d55517 100644 --- a/course-matrix/frontend/src/api/baseApiSlice.ts +++ b/course-matrix/frontend/src/api/baseApiSlice.ts @@ -1,16 +1,10 @@ -import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react' -import {BASE_URL} from "./config" +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { BASE_URL } from "./config"; -const baseQuery = fetchBaseQuery({baseUrl: BASE_URL}) +const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL }); export const apiSlice = createApi({ - baseQuery, - tagTypes: [ - 'Auth', - 'Course', - 'Department', - 'Offering', - 'Timetable', - ], - endpoints: () => ({}), -}) \ No newline at end of file + baseQuery, + tagTypes: ["Auth", "Course", "Department", "Offering", "Timetable"], + endpoints: () => ({}), +}); diff --git a/course-matrix/frontend/src/api/config.ts b/course-matrix/frontend/src/api/config.ts index 97b44c91..f19b290b 100644 --- a/course-matrix/frontend/src/api/config.ts +++ b/course-matrix/frontend/src/api/config.ts @@ -1,7 +1,7 @@ export const SERVER_URL = import.meta.env.VITE_SERVER_URL; -export const BASE_URL = '' +export const BASE_URL = ""; -export const AUTH_URL = `${SERVER_URL}/auth` -export const COURSES_URL = `${SERVER_URL}/api/courses` -export const DEPARTMENT_URL = `${SERVER_URL}/api/departments` -export const OFFERINGS_URL = `${SERVER_URL}/api/offerings` +export const AUTH_URL = `${SERVER_URL}/auth`; +export const COURSES_URL = `${SERVER_URL}/api/courses`; +export const DEPARTMENT_URL = `${SERVER_URL}/api/departments`; +export const OFFERINGS_URL = `${SERVER_URL}/api/offerings`; diff --git a/course-matrix/frontend/src/api/coursesApiSlice.ts b/course-matrix/frontend/src/api/coursesApiSlice.ts index bc96263b..5e72775f 100644 --- a/course-matrix/frontend/src/api/coursesApiSlice.ts +++ b/course-matrix/frontend/src/api/coursesApiSlice.ts @@ -1,25 +1,23 @@ -import {apiSlice} from './baseApiSlice' -import {COURSES_URL} from "./config" +import { apiSlice } from "./baseApiSlice"; +import { COURSES_URL } from "./config"; // Endpoints for /api/courses export const coursesApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ - getCourses: builder.query({ - query: (filters) => ({ - url: `${COURSES_URL}`, - method: 'GET', - params: filters, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - }, - providesTags: ["Course"], - credentials: 'include', - }), + getCourses: builder.query({ + query: (filters) => ({ + url: `${COURSES_URL}`, + method: "GET", + params: filters, + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Course"], + credentials: "include", }), - }) -}) + }), + }), +}); -export const { - useGetCoursesQuery, -} = coursesApiSlice; \ No newline at end of file +export const { useGetCoursesQuery } = coursesApiSlice; diff --git a/course-matrix/frontend/src/api/departmentsApiSlice.ts b/course-matrix/frontend/src/api/departmentsApiSlice.ts index 6a8028e4..061ccd4e 100644 --- a/course-matrix/frontend/src/api/departmentsApiSlice.ts +++ b/course-matrix/frontend/src/api/departmentsApiSlice.ts @@ -1,24 +1,22 @@ -import {apiSlice} from './baseApiSlice' -import {DEPARTMENT_URL} from "./config" +import { apiSlice } from "./baseApiSlice"; +import { DEPARTMENT_URL } from "./config"; // Endpoints for /api/departments export const departmentApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ - getDepartments: builder.query({ - query: () => ({ - url: `${DEPARTMENT_URL}`, - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - }, - providesTags: ["Department"], - credentials: 'include', - }), + getDepartments: builder.query({ + query: () => ({ + url: `${DEPARTMENT_URL}`, + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Department"], + credentials: "include", }), - }) -}) + }), + }), +}); -export const { - useGetDepartmentsQuery, -} = departmentApiSlice; \ No newline at end of file +export const { useGetDepartmentsQuery } = departmentApiSlice; diff --git a/course-matrix/frontend/src/api/offeringsApiSlice.ts b/course-matrix/frontend/src/api/offeringsApiSlice.ts index 37841d28..16ce173e 100644 --- a/course-matrix/frontend/src/api/offeringsApiSlice.ts +++ b/course-matrix/frontend/src/api/offeringsApiSlice.ts @@ -1,25 +1,23 @@ -import {apiSlice} from './baseApiSlice' -import {OFFERINGS_URL} from "./config" +import { apiSlice } from "./baseApiSlice"; +import { OFFERINGS_URL } from "./config"; // Endpoints for /api/offerings export const offeringsApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ - getOfferings: builder.query({ - query: (params) => ({ - url: `${OFFERINGS_URL}`, - method: 'GET', - params: params, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - }, - providesTags: ["Offerings"], - credentials: 'include', - }), + getOfferings: builder.query({ + query: (params) => ({ + url: `${OFFERINGS_URL}`, + method: "GET", + params: params, + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Offerings"], + credentials: "include", }), - }) -}) + }), + }), +}); -export const { - useGetOfferingsQuery, -} = offeringsApiSlice; \ No newline at end of file +export const { useGetOfferingsQuery } = offeringsApiSlice; diff --git a/course-matrix/frontend/src/app/dashboard/page.tsx b/course-matrix/frontend/src/app/dashboard/page.tsx index a79a2d52..9ab71743 100644 --- a/course-matrix/frontend/src/app/dashboard/page.tsx +++ b/course-matrix/frontend/src/app/dashboard/page.tsx @@ -1,4 +1,4 @@ -import { AppSidebar } from "@/components/app-sidebar" +import { AppSidebar } from "@/components/app-sidebar"; import { Breadcrumb, BreadcrumbItem, @@ -6,13 +6,13 @@ import { BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" -import { Separator } from "@/components/ui/separator" +} from "@/components/ui/breadcrumb"; +import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger, -} from "@/components/ui/sidebar" +} from "@/components/ui/sidebar"; export default function Page() { return ( @@ -46,5 +46,5 @@ export default function Page() { - ) + ); } diff --git a/course-matrix/frontend/src/components/UserMenu.tsx b/course-matrix/frontend/src/components/UserMenu.tsx index 7db47410..f617a2e4 100644 --- a/course-matrix/frontend/src/components/UserMenu.tsx +++ b/course-matrix/frontend/src/components/UserMenu.tsx @@ -1,19 +1,19 @@ import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, - DialogClose, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -25,112 +25,114 @@ import { clearCredentials } from "@/stores/authslice"; import { useNavigate } from "react-router-dom"; export function UserMenu() { - const dispatch = useDispatch(); - const [logout] = useLogoutMutation(); - const navigate = useNavigate(); + const dispatch = useDispatch(); + const [logout] = useLogoutMutation(); + const navigate = useNavigate(); - const handleLogout = async () => { - try { - await logout({}).unwrap(); - dispatch(clearCredentials()); - navigate('/'); - } catch (err) { - console.error('Logout failed:', err); - } - }; + const handleLogout = async () => { + try { + await logout({}).unwrap(); + dispatch(clearCredentials()); + navigate("/"); + } catch (err) { + console.error("Logout failed:", err); + } + }; - return ( - - -
- {/* John Doe is just a placeholder name for now */} - John Doe - - {/* Avatar Image is the profile picture of the user. The default avatar is used as a placeholder for now. */} - - {/* Avatar Fallback is the initials of the user. Avatar Fallback will be used if Avatar Image fails to load */} - JD - -
-
- - {/* Placeholder email of john.doe@gmail.com for now */} -
- -

john.doe@gmail.com

-
- e.preventDefault()}> - - - - - - - Edit Account - - Edit your account details. - - - - {/* Disable this email input box for now until we have the backend for accounts set up */} - - - {/* Disable this password input box for now until we have the backend for accounts set up */} - - - - - - - - - - - - - - - - e.preventDefault()}> - - - - - - - - Delete Account - - - Are you sure you want to delete your account? This action - cannot be undone. - - - - - - - {/* The logic for deleting accounts has not been implemented yet. Currently, clicking 'Delete' here will just close the Delete dialog. */} - - - - - - - -
-
- ); + return ( + + +
+ {/* John Doe is just a placeholder name for now */} + John Doe + + {/* Avatar Image is the profile picture of the user. The default avatar is used as a placeholder for now. */} + + {/* Avatar Fallback is the initials of the user. Avatar Fallback will be used if Avatar Image fails to load */} + JD + +
+
+ + {/* Placeholder email of john.doe@gmail.com for now */} +
+ +

john.doe@gmail.com

+
+ e.preventDefault()}> + + + + + + + Edit Account + + Edit your account details. + + + + {/* Disable this email input box for now until we have the backend for accounts set up */} + + + {/* Disable this password input box for now until we have the backend for accounts set up */} + + + + + + + + + + + + + + + + e.preventDefault()}> + + + + + + + + Delete Account + + + Are you sure you want to delete your account? This action + cannot be undone. + + + + + + + {/* The logic for deleting accounts has not been implemented yet. Currently, clicking 'Delete' here will just close the Delete dialog. */} + + + + + + + +
+
+ ); } diff --git a/course-matrix/frontend/src/components/app-sidebar.tsx b/course-matrix/frontend/src/components/app-sidebar.tsx index 932be6f4..b66288a4 100644 --- a/course-matrix/frontend/src/components/app-sidebar.tsx +++ b/course-matrix/frontend/src/components/app-sidebar.tsx @@ -1,7 +1,7 @@ -import * as React from "react" +import * as React from "react"; -import { SearchForm } from "@/components/search-form" -import { VersionSwitcher } from "@/components/version-switcher" +import { SearchForm } from "@/components/search-form"; +import { VersionSwitcher } from "@/components/version-switcher"; import { Sidebar, SidebarContent, @@ -13,25 +13,21 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarRail, -} from "@/components/ui/sidebar" -import Logo from "./logo" -import { Link, useLocation } from "react-router-dom" -import { Home, Calendar, Bot } from 'lucide-react'; - - +} from "@/components/ui/sidebar"; +import Logo from "./logo"; +import { Link, useLocation } from "react-router-dom"; +import { Home, Calendar, Bot } from "lucide-react"; const checkIfActive = (url: string, current: string) => { - return url === current -} - - + return url === current; +}; export function AppSidebar({ ...props }: React.ComponentProps) { - const location = useLocation() + const location = useLocation(); // Information for sidebar const data = { - versions: ["1.0.1",], + versions: ["1.0.1"], navMain: [ { title: "You", @@ -64,7 +60,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { ], }, ], - } + }; return ( @@ -83,7 +79,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - +
{item.title}
@@ -96,5 +92,5 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
- ) + ); } diff --git a/course-matrix/frontend/src/components/auth-route.tsx b/course-matrix/frontend/src/components/auth-route.tsx index 8adf5f71..c3a09ee0 100644 --- a/course-matrix/frontend/src/components/auth-route.tsx +++ b/course-matrix/frontend/src/components/auth-route.tsx @@ -1,20 +1,20 @@ -import { useGetSessionQuery } from '@/api/authApiSlice'; -import {Navigate} from 'react-router-dom' -import LoadingPage from '@/pages/Loading/LoadingPage'; +import { useGetSessionQuery } from "@/api/authApiSlice"; +import { Navigate } from "react-router-dom"; +import LoadingPage from "@/pages/Loading/LoadingPage"; interface AuthRouteProps { - component: React.ComponentType; // Type for the component prop + component: React.ComponentType; // Type for the component prop } -const AuthRoute: React.FC = ({component: Component}) => { - const {data, isLoading, error} = useGetSessionQuery() +const AuthRoute: React.FC = ({ component: Component }) => { + const { data, isLoading, error } = useGetSessionQuery(); - if (isLoading) { - return - } + if (isLoading) { + return ; + } - // TODO modify check based on return type of data - return data?.user ? : -} + // TODO modify check based on return type of data + return data?.user ? : ; +}; -export default AuthRoute \ No newline at end of file +export default AuthRoute; diff --git a/course-matrix/frontend/src/components/logo.tsx b/course-matrix/frontend/src/components/logo.tsx index 6c117f23..d05d3411 100644 --- a/course-matrix/frontend/src/components/logo.tsx +++ b/course-matrix/frontend/src/components/logo.tsx @@ -1,23 +1,21 @@ -import logoImg from "/img/logo.png" +import logoImg from "/img/logo.png"; const Logo = () => { - return <> -
- profile-img - -
-
- Course -
-
- Matrix + return ( + <> +
+ profile-img +
+
Course
+
Matrix
-
- -} + + ); +}; -export default Logo \ No newline at end of file +export default Logo; diff --git a/course-matrix/frontend/src/components/password-input.tsx b/course-matrix/frontend/src/components/password-input.tsx index c949cf4c..f581d1c8 100644 --- a/course-matrix/frontend/src/components/password-input.tsx +++ b/course-matrix/frontend/src/components/password-input.tsx @@ -1,16 +1,16 @@ -import React from 'react'; -import { Eye, EyeOff } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import React from "react"; +import { Eye, EyeOff } from "lucide-react"; +import { Button } from "@/components/ui/button"; import { FormControl, FormField, FormItem, FormLabel, FormMessage, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { useState } from 'react'; -import { UseFormReturn } from 'react-hook-form'; +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { useState } from "react"; +import { UseFormReturn } from "react-hook-form"; interface PasswordInputProps { form: UseFormReturn; @@ -20,12 +20,12 @@ interface PasswordInputProps { className?: string; } -const PasswordInput = ({ - form, - name, - label, +const PasswordInput = ({ + form, + name, + label, placeholder = "Password", - className + className, }: PasswordInputProps) => { const [showPassword, setShowPassword] = useState(false); @@ -35,12 +35,12 @@ const PasswordInput = ({ name={name} render={({ field }) => ( - {label || 'Password'} + {label || "Password"}
@@ -66,4 +66,4 @@ const PasswordInput = ({ ); }; -export default PasswordInput; \ No newline at end of file +export default PasswordInput; diff --git a/course-matrix/frontend/src/components/period-select.tsx b/course-matrix/frontend/src/components/period-select.tsx index 7adc00c5..d4df97bc 100644 --- a/course-matrix/frontend/src/components/period-select.tsx +++ b/course-matrix/frontend/src/components/period-select.tsx @@ -1,51 +1,78 @@ "use client"; - + import * as React from "react"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Period, display12HourValue, setDateByType } from "../utils/time-picker-utils"; - +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Period, + display12HourValue, + setDateByType, +} from "../utils/time-picker-utils"; + export interface PeriodSelectorProps { - period: Period; - setPeriod: (m: Period) => void; - date: Date | undefined; - setDate: (date: Date | undefined) => void; - onRightFocus?: () => void; - onLeftFocus?: () => void; + period: Period; + setPeriod: (m: Period) => void; + date: Date | undefined; + setDate: (date: Date | undefined) => void; + onRightFocus?: () => void; + onLeftFocus?: () => void; } - -export const TimePeriodSelect = React.forwardRef(({ period, setPeriod, date, setDate, onLeftFocus, onRightFocus }, ref) => { - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "ArrowRight") onRightFocus?.(); - if (e.key === "ArrowLeft") onLeftFocus?.(); - }; - - const handleValueChange = (value: Period) => { - setPeriod(value); - - /** - * trigger an update whenever the user switches between AM and PM; - * otherwise user must manually change the hour each time - */ - if (date) { - const tempDate = new Date(date); - const hours = display12HourValue(date.getHours()); - setDate(setDateByType(tempDate, hours.toString(), "12hours", period === "AM" ? "PM" : "AM")); - } - }; - - return ( -
- -
- ); + +export const TimePeriodSelect = React.forwardRef< + HTMLButtonElement, + PeriodSelectorProps +>(({ period, setPeriod, date, setDate, onLeftFocus, onRightFocus }, ref) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowRight") onRightFocus?.(); + if (e.key === "ArrowLeft") onLeftFocus?.(); + }; + + const handleValueChange = (value: Period) => { + setPeriod(value); + + /** + * trigger an update whenever the user switches between AM and PM; + * otherwise user must manually change the hour each time + */ + if (date) { + const tempDate = new Date(date); + const hours = display12HourValue(date.getHours()); + setDate( + setDateByType( + tempDate, + hours.toString(), + "12hours", + period === "AM" ? "PM" : "AM", + ), + ); + } + }; + + return ( +
+ +
+ ); }); - -TimePeriodSelect.displayName = "TimePeriodSelect"; \ No newline at end of file + +TimePeriodSelect.displayName = "TimePeriodSelect"; diff --git a/course-matrix/frontend/src/components/search-form.tsx b/course-matrix/frontend/src/components/search-form.tsx index bda415b4..6196776e 100644 --- a/course-matrix/frontend/src/components/search-form.tsx +++ b/course-matrix/frontend/src/components/search-form.tsx @@ -1,11 +1,11 @@ -import { Search } from "lucide-react" +import { Search } from "lucide-react"; -import { Label } from "@/components/ui/label" +import { Label } from "@/components/ui/label"; import { SidebarGroup, SidebarGroupContent, SidebarInput, -} from "@/components/ui/sidebar" +} from "@/components/ui/sidebar"; export function SearchForm({ ...props }: React.ComponentProps<"form">) { return ( @@ -24,5 +24,5 @@ export function SearchForm({ ...props }: React.ComponentProps<"form">) { - ) + ); } diff --git a/course-matrix/frontend/src/components/time-picker-hr.tsx b/course-matrix/frontend/src/components/time-picker-hr.tsx index c6086660..53f7f14a 100644 --- a/course-matrix/frontend/src/components/time-picker-hr.tsx +++ b/course-matrix/frontend/src/components/time-picker-hr.tsx @@ -3,20 +3,20 @@ import { Label } from "@/components/ui/label"; import { TimePickerInput } from "./time-picker-input"; import { TimePeriodSelect } from "./period-select"; import { Period } from "../utils/time-picker-utils"; - + interface TimePickerHrProps { date: Date | undefined; setDate: (date: Date | undefined) => void; } - + export function TimePickerHr({ date, setDate }: TimePickerHrProps) { const [period, setPeriod] = React.useState("AM"); - + const minuteRef = React.useRef(null); const hourRef = React.useRef(null); const secondRef = React.useRef(null); const periodRef = React.useRef(null); - + return (
@@ -75,4 +75,4 @@ export function TimePickerHr({ date, setDate }: TimePickerHrProps) {
); -} \ No newline at end of file +} diff --git a/course-matrix/frontend/src/components/time-picker-input.tsx b/course-matrix/frontend/src/components/time-picker-input.tsx index 4ca94c39..baddbdf0 100644 --- a/course-matrix/frontend/src/components/time-picker-input.tsx +++ b/course-matrix/frontend/src/components/time-picker-input.tsx @@ -1,5 +1,5 @@ import { Input } from "@/components/ui/input"; - + import { cn } from "@/lib/utils"; import React from "react"; import { @@ -9,7 +9,7 @@ import { getDateByType, setDateByType, } from "../utils/time-picker-utils"; - + export interface TimePickerInputProps extends React.InputHTMLAttributes { picker: TimePickerType; @@ -19,7 +19,7 @@ export interface TimePickerInputProps onRightFocus?: () => void; onLeftFocus?: () => void; } - + const TimePickerInput = React.forwardRef< HTMLInputElement, TimePickerInputProps @@ -41,11 +41,11 @@ const TimePickerInput = React.forwardRef< onRightFocus, ...props }, - ref + ref, ) => { const [flag, setFlag] = React.useState(false); const [prevIntKey, setPrevIntKey] = React.useState("0"); - + /** * allow the user to enter the second digit within 2 seconds * otherwise start again with entering first digit @@ -55,15 +55,15 @@ const TimePickerInput = React.forwardRef< const timer = setTimeout(() => { setFlag(false); }, 2000); - + return () => clearTimeout(timer); } }, [flag]); - + const calculatedValue = React.useMemo(() => { return getDateByType(date, picker); }, [date, picker]); - + const calculateNewValue = (key: string) => { /* * If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1. @@ -73,10 +73,10 @@ const TimePickerInput = React.forwardRef< if (flag && calculatedValue.slice(1, 2) === "1" && prevIntKey === "0") return "0" + key; } - + return !flag ? "0" + key : calculatedValue.slice(1, 2) + key; }; - + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Tab") return; e.preventDefault(); @@ -91,7 +91,7 @@ const TimePickerInput = React.forwardRef< } if (e.key >= "0" && e.key <= "9") { if (picker === "12hours") setPrevIntKey(e.key); - + const newValue = calculateNewValue(e.key); if (flag) onRightFocus?.(); setFlag((prev) => !prev); @@ -99,7 +99,7 @@ const TimePickerInput = React.forwardRef< setDate(setDateByType(tempDate, newValue, picker, period)); } }; - + return ( { @@ -123,9 +123,9 @@ const TimePickerInput = React.forwardRef< {...props} /> ); - } + }, ); - + TimePickerInput.displayName = "TimePickerInput"; - -export { TimePickerInput }; \ No newline at end of file + +export { TimePickerInput }; diff --git a/course-matrix/frontend/src/components/ui/accordion.tsx b/course-matrix/frontend/src/components/ui/accordion.tsx index e6a723d0..83ff0179 100644 --- a/course-matrix/frontend/src/components/ui/accordion.tsx +++ b/course-matrix/frontend/src/components/ui/accordion.tsx @@ -1,10 +1,10 @@ -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDown } from "lucide-react" +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Accordion = AccordionPrimitive.Root +const Accordion = AccordionPrimitive.Root; const AccordionItem = React.forwardRef< React.ElementRef, @@ -15,8 +15,8 @@ const AccordionItem = React.forwardRef< className={cn("border-b", className)} {...props} /> -)) -AccordionItem.displayName = "AccordionItem" +)); +AccordionItem.displayName = "AccordionItem"; const AccordionTrigger = React.forwardRef< React.ElementRef, @@ -27,7 +27,7 @@ const AccordionTrigger = React.forwardRef< ref={ref} className={cn( "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", - className + className, )} {...props} > @@ -35,8 +35,8 @@ const AccordionTrigger = React.forwardRef< -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; const AccordionContent = React.forwardRef< React.ElementRef, @@ -49,8 +49,8 @@ const AccordionContent = React.forwardRef< >
{children}
-)) +)); -AccordionContent.displayName = AccordionPrimitive.Content.displayName +AccordionContent.displayName = AccordionPrimitive.Content.displayName; -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/course-matrix/frontend/src/components/ui/avatar.tsx b/course-matrix/frontend/src/components/ui/avatar.tsx index 991f56ec..444b1dba 100644 --- a/course-matrix/frontend/src/components/ui/avatar.tsx +++ b/course-matrix/frontend/src/components/ui/avatar.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< React.ElementRef, @@ -11,12 +11,12 @@ const Avatar = React.forwardRef< ref={ref} className={cn( "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className + className, )} {...props} /> -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, @@ -27,8 +27,8 @@ const AvatarImage = React.forwardRef< className={cn("aspect-square h-full w-full", className)} {...props} /> -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, @@ -38,11 +38,11 @@ const AvatarFallback = React.forwardRef< ref={ref} className={cn( "flex h-full w-full items-center justify-center rounded-full bg-muted", - className + className, )} {...props} /> -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/course-matrix/frontend/src/components/ui/breadcrumb.tsx b/course-matrix/frontend/src/components/ui/breadcrumb.tsx index 60e6c96f..ecfc6a44 100644 --- a/course-matrix/frontend/src/components/ui/breadcrumb.tsx +++ b/course-matrix/frontend/src/components/ui/breadcrumb.tsx @@ -1,16 +1,16 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Breadcrumb = React.forwardRef< HTMLElement, React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode + separator?: React.ReactNode; } ->(({ ...props }, ref) =>