diff --git a/src/actions/intermediateTasks.js b/src/actions/intermediateTasks.js index 7c80bfbfad..9d9adccde2 100644 --- a/src/actions/intermediateTasks.js +++ b/src/actions/intermediateTasks.js @@ -1,43 +1,120 @@ import { toast } from 'react-toastify'; import httpService from '../services/httpService'; import { ENDPOINTS } from '~/utils/URL'; +import { updateStudentTask } from './studentTasks'; -// Re-export all actions from studentTasks -export { - setStudentTasksStart, - setStudentTasks, - setStudentTasksError, - updateStudentTask, - fetchStudentTasks, - markStudentTaskAsDone, - // Aliases - fetchStudentTasks as fetchIntermediateTasks, - markStudentTaskAsDone as markIntermediateTaskAsDone, - updateStudentTask as updateIntermediateTask, -} from './studentTasks'; +/** + * Action types for intermediate tasks + */ +export const FETCH_INTERMEDIATE_TASKS_START = 'FETCH_INTERMEDIATE_TASKS_START'; +export const FETCH_INTERMEDIATE_TASKS_SUCCESS = 'FETCH_INTERMEDIATE_TASKS_SUCCESS'; +export const FETCH_INTERMEDIATE_TASKS_ERROR = 'FETCH_INTERMEDIATE_TASKS_ERROR'; +export const CREATE_INTERMEDIATE_TASK_SUCCESS = 'CREATE_INTERMEDIATE_TASK_SUCCESS'; +export const UPDATE_INTERMEDIATE_TASK_SUCCESS = 'UPDATE_INTERMEDIATE_TASK_SUCCESS'; +export const DELETE_INTERMEDIATE_TASK_SUCCESS = 'DELETE_INTERMEDIATE_TASK_SUCCESS'; +export const MARK_INTERMEDIATE_TASK_DONE = 'MARK_INTERMEDIATE_TASK_DONE'; + +/** + * Fetch intermediate tasks for a parent task + */ +export const fetchIntermediateTasks = taskId => { + return async dispatch => { + try { + const response = await httpService.get(ENDPOINTS.INTERMEDIATE_TASKS_BY_PARENT(taskId)); + return response.data; + } catch (error) { + toast.error('Failed to fetch sub-tasks'); + throw error; + } + }; +}; + +/** + * Calculate total expected hours from intermediate tasks + */ +const calculateTotalExpectedHours = intermediateTasks => { + return intermediateTasks.reduce((total, task) => { + return total + (task.expected_hours || 0); + }, 0); +}; + +/** + * Update parent task's expected hours based on intermediate tasks + */ +const updateParentTaskExpectedHours = async (dispatch, getState, parentTaskId) => { + try { + const intermediateTasks = await dispatch(fetchIntermediateTasks(parentTaskId)); + const totalExpectedHours = calculateTotalExpectedHours(intermediateTasks); + const state = getState(); + const parentTask = state.studentTasks.taskItems.find(t => t.id === parentTaskId); + + if (parentTask) { + dispatch( + updateStudentTask(parentTaskId, { + ...parentTask, + suggested_total_hours: totalExpectedHours, + }), + ); + } + } catch (error) { + toast.error('Failed to update parent task hours'); + } +}; /** * Create a new intermediate task * @param {Object} taskData - The task data to create */ -export const createIntermediateTask = (taskData) => { +export const createIntermediateTask = taskData => { return async (dispatch, getState) => { try { - const state = getState(); - const userId = state.auth.user.userid; - - const response = await httpService.post( - `${ENDPOINTS.APIEndpoint()}/education-tasks/intermediate`, - { ...taskData, createdBy: userId } - ); + const response = await httpService.post(ENDPOINTS.INTERMEDIATE_TASKS(), taskData); if (response.data.success) { toast.success('Task created successfully!'); + + if (taskData.parentTaskId) { + await updateParentTaskExpectedHours(dispatch, getState, taskData.parentTaskId); + } + return response.data.task; } + + return response.data; } catch (error) { - const errorMessage = error.response?.data?.error || 'Failed to create task'; - toast.error(errorMessage); + const errorMessage = + error.response?.data?.error || error.message || 'Failed to create sub-task'; + toast.error(`Error: ${errorMessage}`); + throw error; + } + }; +}; + +/** + * Update an intermediate task + * @param {string} id - The task ID + * @param {Object} taskData - The updated task data + */ +export const updateIntermediateTask = (id, taskData) => { + return async (dispatch, getState) => { + try { + const response = await httpService.put(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id), taskData); + + if (response.data.success) { + toast.success('Task updated successfully!'); + + if (taskData.parentTaskId) { + await updateParentTaskExpectedHours(dispatch, getState, taskData.parentTaskId); + } + + return response.data.task; + } + + return response.data; + } catch (error) { + const errorMessage = + error.response?.data?.error || error.message || 'Failed to update sub-task'; + toast.error(`Error: ${errorMessage}`); throw error; } }; @@ -47,20 +124,45 @@ export const createIntermediateTask = (taskData) => { * Delete an intermediate task * @param {string} taskId - The task ID to delete */ -export const deleteIntermediateTask = (taskId) => { +export const deleteIntermediateTask = taskId => { return async (dispatch, getState) => { try { - const response = await httpService.delete( - `${ENDPOINTS.APIEndpoint()}/education-tasks/intermediate/${taskId}` - ); + const response = await httpService.delete(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(taskId)); if (response.data.success) { toast.success('Task deleted successfully!'); return true; } + + return true; + } catch (error) { + const errorMessage = + error.response?.data?.error || error.message || 'Failed to delete sub-task'; + toast.error(`Error: ${errorMessage}`); + throw error; + } + }; +}; + +/** + * Mark an intermediate task as done (for students) + */ +export const markIntermediateTaskAsDone = (id, parentTaskId) => { + return async dispatch => { + try { + const currentTask = await httpService.get(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id)); + + const response = await httpService.put(ENDPOINTS.INTERMEDIATE_TASK_BY_ID(id), { + ...currentTask.data, + status: 'completed', + }); + + toast.success('Sub-task marked as done'); + return response.data; } catch (error) { - const errorMessage = error.response?.data?.error || 'Failed to delete task'; - toast.error(errorMessage); + const errorMessage = + error.response?.data?.error || error.message || 'Failed to mark sub-task as done'; + toast.error(`Error: ${errorMessage}`); throw error; } }; diff --git a/src/actions/studentTasks.js b/src/actions/studentTasks.js index 6d6d587e42..99679ea819 100644 --- a/src/actions/studentTasks.js +++ b/src/actions/studentTasks.js @@ -115,9 +115,13 @@ const flattenGroupedTasks = (groupedTasks) => { * @returns {Promise} Array of flattened tasks */ const fetchTasksFromPrimaryEndpoint = async () => { + const response = await httpService.get(ENDPOINTS.STUDENT_TASKS()); + + // The API returns grouped tasks, we need to flatten them for our UI const groupedTasks = response.data.tasks; const uniqueTasks = flattenGroupedTasks(groupedTasks); + return uniqueTasks; }; @@ -128,6 +132,11 @@ const fetchTasksFromPrimaryEndpoint = async () => { * @returns {Promise} Array of tasks (from fallback or mock data) */ const handleApiError = async (apiError, dispatch) => { + console.error('Error response:', apiError.response?.data); + console.error('Error status:', apiError.response?.status); + console.error('Error config:', apiError.config); + + // Try alternative endpoint if the first one fails if (apiError.response?.status === 404) { try { const altResponse = await httpService.post(`${ENDPOINTS.APIEndpoint()}/student-tasks`); diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index 8f537e5735..d94cc5c6da 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -5,6 +5,7 @@ import { useSelector } from 'react-redux'; import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; import classnames from 'classnames'; import SocialMediaComposer from './SocialMediaComposer'; +import TruthSocialAutoPoster from '../AutoPoster/TruthSocialAutoPoster'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEnvelope, @@ -143,6 +144,10 @@ function Announcements({ title, email: initialEmail }) { + + + + {[ 'x', 'facebook', @@ -164,7 +169,6 @@ function Announcements({ title, email: initialEmail }) { 'livejournal', 'slashdot', 'blogger', - 'truthsocial', ].map(platform => ( diff --git a/src/components/AutoPoster/TruthSocialAutoPoster.jsx b/src/components/AutoPoster/TruthSocialAutoPoster.jsx new file mode 100644 index 0000000000..826315dd57 --- /dev/null +++ b/src/components/AutoPoster/TruthSocialAutoPoster.jsx @@ -0,0 +1,937 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { toast } from 'react-toastify'; +import axios from 'axios'; +import styles from './TruthSocialAutoPoster.module.css'; + +// API Base for backend +const API_BASE = process.env.REACT_APP_APIENDPOINT || 'http://localhost:4500/api'; + +// LocalStorage key for token +const TOKEN_KEY = 'truthSocialAccessToken'; + +// Custom Confirmation Modal Component +const ConfirmationModal = ({ + isOpen, + title, + message, + onConfirm, + onCancel, + confirmText, + cancelText, + confirmStyle, +}) => { + if (!isOpen) return null; + + const getConfirmButtonClass = () => { + if (confirmStyle === 'danger') return styles.confirmBtnDanger; + if (confirmStyle === 'success') return styles.confirmBtnSuccess; + return styles.confirmBtnPrimary; + }; + + return ( + +
+

{title}

+

{message}

+
+ + +
+
+
+ ); +}; + +ConfirmationModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + confirmText: PropTypes.string, + cancelText: PropTypes.string, + confirmStyle: PropTypes.string, +}; + +ConfirmationModal.defaultProps = { + confirmText: 'Confirm', + cancelText: 'Cancel', + confirmStyle: 'primary', +}; + +// Edit Scheduled Post Modal +const EditScheduledModal = ({ isOpen, post, onSave, onCancel, darkMode }) => { + const [subject, setSubject] = useState(''); + const [content, setContent] = useState(''); + const [visibility, setVisibility] = useState('public'); + const [tags, setTags] = useState(''); + + useEffect(() => { + if (post) { + setSubject(post.subject || ''); + setContent(post.content || ''); + setVisibility(post.visibility || 'public'); + setTags(post.tags || ''); + } + }, [post]); + + if (!isOpen || !post) return null; + + const handleSave = () => { + onSave({ + ...post, + subject, + content, + visibility, + tags, + }); + }; + + const darkModeClass = darkMode ? styles.darkMode : ''; + + return ( + +
+

Edit Scheduled Post

+ +
+ + setSubject(e.target.value)} + className={styles.editInput} + maxLength={255} + /> +
+ +
+ +