diff --git a/src/actions/studentActions.js b/src/actions/studentActions.js
new file mode 100644
index 0000000000..5c1d2e9d85
--- /dev/null
+++ b/src/actions/studentActions.js
@@ -0,0 +1,74 @@
+import axios from 'axios';
+import { ENDPOINTS } from '~/utils/URL';
+import {
+ STUDENT_PROFILE_REQUEST,
+ STUDENT_PROFILE_SUCCESS,
+ STUDENT_PROFILE_FAIL,
+ STUDENT_SUBJECT_TASKS_REQUEST,
+ STUDENT_SUBJECT_TASKS_SUCCESS,
+ STUDENT_SUBJECT_TASKS_FAIL,
+} from '../constants/studentProfileConstants';
+
+// --- THIS IS THE KEY PART ---
+// This helper function gets the token from localStorage
+// and builds the authorization header.
+const getTokenConfig = () => {
+ // 1. Get the token from where you stored it (usually localStorage)
+ const token = localStorage.getItem('token'); // <-- Change 'token' if you named it something else
+
+ // 2. Create the config object
+ const config = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ // 3. If the token exists, add it to the headers
+ if (token) {
+ config.headers['Authorization'] = token;
+ }
+
+ return config;
+};
+
+// --- END KEY PART ---
+
+// Fetch Student Profile
+export const fetchStudentProfile = () => async dispatch => {
+ try {
+ dispatch({ type: STUDENT_PROFILE_REQUEST });
+
+ // 1. Get the config with the token
+ const config = getTokenConfig();
+
+ // 2. Pass the config to your axios request
+ const { data } = await axios.get(ENDPOINTS.STUDENT_PROFILE, config);
+
+ dispatch({ type: STUDENT_PROFILE_SUCCESS, payload: data });
+ } catch (error) {
+ dispatch({
+ type: STUDENT_PROFILE_FAIL,
+ payload: error.response?.data?.message || error.message,
+ });
+ }
+};
+
+// Fetch Subject Tasks
+export const fetchSubjectTasks = subjectId => async dispatch => {
+ try {
+ dispatch({ type: STUDENT_SUBJECT_TASKS_REQUEST });
+
+ // 1. Get the config with the token
+ const config = getTokenConfig();
+
+ // 2. Pass the config to your axios request
+ const { data } = await axios.get(ENDPOINTS.STUDENT_SUBJECT_TASKS(subjectId), config);
+
+ dispatch({ type: STUDENT_SUBJECT_TASKS_SUCCESS, payload: data });
+ } catch (error) {
+ dispatch({
+ type: STUDENT_SUBJECT_TASKS_FAIL,
+ payload: error.response?.data?.message || error.message,
+ });
+ }
+};
diff --git a/src/components/EductionPortal/StudentProfile/StudentProfilePage.jsx b/src/components/EductionPortal/StudentProfile/StudentProfilePage.jsx
new file mode 100644
index 0000000000..9785329479
--- /dev/null
+++ b/src/components/EductionPortal/StudentProfile/StudentProfilePage.jsx
@@ -0,0 +1,162 @@
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchStudentProfile } from '~/actions/studentActions';
+import styles from './StudentProfilePage.module.css';
+import { formatByTimeZone } from '../../../utils/formatDate';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faUser,
+ faLocationDot,
+ faClock,
+ faGraduationCap,
+ faCalendarAlt,
+ faHandHoldingHand,
+} from '@fortawesome/free-solid-svg-icons';
+import { useHistory } from 'react-router-dom';
+
+const StudentProfile = () => {
+ const dispatch = useDispatch();
+ const { profile, subjectProgress, error } = useSelector(state => state.student);
+ const [activeTab, setActiveTab] = useState('Educational Progress');
+ const history = useHistory();
+
+ useEffect(() => {
+ dispatch(fetchStudentProfile());
+ }, [dispatch]);
+
+ if (error) return
Error: {error}
;
+ if (!profile) return Loading student profile...
;
+
+ const formattedDateJoined = formatByTimeZone(profile.dateJoined, profile.timezone);
+
+ console.log('Student Details:', profile);
+ console.log('Subject Progress:', subjectProgress);
+ console.log('Subject Progress:', subjectProgress.completionPercentage);
+
+ return (
+
+ {/* Header Section */}
+
+
+

+
+
{profile.fullName}
+
Student ID: {profile.studentId}
+
+
+
+
+
+
+ {/* Info Section */}
+
+
+
+
+ Teacher
+
+ {profile.teacherName}
+
+
+
+
+ Support Member
+
+ {profile.supportMemberName}
+
+
+
+
+ Location
+
+ {profile.location}
+
+
+
+
+ Grade Level
+
+ {profile.gradeLevel.charAt(0).toUpperCase() + profile.gradeLevel.slice(1)}
+
+
+
+
+ Joined
+
+ {formattedDateJoined}
+
+
+
+
+ Timezone
+
+ {profile.timezone}
+
+
+
+ {/* Tabs */}
+
+ {['Educational Progress', 'Completed Lessons', 'Current Tasks', 'Student Interests'].map(
+ tab => (
+
+ ),
+ )}
+
+
+ {/* Educational Progress Section */}
+ {activeTab === 'Educational Progress' && (
+
+
Educational Progress Overview
+
+ Visual representation of learning progress across subjects
+
+
+
+ {subjectProgress?.map(subject => (
+
+
+ {/* The colored circle with text inside */}
+
+ {subject.name}
+
+
+ {/* Subject name text below the circle */}
+
{subject.name}
+
+
✅ {subject.completed} completed
+
🟣 {subject.inProgress} in progress
+
⚪ {subject.remaining} remaining
+
+
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default StudentProfile;
diff --git a/src/components/EductionPortal/StudentProfile/StudentProfilePage.module.css b/src/components/EductionPortal/StudentProfile/StudentProfilePage.module.css
new file mode 100644
index 0000000000..ad9408a58e
--- /dev/null
+++ b/src/components/EductionPortal/StudentProfile/StudentProfilePage.module.css
@@ -0,0 +1,220 @@
+.container {
+ padding: 2rem;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
+ font-family: 'Inter', sans-serif;
+}
+
+/* Header */
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #e2e8f0;
+ padding-bottom: 1rem;
+}
+
+.profileSection {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.avatar {
+ width: 70px;
+ height: 70px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.name {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #1f2937;
+}
+
+.studentId {
+ font-size: 0.9rem;
+ color: #6b7280;
+}
+
+.portfolioBtn {
+ background: #f3f4f6;
+ border: 1px solid #d1d5db;
+ padding: 0.6rem 1.2rem;
+ border-radius: 8px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.portfolioBtn:hover {
+ background: #e5e7eb;
+}
+
+/* Info Row */
+.infoRow {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 1rem;
+ margin-top: 1rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.infoItem {
+ display: flex;
+ flex-direction: column;
+ color: #374151;
+}
+
+.label {
+ font-size: 0.8rem;
+ color: #6b7280;
+ margin-bottom: 0.2rem;
+}
+
+/* Tabs */
+.tabs {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 1.2rem;
+ background-color: #f9fafb;
+ border-radius: 8px;
+ padding: 0.4rem;
+}
+
+.tabButton {
+ flex: 1;
+ padding: 0.6rem 1rem;
+ border: none;
+ background: none;
+ cursor: pointer;
+ font-weight: 500;
+ color: #4b5563;
+ transition: all 0.2s ease;
+ border-radius: 6px;
+}
+
+.tabButton:hover {
+ background: #e5e7eb;
+}
+
+.activeTab {
+ background: #fff;
+ color: #1f2937;
+ font-weight: 600;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+/* Educational Progress */
+.progressSection {
+ margin-top: 1.5rem;
+}
+
+.subText {
+ font-size: 0.9rem;
+ color: #6b7280;
+ margin-bottom: 1rem;
+}
+
+.subjectGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 1rem;
+}
+
+.subjectCard {
+ background: #fff; /* Optional: Change background for better contrast */
+ border: 1px solid #e2e8f0; /* Optional: Add a light border */
+ border-radius: 10px;
+ padding: 1rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+ transition: transform 0.2s ease, box-shadow 0.2s ease; /* Added box-shadow transition */
+ text-align: left; /* Ensure details text is left-aligned */
+}
+
+.subjectCard:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); /* Slightly enhance shadow on hover */
+}
+
+.subjectCircleContainer {
+ display: flex; /* Use flexbox */
+ justify-content: center; /* Center horizontally */
+ margin-bottom: 0.8rem; /* Space below the circle */
+}
+
+/* Styles for the colored circle */
+.subjectCircle {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ border: 4px solid #444343;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ffffff;
+ font-weight: 600;
+ font-size: 0.9rem;
+ text-align: center;
+ padding: 5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ word-break: break-word;
+}
+
+.subjectNameBelow {
+ text-align: center;
+ font-weight: 600;
+ font-size: 1.1rem;
+ color: #1f2937;
+ margin-bottom: 0.8rem;
+}
+
+.subjectName {
+ font-weight: 600;
+ color: #1f2937;
+}
+
+.progressDetails {
+ font-size: 0.85rem;
+ color: #4b5563;
+ line-height: 1.4;
+ margin-bottom: 0.8rem;
+}
+.progressDetails p {
+ margin: 0.2rem 0;
+}
+
+.progressBar {
+ background: #e5e7eb;
+ height: 8px;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.progressFill {
+ height: 100%;
+ border-radius: 4px;
+ transition: width 0.3s ease-in-out;
+}
+
+.loading {
+ text-align: center;
+ margin-top: 2rem;
+ color: #6b7280;
+}
+
+.error {
+ color: red;
+ text-align: center;
+ font-weight: 600;
+ margin-top: 2rem;
+}
+/* Add this to StudentProfilePage.module.css */
+.icon {
+ margin-right: 8px;
+ color: #555; /* Or whatever color you prefer */
+}
diff --git a/src/constants/studentProfileConstants.js b/src/constants/studentProfileConstants.js
new file mode 100644
index 0000000000..e7873d55e4
--- /dev/null
+++ b/src/constants/studentProfileConstants.js
@@ -0,0 +1,9 @@
+export const STUDENT_PROFILE_REQUEST = 'STUDENT_PROFILE_REQUEST';
+export const STUDENT_PROFILE_SUCCESS = 'STUDENT_PROFILE_SUCCESS';
+export const STUDENT_PROFILE_FAIL = 'STUDENT_PROFILE_FAIL';
+
+export const STUDENT_SUBJECT_TASKS_REQUEST = 'STUDENT_SUBJECT_TASKS_REQUEST';
+export const STUDENT_SUBJECT_TASKS_SUCCESS = 'STUDENT_SUBJECT_TASKS_SUCCESS';
+export const STUDENT_SUBJECT_TASKS_FAIL = 'STUDENT_SUBJECT_TASKS_FAIL';
+
+export const CLEAR_STUDENT_DATA = 'CLEAR_STUDENT_DATA';
diff --git a/src/reducers/index.js b/src/reducers/index.js
index ca43404639..af27180784 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -102,12 +102,10 @@ import reviewsInsightReducer from './prAnalytics/reviewsInsightReducer';
import { hoursPledgedReducer } from './jobAnalytics/hoursPledgedReducer';
import { studentTasksReducer } from './studentTasksReducer';
-// education portal
+// Education Dashboard Reducers
+import { studentReducer } from './studentProfileReducer';
import { atomReducer } from './educationPortal/atomReducer';
import { weeklySummariesFiltersApi } from '../actions/weeklySummariesFilterAction';
-
-//education portal
-
import browseLessonPlanReducer from './educationPortal/broweLPReducer';
const localReducers = {
@@ -216,6 +214,7 @@ const sessionReducers = {
timelogTracking: timelogTrackingReducer,
teamMemberTasks: teamMemberTasksReducer,
warning: warningsByUserIdReducer,
+ student: studentReducer,
};
export { localReducers, sessionReducers };
diff --git a/src/reducers/studentProfileReducer.js b/src/reducers/studentProfileReducer.js
new file mode 100644
index 0000000000..4d949ed038
--- /dev/null
+++ b/src/reducers/studentProfileReducer.js
@@ -0,0 +1,60 @@
+// src/redux/reducers/studentReducer.js
+import {
+ STUDENT_PROFILE_REQUEST,
+ STUDENT_PROFILE_SUCCESS,
+ STUDENT_PROFILE_FAIL,
+ STUDENT_SUBJECT_TASKS_REQUEST,
+ STUDENT_SUBJECT_TASKS_SUCCESS,
+ STUDENT_SUBJECT_TASKS_FAIL,
+ CLEAR_STUDENT_DATA,
+} from '../constants/studentProfileConstants';
+
+const initialState = {
+ loading: false,
+ profile: null, // This will hold the 'studentDetails' object
+ subjectProgress: [], // This will hold the 'subjects' array
+ subjectTasks: {}, // This will store tasks, organized by subject ID
+ error: null,
+};
+
+export const studentReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case STUDENT_PROFILE_REQUEST:
+ case STUDENT_SUBJECT_TASKS_REQUEST:
+ return { ...state, loading: true };
+
+ case STUDENT_PROFILE_SUCCESS:
+ return {
+ ...state,
+ loading: false,
+ profile: action.payload.studentDetails, // <-- Store only studentDetails here
+ subjectProgress: action.payload.subjects, // <-- Store the subjects array here
+ error: null,
+ };
+
+ case STUDENT_SUBJECT_TASKS_SUCCESS:
+ // Assuming payload is { subjectId: 'math101', tasks: [...] }
+ // This is a guess; you'll need to confirm your task API response
+ const { subjectId, tasks } = action.payload;
+ return {
+ ...state,
+ loading: false,
+ subjectTasks: {
+ ...state.subjectTasks, // Keep old tasks
+ [subjectId]: tasks, // Add new tasks under their subjectId
+ },
+ };
+
+ case STUDENT_PROFILE_FAIL:
+ case STUDENT_SUBJECT_TASKS_FAIL:
+ return { ...state, loading: false, error: action.payload };
+
+ case CLEAR_STUDENT_DATA:
+ return initialState;
+
+ default:
+ return state;
+ }
+};
+
+export default studentReducer;
diff --git a/src/routes.jsx b/src/routes.jsx
index 3ec84437c6..c59545cd32 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -163,6 +163,8 @@ import EPProtectedRoute from './components/common/EPDashboard/EPProtectedRoute';
import EPLogin from './components/EductionPortal/Login';
import BrowseLessonPlan from './components/EductionPortal/BrowseLessonPlan/BrowseLP';
import EPDashboard from './components/EductionPortal';
+
+import StudentProfilePage from './components/EductionPortal/StudentProfile/StudentProfilePage';
import AssignAtoms from './components/EductionPortal/AssignAtoms/AssignAtoms';
import ReportDownloadButton from './components/EductionPortal/AnalyticsDashboard/ReportDownloadButton';
import GroupList from './components/EductionPortal/GroupList/GroupList';
@@ -171,6 +173,7 @@ import InsightWidget from './components/EductionPortal/AnalyticsDashboard/Insigh
import StudentDashboard from './components/EductionPortal/StudentTasks/StudentDashboard';
import StudentTasks from './components/EductionPortal/StudentTasks/StudentTasks';
import TaskDetails from './components/EductionPortal/StudentTasks/TaskDetails';
+
import PRReviewTeamAnalytics from './components/HGNPRDashboard/PRReviewTeamAnalytics';
import PRDashboardOverview from './components/HGNPRDashboard/PRDashboardOverview';
import PRDashboardPromotionEligibility from './components/HGNPRDashboard/PRDashboardPromotionEligibility';
@@ -806,6 +809,11 @@ export default (
{/* Good Education Portal Routes */}
+
+
+
+ {/* PR Analytics Dashboard */}
+
`${APIEndpoint}/student/profile/subject/${subjectId}`,
EDUCATOR_ASSIGN_ATOMS: () => `${APIEndpoint}/educator/assign-atoms`,
LESSON_PLANS: `${APIEndpoint}/education/lesson-plans`,
diff --git a/src/utils/formatDate.js b/src/utils/formatDate.js
index d4eb3215d8..1d65ca6ba9 100644
--- a/src/utils/formatDate.js
+++ b/src/utils/formatDate.js
@@ -19,15 +19,19 @@ export const formatDate = date =>
.tz(COMPANY_TZ)
.startOf('day')
.format('MMM-DD-YY');
-export const formatDateLocal = (val) => {
+export const formatDateLocal = val => {
if (!val) return '';
// Strict ISO parse to avoid fallback warnings
- return moment(val, moment.ISO_8601, true).local().format('MMM DD, YYYY');
+ return moment(val, moment.ISO_8601, true)
+ .local()
+ .format('MMM DD, YYYY');
};
-export const formatDateUtcYYYYMMDD = (val) => {
+export const formatDateUtcYYYYMMDD = val => {
if (!val) return '';
// Always return YYYY-MM-DD for
- return moment(val, moment.ISO_8601, true).utc().format('YYYY-MM-DD');
+ return moment(val, moment.ISO_8601, true)
+ .utc()
+ .format('YYYY-MM-DD');
};
export const formatDateCompany = (val) => {
@@ -76,3 +80,21 @@ export const getDayOfWeekStringFromUTC = utcTs =>
.day();
export const CREATED_DATE_CRITERIA = '2022-01-01';
+
+/**
+ * Converts a UTC timestamp to a formatted string in the user's
+ * specific profile timezone.
+ *
+ * @param {string} utcDate - The UTC date string (e.g., "2025-08-28T03:04:50.054Z")
+ * @param {string} timeZone - The user's timezone from their profile (e.g., "America/Los_Angeles")
+ * @returns {string} Formatted date and time (e.g., "Aug 27, 2025, 8:04 PM")
+ */
+export const formatByTimeZone = (utcDate, timeZone) => {
+ if (!utcDate || !timeZone) {
+ return 'Invalid Date';
+ }
+
+ return moment(utcDate)
+ .tz(timeZone)
+ .format('MMM DD, YYYY, h:mm A');
+};
diff --git a/yarn.lock b/yarn.lock
index 6ef249c189..9c8e159d8c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3609,7 +3609,7 @@
resolved "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz"
integrity sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==
dependencies:
- csstype "^3.0.2"
+ csstype "^3.2.2"
"@types/sizzle@^2.3.3":
version "2.3.10"