From 8e07f1c60b46a71dcd405dc20c9884bd674d6056 Mon Sep 17 00:00:00 2001 From: shashank-madan Date: Mon, 27 Oct 2025 18:57:59 -0400 Subject: [PATCH 1/4] assign atoms frontend boilerplate code with reducers and actions --- src/actions/educationPortal/atomActions.js | 113 +++++++ .../AssignAtomModal/AssignAtomModal.jsx | 300 +++++++++++++++++ .../AssignAtomModal.module.css | 309 ++++++++++++++++++ .../EductionPortal/AssignAtomModal/index.jsx | 1 + src/constants/educationPortal/atom.js | 19 ++ src/reducers/educationPortal/atomReducer.js | 145 ++++++++ src/reducers/index.js | 6 + src/utils/URL.js | 29 +- 8 files changed, 909 insertions(+), 13 deletions(-) create mode 100644 src/actions/educationPortal/atomActions.js create mode 100644 src/components/EductionPortal/AssignAtomModal/AssignAtomModal.jsx create mode 100644 src/components/EductionPortal/AssignAtomModal/AssignAtomModal.module.css create mode 100644 src/components/EductionPortal/AssignAtomModal/index.jsx create mode 100644 src/constants/educationPortal/atom.js create mode 100644 src/reducers/educationPortal/atomReducer.js diff --git a/src/actions/educationPortal/atomActions.js b/src/actions/educationPortal/atomActions.js new file mode 100644 index 0000000000..bae0c036e9 --- /dev/null +++ b/src/actions/educationPortal/atomActions.js @@ -0,0 +1,113 @@ +import axios from 'axios'; +import { toast } from 'react-toastify'; +import { ENDPOINTS } from '~/utils/URL'; +import { + FETCH_AVAILABLE_ATOMS_START, + FETCH_AVAILABLE_ATOMS_SUCCESS, + FETCH_AVAILABLE_ATOMS_ERROR, + ASSIGN_ATOMS_START, + ASSIGN_ATOMS_SUCCESS, + ASSIGN_ATOMS_ERROR, + SELECT_ATOM, + DESELECT_ATOM, + CLEAR_SELECTIONS, + SET_NOTE, + CLEAR_FORM, + SHOW_MODAL, + HIDE_MODAL, +} from '~/constants/educationPortal/atom'; + +// Fetch available atoms +export const fetchAvailableAtoms = () => { + return async dispatch => { + dispatch({ type: FETCH_AVAILABLE_ATOMS_START }); + try { + const response = await axios.get(`${ENDPOINTS.APIEndpoint()}/atoms`); + dispatch({ + type: FETCH_AVAILABLE_ATOMS_SUCCESS, + payload: response.data, + }); + } catch (error) { + dispatch({ + type: FETCH_AVAILABLE_ATOMS_ERROR, + payload: error.message, + }); + toast.error('Failed to load atoms. Please try again.'); + } + }; +}; + +// Assign atoms to a student +export const assignAtoms = (studentId, atoms, note) => { + return async dispatch => { + dispatch({ type: ASSIGN_ATOMS_START }); + try { + const payload = { + studentId, + atoms: atoms || [], + note: note || '', + }; + + const response = await axios.post(ENDPOINTS.EDUCATOR_ASSIGN_ATOMS(), payload); + + dispatch({ + type: ASSIGN_ATOMS_SUCCESS, + payload: response.data, + }); + + toast.success('Atoms have been assigned', { + position: 'top-right', + autoClose: 3000, + }); + + return response.data; + } catch (error) { + dispatch({ + type: ASSIGN_ATOMS_ERROR, + payload: error.message, + }); + + toast.error('Failed to assign atoms. Please try again.', { + position: 'top-right', + autoClose: 5000, + }); + + throw error; + } + }; +}; + +// Selection actions +export const selectAtom = atomId => ({ + type: SELECT_ATOM, + payload: atomId, +}); + +export const deselectAtom = atomId => ({ + type: DESELECT_ATOM, + payload: atomId, +}); + +export const clearSelections = () => ({ + type: CLEAR_SELECTIONS, +}); + +// Form actions +export const setNote = note => ({ + type: SET_NOTE, + payload: note, +}); + +export const clearForm = () => ({ + type: CLEAR_FORM, +}); + +// Modal actions +export const showModal = (studentId, studentName) => ({ + type: SHOW_MODAL, + payload: { studentId, studentName }, +}); + +export const hideModal = () => ({ + type: HIDE_MODAL, +}); diff --git a/src/components/EductionPortal/AssignAtomModal/AssignAtomModal.jsx b/src/components/EductionPortal/AssignAtomModal/AssignAtomModal.jsx new file mode 100644 index 0000000000..1fda5f196a --- /dev/null +++ b/src/components/EductionPortal/AssignAtomModal/AssignAtomModal.jsx @@ -0,0 +1,300 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + Modal, + ModalHeader, + ModalBody, + ModalFooter, + Button, + FormGroup, + Label, + Input, + FormFeedback, +} from 'reactstrap'; +import styles from './AssignAtomModal.module.css'; +import { + fetchAvailableAtoms, + assignAtoms, + selectAtom, + deselectAtom, + setNote, + hideModal, + clearForm, +} from '~/actions/educationPortal/atomActions'; + +const AssignAtomModal = ({ + // Redux state + isModalOpen, + studentId, + studentName, + availableAtoms, + selectedAtoms, + note, + isLoadingAtoms, + isSubmitting, + submitError, + darkMode, + + // Redux actions + fetchAvailableAtoms, + assignAtoms, + selectAtom, + deselectAtom, + setNote, + hideModal, + clearForm, +}) => { + const [localNote, setLocalNote] = useState(''); + const [validationError, setValidationError] = useState(''); + + // Sync local note with Redux state + useEffect(() => { + setLocalNote(note); + }, [note]); + + // Load atoms when modal opens + useEffect(() => { + if (isModalOpen && availableAtoms.length === 0) { + fetchAvailableAtoms(); + } + }, [isModalOpen, availableAtoms.length, fetchAvailableAtoms]); + + const handleAtomToggle = useCallback( + atomId => { + if (selectedAtoms.includes(atomId)) { + deselectAtom(atomId); + } else { + selectAtom(atomId); + } + }, + [selectedAtoms, selectAtom, deselectAtom], + ); + + const handleNoteChange = useCallback( + e => { + const value = e.target.value; + if (value.length <= 500) { + setLocalNote(value); + setNote(value); + } + }, + [setNote], + ); + + const handleSubmit = useCallback(async () => { + // Validation + if (selectedAtoms.length === 0) { + setValidationError('Please select at least one atom'); + return; + } + + if (!studentId) { + setValidationError('Student ID is required'); + return; + } + + setValidationError(''); + + try { + await assignAtoms(studentId, selectedAtoms, localNote); + hideModal(); + } catch (error) { + // Error is handled by the action + // console.error('Assignment failed:', error); + } + }, [selectedAtoms, localNote, studentId, assignAtoms, hideModal]); + + const handleCancel = useCallback(() => { + if (selectedAtoms.length > 0 || localNote.trim()) { + // eslint-disable-next-line no-alert + if (window.confirm('You have unsaved changes. Are you sure you want to cancel?')) { + clearForm(); + hideModal(); + } + } else { + hideModal(); + } + }, [selectedAtoms.length, localNote, clearForm, hideModal]); + + const handleClose = useCallback(() => { + handleCancel(); + }, [handleCancel]); + + const getCharacterCountClass = () => { + const count = localNote.length; + if (count > 450) return styles.error; + if (count > 400) return styles.warning; + return ''; + }; + + const isSubmitDisabled = isSubmitting || selectedAtoms.length === 0; + + return ( + + +

Assign Atoms

+
+ + + {/* Student Information */} +
+
Student:
+
{studentName || 'Unknown Student'}
+
+ + {/* Associated Atoms */} + + +
+ { + const values = Array.from(e.target.selectedOptions, option => option.value); + // Clear current selections and set new ones + selectedAtoms.forEach(atomId => deselectAtom(atomId)); + values.forEach(atomId => selectAtom(atomId)); + }} + disabled={isLoadingAtoms} + > + {isLoadingAtoms ? ( + + ) : ( + availableAtoms.map(atom => ( + + )) + )} + +
+ + {/* Selected Atoms Display */} + {selectedAtoms.length > 0 && ( +
+ {selectedAtoms.map(atomId => { + const atom = availableAtoms.find(a => a._id === atomId); + return ( +
+ {atom?.name || atom?.title || atom?.atomName || 'Unknown Atom'} + +
+ ); + })} +
+ )} +
+ + {/* Note Field */} + + +