From a14ea6d80519e55154f1cdb0260fdecb6a074c9b Mon Sep 17 00:00:00 2001 From: myeeli Date: Fri, 17 Oct 2025 12:21:12 -0700 Subject: [PATCH 1/7] Impelemented PM educator view --- .../ProjectManagerEducatorView.jsx | 165 ++++++++++++++++ .../ProjectManagerEducatorView.module.css | 183 ++++++++++++++++++ src/routes.jsx | 2 + 3 files changed, 350 insertions(+) create mode 100644 src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx create mode 100644 src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css diff --git a/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx new file mode 100644 index 0000000000..48b2a9c8d5 --- /dev/null +++ b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx @@ -0,0 +1,165 @@ +import React from "react"; +import styles from "./ProjectManagerEducatorView.module.css"; + +const mockEducators = [ + { + id: "t-001", + name: "Alice Johnson", + subject: "Mathematics", + studentCount: 3, + students: [ + { id: "s-101", name: "Jay", grade: "7", progress: 0.78 }, + { id: "s-102", name: "Kate", grade: "7", progress: 0.62 }, + { id: "s-103", name: "Sam", grade: "8", progress: 0.85 }, + ], + }, + { + id: "t-002", + name: "Brian Lee", + subject: "Science", + studentCount: 2, + students: [ + { id: "s-201", name: "Alina Gupta", grade: "6", progress: 0.54 }, + { id: "s-202", name: "Samir Khan", grade: "6", progress: 0.91 }, + ], + }, + { + id: "t-003", + name: "John Doe", + subject: "English", + studentCount: 1, + students: [{ id: "s-301", name: "Ryan", grade: "7", progress: 0.73 }], + }, +]; + +async function fetchEducators() { + await new Promise((r) => setTimeout(r, 250)); + return mockEducators.map(({ students, ...rest }) => rest); +} + +async function fetchStudentsByEducator(educatorId) { + await new Promise((r) => setTimeout(r, 200)); + const edu = mockEducators.find((e) => e.id === educatorId); + return edu ? edu.students : []; +} + +function StudentCard({ s }) { + const pct = Math.round((s.progress ?? 0) * 100); + return ( +
+
{s.name}
+
Grade {s.grade}
+
+
+
+
+
{pct}%
+
+
+ ); +} + +function EducatorRow({ educator }) { + const [open, setOpen] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const [students, setStudents] = React.useState([]); + + async function toggle() { + if (!open && students.length === 0) { + setLoading(true); + const data = await fetchStudentsByEducator(educator.id); + setStudents(data); + setLoading(false); + } + setOpen((v) => !v); + } + + return ( +
+ + + {open && ( +
+ {loading ? ( +
Loading students…
+ ) : ( +
+ {students.map((s) => ( + + ))} +
+ )} +
+ )} +
+ ); +} + +export default function ProjectManagerEducatorView() { + const [educators, setEducators] = React.useState([]); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + (async () => { + try { + const data = await fetchEducators(); + setEducators(data); + } catch (e) { + setError("Failed to load educators"); + } finally { + setLoading(false); + } + })(); + }, []); + + return ( +
+
+ Home + / + Project Manager +
+
+
+

Project Manager Dashboard

+

View educators and their assigned students

+
+
+
+
+

Educators

+
+
+ {loading &&
Loading…
} + {error &&
{error}
} + {!loading && !error && educators.length === 0 && ( +
No educators found.
+ )} + {!loading && !error && educators.length > 0 && ( +
+ {educators.map((e) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css new file mode 100644 index 0000000000..e959f974a3 --- /dev/null +++ b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css @@ -0,0 +1,183 @@ +:root { + --bg: #f6f7fb; + --text: #111827; + --muted: #6b7280; + --card-bg: #ffffff; + --border: #e5e7eb; + --badge-bg: #eef2ff; + --badge-text: #3730a3; + --indigo: #4f46e5; + --progress-bg: #e5e7eb; + --error: #b91c1c; +} + +.container { + max-width: 72rem; + margin: 0 auto; + padding: 16px; + color: var(--text); +} + +.breadcrumb { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted); + font-size: 0.9rem; + margin-bottom: 8px; +} +.sep { + opacity: 0.7; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.title { + font-size: 1.875rem; + line-height: 1.2; + font-weight: 600; + margin: 0; +} +.subtitle { + color: var(--muted); + font-size: 0.95rem; + margin-top: 4px; +} + +.card { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 16px; + box-shadow: 0 1px 3px rgba(17, 24, 39, 0.06); + overflow: hidden; +} +.cardHeader { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; +} +.cardTitle { + font-size: 1rem; + font-weight: 600; + margin: 0; +} +.cardBody { + padding: 16px; +} + +.row { + border-bottom: 1px solid var(--border); +} +.row:last-child { + border-bottom: none; +} +.rowHeader { + width: 100%; + background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: space-between; + text-align: left; + padding: 12px 16px; + cursor: pointer; + transition: background 0.15s ease-in-out; +} +.rowHeader:hover { + background: #f9fafb; +} +.rowHeaderLeft { + display: flex; + flex-direction: column; + gap: 2px; +} +.rowTitle { + font-weight: 600; + font-size: 1rem; +} +.rowHeaderRight { + display: flex; + align-items: center; + gap: 8px; +} +.badge { + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 999px; + background: var(--badge-bg); + color: var(--badge-text); +} +.toggleText { + font-size: 0.9rem; + color: var(--indigo); +} + +.meta { + color: var(--muted); + font-size: 0.9rem; +} + +.studentsWrap { + padding: 0 16px 16px 16px; +} +.students { + display: grid; + grid-template-columns: 1fr; + gap: 12px; + margin-top: 8px; +} +@media (min-width: 640px) { + .students { grid-template-columns: repeat(2, 1fr); } +} +@media (min-width: 1024px) { + .students { grid-template-columns: repeat(3, 1fr); } +} + +.studentCard { + background: #f9fafb; + border: 1px solid var(--border); + border-radius: 12px; + padding: 12px; +} +.studentName { + font-weight: 600; +} +.progressWrap { + margin-top: 8px; +} +.progressBar { + height: 8px; + width: 100%; + background: var(--progress-bg); + border-radius: 6px; + overflow: hidden; +} +.progressFill { + height: 8px; + background: var(--indigo); + border-radius: 6px; + width: 0%; + transition: width 0.3s ease; +} +.progressPct { + text-align: right; + font-size: 0.75rem; + margin-top: 4px; + color: var(--muted); +} + +.loadingText, +.emptyText { + color: var(--muted); + font-size: 0.95rem; +} +.errorText { + color: var(--error); + font-size: 0.95rem; +} diff --git a/src/routes.jsx b/src/routes.jsx index f8327aaf38..dacd7c6054 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -239,6 +239,7 @@ const MonthsPledgedChart = lazy(() => // PR Analytics Dashboard import ReviewsInsight from './components/PRAnalyticsDashboard/ReviewsInsight/ReviewsInsight'; +import ProjectManagerEducatorView from 'components/EductionPortal/ProjectManager/ProjectManagerEducatorView'; const JobAnalyticsPage = lazy(() => import('./components/Reports/HitsAndApplicationRatio/JobAnalyticsPage'), @@ -735,6 +736,7 @@ export default ( + {/* PR Analytics Dashboard */} Date: Fri, 17 Oct 2025 17:50:01 -0700 Subject: [PATCH 2/7] Implementing PMNotification Component --- package-lock.json | 18 - package.json | 1 - .../ProjectManagerEducatorView.jsx | 76 +++- .../ProjectManagerEducatorView.module.css | 80 +++- .../ProjectManagerNotification.jsx | 120 ++++++ .../ProjectManagerNotification.module.css | 180 +++++++++ src/routes.jsx | 4 +- yarn.lock | 371 +++++++++++++++--- 8 files changed, 772 insertions(+), 78 deletions(-) create mode 100644 src/components/EductionPortal/ProjectManager/ProjectManagerNotification.jsx create mode 100644 src/components/EductionPortal/ProjectManager/ProjectManagerNotification.module.css diff --git a/package-lock.json b/package-lock.json index 7f9a1a3e06..1feefe04dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,7 +165,6 @@ "sass": "^1.86.3", "sass-loader": "^16.0.5", "stylelint": "^16.25.0", - "stylelint-config-prettier": "^9.0.5", "stylelint-config-standard": "^39.0.1", "typescript": "^4.8.4", "vite": "^6.3.5", @@ -17761,23 +17760,6 @@ "node": ">=18.12.0" } }, - "node_modules/stylelint-config-prettier": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-9.0.5.tgz", - "integrity": "sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==", - "dev": true, - "license": "MIT", - "bin": { - "stylelint-config-prettier": "bin/check.js", - "stylelint-config-prettier-check": "bin/check.js" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "stylelint": ">= 11.x < 15" - } - }, "node_modules/stylelint-config-recommended": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-17.0.0.tgz", diff --git a/package.json b/package.json index 2d9f481a19..62de266bf8 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,6 @@ "sass": "^1.86.3", "sass-loader": "^16.0.5", "stylelint": "^16.25.0", - "stylelint-config-prettier": "^9.0.5", "stylelint-config-standard": "^39.0.1", "typescript": "^4.8.4", "vite": "^6.3.5", diff --git a/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx index 48b2a9c8d5..748d402068 100644 --- a/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx +++ b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.jsx @@ -1,5 +1,6 @@ import React from "react"; import styles from "./ProjectManagerEducatorView.module.css"; +import NotificationComposer from "./ProjectManagerNotification"; const mockEducators = [ { @@ -112,6 +113,11 @@ function EducatorRow({ educator }) { export default function ProjectManagerEducatorView() { const [educators, setEducators] = React.useState([]); + const [filtered, setFiltered] = React.useState([]); + const [query, setQuery] = React.useState(""); + const [showComposer, setShowComposer] = React.useState(false); + const [lastSentInfo, setLastSentInfo] = React.useState(null); + const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); @@ -120,6 +126,7 @@ export default function ProjectManagerEducatorView() { try { const data = await fetchEducators(); setEducators(data); + setFiltered(data); } catch (e) { setError("Failed to load educators"); } finally { @@ -128,6 +135,35 @@ export default function ProjectManagerEducatorView() { })(); }, []); + React.useEffect(() => { + const q = query.trim().toLowerCase(); + if (!q) { + setFiltered(educators); + } else { + setFiltered( + educators.filter( + (e) => + e.name.toLowerCase().includes(q) || + e.subject.toLowerCase().includes(q) + ) + ); + } + }, [query, educators]); + + function handleOpenComposer() { + setShowComposer(true); + } + function handleCloseComposer() { + setShowComposer(false); + } + function handleSent(payload) { + setLastSentInfo({ + educatorCount: payload.educatorIds.length, + timestamp: new Date().toISOString(), + }); + setShowComposer(false); + } + return (
@@ -135,12 +171,34 @@ export default function ProjectManagerEducatorView() { / Project Manager
+

Project Manager Dashboard

-

View educators and their assigned students

+
+
+ setQuery(e.target.value)} + aria-label="Search educators" + /> +
+ + {lastSentInfo && ( +
+ Sent to {lastSentInfo.educatorCount} educator + {lastSentInfo.educatorCount === 1 ? "" : "s"} at{" "} + {new Date(lastSentInfo.timestamp).toLocaleTimeString()} +
+ )} +

Educators

@@ -148,18 +206,26 @@ export default function ProjectManagerEducatorView() {
{loading &&
Loading…
} {error &&
{error}
} - {!loading && !error && educators.length === 0 && ( -
No educators found.
+ {!loading && !error && filtered.length === 0 && ( +
No educators match your search.
)} - {!loading && !error && educators.length > 0 && ( + {!loading && !error && filtered.length > 0 && (
- {educators.map((e) => ( + {filtered.map((e) => ( ))}
)}
+ + {showComposer && ( + + )}
); } diff --git a/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css index e959f974a3..f54010f6f5 100644 --- a/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css +++ b/src/components/EductionPortal/ProjectManager/ProjectManagerEducatorView.module.css @@ -12,7 +12,7 @@ } .container { - max-width: 72rem; + max-width: 72rem; margin: 0 auto; padding: 16px; color: var(--text); @@ -26,6 +26,7 @@ font-size: 0.9rem; margin-bottom: 8px; } + .sep { opacity: 0.7; } @@ -36,12 +37,14 @@ justify-content: space-between; margin-bottom: 16px; } + .title { - font-size: 1.875rem; + font-size: 1.875rem; line-height: 1.2; font-weight: 600; margin: 0; } + .subtitle { color: var(--muted); font-size: 0.95rem; @@ -55,6 +58,7 @@ box-shadow: 0 1px 3px rgba(17, 24, 39, 0.06); overflow: hidden; } + .cardHeader { padding: 12px 16px; border-bottom: 1px solid var(--border); @@ -62,11 +66,13 @@ align-items: center; justify-content: space-between; } + .cardTitle { font-size: 1rem; font-weight: 600; margin: 0; } + .cardBody { padding: 16px; } @@ -74,9 +80,11 @@ .row { border-bottom: 1px solid var(--border); } + .row:last-child { border-bottom: none; } + .rowHeader { width: 100%; background: transparent; @@ -89,23 +97,28 @@ cursor: pointer; transition: background 0.15s ease-in-out; } + .rowHeader:hover { background: #f9fafb; } + .rowHeaderLeft { display: flex; flex-direction: column; gap: 2px; } + .rowTitle { font-weight: 600; font-size: 1rem; } + .rowHeaderRight { display: flex; align-items: center; gap: 8px; } + .badge { font-size: 0.75rem; padding: 2px 8px; @@ -113,6 +126,7 @@ background: var(--badge-bg); color: var(--badge-text); } + .toggleText { font-size: 0.9rem; color: var(--indigo); @@ -126,17 +140,24 @@ .studentsWrap { padding: 0 16px 16px 16px; } + .students { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 8px; } + @media (min-width: 640px) { - .students { grid-template-columns: repeat(2, 1fr); } + .students { + grid-template-columns: repeat(2, 1fr); + } } + @media (min-width: 1024px) { - .students { grid-template-columns: repeat(3, 1fr); } + .students { + grid-template-columns: repeat(3, 1fr); + } } .studentCard { @@ -145,12 +166,15 @@ border-radius: 12px; padding: 12px; } + .studentName { font-weight: 600; } + .progressWrap { margin-top: 8px; } + .progressBar { height: 8px; width: 100%; @@ -158,6 +182,7 @@ border-radius: 6px; overflow: hidden; } + .progressFill { height: 8px; background: var(--indigo); @@ -165,6 +190,7 @@ width: 0%; transition: width 0.3s ease; } + .progressPct { text-align: right; font-size: 0.75rem; @@ -177,7 +203,53 @@ color: var(--muted); font-size: 0.95rem; } + .errorText { color: var(--error); font-size: 0.95rem; } + +.toolbar { + display: flex; + gap: 10px; + align-items: center; +} + +.searchInput { + width: 320px; + max-width: 52vw; + border: 1px solid var(--border, #e5e7eb); + border-radius: 10px; + padding: 8px 10px; + font: inherit; + outline: none; +} + +.searchInput:focus { + border-color: var(--primary, #4f46e5); + box-shadow: 0 0 0 3px var(--primary-100, #eef2ff); +} + +.primaryBtn { + background: var(--primary, #4f46e5); + color: #fff; + border: none; + border-radius: 10px; + padding: 10px 14px; + cursor: pointer; + font-weight: 600; +} + +.primaryBtn:hover { + filter: brightness(0.95); +} + +.successBanner { + margin: 8px 0 12px; + padding: 10px 12px; + border: 1px solid #d1fae5; + background: #ecfdf5; + color: #065f46; + border-radius: 10px; + font-size: 0.95rem; +} \ No newline at end of file diff --git a/src/components/EductionPortal/ProjectManager/ProjectManagerNotification.jsx b/src/components/EductionPortal/ProjectManager/ProjectManagerNotification.jsx new file mode 100644 index 0000000000..14693b6fd4 --- /dev/null +++ b/src/components/EductionPortal/ProjectManager/ProjectManagerNotification.jsx @@ -0,0 +1,120 @@ +import React from "react"; +import styles from "./ProjectManagerNotification.module.css"; + +async function sendNotification({ educatorIds, message }) { + await new Promise((r) => setTimeout(r, 550)); + if (!educatorIds.length || !message.trim()) { + throw new Error("Select at least one educator and enter a message."); + } + return { ok: true }; +} + +export default function ProjectManagerNotification({ educators, onClose, onSent }) { + const [selected, setSelected] = React.useState([]); + const [message, setMessage] = React.useState(""); + const [sending, setSending] = React.useState(false); + const [error, setError] = React.useState(null); + + const allChecked = selected.length === educators.length && educators.length > 0; + const someChecked = selected.length > 0 && selected.length < educators.length; + + function toggleAll() { + if (allChecked) { + setSelected([]); + } else { + setSelected(educators.map((e) => e.id)); + } + } + + function toggleOne(id) { + setSelected((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + ); + } + + async function handleSend() { + setSending(true); + setError(null); + try { + await sendNotification({ educatorIds: selected, message }); + onSent({ educatorIds: selected, message }); + } catch (e) { + setError(e.message || "Failed to send."); + } finally { + setSending(false); + } + } + + return ( +
+
+
+

New Announcement

+ +
+ +
+
+
Recipients
+ + + +
+ {educators.map((e) => ( + + ))} +
+
+ +
+
Message
+