diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index e587e28..910d483 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
- "Bash(cd:*)"
+ "Bash(cd:*)",
+ "Bash(git -C \"C:\\\\Users\\\\Oling\\\\documents\\\\courseconnect\" log --oneline -20)"
]
}
}
diff --git a/.gitignore b/.gitignore
index fb3a957..66361ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,4 +43,5 @@ courseconnect-c6a7b-firebase-adminsdk-dqqis-af57e2e045.json
playwright-report
test-results
playwright/.auth
-playwright.accounts.json
\ No newline at end of file
+playwright.accounts.json
+nul
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..724698e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,76 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+CourseConnect is a web application for managing teaching position applications (TAs, UPIs, Graders) and course information. It serves students (submit/track applications), faculty (review applications, manage courses), and department leaders (manage users/courses, upload data via spreadsheets).
+
+## Tech Stack
+
+- **Frontend:** React 19, Next.js 15 (App Router), TypeScript
+- **UI:** Material UI (MUI) 6, Tailwind CSS 4
+- **Backend:** Firebase/Firestore, Firebase Cloud Functions
+- **State:** React Query (TanStack), React Context
+- **Forms:** React Hook Form
+
+## Common Commands
+
+```bash
+# Development
+npm run dev # Start dev server at localhost:3000
+npm run build # Production build
+npm run start # Start production server
+npm run lint # Run ESLint
+
+# Testing (Playwright E2E)
+npm run test:e2e # Run all E2E tests
+npm run test:e2e:ui # Run tests with Playwright UI
+npm run test:e2e:debug # Debug tests
+npx playwright show-report # View test report after failure
+
+# Firebase Functions (from /functions directory)
+npm run build # Compile TypeScript
+npm run serve # Run functions locally with emulator
+npm run deploy # Deploy functions to Firebase
+```
+
+## Architecture
+
+```
+src/
+├── app/ # Next.js App Router pages
+│ ├── admin-applications/ # Admin: manage all applications
+│ ├── admincourses/ # Admin: course management
+│ ├── announcements/ # Announcements feature
+│ ├── applications/ # Student application submission
+│ ├── courses/ # Course viewing
+│ ├── dashboard/ # Main dashboard
+│ ├── faculty/ # Faculty management
+│ ├── users/ # Admin: user management
+│ └── layout.tsx # Root layout with providers
+├── components/ # Reusable React components
+├── contexts/ # React contexts (Auth, Announcements)
+├── firebase/ # Firebase config and utilities
+├── hooks/ # Custom React hooks (useGetItems, etc.)
+├── types/ # TypeScript type definitions
+└── utils/ # Utility functions
+
+functions/ # Firebase Cloud Functions (email notifications)
+tests/e2e/ # Playwright E2E tests
+```
+
+## Key Patterns
+
+- **Path alias:** Use `@/*` to import from `src/*`
+- **Providers:** AuthProvider and AnnouncementsProvider wrap the app in `layout.tsx`
+- **Data fetching:** React Query hooks in `src/hooks/` for Firestore operations
+- **E2E test mode:** Feature flags via `NEXT_PUBLIC_E2E` env var and localStorage
+
+## Pre-commit Hooks
+
+Husky runs ESLint and Prettier on staged files automatically. If commits fail, check lint errors with `npm run lint`.
+
+## Firebase Functions
+
+Cloud Functions in `functions/src/index.ts` handle email notifications (application confirmations, status updates, faculty notifications). Uses Nodemailer with configured SMTP credentials.
diff --git a/src/app/announcements/AnnouncementSections.tsx b/src/app/announcements/AnnouncementSections.tsx
index bada238..898fae8 100644
--- a/src/app/announcements/AnnouncementSections.tsx
+++ b/src/app/announcements/AnnouncementSections.tsx
@@ -1,17 +1,24 @@
/* components/DashboardSections.tsx */
import { PrimaryButton } from '@/components/Buttons/PrimaryButton';
+import { NavbarItem } from '@/types/navigation';
import { Role } from '@/types/User';
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import AnnouncementDialog from './AnnouncementDialogue';
import AnnouncementsRow from './AnnouncementsRow';
import { usePostAnnouncement } from '@/hooks/Announcements/usePostAnnouncement';
-import { useAnnouncements } from '@/contexts/AnnouncementsContext';
+import { useFetchAnnouncementsForAccount } from '@/hooks/Announcements/useFetchAnnouncements';
-import { Announcement } from '@/types/announcement';
+import { Announcement, AudienceRole } from '@/types/announcement';
-export default function AnnouncementSections({ role }: { role: Role }) {
+export default function AnnouncementSections({
+ role,
+ uemail,
+}: {
+ role: Role;
+ uemail: string;
+}) {
const [open, setOpen] = useState(false);
const { postAnnouncement, posting, error: postError } = usePostAnnouncement();
@@ -19,9 +26,18 @@ export default function AnnouncementSections({ role }: { role: Role }) {
read,
unread,
loading,
+ loadingMore,
+ hasMore,
error: fetchError,
refresh,
- } = useAnnouncements();
+ loadMore,
+ } = useFetchAnnouncementsForAccount({
+ userRole: role,
+ userEmail: uemail,
+ userDepartment: 'ECE', // TODO: make real
+ channel: 'inApp',
+ realtime: true,
+ });
async function handleSubmit(draft: Announcement) {
await postAnnouncement({
@@ -125,6 +141,18 @@ export default function AnnouncementSections({ role }: { role: Role }) {
)}
)}
+
+ {hasMore ? (
+
+
+
+ ) : null}
);
}
diff --git a/src/app/announcements/page.tsx b/src/app/announcements/page.tsx
index af8be83..5f72826 100644
--- a/src/app/announcements/page.tsx
+++ b/src/app/announcements/page.tsx
@@ -9,6 +9,7 @@ import { markAnnouncementsSeen } from '@/hooks/Announcements/markAnnouncementAsS
const AnnouncementsPage: FC = () => {
const [user, role, loading, error] = useUserInfo();
+ const uemail = user?.email;
// prevent double-call in dev Strict Mode
const didMarkRef = useRef(false);
@@ -26,7 +27,7 @@ const AnnouncementsPage: FC = () => {
return (
-
+
);
};
diff --git a/src/app/applications/applicationSections.tsx b/src/app/applications/applicationSections.tsx
index 487bfce..b660017 100644
--- a/src/app/applications/applicationSections.tsx
+++ b/src/app/applications/applicationSections.tsx
@@ -7,6 +7,7 @@ import SemesterSelect from '@/components/SemesterSelect/SemesterSelect';
import { useSemesterData } from '@/hooks/Courses/useSemesterData';
import { CoursesGrid } from '@/components/CoursesGrid/CoursesGrid';
import { type SemesterName } from '@/hooks/useSemesterOptions';
+import SchoolIcon from '@mui/icons-material/School';
export default function ApplicationSections({
role,
navItems,
@@ -45,6 +46,19 @@ export default function ApplicationSections({
))}
+
+
+
Supervised Teaching
+
+
+
+
+
Research
No available applications at this time.
diff --git a/src/app/applications/supervised-teaching/page.tsx b/src/app/applications/supervised-teaching/page.tsx
new file mode 100644
index 0000000..7e21059
--- /dev/null
+++ b/src/app/applications/supervised-teaching/page.tsx
@@ -0,0 +1,474 @@
+'use client';
+
+import * as React from 'react';
+import Button from '@mui/material/Button';
+import CssBaseline from '@mui/material/CssBaseline';
+import TextField from '@mui/material/TextField';
+import Grid from '@mui/material/Grid';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { useAuth } from '@/firebase/auth/auth_context';
+import { Toaster, toast } from 'react-hot-toast';
+import Snackbar from '@mui/material/Snackbar';
+import MuiAlert, { AlertProps } from '@mui/material/Alert';
+import HeaderCard from '@/components/HeaderCard/HeaderCard';
+import firebase from '@/firebase/firebase_config';
+import { useRouter } from 'next/navigation';
+import { fetchClosestSemesters } from '@/hooks/useSemesterOptions';
+import MenuItem from '@mui/material/MenuItem';
+import InputLabel from '@mui/material/InputLabel';
+import FormControl from '@mui/material/FormControl';
+import Select, { SelectChangeEvent } from '@mui/material/Select';
+
+export default function SupervisedTeachingApplication() {
+ const { user } = useAuth();
+ const userId = user?.uid || '';
+ const router = useRouter();
+
+ const [success, setSuccess] = React.useState(false);
+ const [loading, setLoading] = React.useState(false);
+
+ // Teaching choices (dropdowns)
+ const [teachingFirstChoice, setTeachingFirstChoice] = React.useState('');
+ const [teachingSecondChoice, setTeachingSecondChoice] = React.useState('');
+ const [teachingThirdChoice, setTeachingThirdChoice] = React.useState('');
+
+ const Alert = React.forwardRef
(function Alert(
+ props,
+ ref
+ ) {
+ return ;
+ });
+
+ const handleSuccess = (
+ event?: React.SyntheticEvent | Event,
+ reason?: string
+ ) => {
+ if (reason === 'clickaway') return;
+ setSuccess(false);
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ setLoading(true);
+
+ const formData = new FormData(event.currentTarget);
+
+ const firstName = (formData.get('firstName') as string) || '';
+ const lastName = (formData.get('lastName') as string) || '';
+ const ufid = (formData.get('ufid') as string) || '';
+ const email = (formData.get('email') as string) || '';
+ const confirmEmail = (formData.get('confirmEmail') as string) || '';
+ const phdAdmissionTerm = (formData.get('phdAdmissionTerm') as string) || '';
+ const phdAdvisor = (formData.get('phdAdvisor') as string) || '';
+ const admittedToCandidacy =
+ (formData.get('admittedToCandidacy') as string) || '';
+ const registerTerm = (formData.get('registerTerm') as string) || '';
+ const previouslyRegistered =
+ (formData.get('previouslyRegistered') as string) || '';
+ const previousDetails = (formData.get('previousDetails') as string) || '';
+ const coursesComfortable =
+ (formData.get('coursesComfortable') as string) || '';
+ // teaching choices come from Select state
+ const teachingFirst = teachingFirstChoice || '';
+ const teachingSecond = teachingSecondChoice || '';
+ const teachingThird = teachingThirdChoice || '';
+ const captcha = formData.get('captcha') === 'on';
+
+ // basic validations
+ if (!email.includes('ufl.edu')) {
+ toast.error('Please enter a valid GatorLink (ufl.edu) email');
+ setLoading(false);
+ return;
+ }
+ if (email !== confirmEmail) {
+ toast.error('Email and Confirm Email must match');
+ setLoading(false);
+ return;
+ }
+ if (!firstName || !lastName || !ufid) {
+ toast.error('Please complete all required personal fields');
+ setLoading(false);
+ return;
+ }
+ if (!registerTerm) {
+ toast.error('Please select the term you want to register for EEL 6940');
+ setLoading(false);
+ return;
+ }
+ if (!coursesComfortable) {
+ toast.error('Please list at least one course you could teach');
+ setLoading(false);
+ return;
+ }
+ if (!captcha) {
+ toast.error('Please complete the CAPTCHA confirmation');
+ setLoading(false);
+ return;
+ }
+
+ // date
+ const current = new Date();
+ const current_date = `${
+ current.getMonth() + 1
+ }-${current.getDate()}-${current.getFullYear()}`;
+
+ const applicationData = {
+ application_type: 'supervised_teaching',
+ firstname: firstName,
+ lastname: lastName,
+ ufid,
+ email,
+ phdAdmissionTerm,
+ phdAdvisor,
+ admittedToCandidacy,
+ registerTerm,
+ previouslyRegistered,
+ previousDetails,
+ coursesComfortable,
+ teachingFirst,
+ teachingSecond,
+ teachingThird,
+ uid: userId,
+ date: current_date,
+ status: 'Submitted',
+ };
+
+ try {
+ const toastId = toast.loading('Submitting application...');
+ const response = await fetch(
+ 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/processApplicationForm',
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(applicationData),
+ }
+ );
+
+ if (response.ok) {
+ toast.dismiss(toastId);
+ toast.success('Application submitted!');
+ setSuccess(true);
+ // optional: update role or navigate
+ router.push('/');
+ } else {
+ toast.dismiss(toastId);
+ toast.error('Submission failed. Please try again later.');
+ }
+ } catch (err) {
+ console.error(err);
+ toast.error('Submission failed. Please try again later.');
+ }
+
+ setLoading(false);
+ };
+
+ const [visibleSems, setVisibleSems] = React.useState([]);
+ React.useEffect(() => {
+ async function load() {
+ const sems = await fetchClosestSemesters(3);
+ setVisibleSems(sems);
+ }
+ load();
+ }, []);
+
+ return (
+
+
+
+
+
+
+ ECE Ph.D. students may register for EEL 6940 Supervised Teaching to
+ fulfill professional development requirements. Deadlines:
+
+
+ - Fall Semester — August 2
+ - Spring Semester — January 2
+ - Summer Semester — April 30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Admitted to candidacy? (registered for EEL 7980 hours)
+
+
+
+
+
+
+
+
+
+
+
+ {visibleSems.map((s) => (
+
+ ))}
+
+
+
+
+
+ Previously registered for EEL 6940?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Teaching First Choice
+
+
+
+
+
+
+
+
+ Teaching Second Choice
+
+
+
+
+
+
+
+
+ Teaching Third Choice
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Application submitted successfully!
+
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6a67f9f..3ec96b6 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,5 @@
'use client';
import { AuthProvider } from '@/firebase/auth/auth_context';
-import { AnnouncementsProvider } from '@/contexts/AnnouncementsContext';
import React, { useEffect } from 'react';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
@@ -68,11 +67,9 @@ export default function RootLayout({
theme={mode === 'dark' ? darkThemeChosen : lightThemeChosen}
>
-
-
- {/**/}
- {children}
-
+
+ {/**/}
+ {children}
diff --git a/src/app/users/page.tsx b/src/app/users/page.tsx
index 2c9acf2..952a61c 100644
--- a/src/app/users/page.tsx
+++ b/src/app/users/page.tsx
@@ -1,94 +1,152 @@
'use client';
import * as React from 'react';
-import { FC } from 'react';
-
+import Avatar from '@mui/material/Avatar';
+import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
+import TextField from '@mui/material/TextField';
+import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
-import Paper from '@mui/material/Paper';
-import Tabs from '@mui/material/Tabs';
-import Tab from '@mui/material/Tab';
+import EditNoteIcon from '@mui/icons-material/EditNote';
+import DepartmentSelect from '@/component/FormUtil/DepartmentSelect';
+import GPA_Select from '@/component/FormUtil/GPASelect';
import Typography from '@mui/material/Typography';
-
-import { Toaster } from 'react-hot-toast';
-
-import PageLayout from '@/components/PageLayout/PageLayout';
-import { getNavItems } from '@/hooks/useGetItems';
-
+import Container from '@mui/material/Container';
+import DegreeSelect from '@/component/FormUtil/DegreeSelect';
+import SemesterStatusSelect from '@/component/FormUtil/SemesterStatusSelect';
+import NationalitySelect from '@/component/FormUtil/NationalitySelect';
+import ProficiencySelect from '@/component/FormUtil/ProficiencySelect';
+import PositionSelect from '@/component/FormUtil/PositionSelect';
+import AvailabilityCheckbox from '@/component/FormUtil/AvailabilityCheckbox';
+import SemesterCheckbox from '@/component/FormUtil/SemesterCheckbox';
+import AdditionalSemesterPrompt from '@/component/FormUtil/AddtlSemesterPrompt';
+import UpdateRole from '@/firebase/util/UpdateUserRole';
import { useAuth } from '@/firebase/auth/auth_context';
+import { Toaster, toast } from 'react-hot-toast';
+import { LinearProgress } from '@mui/material';
+import Snackbar from '@mui/material/Snackbar';
+import MuiAlert, { AlertProps } from '@mui/material/Alert';
+import { ApplicationStatusCard } from '@/components/ApplicationStatusCard/ApplicationStatusCard';
+import { useState } from 'react';
+import { TopNavBarSigned } from '@/component/TopNavBarSigned/TopNavBarSigned';
+import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng';
+import Users from '@/component/Dashboard/Users/Users';
+
import GetUserRole from '@/firebase/util/GetUserRole';
-import UserGrid from '@/component/Dashboard/Users/UserGrid';
-import ApprovalGrid from '@/component/Dashboard/Users/ApprovalGrid';
+import GetUserUfid from '@/firebase/util/GetUserUfid';
+import { ApplicationStatusCardDenied } from '@/components/ApplicationStatusCardDenied/ApplicationStatusCardDenied';
-interface PageProps { }
+import { ApplicationStatusCardAccepted } from '@/components/ApplicationStatusCardAccepted/ApplicationStatusCardAccepted';
+import styles from './style.module.css';
+import 'firebase/firestore';
-const User: FC = () => {
+import firebase from '@/firebase/firebase_config';
+import { read, utils, writeFile, readFile } from 'xlsx';
+import InputLabel from '@mui/material/InputLabel';
+import MenuItem from '@mui/material/MenuItem';
+import FormControl from '@mui/material/FormControl';
+import Select, { SelectChangeEvent } from '@mui/material/Select';
+import HeaderCard from '@/component/HeaderCard/HeaderCard';
+
+export default function User() {
const { user } = useAuth();
const [role, loading, error] = GetUserRole(user?.uid);
+ const [activeComponent, setActiveComponent] = React.useState('welcome');
+ const [semester, setSemester] = React.useState('Fall 2024');
+
+ const handleChange = (event: SelectChangeEvent) => {
+ setSemester(event.target.value as string);
+ };
+
+ const readExcelFile = async (e) => {
+ // https://docs.sheetjs.com/docs/demos/local/file/
+ console.log('ACTIVE');
+
+ try {
+ const val = e.target.files[0];
+ const ab = await val.arrayBuffer();
+ let data = [];
+ var file = read(ab);
- // 0 = All Users, 1 = Unapproved Users
- const [tab, setTab] = React.useState(0);
+ const sheets = file.SheetNames;
- if (loading) return Loading…
;
+ for (let i = 0; i < sheets.length; i++) {
+ const temp = utils.sheet_to_json(file.Sheets[file.SheetNames[i]]);
+
+ temp.forEach((res) => {
+ data.push(res);
+ });
+ }
+ for (let i = 0; i < data.length; i++) {
+ await firebase
+ .firestore()
+ .collection('courses')
+ .doc(
+ data[i]['__EMPTY_5'] +
+ ' (' +
+ semester +
+ ') ' +
+ ': ' +
+ data[i]['__EMPTY_22']
+ )
+ .set({
+ professor_emails:
+ data[i]['__EMPTY_23'] == undefined
+ ? 'undef'
+ : data[i]['__EMPTY_23'],
+ professor_names:
+ data[i]['__EMPTY_22'] == undefined
+ ? 'undef'
+ : data[i]['__EMPTY_22'],
+ code:
+ data[i]['__EMPTY_5'] == undefined
+ ? 'undef'
+ : data[i]['__EMPTY_5'],
+ credits:
+ data[i]['__EMPTY_9'] == undefined
+ ? 'undef'
+ : data[i]['__EMPTY_9'],
+ enrollment_cap:
+ data[i]['__EMPTY_24'] == undefined
+ ? 'undef'
+ : data[i]['__EMPTY_24'],
+ enrolled:
+ data[i]['__EMPTY_26'] == undefined
+ ? 'undef'
+ : data[i]['__EMPTY_26'],
+ title:
+ data[i]['__EMPTY_21'] == undefined
+ ? 'undef'
+ : data[i]['__EMPTY_21'],
+ semester: semester,
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ };
+ if (loading) return Loadingƒ?İ
;
if (error) return Error loading role
;
- if (role !== 'admin') return Forbidden
;
+ if (role !== 'admin') return Forbidden
;
return (
-
-
+ <>
+
- {/* Tabs Container */}
-
-
- setTab(newValue)}
- aria-label="users view switch"
- TabIndicatorProps={{
- style: {
- height: 3,
- borderRadius: 2,
- },
- }}
- >
-
-
-
-
-
-
- {/* Content */}
- {tab === 0 ? (
-
-
-
- ) : (
-
-
+
+
+
+
- )}
-
+
+ >
);
-};
-
-export default User;
-
+}
diff --git a/src/component/Dashboard/Users/ApprovalGrid.tsx b/src/component/Dashboard/Users/ApprovalGrid.tsx
index f8cbe9e..d4e24ec 100644
--- a/src/component/Dashboard/Users/ApprovalGrid.tsx
+++ b/src/component/Dashboard/Users/ApprovalGrid.tsx
@@ -1,13 +1,11 @@
-
'use client';
-
import * as React from 'react';
import Box from '@mui/material/Box';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
+import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt';
import CancelIcon from '@mui/icons-material/Close';
import SaveIcon from '@mui/icons-material/Save';
-import { ThumbDownOffAlt, ThumbUpOffAlt } from '@mui/icons-material';
import {
GridRowModesModel,
@@ -24,16 +22,17 @@ import {
GridRowId,
GridRowModel,
GridRowEditStopReasons,
+ useGridApiContext,
gridClasses,
} from '@mui/x-data-grid';
-
-import { LinearProgress } from '@mui/material';
-import { styled } from '@mui/material/styles';
-
+import { Button } from '@mui/material';
import { deleteUserHTTPRequest } from '@/firebase/auth/auth_delete_user';
import firebase from '@/firebase/firebase_config';
import 'firebase/firestore';
-
+import { LinearProgress } from '@mui/material';
+import { alpha, styled } from '@mui/material/styles';
+import CheckIcon from '@mui/icons-material/Check';
+import { ThumbDownOffAlt, ThumbUp, ThumbUpOffAlt } from '@mui/icons-material';
interface User {
id: string;
firstname: string;
@@ -55,13 +54,17 @@ interface EditToolbarProps {
) => void;
}
-function EditToolbar(_props: EditToolbarProps) {
- // ✅ match CourseGrid toolbar (default styling)
+function EditToolbar(props: EditToolbarProps) {
+ const { setApplicationData, setRowModesModel } = props;
+
+ // Add state to control the dialog open status
+ const [open, setOpen] = React.useState(false);
+
return (
-
-
-
+
+
+
);
}
@@ -72,8 +75,6 @@ interface ApprovalGridProps {
export default function ApprovalGrid(props: ApprovalGridProps) {
const { userRole } = props;
-
- const [loading, setLoading] = React.useState(false);
const [userData, setUserData] = React.useState([]);
React.useEffect(() => {
@@ -81,15 +82,14 @@ export default function ApprovalGrid(props: ApprovalGridProps) {
.firestore()
.collection('users')
.where('role', '==', 'unapproved');
-
const unsubscribe = usersRef.onSnapshot((querySnapshot) => {
const data = querySnapshot.docs.map(
(doc) =>
- ({
- id: doc.id,
- fullname: `${doc.data().firstname ?? ''} ${doc.data().lastname ?? ''}`,
- ...doc.data(),
- } as User)
+ ({
+ id: doc.id,
+ fullname: doc.data().firstname + ' ' + doc.data().lastname,
+ ...doc.data(),
+ } as User)
);
setUserData(data);
@@ -112,127 +112,164 @@ export default function ApprovalGrid(props: ApprovalGridProps) {
};
const handleEditClick = (id: GridRowId) => () => {
- setLoading(true);
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
- setLoading(false);
};
- const handleSaveClick = (id: GridRowId) => async () => {
- setLoading(true);
- try {
- const updatedRow = userData.find((row) => row.id === id);
- if (!updatedRow) throw new Error(`No matching user data found for id: ${id}`);
-
- await firebase.firestore().collection('users').doc(id.toString()).update(updatedRow);
-
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View },
- });
- } catch (error) {
- console.error('Error updating document: ', error);
- } finally {
- setLoading(false);
+ const handleSaveClick = (id: GridRowId) => () => {
+ const updatedRow = userData.find((row) => row.id === id);
+ if (updatedRow) {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .update(updatedRow)
+ .then(() => {
+ setRowModesModel({
+ ...rowModesModel,
+ [id]: { mode: GridRowModes.View },
+ });
+ })
+ .catch((error) => {
+ console.error('Error updating document: ', error);
+ });
+ } else {
+ console.error('No matching user data found for id: ', id);
}
};
- const handleApproveClick = (id: GridRowId) => async () => {
- setLoading(true);
- try {
- await firebase.firestore().collection('users').doc(id.toString()).update({ role: 'faculty' });
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View },
- });
- } catch (error) {
- console.error('Error updating document: ', error);
- } finally {
- setLoading(false);
+ const handleApproveClick = (id: GridRowId) => () => {
+ const updatedRow = userData.find((row) => row.id === id);
+ if (updatedRow) {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .update({ role: 'faculty' })
+ .then(() => {
+ setRowModesModel({
+ ...rowModesModel,
+ [id]: { mode: GridRowModes.View },
+ });
+ })
+ .catch((error) => {
+ console.error('Error updating document: ', error);
+ });
+ } else {
+ console.error('No matching user data found for id: ', id);
}
};
- const handleDenyClick = (id: GridRowId) => async () => {
- setLoading(true);
- try {
- await firebase.firestore().collection('users').doc(id.toString()).update({ role: 'denied' });
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View },
- });
- } catch (error) {
- console.error('Error updating document: ', error);
- } finally {
- setLoading(false);
+ const handleDenyClick = (id: GridRowId) => () => {
+ const updatedRow = userData.find((row) => row.id === id);
+ if (updatedRow) {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .update({ role: 'denied' })
+ .then(() => {
+ setRowModesModel({
+ ...rowModesModel,
+ [id]: { mode: GridRowModes.View },
+ });
+ })
+ .catch((error) => {
+ console.error('Error updating document: ', error);
+ });
+ } else {
+ console.error('No matching user data found for id: ', id);
}
};
- const handleDeleteClick = (id: GridRowId) => async () => {
- setLoading(true);
- try {
- await firebase.firestore().collection('users').doc(id.toString()).delete();
- deleteUserHTTPRequest(id.toString());
- setUserData((prev) => prev.filter((row) => row.id !== id));
- } catch (error) {
- console.error('Error removing document: ', error);
- } finally {
- setLoading(false);
- }
+ const handleDeleteClick = (id: GridRowId) => () => {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .delete()
+ .then(() => {
+ deleteUserHTTPRequest(id.toString());
+ setUserData(userData.filter((row) => row.id !== id));
+ })
+ .catch((error) => {
+ console.error('Error removing document: ', error);
+ });
};
- const handleCancelClick = (id: GridRowId) => async () => {
- setLoading(true);
- try {
- const editedRow = userData.find((row) => row.id === id);
- if (editedRow?.isNew) {
- await firebase.firestore().collection('users').doc(id.toString()).delete();
- setUserData((prev) => prev.filter((row) => row.id !== id));
- } else {
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View, ignoreModifications: true },
+ const handleCancelClick = (id: GridRowId) => () => {
+ const editedRow = userData.find((row) => row.id === id);
+ if (editedRow!.isNew) {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .delete()
+ .then(() => {
+ setUserData(userData.filter((row) => row.id !== id));
+ })
+ .catch((error) => {
+ console.error('Error removing document: ', error);
});
- }
- } catch (error) {
- console.error('Error removing document: ', error);
- } finally {
- setLoading(false);
+ } else {
+ setRowModesModel({
+ ...rowModesModel,
+ [id]: { mode: GridRowModes.View, ignoreModifications: true },
+ });
}
};
- const processRowUpdate = async (newRow: GridRowModel) => {
- setLoading(true);
- try {
- const updatedRow = { ...(newRow as User), isNew: false };
-
- if ((updatedRow as any).isNew) {
- await firebase.firestore().collection('users').add(updatedRow);
+ const processRowUpdate = (newRow: GridRowModel) => {
+ const updatedRow = { ...(newRow as User), isNew: false };
+ if (updatedRow) {
+ if (updatedRow.isNew) {
+ return firebase
+ .firestore()
+ .collection('users')
+ .add(updatedRow)
+ .then(() => {
+ setUserData(
+ userData.map((row) => (row.id === newRow.id ? updatedRow : row))
+ );
+ return updatedRow;
+ })
+ .catch((error) => {
+ console.error('Error adding document: ', error);
+ throw error;
+ });
} else {
- await firebase.firestore().collection('users').doc(updatedRow.id).update(updatedRow);
+ return firebase
+ .firestore()
+ .collection('users')
+ .doc(updatedRow.id)
+ .update(updatedRow)
+ .then(() => {
+ setUserData(
+ userData.map((row) => (row.id === newRow.id ? updatedRow : row))
+ );
+ return updatedRow;
+ })
+ .catch((error) => {
+ console.error('Error updating document: ', error);
+ throw error;
+ });
}
-
- setUserData((prev) =>
- prev.map((row) => (row.id === newRow.id ? (updatedRow as User) : row))
+ } else {
+ return Promise.reject(
+ new Error('No matching user data found for id: ' + newRow.id)
);
-
- return updatedRow;
- } catch (error) {
- console.error('Error processing row update: ', error);
- throw error;
- } finally {
- setLoading(false);
}
};
- const columns: GridColDef[] = [
- { field: 'fullname', headerName: 'Full Name', width: 202, editable: true },
- { field: 'email', headerName: 'Email', width: 215, editable: true },
- { field: 'department', headerName: 'Department', width: 119, editable: true },
- { field: 'role', headerName: 'Role', width: 150, editable: true },
+ const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
+ setRowModesModel(newRowModesModel);
+ };
+
+ let columns: GridColDef[] = [
{
field: 'actions',
type: 'actions',
headerName: 'Actions',
- width: 180,
+ width: 280,
cellClassName: 'actions',
getActions: ({ id }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
@@ -240,17 +277,18 @@ export default function ApprovalGrid(props: ApprovalGridProps) {
if (isInEditMode) {
return [
}
label="Save"
- sx={{ color: 'primary.main' }}
+ sx={{
+ color: 'primary.main',
+ }}
onClick={handleSaveClick(id)}
/>,
}
label="Cancel"
- className="textPrimary"
onClick={handleCancelClick(id)}
color="inherit"
/>,
@@ -258,30 +296,42 @@ export default function ApprovalGrid(props: ApprovalGridProps) {
}
return [
- }
- label="Edit"
- className="textPrimary"
- onClick={handleEditClick(id)}
+ ,
- }
- label="Delete"
+ size="small"
+ style={{ marginLeft: 0, height: '25px', textTransform: 'none' }}
+ startIcon={}
+ onClick={handleEditClick(id)}
+ >
+ Edit
+ ,
+
+ }
onClick={handleDeleteClick(id)}
- color="inherit"
- />,
+ >
+ Delete
+ ,
}
label="Approve"
onClick={handleApproveClick(id)}
color="success"
/>,
}
label="Deny"
onClick={handleDenyClick(id)}
@@ -290,94 +340,104 @@ export default function ApprovalGrid(props: ApprovalGridProps) {
];
},
},
- ];
-
- // ✅ Copy CourseGrid UI styling
- const StripedDataGrid = styled(DataGrid)(() => ({
- border: 'none',
- borderRadius: '16px',
- fontFamily: 'Inter, sans-serif',
- fontSize: '0.95rem',
-
- '& .MuiDataGrid-columnHeaders': {
- backgroundColor: '#D8C6F8',
- color: '#1C003D',
- fontWeight: 700,
- borderBottom: 'none',
+ {
+ field: 'fullname',
+ headerName: 'Full Name',
+ width: 202,
+ editable: true,
},
-
- '& .MuiDataGrid-columnHeaderTitle': {
- fontWeight: 700,
+ { field: 'email', headerName: 'Email', width: 215, editable: true },
+ {
+ field: 'department',
+ headerName: 'Department',
+ width: 119,
+ editable: true,
},
-
- '& .MuiDataGrid-columnHeader:first-of-type': {
- paddingLeft: '20px',
+ {
+ field: 'courses',
+ headerName: 'Course Code',
+ width: 300,
+ editable: true,
},
- '& .MuiDataGrid-cell:first-of-type': {
- paddingLeft: '25px',
+ {
+ field: 'semester',
+ headerName: 'Semester',
+ width: 130,
+ editable: true,
},
+ { field: 'role', headerName: 'Role', width: 100, editable: true },
+ ];
+ const ODD_OPACITY = 0.2;
+ const StripedDataGrid = styled(DataGrid)(({ theme }) => ({
[`& .${gridClasses.row}.even`]: {
- backgroundColor: '#FFFFFF',
- },
- [`& .${gridClasses.row}.odd`]: {
- backgroundColor: '#EEEEEE',
- },
-
- '& .MuiDataGrid-row:hover': {
- backgroundColor: '#EFE6FF',
- },
-
- '& .MuiDataGrid-cell': {
- borderBottom: '1px solid #ECE4FA',
- },
-
- '& .MuiDataGrid-footerContainer': {
- borderTop: 'none',
- },
-
- '& .MuiTablePagination-root': {
- color: '#5D3FC4',
- fontWeight: 500,
+ backgroundColor: '#562EBA1F',
+ '&:hover, &.Mui-hovered': {
+ backgroundColor: alpha(theme.palette.primary.main, ODD_OPACITY),
+ '@media (hover: none)': {
+ backgroundColor: 'transparent',
+ },
+ },
+ '&.Mui-selected': {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ ODD_OPACITY + theme.palette.action.selectedOpacity
+ ),
+ '&:hover, &.Mui-hovered': {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ ODD_OPACITY +
+ theme.palette.action.selectedOpacity +
+ theme.palette.action.hoverOpacity
+ ),
+ // Reset on touch devices, it doesn't add specificity
+ '@media (hover: none)': {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ ODD_OPACITY + theme.palette.action.selectedOpacity
+ ),
+ },
+ },
+ },
},
}));
return (
- {loading ? : null}
-
setRowModesModel(m)}
+ onRowModesModelChange={handleRowModesModelChange}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
- onProcessRowUpdateError={(error) =>
- console.error('Error processing row update: ', error)
- }
- slots={{ toolbar: EditToolbar }}
- slotProps={{ toolbar: { setApplicationData: setUserData, setRowModesModel } }}
initialState={{
pagination: { paginationModel: { pageSize: 25 } },
}}
getRowClassName={(params) =>
params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd'
}
+ sx={{ borderRadius: '16px' }}
/>
);
}
-
diff --git a/src/component/Dashboard/Users/UserGrid.tsx b/src/component/Dashboard/Users/UserGrid.tsx
index 7fe1f69..dcd31d9 100644
--- a/src/component/Dashboard/Users/UserGrid.tsx
+++ b/src/component/Dashboard/Users/UserGrid.tsx
@@ -1,5 +1,4 @@
'use client';
-
import * as React from 'react';
import Box from '@mui/material/Box';
import EditIcon from '@mui/icons-material/Edit';
@@ -21,6 +20,7 @@ import {
GridRowId,
GridRowModel,
GridRowEditStopReasons,
+ useGridApiContext,
gridClasses,
} from '@mui/x-data-grid';
import {
@@ -29,14 +29,13 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
- LinearProgress,
- Button,
+ TextField,
} from '@mui/material';
-import { styled } from '@mui/material/styles';
-
+import { deleteUserHTTPRequest } from '@/firebase/auth/auth_delete_user';
import firebase from '@/firebase/firebase_config';
import 'firebase/firestore';
-import { deleteUserHTTPRequest } from '@/firebase/auth/auth_delete_user';
+import { LinearProgress, Button } from '@mui/material';
+import { alpha, styled } from '@mui/material/styles';
import { isE2EMode } from '@/utils/featureFlags';
interface User {
@@ -59,13 +58,18 @@ interface EditToolbarProps {
) => void;
}
-function EditToolbar(_props: EditToolbarProps) {
- // match CourseGrid toolbar look (default MUI icons/colors)
+function EditToolbar(props: EditToolbarProps) {
+ const { setUserData, setRowModesModel } = props;
+
+ // Add state to control the dialog open status
+ const [open, setOpen] = React.useState(false);
+
return (
-
-
-
+ {/* Include your Dialog component here and pass the open state and setOpen function as props */}
+
+
+
);
}
@@ -77,35 +81,35 @@ interface UserGridProps {
export default function UserGrid(props: UserGridProps) {
const { userRole } = props;
const e2e = isE2EMode();
-
- const [loading, setLoading] = React.useState(false);
const [userData, setUserData] = React.useState([]);
-
+ const [open, setOpen] = React.useState(false);
const [delDia, setDelDia] = React.useState(false);
- const [delId, setDelId] = React.useState(null);
+ const [delId, setDelId] = React.useState();
React.useEffect(() => {
if (e2e) {
setUserData([]);
return;
}
-
const usersRef = firebase.firestore().collection('users');
const unsubscribe = usersRef.onSnapshot((querySnapshot) => {
const data = querySnapshot.docs.map(
(doc) =>
- ({
- id: doc.id,
- fullname: `${doc.data().firstname ?? ''} ${doc.data().lastname ?? ''}`,
- ...doc.data(),
- } as unknown as User)
+ ({
+ id: doc.id,
+ fullname: doc.data().firstname + ' ' + doc.data().lastname,
+ ...doc.data(),
+ } as unknown as User)
);
+
setUserData(data);
});
return () => unsubscribe();
}, [e2e]);
-
+ const handleDeleteDiagClose = () => {
+ setDelDia(false);
+ };
const [rowModesModel, setRowModesModel] = React.useState(
{}
);
@@ -120,27 +124,32 @@ export default function UserGrid(props: UserGridProps) {
};
const handleEditClick = (id: GridRowId) => () => {
- setLoading(true);
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
- setLoading(false);
};
- const handleSaveClick = (id: GridRowId) => async () => {
- setLoading(true);
- try {
- const updatedRow = userData.find((row) => row.id === id);
- if (!updatedRow) throw new Error(`No matching user data for id: ${id}`);
-
- await firebase.firestore().collection('users').doc(id.toString()).update(updatedRow);
-
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View },
- });
- } catch (err) {
- console.error('Error updating document: ', err);
- } finally {
- setLoading(false);
+ const handleSaveClick = (id: GridRowId) => () => {
+ console.log('Clicked Save for ID:', id);
+ console.log('Current userData:', userData);
+
+ const updatedRow = userData.find((row) => row.id === id);
+ if (updatedRow) {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .update(updatedRow)
+ .then(() => {
+ console.log('Document successfully updated!');
+ setRowModesModel({
+ ...rowModesModel,
+ [id]: { mode: GridRowModes.View },
+ });
+ })
+ .catch((error) => {
+ console.error('Error updating document: ', error);
+ });
+ } else {
+ console.error('No matching user data found for id: ', id);
}
};
@@ -149,89 +158,111 @@ export default function UserGrid(props: UserGridProps) {
setDelDia(true);
};
- const handleDeleteDiagClose = () => {
- setDelDia(false);
- setDelId(null);
- };
-
- const handleDeleteClick = async (id: GridRowId) => {
- setLoading(true);
- try {
- await firebase.firestore().collection('users').doc(id.toString()).delete();
- deleteUserHTTPRequest(id.toString());
- setUserData((prev) => prev.filter((row) => row.id !== id));
- } catch (err) {
- console.error('Error removing document: ', err);
- } finally {
- setLoading(false);
- }
+ const handleDeleteClick = (id: GridRowId) => {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .delete()
+ .then(() => {
+ deleteUserHTTPRequest(id.toString());
+ setUserData(userData.filter((row) => row.id !== id));
+ })
+ .catch((error) => {
+ console.error('Error removing document: ', error);
+ });
};
- const handleSubmit = async (e: React.FormEvent) => {
+ const handleSubmit = (e) => {
e.preventDefault();
- if (delId == null) return;
- await handleDeleteClick(delId);
+ console.log(delId.toString());
+ handleDeleteClick(delId);
setDelDia(false);
- setDelId(null);
};
-
- const handleCancelClick = (id: GridRowId) => async () => {
- setLoading(true);
- try {
- const editedRow = userData.find((row) => row.id === id);
- if (editedRow?.isNew) {
- await firebase.firestore().collection('users').doc(id.toString()).delete();
- setUserData((prev) => prev.filter((row) => row.id !== id));
- } else {
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View, ignoreModifications: true },
+ function CustomToolbar() {
+ const apiRef = useGridApiContext();
+
+ return (
+
+
+
+ );
+ }
+
+ const handleCancelClick = (id: GridRowId) => () => {
+ const editedRow = userData.find((row) => row.id === id);
+ if (editedRow!.isNew) {
+ firebase
+ .firestore()
+ .collection('users')
+ .doc(id.toString())
+ .delete()
+ .then(() => {
+ setUserData(userData.filter((row) => row.id !== id));
+ })
+ .catch((error) => {
+ console.error('Error removing document: ', error);
});
- }
- } catch (err) {
- console.error('Error canceling edit: ', err);
- } finally {
- setLoading(false);
+ } else {
+ setRowModesModel({
+ ...rowModesModel,
+ [id]: { mode: GridRowModes.View, ignoreModifications: true },
+ });
}
};
- const processRowUpdate = async (newRow: GridRowModel) => {
- setLoading(true);
- try {
- const updatedRow = { ...(newRow as User), isNew: false };
-
- // new rows not really used in your flow, but keep parity with CourseGrid
- if ((updatedRow as any).isNew) {
- await firebase.firestore().collection('users').add(updatedRow);
+ const processRowUpdate = (newRow: GridRowModel) => {
+ const updatedRow = { ...(newRow as User), isNew: false };
+ if (updatedRow) {
+ if (updatedRow.isNew) {
+ return firebase
+ .firestore()
+ .collection('users')
+ .add(updatedRow)
+ .then(() => {
+ setUserData(
+ userData.map((row) => (row.id === newRow.id ? updatedRow : row))
+ );
+ return updatedRow;
+ })
+ .catch((error) => {
+ console.error('Error adding document: ', error);
+ throw error;
+ });
} else {
- await firebase.firestore().collection('users').doc(updatedRow.id).update(updatedRow);
+ return firebase
+ .firestore()
+ .collection('users')
+ .doc(updatedRow.id)
+ .update(updatedRow)
+ .then(() => {
+ setUserData(
+ userData.map((row) => (row.id === newRow.id ? updatedRow : row))
+ );
+ return updatedRow;
+ })
+ .catch((error) => {
+ console.error('Error updating document: ', error);
+ throw error;
+ });
}
-
- setUserData((prev) =>
- prev.map((row) => (row.id === newRow.id ? (updatedRow as User) : row))
+ } else {
+ return Promise.reject(
+ new Error('No matching user data found for id: ' + newRow.id)
);
-
- return updatedRow;
- } catch (err) {
- console.error('Error processing row update: ', err);
- throw err;
- } finally {
- setLoading(false);
}
};
+ const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
+ setRowModesModel(newRowModesModel);
+ };
+
const columns: GridColDef[] = [
- { field: 'firstname', headerName: 'First Name', width: 150, editable: true },
- { field: 'lastname', headerName: 'Last Name', width: 150, editable: true },
- { field: 'email', headerName: 'Email', width: 250, editable: true },
- { field: 'department', headerName: 'Department', width: 130, editable: true },
- { field: 'role', headerName: 'Role', width: 150, editable: true },
- { field: 'id', headerName: 'User ID', width: 290, editable: true },
{
field: 'actions',
type: 'actions',
headerName: 'Actions',
- width: 130,
+ width: 200,
cellClassName: 'actions',
getActions: ({ id }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
@@ -239,14 +270,16 @@ export default function UserGrid(props: UserGridProps) {
if (isInEditMode) {
return [
}
label="Save"
- sx={{ color: 'primary.main' }}
+ sx={{
+ color: '#562EBA',
+ }}
onClick={handleSaveClick(id)}
/>,
}
label="Cancel"
className="textPrimary"
@@ -257,119 +290,100 @@ export default function UserGrid(props: UserGridProps) {
}
return [
- }
- label="Edit"
- className="textPrimary"
- onClick={handleEditClick(id)}
+ ,
- }
- label="Delete"
+ size="small"
+ style={{ marginLeft: 0, height: '25px', textTransform: 'none' }}
+ startIcon={}
+ onClick={handleEditClick(id)}
+ >
+ Edit
+ ,
+
+ }
onClick={handleDel(id)}
- color="inherit"
- />,
+ >
+ Delete
+ ,
];
},
},
- ];
-
- // ✅ Copy CourseGrid UI styling
- const StripedDataGrid = styled(DataGrid)(() => ({
- border: 'none',
- borderRadius: '16px',
- fontFamily: 'Inter, sans-serif',
- fontSize: '0.95rem',
-
- '& .MuiDataGrid-columnHeaders': {
- backgroundColor: '#D8C6F8',
- color: '#1C003D',
- fontWeight: 700,
- borderBottom: 'none',
- },
-
- '& .MuiDataGrid-columnHeaderTitle': {
- fontWeight: 700,
- },
-
- '& .MuiDataGrid-columnHeader:first-of-type': {
- paddingLeft: '20px',
+ {
+ field: 'firstname',
+ headerName: 'First Name',
+ width: 150,
+ editable: true,
},
- '& .MuiDataGrid-cell:first-of-type': {
- paddingLeft: '25px',
+ { field: 'lastname', headerName: 'Last Name', width: 150, editable: true },
+ { field: 'email', headerName: 'Email', width: 250, editable: true },
+ {
+ field: 'department',
+ headerName: 'Department',
+ width: 130,
+ editable: true,
},
+ { field: 'role', headerName: 'Role', width: 150, editable: true },
+ { field: 'id', headerName: 'User ID', width: 290, editable: true },
+ ];
+ const ODD_OPACITY = 0.2;
+ const StripedDataGrid = styled(DataGrid)(({ theme }) => ({
[`& .${gridClasses.row}.even`]: {
- backgroundColor: '#FFFFFF',
- },
- [`& .${gridClasses.row}.odd`]: {
- backgroundColor: '#EEEEEE',
- },
-
- '& .MuiDataGrid-row:hover': {
- backgroundColor: '#EFE6FF',
- },
-
- '& .MuiDataGrid-cell': {
- borderBottom: '1px solid #ECE4FA',
- },
-
- '& .MuiDataGrid-footerContainer': {
- borderTop: 'none',
- },
-
- '& .MuiTablePagination-root': {
- color: '#5D3FC4',
- fontWeight: 500,
+ backgroundColor: '#562EBA1F',
+ '&:hover, &.Mui-hovered': {
+ backgroundColor: alpha(theme.palette.primary.main, ODD_OPACITY),
+ '@media (hover: none)': {
+ backgroundColor: 'transparent',
+ },
+ },
+ '&.Mui-selected': {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ ODD_OPACITY + theme.palette.action.selectedOpacity
+ ),
+ '&:hover, &.Mui-hovered': {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ ODD_OPACITY +
+ theme.palette.action.selectedOpacity +
+ theme.palette.action.hoverOpacity
+ ),
+ // Reset on touch devices, it doesn't add specificity
+ '@media (hover: none)': {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ ODD_OPACITY + theme.palette.action.selectedOpacity
+ ),
+ },
+ },
+ },
},
}));
-
return (
- <>
-
- {loading ? : null}
-
- setRowModesModel(m)}
- onRowEditStop={handleRowEditStop}
- processRowUpdate={processRowUpdate}
- onProcessRowUpdateError={(error) =>
- console.error('Error processing row update: ', error)
- }
- slots={{
- toolbar: EditToolbar,
- }}
- slotProps={{
- toolbar: { setUserData, setRowModesModel },
- }}
- initialState={{
- pagination: { paginationModel: { pageSize: 25 } },
- }}
- getRowClassName={(params) =>
- params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd'
- }
- />
-
-
- {/* keep your confirm-delete dialog (CourseGrid doesn't have it, but UI matches your app style) */}
+
- >
+
+ params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd'
+ }
+ sx={{ borderRadius: '16px' }}
+ />
+
);
}
-
diff --git a/src/components/TopBar/TopBar.tsx b/src/components/TopBar/TopBar.tsx
index dec24ba..f44897e 100644
--- a/src/components/TopBar/TopBar.tsx
+++ b/src/components/TopBar/TopBar.tsx
@@ -2,17 +2,37 @@ import { useUserInfo } from '@/hooks/User/useGetUserInfo';
import { AppBar } from '@mui/material';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
+import Badge from '@mui/material/Badge';
import NotificationsNoneOutlinedIcon from '@mui/icons-material/NotificationsNoneOutlined';
import NotificationsActiveOutlinedIcon from '@mui/icons-material/NotificationsActiveOutlined';
import AccountCircleTwoToneIcon from '@mui/icons-material/AccountCircleTwoTone';
import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng';
import Link from 'next/link';
import { Role, roleMapping } from '@/types/User';
-import { useAnnouncements } from '@/contexts/AnnouncementsContext';
+import { useFetchAnnouncementsForAccount } from '@/hooks/Announcements/useFetchAnnouncements';
+// type TopNavProps = {
+// notification?: boolean;
+// };
export default function TopNav() {
const [user, role, loading, error] = useUserInfo();
- const { unread } = useAnnouncements();
+ const uemail = user?.email;
+ const {
+ read,
+ unread,
+ loading: announcementLoading,
+ loadingMore,
+ hasMore,
+ error: fetchError,
+ refresh,
+ loadMore,
+ } = useFetchAnnouncementsForAccount({
+ userRole: role,
+ userEmail: uemail,
+ userDepartment: 'ECE',
+ channel: 'inApp',
+ realtime: true,
+ });
const onNotifications = () => {};
const display = (v: unknown, fallback = 'Not listed'): string => {
if (v == null) return fallback; // null/undefined
diff --git a/src/contexts/AnnouncementsContext.tsx b/src/contexts/AnnouncementsContext.tsx
deleted file mode 100644
index 3ba4682..0000000
--- a/src/contexts/AnnouncementsContext.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-'use client';
-
-import React, {
- createContext,
- useContext,
- useEffect,
- useState,
- useMemo,
- useCallback,
- ReactNode,
-} from 'react';
-import firebase from '@/firebase/firebase_config';
-import 'firebase/firestore';
-import { useAuth } from '@/firebase/auth/auth_context';
-import GetUserRole from '@/firebase/util/GetUserRole';
-import GetAnnouncementTimestamp from '@/firebase/util/GetAnnouncementTimestamp';
-import {
- Announcement,
- Audience,
- AudienceDepartment,
- AudienceRole,
-} from '@/types/announcement';
-import { Role } from '@/types/User';
-
-type AnnouncementsContextType = {
- read: Announcement[];
- unread: Announcement[];
- loading: boolean;
- error: Error | null;
- refresh: () => void;
-};
-
-const AnnouncementsContext = createContext(
- null
-);
-
-function convertRole(role: string): AudienceRole {
- switch (role) {
- case 'admin':
- return 'admin';
- case 'faculty':
- return 'faculty';
- default:
- return 'student';
- }
-}
-
-function toDate(v: any): Date | null {
- if (!v) return null;
- if (v instanceof Date) return v;
- if (typeof v.toDate === 'function') return v.toDate();
- return null;
-}
-
-function mapDoc(doc: firebase.firestore.QueryDocumentSnapshot): Announcement {
- const d = doc.data() as any;
-
- return {
- id: doc.id,
- title: d.title ?? '',
- bodyMd: d.bodyMd ?? '',
- pinned: !!d.pinned,
-
- createdAt: toDate(d.createdAt),
- updatedAt: toDate(d.updatedAt),
- scheduledAt: toDate(d.scheduledAt),
- expiresAt: toDate(d.expiresAt),
-
- senderId: d.senderId ?? '',
- senderName: d.senderName ?? null,
- audienceTokens: Array.isArray(d.audienceTokens) ? d.audienceTokens : [],
- channels: Array.isArray(d.channels) ? d.channels : [],
- audience: (d.audience ?? { type: 'all' }) as Audience,
-
- dispatchStatus: d.dispatchStatus ?? 'unknown',
- };
-}
-
-type AnnouncementsProviderProps = {
- children: ReactNode;
-};
-
-export function AnnouncementsProvider({
- children,
-}: AnnouncementsProviderProps) {
- const { user } = useAuth();
- const [roleData, roleLoading] = GetUserRole(user?.uid);
- const [timestamp] = GetAnnouncementTimestamp(user?.uid);
-
- const [announcements, setAnnouncements] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [refreshTrigger, setRefreshTrigger] = useState(0);
-
- const userEmail = user?.email ?? '';
- const userDepartment: AudienceDepartment = 'ECE';
- const channel = 'inApp';
-
- const userRole = useMemo(() => {
- return convertRole(roleData ?? '');
- }, [roleData]);
-
- const buildQuery = useCallback(() => {
- const col = firebase.firestore().collection('announcements');
- let q: firebase.firestore.Query = col;
-
- q = q.where(`channels.${channel}`, '==', true);
- q = q.where('dispatchStatus', '==', 'completed');
- q = q.orderBy('pinned', 'desc').orderBy('createdAt', 'desc');
-
- if (userRole === 'admin') {
- return q;
- }
-
- const isNonEmpty = (v: string | null | undefined): v is string =>
- typeof v === 'string' && v.trim().length > 0;
-
- const norm = (s: string) => s.trim().toLowerCase();
-
- const tokens = [
- 'all',
- userRole ? `role:${norm(userRole)}` : null,
- userRole === 'faculty' ? `role:student` : null,
- userDepartment ? `dept:${norm(userDepartment)}` : null,
- userEmail ? `user:${norm(userEmail)}` : null,
- ].filter(isNonEmpty);
-
- q = q.where('audienceTokens', 'array-contains-any', tokens);
- return q;
- }, [userRole, userDepartment, userEmail, channel]);
-
- useEffect(() => {
- if (roleLoading || !user) {
- return;
- }
-
- setLoading(true);
- setError(null);
-
- const q = buildQuery().limit(50);
-
- const unsub = q.onSnapshot(
- (snap) => {
- const docs = snap.docs;
- const items = docs.map(mapDoc);
- setAnnouncements(items);
- setLoading(false);
- },
- (err) => {
- console.error('Announcements listener error:', err);
- setError(err);
- setLoading(false);
- }
- );
-
- return () => {
- unsub();
- };
- }, [buildQuery, roleLoading, user, refreshTrigger]);
-
- const refresh = useCallback(() => {
- setRefreshTrigger((prev) => prev + 1);
- }, []);
-
- const { read, unread } = useMemo(() => {
- return announcements.reduce(
- (acc, a) => {
- const tMs = a.scheduledAt ?? a.createdAt ?? new Date(Date.now());
- const isUnread = timestamp === null ? true : tMs > timestamp;
-
- (isUnread ? acc.unread : acc.read).push(a);
- return acc;
- },
- { unread: [] as Announcement[], read: [] as Announcement[] }
- );
- }, [announcements, timestamp]);
-
- const value = useMemo(
- () => ({
- read,
- unread,
- loading: loading || roleLoading,
- error,
- refresh,
- }),
- [read, unread, loading, roleLoading, error, refresh]
- );
-
- return (
-
- {children}
-
- );
-}
-
-export function useAnnouncements(): AnnouncementsContextType {
- const context = useContext(AnnouncementsContext);
- if (!context) {
- throw new Error(
- 'useAnnouncements must be used within an AnnouncementsProvider'
- );
- }
- return context;
-}