From eb288ff72829e39dfc39dcfc69fa7bd59eff5e65 Mon Sep 17 00:00:00 2001 From: Lucas Yan Date: Wed, 25 Feb 2026 19:43:05 -0800 Subject: [PATCH 1/6] section card + general page layout --- .../src/app/(pages)/programs/page.module.css | 91 +++++++++++++++ frontend/src/app/(pages)/programs/page.tsx | 104 +++++++++++++++++- .../src/components/SectionCard.module.css | 93 ++++++++++++++++ frontend/src/components/SectionCard.tsx | 79 +++++++++++++ package-lock.json | 22 +++- package.json | 3 +- 6 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/(pages)/programs/page.module.css create mode 100644 frontend/src/components/SectionCard.module.css create mode 100644 frontend/src/components/SectionCard.tsx diff --git a/frontend/src/app/(pages)/programs/page.module.css b/frontend/src/app/(pages)/programs/page.module.css new file mode 100644 index 0000000..969d8c4 --- /dev/null +++ b/frontend/src/app/(pages)/programs/page.module.css @@ -0,0 +1,91 @@ +.pageWrapper { + padding: 40px 48px; + background: white; + min-height: 100vh; +} + +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 32px; +} + +.titleSection h1 { + font-size: 28px; + font-weight: 700; + margin: 0 0 4px; + color: var(--grey-900); +} + +.titleSection p { + font-size: 14px; + color: var(--grey-500); + margin: 0; +} + +.createButton { + display: flex; + align-items: center; + gap: 8px; + background-color: var(--primary-500); + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} + +.createButton:hover { + background-color: var(--primary-600); +} + +.controls { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.tabs { + display: flex; + gap: 8px; +} + +.tabButton { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: 1px solid var(--grey-100); + background: white; + color: var(--grey-600); +} + +.tabButton:hover { + background: var(--grey-25); +} + +.tabActive { + background-color: var(--primary-500); + color: white; + border-color: var(--primary-500); +} + +.tabActive:hover { + background-color: var(--primary-600); + border-color: var(--primary-600); +} + +.grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; +} diff --git a/frontend/src/app/(pages)/programs/page.tsx b/frontend/src/app/(pages)/programs/page.tsx index 33c8150..37c85cd 100644 --- a/frontend/src/app/(pages)/programs/page.tsx +++ b/frontend/src/app/(pages)/programs/page.tsx @@ -1,3 +1,105 @@ +"use client"; + +import { useState } from "react"; +import { Clock, Archive, Plus } from "lucide-react"; +import { SectionCard } from "@/src/components/SectionCard"; +import styles from "./page.module.css"; + +type Tab = "active" | "archived"; + export default function Programs() { - return
This is Programs page
; + const [activeTab, setActiveTab] = useState("active"); + + return ( +
+
+
+

Classes

+

Take attendance, add notes, and see trends

+
+ +
+ +
+
+ + +
+
+ +
+ console.log("edit")} + onArchive={() => console.log("archive")} + onDelete={() => console.log("delete")} + /> + console.log("edit")} + onArchive={() => console.log("archive")} + onDelete={() => console.log("delete")} + /> + console.log("edit")} + onArchive={() => console.log("archive")} + onDelete={() => console.log("delete")} + /> + console.log("edit")} + onArchive={() => console.log("archive")} + onDelete={() => console.log("delete")} + /> +
+
+ ); } diff --git a/frontend/src/components/SectionCard.module.css b/frontend/src/components/SectionCard.module.css new file mode 100644 index 0000000..5579b56 --- /dev/null +++ b/frontend/src/components/SectionCard.module.css @@ -0,0 +1,93 @@ +.cardWrapper { + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: white; + width: 100%; +} + +.topBar { + height: 64px; + display: flex; + justify-content: flex-end; + align-items: flex-start; + padding: 12px; + position: relative; +} + +.menuButton { + background: none; + border: none; + color: white; + cursor: pointer; + font-size: 20px; + letter-spacing: 2px; + padding: 4px 8px; + border-radius: 4px; +} + +.menuButton:hover { + background: rgba(255, 255, 255, 0.2); +} + +.dropdown { + position: absolute; + top: 44px; + right: 12px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10; + min-width: 130px; + overflow: hidden; +} + +.dropdown button { + display: block; + width: 100%; + padding: 10px 16px; + text-align: left; + background: none; + border: none; + cursor: pointer; + font-size: 14px; + color: var(--grey-900); +} + +.dropdown button:hover { + background: var(--grey-25); +} + +.cardBody { + padding: 20px 20px 25px 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.cardBody h3 { + font-size: 18px; + font-weight: 600; + margin: 0; + color: var(--grey-900); +} + +.cardBody p { + font-size: 14px; + color: var(--grey-500); + margin: 0; +} + +.cardBody hr { + border: none; + border-top: 1px solid var(--grey-100); + margin: 0; +} + +.row { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--grey-600); +} diff --git a/frontend/src/components/SectionCard.tsx b/frontend/src/components/SectionCard.tsx new file mode 100644 index 0000000..172a53d --- /dev/null +++ b/frontend/src/components/SectionCard.tsx @@ -0,0 +1,79 @@ +"use client"; +import styles from "./SectionCard.module.css"; +import { GraduationCap, Calendar } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; + +export const SectionCard = function SectionCard({ + code, + day, + teachers, + startTime, + endTime, + startDate, + endDate, + archived, + color, + days, + onEdit, + onArchive, + onDelete, +}: { + code: string; + day: string; + teachers: string[]; + startTime: string; + endTime: string; + teacherName: string; + startDate: string; + endDate: string; + color: string; + archived: boolean; + days: string; + onEdit: () => void; + onArchive: () => void; + onDelete: () => void; +}) { + const [menuOpen, setMenuOpen] = useState(false); + const toggleMenu = () => setMenuOpen((prev) => !prev); + const menuRef = useRef(null); + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setMenuOpen(false); + } + }; + if (menuOpen) document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [menuOpen]); + + return ( +
+
+ + {menuOpen && ( +
+ + + +
+ )} +
+ +
+

{code}

+

+ {day} {startTime} - {endTime} +

+ +
+ +
+ {teachers[0]} +
+
+ {startDate} - {endDate} +
+
+
+ ); +}; diff --git a/package-lock.json b/package-lock.json index 70ce654..866b5ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "firebase": "^12.9.0" + "firebase": "^12.9.0", + "lucide-react": "^0.575.0" } }, "node_modules/@firebase/ai": { @@ -885,6 +886,15 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lucide-react": { + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -909,6 +919,16 @@ "node": ">=12.0.0" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 581214b..59b8902 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "firebase": "^12.9.0" + "firebase": "^12.9.0", + "lucide-react": "^0.575.0" } } From c68f78e1883018ab4f8f4547090367fdedff4afc Mon Sep 17 00:00:00 2001 From: Lucas Yan Date: Fri, 27 Feb 2026 12:42:41 -0800 Subject: [PATCH 2/6] create section cards --- frontend/src/app/(pages)/programs/page.tsx | 2 ++ frontend/src/components/SectionCard.tsx | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/(pages)/programs/page.tsx b/frontend/src/app/(pages)/programs/page.tsx index 37c85cd..6964501 100644 --- a/frontend/src/app/(pages)/programs/page.tsx +++ b/frontend/src/app/(pages)/programs/page.tsx @@ -5,6 +5,8 @@ import { Clock, Archive, Plus } from "lucide-react"; import { SectionCard } from "@/src/components/SectionCard"; import styles from "./page.module.css"; +// next steps : fetch data, render actual data, filter by active archived, search bar + type Tab = "active" | "archived"; export default function Programs() { diff --git a/frontend/src/components/SectionCard.tsx b/frontend/src/components/SectionCard.tsx index 172a53d..a11b6ba 100644 --- a/frontend/src/components/SectionCard.tsx +++ b/frontend/src/components/SectionCard.tsx @@ -49,7 +49,9 @@ export const SectionCard = function SectionCard({ return (
- + {menuOpen && (
From 7e401fc76cf930d0bbdc969bfad5a7dbf7b9cd19 Mon Sep 17 00:00:00 2001 From: Lucas Yan Date: Tue, 3 Mar 2026 22:00:21 -0800 Subject: [PATCH 3/6] update backend to match schema doc --- backend/src/models/sections.ts | 46 +++++++++++++++++++----------- backend/src/validators/sections.ts | 23 +++++++++------ 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/backend/src/models/sections.ts b/backend/src/models/sections.ts index c37a35d..e0c9254 100644 --- a/backend/src/models/sections.ts +++ b/backend/src/models/sections.ts @@ -1,15 +1,18 @@ import mongoose from "mongoose"; -import type { Document, Types } from "mongoose"; +import type { Document, Types } from "mongoose"; // Types still used for ObjectId arrays in teachers/enrolledStudents // Type definition for Section documents export type SectionDoc = Document & { code: string; - program: Types.ObjectId; teachers: Types.ObjectId[]; enrolledStudents: Types.ObjectId[]; startTime: string; endTime: string; + startDate: string; + endDate: string; + archived: boolean; + color: string; days: string[]; }; @@ -18,36 +21,45 @@ const sectionSchema = new mongoose.Schema( { code: { type: String, - required: true, // Code must be provided - }, - program: { - type: mongoose.Schema.Types.ObjectId, - ref: "Program", // Reference to the Program model - required: true, // Must be associated with a program + required: true, }, teachers: { type: [mongoose.Schema.Types.ObjectId], - ref: "User", // Reference to the User model - required: true, // Must contain at least one teacher - default: [], // Default to an empty array if no teachers are added + ref: "User", + default: [], }, enrolledStudents: { type: [mongoose.Schema.Types.ObjectId], - ref: "Student", // Reference to the Student model - required: false, // Not required at creation - default: [], // Default to an empty array if no students are enrolled + ref: "Student", + default: [], }, startTime: { type: String, - required: true, // Start time must be provided + required: true, }, endTime: { type: String, - required: true, // End time must be provided + required: true, + }, + startDate: { + type: String, + required: true, + }, + endDate: { + type: String, + required: true, + }, + archived: { + type: Boolean, + default: false, + }, + color: { + type: String, + required: true, }, days: { type: [String], - required: true, // Must provide an array of days + required: true, }, }, { diff --git a/backend/src/validators/sections.ts b/backend/src/validators/sections.ts index 8de88e5..36c6585 100644 --- a/backend/src/validators/sections.ts +++ b/backend/src/validators/sections.ts @@ -6,11 +6,10 @@ const TIME_24H_REGEX = /^(?:[01]\d|2[0-3]):[0-5]\d$/; const validateCode = body("code").notEmpty().withMessage("Code is required"); -const validateProgram = body("program") - .notEmpty() - .withMessage("Program is required") - .isMongoId() - .withMessage("Program must be a valid MongoDB ObjectID"); +const validateStartDate = body("startDate").notEmpty().withMessage("Start date is required"); +const validateEndDate = body("endDate").notEmpty().withMessage("End date is required"); +const validateColor = body("color").notEmpty().withMessage("Color is required"); +const validateArchived = body("archived").optional().isBoolean().withMessage("Archived must be a boolean"); export const validateTeachers: ValidationChain[] = [ body("teachers").isArray().withMessage("Teachers must be an array"), @@ -81,19 +80,25 @@ const validateEndTime = body("endTime") export const createSectionValidator = [ validateCode, validateDays, + validateStartTime, validateEndTime, + validateStartDate, + validateEndDate, + validateColor, + validateArchived, ...validateEnrolledStudents, - validateProgram, - validateStartTime, ...validateTeachers, ]; export const updateSectionValidator = [ validateCode.optional(), validateDays.optional(), + validateStartTime.optional(), validateEndTime.optional(), + validateStartDate.optional(), + validateEndDate.optional(), + validateColor.optional(), + validateArchived.optional(), ...validateEnrolledStudents.map((v) => v.optional()), - validateProgram.optional(), - validateStartTime.optional(), ...validateTeachers.map((v) => v.optional()), ]; From b8e0d32eb76c39889b3750742f0c44c1cc32f1d0 Mon Sep 17 00:00:00 2001 From: Lucas Yan Date: Tue, 3 Mar 2026 22:02:40 -0800 Subject: [PATCH 4/6] get controllers/sections.ts to match new backend schema --- backend/src/controllers/sections.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/sections.ts b/backend/src/controllers/sections.ts index 63ddc82..8a69a5e 100644 --- a/backend/src/controllers/sections.ts +++ b/backend/src/controllers/sections.ts @@ -22,7 +22,7 @@ export const createSection: RequestHandler = async (req, res) => { // Only keep the fields clients can update export type UpdateSectionBody = Pick< SectionDoc, - "code" | "program" | "teachers" | "enrolledStudents" | "startTime" | "endTime" | "days" + "code" | "teachers" | "enrolledStudents" | "startTime" | "endTime" | "startDate" | "endDate" | "archived" | "color" | "days" >; // ---------------------- UPDATE ---------------------- From e80e12d852fdfe3a9aa7d1eab82ef22653606690 Mon Sep 17 00:00:00 2001 From: Lucas Yan Date: Thu, 12 Mar 2026 22:56:54 -0700 Subject: [PATCH 5/6] feat: add components for program page with custom hook of useToast --- frontend/package-lock.json | 141 +++++--- frontend/package.json | 1 + frontend/src/api/sections.ts | 25 +- .../src/app/(pages)/programs/page.module.css | 165 ++++++++- frontend/src/app/(pages)/programs/page.tsx | 333 ++++++++++++++---- .../src/components/CustomSelect.module.css | 89 +++++ frontend/src/components/CustomSelect.tsx | 68 ++++ .../src/components/SectionCard.module.css | 2 +- frontend/src/components/SectionCard.tsx | 49 ++- .../src/components/SectionForm.module.css | 262 ++++++++++++++ frontend/src/components/SectionForm.tsx | 253 +++++++++++++ frontend/src/components/Toast.module.css | 101 ++++++ frontend/src/components/Toast.tsx | 79 +++++ frontend/src/hooks/useToast.tsx | 27 ++ 14 files changed, 1464 insertions(+), 131 deletions(-) create mode 100644 frontend/src/components/CustomSelect.module.css create mode 100644 frontend/src/components/CustomSelect.tsx create mode 100644 frontend/src/components/SectionForm.module.css create mode 100644 frontend/src/components/SectionForm.tsx create mode 100644 frontend/src/components/Toast.module.css create mode 100644 frontend/src/components/Toast.tsx create mode 100644 frontend/src/hooks/useToast.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dc21499..fdd084c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@mui/material": "^7.3.7", "date-fns": "^4.1.0", "lucide-react": "^0.564.0", + "mui": "^0.0.1", "next": "16.0.10", "react": "19.2.1", "react-day-picker": "^9.13.2", @@ -227,7 +228,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2152,6 +2152,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -2169,6 +2170,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -2186,6 +2188,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -2203,6 +2206,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -2220,6 +2224,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -2237,6 +2242,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -2254,6 +2260,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2271,6 +2278,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2288,6 +2296,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2305,6 +2314,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2322,6 +2332,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2339,6 +2350,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2356,6 +2368,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2373,6 +2386,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2390,6 +2404,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2407,6 +2422,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2424,6 +2440,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2441,6 +2458,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2458,6 +2476,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2475,6 +2494,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2492,6 +2512,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2509,6 +2530,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -2526,6 +2548,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -2543,6 +2566,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2560,6 +2584,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2577,6 +2602,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -3690,7 +3716,6 @@ "integrity": "sha512-b2NlWN70bbPLmfyoLvvidPKWENBYYIe017ZGUpElvQjDytCWgxPJx7L9juxHt0xHvNVA08ZHJdOyhGzon/KJuw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-glob": "3.3.1" } @@ -3942,7 +3967,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.57.1", @@ -3956,7 +3982,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.57.1", @@ -3970,7 +3997,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.57.1", @@ -3984,7 +4012,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.57.1", @@ -3998,7 +4027,8 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.57.1", @@ -4012,7 +4042,8 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.57.1", @@ -4026,7 +4057,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.57.1", @@ -4040,7 +4072,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.57.1", @@ -4054,7 +4087,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.57.1", @@ -4068,7 +4102,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.57.1", @@ -4082,7 +4117,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.57.1", @@ -4096,7 +4132,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.57.1", @@ -4110,7 +4147,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.57.1", @@ -4124,7 +4162,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.57.1", @@ -4138,7 +4177,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.57.1", @@ -4152,7 +4192,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.57.1", @@ -4166,7 +4207,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.57.1", @@ -4180,7 +4222,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.57.1", @@ -4194,7 +4237,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.57.1", @@ -4208,7 +4252,8 @@ "optional": true, "os": [ "openbsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.57.1", @@ -4222,7 +4267,8 @@ "optional": true, "os": [ "openharmony" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.57.1", @@ -4236,7 +4282,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.57.1", @@ -4250,7 +4297,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.57.1", @@ -4264,7 +4312,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.57.1", @@ -4278,7 +4327,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -4503,7 +4553,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5045,7 +5094,6 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5067,7 +5115,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5104,7 +5151,6 @@ "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", @@ -5144,7 +5190,6 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -5671,6 +5716,7 @@ "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.25", @@ -5685,6 +5731,7 @@ "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-core": "3.5.25", "@vue/shared": "3.5.25" @@ -5715,6 +5762,7 @@ "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/shared": "3.5.25" @@ -5725,7 +5773,8 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", @@ -5908,7 +5957,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6414,7 +6462,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7448,6 +7495,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7511,7 +7559,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7870,7 +7917,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8034,7 +8080,6 @@ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -8208,7 +8253,6 @@ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", @@ -8785,6 +8829,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -9822,7 +9867,6 @@ "integrity": "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^3.0.0", @@ -11340,6 +11384,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mui": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/mui/-/mui-0.0.1.tgz", + "integrity": "sha512-iB9zfxsJBcMkZ/SY6X+HGSPr4fftCZIQ76ZMH8iSMfVkidVzRtZlLW2gbWXUe+IMcj8JLv1p+dGKvPVlgtiocA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -12026,7 +12076,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12057,7 +12106,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12326,6 +12374,7 @@ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -12497,7 +12546,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -13150,7 +13198,6 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -13257,7 +13304,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13502,7 +13548,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13774,6 +13819,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -13864,6 +13910,7 @@ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12.0.0" }, @@ -13896,7 +13943,6 @@ "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", @@ -14222,7 +14268,6 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index 3e324ed..c7e2eb6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@mui/material": "^7.3.7", "date-fns": "^4.1.0", "lucide-react": "^0.564.0", + "mui": "^0.0.1", "next": "16.0.10", "react": "19.2.1", "react-day-picker": "^9.13.2", diff --git a/frontend/src/api/sections.ts b/frontend/src/api/sections.ts index dd0ff0d..b00d7e9 100644 --- a/frontend/src/api/sections.ts +++ b/frontend/src/api/sections.ts @@ -1,20 +1,23 @@ -import { get, handleAPIError, post, put } from "./requests"; +import { del, get, handleAPIError, post, put } from "./requests"; import type { APIResult } from "./requests"; export type Section = { _id: string; - code: string; - program: string; // ObjectID in backend teachers: string[]; // ObjectID[] in backend enrolledStudents: string[]; // ObjectID[] in backend startTime: string; endTime: string; + startDate: string; + endDate: string; + archived: boolean; + color: string; days: string[]; + createdAt: string; }; -export type CreateSectionRequest = Omit; +export type CreateSectionRequest = Omit; export type UpdateSectionRequest = Section; export async function getAllSections(): Promise> { @@ -47,9 +50,19 @@ export async function updateSection(section: UpdateSectionRequest): Promise> { +export async function createSection(section: CreateSectionRequest): Promise> { + try { + const response = await post(`/sections`, section); + const json = (await response.json()) as Section; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function deleteSection(id: string): Promise> { try { - const response = await post(`/sections`, student); + const response = await del(`/sections/${id}`); const json = (await response.json()) as Section; return { success: true, data: json }; } catch (error) { diff --git a/frontend/src/app/(pages)/programs/page.module.css b/frontend/src/app/(pages)/programs/page.module.css index 969d8c4..5b42880 100644 --- a/frontend/src/app/(pages)/programs/page.module.css +++ b/frontend/src/app/(pages)/programs/page.module.css @@ -1,6 +1,6 @@ .pageWrapper { padding: 40px 48px; - background: white; + background: #f5f6f8; min-height: 100vh; } @@ -59,12 +59,12 @@ display: flex; align-items: center; gap: 6px; - padding: 8px 16px; - border-radius: 8px; + padding: 8px 18px; + border-radius: 20px; font-size: 14px; font-weight: 500; cursor: pointer; - border: 1px solid var(--grey-100); + border: 1.5px solid var(--grey-200); background: white; color: var(--grey-600); } @@ -82,6 +82,163 @@ .tabActive:hover { background-color: var(--primary-600); border-color: var(--primary-600); + color: white; +} + +.rightControls { + display: flex; + align-items: center; + gap: 12px; +} + +.sortButton { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: 1px solid var(--grey-200); + background: white; + color: var(--grey-600); + white-space: nowrap; +} + +.sortButton:hover { + background: var(--grey-25); +} + +.searchWrapper { + position: relative; + display: flex; + align-items: center; +} + +.searchIcon { + position: absolute; + left: 12px; + color: var(--grey-400); + pointer-events: none; +} + +.searchInput { + padding: 8px 14px 8px 36px; + border: 1px solid var(--grey-200); + border-radius: 8px; + font-size: 14px; + color: var(--grey-900); + outline: none; + width: 200px; +} + +.searchInput:focus { + border-color: var(--primary-500); +} + +.confirmDialog { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 460px; +} + +.confirmTitle { + display: flex; + align-items: center; + gap: 10px; + font-size: 20px; + font-weight: 700; + color: var(--grey-900); + margin: 0; +} + +.confirmWarning { + font-weight: 600; + color: var(--grey-700); + margin: 0; +} + +.confirmFooter { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 8px; +} + +.confirmCancel { + padding: 10px 20px; + border-radius: 8px; + border: none; + background-color: var(--primary-500); + color: white; + font-size: 14px; + font-weight: 600; + cursor: pointer; +} + +.confirmCancel:hover { + background-color: var(--primary-600); +} + +.confirmDelete { + padding: 10px 20px; + border-radius: 8px; + border: 1.5px solid #e53935; + background: white; + color: #e53935; + font-size: 14px; + font-weight: 600; + cursor: pointer; +} + +.confirmDelete:hover { + background: #fff5f5; +} + +.sortContainer { + position: relative; +} + +.sortDropdown { + position: absolute; + top: calc(100% + 6px); + right: 0; + background: white; + border: 1px solid var(--grey-200); + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + z-index: 50; + min-width: 200px; + padding: 6px; +} + +.sortOption { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 10px 14px; + background: none; + border: none; + border-radius: 8px; + font-size: 14px; + color: var(--grey-800); + cursor: pointer; + text-align: left; + gap: 8px; +} + +.sortOption:hover { + background: var(--grey-25); +} + +.sortDivider { + border: none; + border-top: 1px solid var(--grey-100); + margin: 4px 0; } .grid { diff --git a/frontend/src/app/(pages)/programs/page.tsx b/frontend/src/app/(pages)/programs/page.tsx index 6964501..021e187 100644 --- a/frontend/src/app/(pages)/programs/page.tsx +++ b/frontend/src/app/(pages)/programs/page.tsx @@ -1,16 +1,98 @@ "use client"; +// To do : pop up for the delete + archive, toast in bottom right corner when an action (create, edit delete archive is done,), styling +import { + AlertTriangle, + Archive, + ArrowDown, + ArrowUp, + ArrowUpDown, + Check, + Clock, + Plus, + Search, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { Toast } from "@/src/components/Toast"; +import { useToast } from "@/src/hooks/useToast"; -import { useState } from "react"; -import { Clock, Archive, Plus } from "lucide-react"; -import { SectionCard } from "@/src/components/SectionCard"; import styles from "./page.module.css"; +import { + createSection, + deleteSection, + getAllSections, + type Section, + updateSection, +} from "@/src/api/sections"; +import { Modal } from "@/src/components/Modal"; +import { SectionCard } from "@/src/components/SectionCard"; +import { SectionForm } from "@/src/components/SectionForm"; + // next steps : fetch data, render actual data, filter by active archived, search bar type Tab = "active" | "archived"; export default function Programs() { const [activeTab, setActiveTab] = useState("active"); + const [sections, setSections] = useState([]); + const [editingSection, setEditingSection] = useState
(null); + const [deletingSection, setDeletingSection] = useState
(null); + const [showCreateModal, setShowCreateModal] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState< + "alphabetical" | "startDate" | "endDate" | "color" | "createdAt" + >("alphabetical"); + const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); + const [sortOpen, setSortOpen] = useState(false); + const sortMenuRef = useRef(null); + const { toast, showToast, dismissToast } = useToast(); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (sortMenuRef.current && !sortMenuRef.current.contains(e.target as Node)) { + setSortOpen(false); + } + }; + if (sortMenuRef) document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [sortOpen]); + + useEffect(() => { + const fetchData = async () => { + const result = await getAllSections(); + if (result.success) { + setSections(result.data); + } else { + throw new Error("Data could not be fetched"); + } + }; + void fetchData(); + }, []); + + const visibleSections = sections + .filter((section) => (activeTab === "active" ? !section.archived : section.archived)) + .filter((section) => section.code.toLowerCase().includes(searchQuery)) + .sort((a, b) => { + let result = 0; + switch (sortBy) { + case "alphabetical": + result = a.code.localeCompare(b.code); + break; + case "startDate": + result = new Date(a.startDate).getTime() - new Date(b.startDate).getTime(); + break; + case "endDate": + result = new Date(a.endDate).getTime() - new Date(b.endDate).getTime(); + break; + case "color": + result = a.color.localeCompare(b.color); + break; + case "createdAt": + result = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + } + return sortDir === "asc" ? result : -result; + }); return (
@@ -19,7 +101,7 @@ export default function Programs() {

Classes

Take attendance, add notes, and see trends

- @@ -42,66 +124,199 @@ export default function Programs() { Archived
-
+
+
+ + {sortOpen && ( +
+ {[ + { label: "Alphabetical", value: "alphabetical" }, + { label: "Date Created", value: "createdAt" }, + { label: "Start Date", value: "startDate" }, + { label: "End Date", value: "endDate" }, + { label: "Color", value: "color" }, + ].map((opt) => ( + + ))} -
- console.log("edit")} - onArchive={() => console.log("archive")} - onDelete={() => console.log("delete")} - /> - console.log("edit")} - onArchive={() => console.log("archive")} - onDelete={() => console.log("delete")} +
+ + {[ + { label: "Ascending", value: "asc", icon: }, + { label: "Descending", value: "desc", icon: }, + ].map((opt) => ( + + ))} +
+ )} +
+
+ + setSearchQuery(e.target.value)} + /> +
+
+
+ {/* Create new section! */} + {showCreateModal && ( + { + const result = await createSection({ + ...data, + teachers: [], + enrolledStudents: [], + }); + if (result.success) { + setSections((prev) => [...prev, result.data]); + setShowCreateModal(false); + showToast(`${data.code} Successfully Created!`); + } + }} + onCancel={() => setShowCreateModal(false)} + /> + } + onExit={() => setShowCreateModal(false)} /> - console.log("edit")} - onArchive={() => console.log("archive")} - onDelete={() => console.log("delete")} + )} + + {/* pop up for deleting section */} + {deletingSection && ( + +
+ Deleting Class +
+

+ Are you sure you want to delete {deletingSection.code}? +

+

This action cannot be undone.

+
+ + +
+
+ } + onExit={() => setDeletingSection(null)} /> - console.log("edit")} - onArchive={() => console.log("archive")} - onDelete={() => console.log("delete")} + )} + + {/* Edit section modal! */} + {editingSection && ( + { + const result = await updateSection({ ...editingSection, ...data }); + if (result.success) { + setSections((prev) => + prev.map((s) => (s._id === result.data._id ? result.data : s)), + ); + setEditingSection(null); + showToast(`${data.code} Successfully Edited!`); + } + }} + onCancel={() => setEditingSection(null)} + /> + } + onExit={() => setEditingSection(null)} /> + )} + +
+ {visibleSections.map((section) => ( + setEditingSection(section)} + onArchive={async () => { + const result = await updateSection({ ...section, archived: !section.archived }); + if (result.success) { + setSections((prev) => + prev.map((s) => (s._id === result.data._id ? result.data : s)), + ); + const label = !section.archived ? "Archived" : "Unarchived"; + showToast(`${section.code} Successfully ${label}!`, () => { + void updateSection({ ...section, archived: section.archived }).then((undo) => { + if (undo.success) + setSections((prev) => + prev.map((s) => (s._id === undo.data._id ? undo.data : s)), + ); + }); + }); + } + }} + onDelete={() => setDeletingSection(section)} + /> + ))}
+ + {toast && ( + dismissToast()} + /> + )} ); } diff --git a/frontend/src/components/CustomSelect.module.css b/frontend/src/components/CustomSelect.module.css new file mode 100644 index 0000000..219c018 --- /dev/null +++ b/frontend/src/components/CustomSelect.module.css @@ -0,0 +1,89 @@ +.wrapper { + position: relative; + flex: 1; + min-width: 0; +} + +.trigger { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border: 1.5px solid var(--grey-200); + border-radius: 12px; + background: white; + font-size: 14px; + cursor: pointer; + outline: none; + text-align: left; +} + +.trigger:hover { + border-color: var(--grey-400); +} + +.triggerOpen { + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(74, 109, 140, 0.1); +} + +.selectedText { + color: var(--grey-900); +} + +.placeholder { + color: var(--grey-400); +} + +.chevron { + color: var(--grey-500); + flex-shrink: 0; + transition: transform 0.15s; +} + +.chevronOpen { + transform: rotate(180deg); +} + +.panel { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + background: white; + border: 1px solid var(--grey-200); + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + z-index: 50; + max-height: 220px; + overflow-y: auto; + padding: 6px; +} + +.option { + display: block; + width: 100%; + padding: 10px 14px; + text-align: left; + background: none; + border: none; + border-radius: 8px; + font-size: 14px; + color: var(--grey-800); + cursor: pointer; +} + +.option:hover { + background: var(--grey-25); +} + +.optionSelected { + background: #D8EFE8; + color: var(--primary-500); + font-weight: 500; +} + +.optionSelected:hover { + background: #c8e8d8; +} diff --git a/frontend/src/components/CustomSelect.tsx b/frontend/src/components/CustomSelect.tsx new file mode 100644 index 0000000..9d976e0 --- /dev/null +++ b/frontend/src/components/CustomSelect.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { ChevronDown } from "lucide-react"; + +import styles from "./CustomSelect.module.css"; + +type Option = { label: string; value: string }; + +export const CustomSelect = function CustomSelect({ + options, + value, + onChange, + placeholder, +}: { + options: Option[]; + value: string; + onChange: (value: string) => void; + placeholder?: string; +}) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const selectedLabel = options.find((o) => o.value === value)?.label; + + return ( +
+ + + {open && ( +
+ {options.map((opt) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/frontend/src/components/SectionCard.module.css b/frontend/src/components/SectionCard.module.css index 5579b56..98e65ad 100644 --- a/frontend/src/components/SectionCard.module.css +++ b/frontend/src/components/SectionCard.module.css @@ -7,7 +7,7 @@ } .topBar { - height: 64px; + height: 76px; display: flex; justify-content: flex-end; align-items: flex-start; diff --git a/frontend/src/components/SectionCard.tsx b/frontend/src/components/SectionCard.tsx index a11b6ba..c5ee434 100644 --- a/frontend/src/components/SectionCard.tsx +++ b/frontend/src/components/SectionCard.tsx @@ -1,11 +1,13 @@ "use client"; +import { Calendar, GraduationCap } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + import styles from "./SectionCard.module.css"; -import { GraduationCap, Calendar } from "lucide-react"; -import { useState, useRef, useEffect } from "react"; + +// To check -- day is string, days is string[], are we going to accept multiple days for one card? or just one day per card. export const SectionCard = function SectionCard({ code, - day, teachers, startTime, endTime, @@ -19,23 +21,22 @@ export const SectionCard = function SectionCard({ onDelete, }: { code: string; - day: string; teachers: string[]; startTime: string; endTime: string; - teacherName: string; startDate: string; endDate: string; color: string; archived: boolean; - days: string; + days: string[]; onEdit: () => void; - onArchive: () => void; - onDelete: () => void; + onArchive: () => void | Promise; + onDelete: () => void | Promise; }) { const [menuOpen, setMenuOpen] = useState(false); const toggleMenu = () => setMenuOpen((prev) => !prev); const menuRef = useRef(null); + useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { @@ -54,9 +55,31 @@ export const SectionCard = function SectionCard({ {menuOpen && (
- - - + + +
)} @@ -64,13 +87,13 @@ export const SectionCard = function SectionCard({

{code}

- {day} {startTime} - {endTime} + {days} {startTime} - {endTime}


- {teachers[0]} + {teachers && teachers[0]}
{startDate} - {endDate} diff --git a/frontend/src/components/SectionForm.module.css b/frontend/src/components/SectionForm.module.css new file mode 100644 index 0000000..a385429 --- /dev/null +++ b/frontend/src/components/SectionForm.module.css @@ -0,0 +1,262 @@ +.form { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; +} + +.titleBlock { + display: flex; + flex-direction: column; + gap: 4px; +} + +.title { + font-size: 22px; + font-weight: 700; + color: var(--grey-900); + margin: 0; +} + +.subtitle { + font-size: 14px; + color: var(--grey-500); + margin: 0; +} + +.tabs { + display: flex; + gap: 8px; +} + +.tab { + padding: 8px 16px; + border-radius: 8px; + border: 1px solid var(--grey-200); + background: white; + font-size: 14px; + font-weight: 500; + color: var(--grey-600); + cursor: pointer; +} + +.tabActive { + background-color: var(--primary-500); + color: white; + border-color: var(--primary-500); +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.label { + font-size: 14px; + font-weight: 500; + color: var(--grey-700); +} + +.required { + color: var(--primary-500); + margin-left: 2px; +} + +.input { + padding: 12px 16px; + border: 1.5px solid var(--grey-200); + border-radius: 12px; + font-size: 14px; + color: var(--grey-900); + outline: none; + width: 100%; + box-sizing: border-box; + background: white; +} + +.input:focus { + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(74, 109, 140, 0.1); +} + +.colorSwatches { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.swatch { + width: 44px; + height: 44px; + border-radius: 50%; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.1s; +} + +.swatch:hover { + transform: scale(1.1); +} + +.swatchSelected::after { + content: "✓"; + color: white; + font-size: 18px; + font-weight: 700; +} + +.footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 8px; +} + +.cancelButton { + padding: 10px 20px; + border-radius: 8px; + border: 1px solid var(--grey-200); + background: white; + color: var(--grey-700); + font-size: 14px; + font-weight: 500; + cursor: pointer; +} + +.cancelButton:hover { + background: var(--grey-25); +} + +.submitButton { + padding: 10px 20px; + border-radius: 8px; + border: none; + background-color: var(--primary-500); + color: white; + font-size: 14px; + font-weight: 600; + cursor: pointer; +} + +.submitButton:hover { + background-color: var(--primary-600); +} + +.row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +/* Times tab */ +.dateRow { + display: flex; + align-items: center; + gap: 12px; +} + +.dateRow span { + font-size: 14px; + color: var(--grey-500); + white-space: nowrap; +} + +.dateInputWrapper { + position: relative; + flex: 1; +} + +.dateInputWrapper input { + padding: 12px 14px 12px 44px; + border: 1.5px solid var(--grey-200); + border-radius: 12px; + font-size: 14px; + color: var(--grey-900); + outline: none; + width: 100%; + box-sizing: border-box; + background: white; +} + +.dateInputWrapper input:focus { + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(74, 109, 140, 0.1); +} + +.dateIcon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--grey-400); + pointer-events: none; +} + +.timeRow { + display: flex; + gap: 12px; +} + +.select { + flex: 1; + padding: 12px 36px 12px 16px; + border: 1.5px solid var(--grey-200); + border-radius: 12px; + font-size: 14px; + color: var(--grey-900); + background: white; + outline: none; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.5'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 14px center; + min-width: 0; +} + +.select:focus { + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(74, 109, 140, 0.1); +} + +.select option { + padding: 10px; + font-size: 14px; +} + +/* People tab */ +.tagBox { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 10px 14px; + border: 1px solid var(--grey-200); + border-radius: 8px; + min-height: 44px; + position: relative; +} + +.tag { + background: #D8EFE8; + color: #233E3A; + border-radius: 6px; + padding: 4px 10px; + font-size: 13px; + font-weight: 500; +} + +.chevron { + position: absolute; + right: 12px; + color: var(--grey-400); +} + +.placeholderText { + font-size: 14px; + color: var(--grey-400); +} diff --git a/frontend/src/components/SectionForm.tsx b/frontend/src/components/SectionForm.tsx new file mode 100644 index 0000000..1ed1a22 --- /dev/null +++ b/frontend/src/components/SectionForm.tsx @@ -0,0 +1,253 @@ +"use client"; + +import { Calendar, ChevronDown } from "lucide-react"; +import { useState } from "react"; + +import { CustomSelect } from "./CustomSelect"; +import { ProgressBar } from "./ProgressBar"; +import styles from "./SectionForm.module.css"; + +import type { Section } from "@/src/api/sections"; + +type SectionFormData = Omit; +type Tab = "general" | "times" | "people"; +const TABS: Tab[] = ["general", "times", "people"]; +const STEP_SUBTITLES = [ + "Fill out class name and choose color", + "Fill out class meeting times and duration", + "Fill out class teachers and students", +]; + +const DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; +const TIMES = Array.from({ length: 24 }, (_, h) => + ["00", "30"].map((m) => { + const hour = h % 12 === 0 ? 12 : h % 12; + const ampm = h < 12 ? "am" : "pm"; + const val = `${String(h).padStart(2, "0")}:${m}`; + return { label: `${String(hour)}:${m}${ampm}`, value: val }; + }), +).flat(); + +const COLOR_OPTIONS = ["#9E9E9E", "#E53935", "#FDD835", "#1E88E5", "#C4724A", "#4A6D8C", "#43A047"]; + +export const SectionForm = function SectionForm({ + initialData, + onSubmit, + onCancel, +}: { + initialData?: Section; + onSubmit: (data: SectionFormData) => void | Promise; + onCancel: () => void; +}) { + const isEdit = !!initialData; + const [activeTab, setActiveTab] = useState("general"); + const [currentStep, setCurrentStep] = useState(0); + const [formData, setFormData] = useState({ + code: initialData?.code ?? "", + days: initialData?.days ?? [], + startTime: initialData?.startTime ?? "", + endTime: initialData?.endTime ?? "", + startDate: initialData?.startDate ?? "", + endDate: initialData?.endDate ?? "", + color: initialData?.color ?? "#9E9E9E", + archived: initialData?.archived ?? false, + }); + + const handleChange = (e: React.ChangeEvent) => { + setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!isEdit && currentStep < TABS.length - 1) { + setCurrentStep((s) => s + 1); + } else { + void onSubmit(formData); + } + }; + + // which section to show: tabs for edit, step index for create + const visibleSection = isEdit ? activeTab : TABS[currentStep]; + + return ( +
+ {/* Progress bar — create mode only */} + {!isEdit && } + +
+

{isEdit ? "Edit Program" : "Create Class"}

+ {!isEdit &&

{STEP_SUBTITLES[currentStep]}

} +
+ + {/* Tab buttons — edit mode only */} + {isEdit && ( +
+ {TABS.map((tab) => ( + + ))} +
+ )} + + {/* Step content */} + {visibleSection === "general" && ( + <> +
+ + +
+ +
+ +
+ {COLOR_OPTIONS.map((c) => ( +
+
+ + )} + + {visibleSection === "times" && ( + <> +
+ +
+
+ + +
+ to +
+ + +
+
+
+ +
+ +
+ ({ label: d, value: d }))} + value={formData.days[0] ?? ""} + onChange={(val) => setFormData((prev) => ({ ...prev, days: [val] }))} + placeholder="Days" + /> + setFormData((prev) => ({ ...prev, startTime: val }))} + placeholder="Start Time" + /> + setFormData((prev) => ({ ...prev, endTime: val }))} + placeholder="End Time" + /> +
+
+ + )} + + {visibleSection === "people" && ( + <> +
+ +
+ {initialData?.teachers && initialData.teachers.length > 0 ? ( + initialData.teachers.map((t) => ( + + {t} + + )) + ) : ( + Search or select teacher name + )} + +
+
+ +
+ +
+ {initialData?.enrolledStudents && initialData.enrolledStudents.length > 0 ? ( + initialData.enrolledStudents.map((s) => ( + + {s} + + )) + ) : ( + Search or select student name + )} + +
+
+ + )} + +
+ {!isEdit && currentStep > 0 ? ( + + ) : ( + + )} + +
+ + ); +}; diff --git a/frontend/src/components/Toast.module.css b/frontend/src/components/Toast.module.css new file mode 100644 index 0000000..8e0e2c3 --- /dev/null +++ b/frontend/src/components/Toast.module.css @@ -0,0 +1,101 @@ +/* Fixed to bottom-right corner */ +.toast { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 9999; + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px; + border-radius: 999px; /* pill shape */ + background: #1e1e1e; + color: white; + min-width: 320px; + max-width: 480px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35); + font-size: 14px; + font-weight: 500; +} + +/* Slide up + fade in */ +.entering { + animation: slideIn 0.25s ease forwards; +} + +/* Fade out */ +.exiting { + animation: fadeOut 0.3s ease forwards; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(8px); + } +} + +.iconWrapper { + color: #4caf50; /* default: success green */ + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border: 2px solid currentColor; + border-radius: 50%; + flex-shrink: 0; + font-size: 11px; + font-weight: 700; +} + +.message { + flex: 1; +} + +/* "Undo" green text button */ +.undoButton { + background: none; + border: none; + color: #4caf50; + font-weight: 600; + font-size: 14px; + cursor: pointer; + padding: 0; + white-space: nowrap; +} + +.undoButton:hover { + text-decoration: underline; +} + +/* "×" close button */ +.closeButton { + background: none; + border: none; + color: #888; + font-size: 18px; + cursor: pointer; + padding: 0; + line-height: 1; + margin-left: 4px; +} + +.closeButton:hover { + color: white; +} diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx new file mode 100644 index 0000000..edf9c35 --- /dev/null +++ b/frontend/src/components/Toast.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import styles from "./Toast.module.css"; + +export type ToastProps = { + id: string; + message: string; + type?: "success" | "error" | "info"; + duration?: number; // ms, default 4000 + onDismiss: (id: string) => void; + onUndo?: () => void; +}; + +export function Toast({ + id, + message, + type = "success", + duration = 4000, + onDismiss, + onUndo, +}: ToastProps) { + const [isExiting, setIsExiting] = useState(false); + + const handleDismiss = () => { + setIsExiting(true); + setTimeout(() => onDismiss(id), 300); + }; + + useEffect(() => { + const timer = setTimeout(() => handleDismiss(), duration); + return () => clearTimeout(timer); + }, [duration]); + + const iconMap = { + success: "✓", + error: "✕", + info: "ℹ", + }; + + const typeClassMap = { + success: styles.success, + error: styles.error, + info: styles.info, + }; + + return ( +
+ {/* Icon */} +
+ {iconMap[type]} +
+ + {/* Message */} +
{message}
+ + {/* Undo Button — only shown when onUndo is provided */} + {onUndo && ( + + )} + + {/* Close Button */} + +
+ ); +} diff --git a/frontend/src/hooks/useToast.tsx b/frontend/src/hooks/useToast.tsx new file mode 100644 index 0000000..3a9097b --- /dev/null +++ b/frontend/src/hooks/useToast.tsx @@ -0,0 +1,27 @@ +import { useCallback, useRef, useState } from "react"; + +type ToastState = { +message: string; +onUndo?: () => void; +}; + +const TIMEOUT_MS = 4000 + +export function useToast() { + const [toast, setToast] = useState(null); + const timerRef = useRef | null>(null) + const showToast = useCallback((message:string, onUndo?: () => void) => { + if (timerRef.current) clearTimeout(timerRef.current); + setToast({message, onUndo}); + + timerRef.current = setTimeout(() => { + setToast(null); + }, TIMEOUT_MS) + + }, []) + const dismissToast = useCallback(() => { + if (timerRef.current) clearTimeout(timerRef.current); + setToast(null); + }, []) + return { toast, showToast, dismissToast }; +} \ No newline at end of file From bbcff9d4043b032ae3062f97881e99ec34618936 Mon Sep 17 00:00:00 2001 From: Lucas Yan Date: Wed, 18 Mar 2026 23:49:07 -0700 Subject: [PATCH 6/6] feat: multi-select for days, styling cleanup --- frontend/package-lock.json | 7 - frontend/package.json | 1 - .../src/app/(pages)/programs/page.module.css | 209 ++++++--- frontend/src/app/(pages)/programs/page.tsx | 171 ++++---- frontend/src/components/Modal.tsx | 7 +- frontend/src/components/MultiSelect.tsx | 73 ++++ .../src/components/SectionCard.module.css | 46 +- frontend/src/components/SectionCard.tsx | 6 +- frontend/src/components/SectionForm.tsx | 7 +- package-lock.json | 399 +++++++++--------- package.json | 3 +- 11 files changed, 555 insertions(+), 374 deletions(-) create mode 100644 frontend/src/components/MultiSelect.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fdd084c..911c84b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,7 +14,6 @@ "@mui/material": "^7.3.7", "date-fns": "^4.1.0", "lucide-react": "^0.564.0", - "mui": "^0.0.1", "next": "16.0.10", "react": "19.2.1", "react-day-picker": "^9.13.2", @@ -11384,12 +11383,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mui": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/mui/-/mui-0.0.1.tgz", - "integrity": "sha512-iB9zfxsJBcMkZ/SY6X+HGSPr4fftCZIQ76ZMH8iSMfVkidVzRtZlLW2gbWXUe+IMcj8JLv1p+dGKvPVlgtiocA==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index c7e2eb6..3e324ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,6 @@ "@mui/material": "^7.3.7", "date-fns": "^4.1.0", "lucide-react": "^0.564.0", - "mui": "^0.0.1", "next": "16.0.10", "react": "19.2.1", "react-day-picker": "^9.13.2", diff --git a/frontend/src/app/(pages)/programs/page.module.css b/frontend/src/app/(pages)/programs/page.module.css index 5b42880..009e0c0 100644 --- a/frontend/src/app/(pages)/programs/page.module.css +++ b/frontend/src/app/(pages)/programs/page.module.css @@ -1,26 +1,43 @@ .pageWrapper { - padding: 40px 48px; - background: #f5f6f8; + padding: 64px 128px 60px; + background: #fcfbfb; min-height: 100vh; + display: flex; + flex-direction: column; + gap: 32px; +} + +.headerSection { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 48px; } .header { display: flex; justify-content: space-between; - align-items: flex-start; - margin-bottom: 32px; + align-items: center; + width: 100%; } .titleSection h1 { - font-size: 28px; - font-weight: 700; + font-family: "Montserrat", sans-serif; + font-weight: 600; + font-size: 30px; + line-height: 38px; + letter-spacing: -0.02em; + color: #231f20; margin: 0 0 4px; - color: var(--grey-900); } .titleSection p { - font-size: 14px; - color: var(--grey-500); + font-family: "Montserrat", sans-serif; + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: -0.02em; + color: #6c6a6b; margin: 0; } @@ -28,13 +45,17 @@ display: flex; align-items: center; gap: 8px; - background-color: var(--primary-500); - color: white; + background-color: #3f8d7c; + color: #ffffff; border: none; border-radius: 8px; - padding: 10px 20px; - font-size: 14px; + padding: 10px 16px; + height: 44px; + font-family: "Montserrat", sans-serif; + font-size: 16px; font-weight: 600; + line-height: 24px; + letter-spacing: -0.02em; cursor: pointer; white-space: nowrap; } @@ -47,26 +68,30 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: 24px; + width: 100%; } .tabs { display: flex; - gap: 8px; + gap: 16px; } .tabButton { display: flex; align-items: center; - gap: 6px; - padding: 8px 18px; - border-radius: 20px; + gap: 8px; + padding: 10px 14px; + height: 40px; + border-radius: 8px; + font-family: "Montserrat", sans-serif; font-size: 14px; - font-weight: 500; + font-weight: 600; + line-height: 20px; + letter-spacing: -0.02em; cursor: pointer; - border: 1.5px solid var(--grey-200); - background: white; - color: var(--grey-600); + border: 1px solid #cdcfd0; + background: #ffffff; + color: #494445; } .tabButton:hover { @@ -74,35 +99,39 @@ } .tabActive { - background-color: var(--primary-500); - color: white; - border-color: var(--primary-500); + background-color: #3f8d7c; + color: #ffffff; + border-color: #3f8d7c; } .tabActive:hover { background-color: var(--primary-600); border-color: var(--primary-600); - color: white; + color: #ffffff; } .rightControls { display: flex; align-items: center; - gap: 12px; + gap: 16px; } .sortButton { display: flex; align-items: center; - gap: 6px; - padding: 8px 16px; + gap: 8px; + padding: 10px 14px; + height: 40px; border-radius: 8px; + font-family: "Montserrat", sans-serif; font-size: 14px; - font-weight: 500; + font-weight: 600; + line-height: 20px; + letter-spacing: -0.02em; cursor: pointer; - border: 1px solid var(--grey-200); - background: white; - color: var(--grey-600); + border: 1px solid #cdcfd0; + background: #ffffff; + color: #494445; white-space: nowrap; } @@ -119,64 +148,103 @@ .searchIcon { position: absolute; left: 12px; - color: var(--grey-400); + color: #9b9d9f; pointer-events: none; } .searchInput { - padding: 8px 14px 8px 36px; - border: 1px solid var(--grey-200); + padding: 8px 6px 8px 38px; + border: 1px solid #cdcfd0; border-radius: 8px; - font-size: 14px; - color: var(--grey-900); + font-family: "Montserrat", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: -0.02em; + color: #231f20; outline: none; - width: 200px; + width: 240px; + height: 40px; +} + +.searchInput::placeholder { + color: #9b9d9f; } .searchInput:focus { border-color: var(--primary-500); } +/* ── Confirm dialog ───────────────────────────── */ + .confirmDialog { display: flex; flex-direction: column; - gap: 16px; + align-items: center; + gap: 24px; + width: 100%; +} + +.confirmContent { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; width: 100%; - max-width: 460px; } .confirmTitle { display: flex; align-items: center; - gap: 10px; - font-size: 20px; - font-weight: 700; - color: var(--grey-900); + gap: 12px; + font-family: "Montserrat", sans-serif; + font-style: normal; + font-weight: 600; + font-size: 30px; + line-height: 38px; + letter-spacing: -0.02em; + color: #231f20; margin: 0; } -.confirmWarning { - font-weight: 600; - color: var(--grey-700); +.confirmBody { + font-family: "Montserrat", sans-serif; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: -0.02em; + color: #231f20; margin: 0; } .confirmFooter { display: flex; + flex-direction: row; + align-items: flex-end; justify-content: flex-end; - gap: 12px; - margin-top: 8px; + gap: 16px; + width: 100%; } .confirmCancel { - padding: 10px 20px; + display: flex; + justify-content: center; + align-items: center; + padding: 10px 16px; + gap: 8px; + height: 44px; + background: #3f8d7c; border-radius: 8px; border: none; - background-color: var(--primary-500); - color: white; - font-size: 14px; + font-family: "Montserrat", sans-serif; font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: -0.02em; + color: #ffffff; cursor: pointer; + white-space: nowrap; } .confirmCancel:hover { @@ -184,20 +252,31 @@ } .confirmDelete { - padding: 10px 20px; + display: flex; + justify-content: center; + align-items: center; + padding: 10px 16px; + gap: 8px; + height: 44px; + background: #ffffff; + border: 1px solid #fda29b; border-radius: 8px; - border: 1.5px solid #e53935; - background: white; - color: #e53935; - font-size: 14px; + font-family: "Montserrat", sans-serif; font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: -0.02em; + color: #f04438; cursor: pointer; + white-space: nowrap; } .confirmDelete:hover { background: #fff5f5; } +/* ── Sort dropdown ────────────────────────────── */ + .sortContainer { position: relative; } @@ -207,7 +286,7 @@ top: calc(100% + 6px); right: 0; background: white; - border: 1px solid var(--grey-200); + border: 1px solid #cdcfd0; border-radius: 12px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); z-index: 50; @@ -224,8 +303,10 @@ background: none; border: none; border-radius: 8px; + font-family: "Montserrat", sans-serif; font-size: 14px; - color: var(--grey-800); + font-weight: 500; + color: #494445; cursor: pointer; text-align: left; gap: 8px; @@ -237,12 +318,14 @@ .sortDivider { border: none; - border-top: 1px solid var(--grey-100); + border-top: 1px solid #e3e2e2; margin: 4px 0; } +/* ── Grid ─────────────────────────────────────── */ + .grid { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 24px; + gap: 12px; } diff --git a/frontend/src/app/(pages)/programs/page.tsx b/frontend/src/app/(pages)/programs/page.tsx index 021e187..9aaf821 100644 --- a/frontend/src/app/(pages)/programs/page.tsx +++ b/frontend/src/app/(pages)/programs/page.tsx @@ -96,88 +96,90 @@ export default function Programs() { return (
-
-
-

Classes

-

Take attendance, add notes, and see trends

-
- -
- -
-
- -
-
-
- - {sortOpen && ( -
- {[ - { label: "Alphabetical", value: "alphabetical" }, - { label: "Date Created", value: "createdAt" }, - { label: "Start Date", value: "startDate" }, - { label: "End Date", value: "endDate" }, - { label: "Color", value: "color" }, - ].map((opt) => ( - - ))} + +
+
+
+ + {sortOpen && ( +
+ {[ + { label: "Alphabetical", value: "alphabetical" }, + { label: "Date Created", value: "createdAt" }, + { label: "Start Date", value: "startDate" }, + { label: "End Date", value: "endDate" }, + { label: "Color", value: "color" }, + ].map((opt) => ( + + ))} -
+
- {[ - { label: "Ascending", value: "asc", icon: }, - { label: "Descending", value: "desc", icon: }, - ].map((opt) => ( - - ))} -
- )} -
-
- - setSearchQuery(e.target.value)} - /> + {[ + { label: "Ascending", value: "asc", icon: }, + { label: "Descending", value: "desc", icon: }, + ].map((opt) => ( + + ))} +
+ )} +
+
+ + setSearchQuery(e.target.value)} + /> +
@@ -209,15 +211,18 @@ export default function Programs() { {/* pop up for deleting section */} {deletingSection && ( -
- Deleting Class +
+
+ Deleting Class +
+

+ Are you sure you want to delete {deletingSection.code}? This + action cannot be undone. +

-

- Are you sure you want to delete {deletingSection.code}? -

-

This action cannot be undone.

+ + {open && ( +
+ {options.map((opt) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/frontend/src/components/SectionCard.module.css b/frontend/src/components/SectionCard.module.css index 98e65ad..fbb649d 100644 --- a/frontend/src/components/SectionCard.module.css +++ b/frontend/src/components/SectionCard.module.css @@ -1,18 +1,17 @@ .cardWrapper { - border-radius: 12px; + border-radius: 8px; overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - background: white; width: 100%; } .topBar { - height: 76px; + height: 48px; display: flex; justify-content: flex-end; align-items: flex-start; - padding: 12px; + padding: 10px 20px 20px; position: relative; + border-radius: 8px 8px 0 0; } .menuButton { @@ -50,8 +49,9 @@ background: none; border: none; cursor: pointer; + font-family: "Montserrat", sans-serif; font-size: 14px; - color: var(--grey-900); + color: #231f20; } .dropdown button:hover { @@ -59,35 +59,53 @@ } .cardBody { - padding: 20px 20px 25px 20px; + padding: 20px 20px 25px; display: flex; flex-direction: column; - gap: 20px; + gap: 12px; + background: #ffffff; + border-left: 1px solid #e3e2e2; + border-right: 1px solid #e3e2e2; + border-bottom: 1px solid #e3e2e2; + border-radius: 0 0 8px 8px; } .cardBody h3 { - font-size: 18px; + font-family: "Montserrat", sans-serif; font-weight: 600; + font-size: 18px; + line-height: 28px; + letter-spacing: -0.02em; + color: #231f20; margin: 0; - color: var(--grey-900); } .cardBody p { + font-family: "Montserrat", sans-serif; + font-weight: 500; font-size: 14px; - color: var(--grey-500); + line-height: 20px; + letter-spacing: -0.02em; + color: #6c6a6b; margin: 0; } .cardBody hr { border: none; - border-top: 1px solid var(--grey-100); + border-top: 1px solid #e3e2e2; margin: 0; + width: 100%; + align-self: stretch; } .row { display: flex; align-items: center; gap: 8px; - font-size: 14px; - color: var(--grey-600); + font-family: "Montserrat", sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 18px; + letter-spacing: -0.02em; + color: #494445; } diff --git a/frontend/src/components/SectionCard.tsx b/frontend/src/components/SectionCard.tsx index c5ee434..ec76c73 100644 --- a/frontend/src/components/SectionCard.tsx +++ b/frontend/src/components/SectionCard.tsx @@ -87,16 +87,16 @@ export const SectionCard = function SectionCard({

{code}

- {days} {startTime} - {endTime} + {days.join(", ")} {startTime} - {endTime}


- {teachers && teachers[0]} + {teachers && teachers[0]}
- {startDate} - {endDate} + {startDate} - {endDate}
diff --git a/frontend/src/components/SectionForm.tsx b/frontend/src/components/SectionForm.tsx index 1ed1a22..faad827 100644 --- a/frontend/src/components/SectionForm.tsx +++ b/frontend/src/components/SectionForm.tsx @@ -4,6 +4,7 @@ import { Calendar, ChevronDown } from "lucide-react"; import { useState } from "react"; import { CustomSelect } from "./CustomSelect"; +import { MultiSelect } from "./MultiSelect"; import { ProgressBar } from "./ProgressBar"; import styles from "./SectionForm.module.css"; @@ -167,10 +168,10 @@ export const SectionForm = function SectionForm({ Meeting Time *
- ({ label: d, value: d }))} - value={formData.days[0] ?? ""} - onChange={(val) => setFormData((prev) => ({ ...prev, days: [val] }))} + values={formData.days} + onChange={(vals) => setFormData((prev) => ({ ...prev, days: vals }))} placeholder="Days" />