diff --git a/public/locales/en/alert-message.json b/public/locales/en/alert-message.json index 3b2ef9f5d..5caceb88b 100644 --- a/public/locales/en/alert-message.json +++ b/public/locales/en/alert-message.json @@ -78,5 +78,6 @@ "event-access-error": "You dont have access to apply for this event", "error-event-already-started": "You cannot reserve a spot for this event because it has already started.", "success-event-reservation": "You have successfully reserved a spot for this event.", - "error-creating-code-review": "Something went wrong creating the code review" + "error-creating-code-review": "Something went wrong creating the code review", + "error-ai-chat": "Something went wrong opening the AI Chat" } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 350c61dd5..55e34aadb 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -135,6 +135,7 @@ "mark-as-not-done": "Mark as not done" }, "learnpack": { + "title": "This is a Learnpack interactive exercise", "description": "\"{{projectName}}\" uses LearnPack for an interactive and auto-graded experience; choose one of the following options to start working on the project:", "description2": "This practice will reroute your browser to LearnPack - an interactive learning tool that runs integrated with VSCode.", "new-exercise": "Start from the beginning", @@ -147,6 +148,7 @@ "description": "We take care of setup and installations for all the required technologies to run this exercise or project.", "type": "dropdown" }, + "clone-title": "How to clone a project?", "cloneInstructions": "This exercise can be downloaded and run locally if you have node.js installed (installation steps).\n\n Once you have node.js, it's time to install learnpack and clone this project into your computer by typing the following command on your terminal:\n\n``` bash\n$ npm i @learnpack/learnpack -g\n$ git clone {{urlToClone}}\n```\nNote: This will create a new folder \"{{repoName}}\" in your computer.\n\nIf you want to use VSCode: Make sure you have the LearnPack extension installed, open the folder in VSCode and type `learnpack start` on your vscode terminal.\n\nTo run without VSCode: Use your computer terminal to get inside your recently created folder and start learnpack:\n\n```bash\n$ cd {{repoName}}\n$ learnpack start\n```\nRead the README.md file and follow the rest of the instructions." }, "upgrade-plan": { diff --git a/public/locales/en/dashboard.json b/public/locales/en/dashboard.json index 2eba43f76..8b8a5453d 100644 --- a/public/locales/en/dashboard.json +++ b/public/locales/en/dashboard.json @@ -3,6 +3,7 @@ "title": "Dashboard" }, "title": "Your News", + "back-to-dashboard": "Back to dashboard", "backToChooseProgram": "Back to choose program", "moduleMap": "Module map", "progressText": "progress in the program", diff --git a/public/locales/en/syllabus.json b/public/locales/en/syllabus.json index 26908a980..b1663ccd1 100644 --- a/public/locales/en/syllabus.json +++ b/public/locales/en/syllabus.json @@ -3,13 +3,31 @@ "module-not-started": "You haven't started this module yet", "no-modules-to-show": "No modules to show", "edit-page": "Edit in GitHub", + "watch-intro": "Watch intro", + "get-help": "Get help from Rigobot", + "contribute": "Contribute to this lesson", + "show-menu": "Show menu", + "hide-menu": "Hide menu", "open-google-collab": "Open in Colab", - "next-page": "Next page", + "teachers-feedback": "Teacher’s feedback", + "no-feedback": "You don't have any feedback yet", + "task-notification": "You will receive an email when your teacher reviews the task", + "code-reviews": "Code reviews", + "no-code-reviews": "You don't have any code review yet", + "rate-comment": "Rate this comment", + "start-review": "Start you review here...", + "like": "You sent a positive rating to this comment", + "dislike": "You sent a negative rating to this comment", + "you": "You", + "back": "Back", + "next-page": "Next", + "back-to-top": "Back to the top", + "start-next": "Start next module:", "no-traduction-found": "No translation found", "no-traduction-found-description": ">We are sorry, a translation for this content was not found. We are constantly working to offer our modules in various languages. We appreciate your patience and if you wish to contribute with translations, please visit our repository on GitHub. In the meantime, you can try accessing the content in another available language or check back later to see if the translation has been added.", "no-content-found": "No content found", "no-content-found-description": ">Unfortunately, the content you are looking for is not available at the specified path. We are constantly working on improvements and appreciate your understanding. If you need assistance or have additional questions, please do not hesitate to contact us. We are committed to providing you with the best service. Thank you for your patience and understanding.", - "previous-page": "Previous page", + "previous-page": "Previous", "ask-to-done": "Would you like to mark this \"{{taskType}}\" as done before moving on?", "mark-later": "Mark as done later", "blank-page": "This content cannot be visualized inside of 4Geeks.com, please Click here to open it on a new window", @@ -31,5 +49,11 @@ "superseded-message": "This lesson belongs to the legacy archive, we recommend reading a more updated version:", "solution-message": "This project includes a model solution that you can review if you need additional guidance.", "open-solution": "Click here to review the model solution", + "completion-percentage": "Completion percentage", + "total-steps": "Total steps completed", + "total-time": "Total time worked on this package", + "successful-compiles": "Successfull compiles", + "successful-tests": "Successfull tests", + "total-errors": "Total errors", "click-to-review": "Click here to review the solution." } diff --git a/public/locales/es/alert-message.json b/public/locales/es/alert-message.json index 6896847b1..3a00b24e3 100644 --- a/public/locales/es/alert-message.json +++ b/public/locales/es/alert-message.json @@ -77,5 +77,6 @@ "event-access-error": "No tienes acceso para aplicar en este evento", "error-event-already-started": "No puedes reservar cupo para este evento porque ya ha comenzado", "success-event-reservation": "¡Has reservado con éxito un cupo para este evento.!", - "error-creating-code-review": "Algo salió mal al crear la revisión de código" + "error-creating-code-review": "Algo salió mal al crear la revisión de código", + "error-ai-chat": "Algo salio mal abriendo el chat de IA" } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 16947e566..7c9df9e63 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -135,6 +135,7 @@ "mark-as-not-done": "Marcar como no hecho" }, "learnpack": { + "title": "Este es un ejercicio interactivo de Learnpack", "description": "\"{{projectName}}\" utiliza LearnPack para una experiencia interactiva y autoevaluada; elige una de las siguientes opciones para comenzar a trabajar en el proyecto:", "new-exercise": "Comenzar desde el principio", "continue-exercise": "Continuar desde una sesión anterior", @@ -146,6 +147,7 @@ "description": "Nosotros nos encargamos de la configuración e instalación de todas las tecnologías requeridas para realizar este ejercicio.", "type": "dropdown" }, + "clone-title": "¿Cómo clonar un proyecto?", "cloneInstructions": "Este ejercicio se puede descargar y ejecutar localmente si tienes node.js instalado (pasos de instalación).\n\n Si ya tienes node, toca instalar learnpack y clonar el proyecto en tu computadora escribiendo el siguiente comando en tu terminal:\n\n```bash\n$ npm i @learnpack/learnpack -g\n$ git clone {{urlToClone}}\n```\nNota: Esto creará una nueva carpeta \"{{repoName}}\" en tu computadora con el código del proyecto dentro.\n\nSi quieres usar VSCode: asegúrate de tener el LearnPack extension instalado, abre la carpeta en VSCode y escribe `learnpack start` en tu terminal de vscode.\n\nPara realizar los ejercicios sin VSCode: abre tu terminal en la carpeta recién creada y comienza el programa learnpack:\n\n```bash\n$ cd {{repoName}}\n$ learnpack start\n```\nLee el archivo README.md y sigue el resto de las instrucciones." }, "upgrade-plan": { diff --git a/public/locales/es/dashboard.json b/public/locales/es/dashboard.json index 0a4b24808..d113d546e 100644 --- a/public/locales/es/dashboard.json +++ b/public/locales/es/dashboard.json @@ -4,6 +4,7 @@ }, "title": "Tus noticias", "moduleMap": "Mapa de módulos", + "back-to-dashboard": "Volver a dashboard", "backToChooseProgram": "Volver a elegir programa", "progressText": "Progreso en el programa", "whiteLabeledText": "Este curso es traído a ti gracias a nuestra alianza con esta universidad.", diff --git a/public/locales/es/exercises.json b/public/locales/es/exercises.json index 8a87c1fb0..e85d2c5e4 100644 --- a/public/locales/es/exercises.json +++ b/public/locales/es/exercises.json @@ -50,7 +50,7 @@ "or": " o " }, "clone-modal" : { - "title": "¿Como clonar un proyecto?", + "title": "¿Cómo clonar un proyecto?", "text-part-one": "No lo recomendamos, pero algunos ejercicios se pueden descargar y ejecutar localmente usando VSCode y el ", "text-part-two": ". Si quieres hacerlo, debes comenzar por clonar el proyecto en tu computadora escribiendo el siguiente comando en tu terminal:", "note": "Nota: Esto creará una nueva carpeta “{{folder}}” en su computadora con el código del proyecto dentro.", diff --git a/public/locales/es/syllabus.json b/public/locales/es/syllabus.json index 16bb47cf6..9ffc8d182 100644 --- a/public/locales/es/syllabus.json +++ b/public/locales/es/syllabus.json @@ -3,13 +3,31 @@ "module-not-started": "Aún no has iniciado este módulo", "edit-page": "Editar en Github", "no-modules-to-show": "No hay módulos para mostrar", + "watch-intro": "Ver introducción", + "get-help": "Pide ayuda a Rigobot", + "contribute": "Contribuye a esta lección", + "show-menu": "Abrir menú", + "hide-menu": "Cerrar menú", "open-google-collab": "Abrir en Colab", - "next-page": "Siguiente página", + "teachers-feedback": "Comentario del profesor", + "no-feedback": "Aún no tienes ningún comentario", + "notification": "Recibirás un email cuando tu profesor revise la asignación", + "code-reviews": "Revisiones de código", + "no-code-reviews": "No tienes revisiones aún", + "rate-comment": "Califica este comentario", + "start-review": "Deja tu comentario aquí...", + "like": "Enviaste una calificación positiva a este comentario", + "dislike": "Enviaste una calificación negativa a este comentario", + "you": "Tu", + "back": "Volver", + "next-page": "Siguiente", + "back-to-top": "Volver arriba", + "start-next": "Comenzar siguiente module:", "no-traduction-found": "No se encontró traducción", "no-traduction-found-description": ">Lo sentimos, no se encontró una traducción para este contenido. Estamos trabajando constantemente para ofrecer nuestros módulos en varios idiomas. Apreciamos tu paciencia y si deseas contribuir con traducciones, por favor visita nuestro repositorio en Github. Mientras tanto, puedes intentar acceder al contenido en otro idioma disponible o volver más tarde para verificar si la traducción ha sido agregada.", "no-content-found": "No se encontró contenido", "no-content-found-description": ">Lamentablemente, el contenido que buscas no está disponible en la ruta especificada. Estamos trabajando en mejoras continuas y valoramos tu comprensión. Si necesitas asistencia o tienes preguntas adicionales, no dudes en comunicarte con nosotros. Estamos comprometidos a brindarte el mejor servicio. Gracias por tu paciencia y comprensión.", - "previous-page": "Anterior página", + "previous-page": "Anterior", "ask-to-done": "¿Te gustaría marcar este \"{{taskType}}\" como completado antes de continuar?", "mark-later": "Marcar luego", "blank-page": "Este contenido no se puede visualizar dentro de 4Geeks.com, please Haga clic aquí para abrirlo en una nueva ventana", @@ -31,5 +49,11 @@ "superseded-message": "Esta lección pertenece al archivo de legado, recomendamos leer una versión más actualizada:", "solution-message": "Este proyecto tiene una solución modelo que puedes revisar en caso de necesitar orientación adicional.", "open-solution": "Haz clic aquí para revisar el modelo de solución", + "completion-percentage": "Porcentaje de finalización", + "total-steps": "Pasos completados", + "total-time": "Tiempo total", + "successful-compiles": "Compilaciones exitosas", + "successful-tests": "Pasos exitosos", + "total-errors": "Total de errores", "click-to-review": "Pincha aquí para revisar la solución." } diff --git a/src/common/components/AttendanceModal/index.jsx b/src/common/components/AttendanceModal/index.jsx index b12600f86..4c0b28248 100644 --- a/src/common/components/AttendanceModal/index.jsx +++ b/src/common/components/AttendanceModal/index.jsx @@ -17,11 +17,11 @@ import useCohortHandler from '../../hooks/useCohortHandler'; import handlers from '../../handlers'; function AttendanceModal({ - title, message, isOpen, onClose, sortedAssignments, students, + title, message, isOpen, onClose, students, }) { const { t } = useTranslation('dashboard'); const { state, setCohortSession } = useCohortHandler(); - const { cohortSession } = state; + const { cohortSession, sortedAssignments } = state; const [historyLog, setHistoryLog] = useState(); const [day, setDay] = useState(cohortSession.current_day); const [attendanceTaken, setAttendanceTaken] = useState({}); @@ -440,7 +440,6 @@ CheckboxCard.propTypes = { AttendanceModal.propTypes = { title: PropTypes.string, message: PropTypes.string, - sortedAssignments: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)).isRequired, students: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)).isRequired, isOpen: PropTypes.bool, onClose: PropTypes.func, diff --git a/src/common/components/CodeViewer.jsx b/src/common/components/CodeViewer.jsx index 9cb5be3f8..3f607c0f7 100644 --- a/src/common/components/CodeViewer.jsx +++ b/src/common/components/CodeViewer.jsx @@ -125,14 +125,14 @@ function CodeViewer({ languagesData, allowNotLogged, fileContext, ...rest }) { let endpoint; if (path) { - endpoint = 'https://rigobot.herokuapp.com/v1/prompting/completion/code-compiler-with-context/'; + endpoint = `${RIGOBOT_HOST}/v1/prompting/completion/code-compiler-with-context/`; completionJob.inputs = { main_file: `File path: ${path}\nFile content:\n${code}`, language_and_version: language, secondary_files: fileContext, }; } else { - endpoint = 'https://rigobot.herokuapp.com/v1/prompting/completion/code-compiler/'; + endpoint = `${RIGOBOT_HOST}/v1/prompting/completion/code-compiler/`; completionJob.inputs = { code, language_and_version: language, @@ -241,8 +241,9 @@ function CodeViewer({ languagesData, allowNotLogged, fileContext, ...rest }) { ]); }} defaultLanguage={language} - height="300px" + height="290px" options={{ + scrollBeyondLastLine: false, borderRadius: '4px', scrollbar: { alwaysConsumeMouseWheel: false, diff --git a/src/common/components/CohortSideBar.jsx b/src/common/components/CohortSideBar.jsx index 736292741..22d7afe56 100644 --- a/src/common/components/CohortSideBar.jsx +++ b/src/common/components/CohortSideBar.jsx @@ -20,6 +20,7 @@ import AvatarUser from '../../js_modules/cohortSidebar/avatarUser'; import { AvatarSkeleton } from './Skeleton'; import useOnline from '../hooks/useOnline'; import useStyle from '../hooks/useStyle'; +import useCohortHandler from '../hooks/useCohortHandler'; import useProgramList from '../store/actions/programListAction'; import { isWindow } from '../../utils'; @@ -215,7 +216,7 @@ function ProfilesSection({ } function CohortSideBar({ - title, teacherVersionActive, cohort, cohortCity, width, containerStyle, + title, teacherVersionActive, width, containerStyle, studentAndTeachers, isDisabled, }) { const { t } = useTranslation('dashboard'); @@ -226,6 +227,9 @@ function CohortSideBar({ const [activeStudentsLoading, setActiveStudentsLoading] = useState(true); const [graduatedStudentsLoading, setGraduatedStudentsLoading] = useState(true); const { addTeacherProgramList } = useProgramList(); + const { state } = useCohortHandler(); + const { cohortSession: cohort } = state; + const cohortCity = cohort?.name; const teacher = studentAndTeachers.filter((st) => st?.role === 'TEACHER'); const activeStudents = studentAndTeachers.filter( (st) => st?.role === 'STUDENT' && ['ACTIVE', 'GRADUATED'].includes(st?.educational_status), @@ -517,8 +521,6 @@ CohortSideBar.propTypes = { teacherVersionActive: PropTypes.bool, containerStyle: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), studentAndTeachers: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))), - cohortCity: PropTypes.string, - cohort: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), isDisabled: PropTypes.bool, // handleStudySession: PropTypes.func, }; @@ -528,8 +530,6 @@ CohortSideBar.defaultProps = { teacherVersionActive: false, containerStyle: {}, studentAndTeachers: [], - cohortCity: 'Miami Downtown', - cohort: {}, isDisabled: false, // handleStudySession: () => {}, }; diff --git a/src/common/components/DynamicContentCard/index.jsx b/src/common/components/DynamicContentCard/index.jsx index 982efd154..c57a7fa50 100644 --- a/src/common/components/DynamicContentCard/index.jsx +++ b/src/common/components/DynamicContentCard/index.jsx @@ -113,7 +113,7 @@ function DynamicContentCard({ data, type, technologies, usersWorkedHere, ...rest padding={isWorkshop ? '10px 16px 0px' : '16px'} gridGap="14px" width={isWorkshop ? { base: '310px', md: '360px' } : 'auto'} - minWidth={{ base: '280px', md: '310px' }} + minWidth="280px" background={isWorkshopStarted ? featuredColor : backgroundColor} color={fontColor} borderRadius="10px" diff --git a/src/common/components/Icon/set/graph-up.jsx b/src/common/components/Icon/set/graph-up.jsx new file mode 100644 index 000000000..bf19a6a70 --- /dev/null +++ b/src/common/components/Icon/set/graph-up.jsx @@ -0,0 +1,23 @@ +const graphUp = ({ + width, height, style, color, +}) => ( + + + + +); + +export default graphUp; diff --git a/src/common/components/Icon/set/layout.jsx b/src/common/components/Icon/set/layout.jsx new file mode 100644 index 000000000..b139db959 --- /dev/null +++ b/src/common/components/Icon/set/layout.jsx @@ -0,0 +1,50 @@ +const layout = ({ + width, height, color, style, +}) => ( + + + + + + + +); + +export default layout; diff --git a/src/common/components/Icon/set/rigobot-avatar-tiny.jsx b/src/common/components/Icon/set/rigobot-avatar-tiny.jsx index fb30c7deb..0df3a2445 100644 --- a/src/common/components/Icon/set/rigobot-avatar-tiny.jsx +++ b/src/common/components/Icon/set/rigobot-avatar-tiny.jsx @@ -1,5 +1,5 @@ -const rigobotAvatarTiny = ({ width, height }) => ( - +const rigobotAvatarTiny = ({ width, height, style }) => ( + diff --git a/src/common/components/Icon/set/send.jsx b/src/common/components/Icon/set/send.jsx new file mode 100644 index 000000000..b24a54fc2 --- /dev/null +++ b/src/common/components/Icon/set/send.jsx @@ -0,0 +1,19 @@ +const send = ({ + width, height, style, color, +}) => ( + + + +); + +export default send; diff --git a/src/common/components/Icon/set/sync-error.jsx b/src/common/components/Icon/set/sync-error.jsx new file mode 100644 index 000000000..38b7da567 --- /dev/null +++ b/src/common/components/Icon/set/sync-error.jsx @@ -0,0 +1,27 @@ +const syncError = ({ + width, height, style, color, +}) => ( + + + + + +); + +export default syncError; diff --git a/src/common/components/Icon/set/sync-success.jsx b/src/common/components/Icon/set/sync-success.jsx new file mode 100644 index 000000000..bc0608abf --- /dev/null +++ b/src/common/components/Icon/set/sync-success.jsx @@ -0,0 +1,27 @@ +const syncSuccess = ({ + width, height, style, color, +}) => ( + + + + + +); + +export default syncSuccess; diff --git a/src/common/components/KPI.jsx b/src/common/components/KPI.jsx index ff04ae295..b0ddda9b5 100644 --- a/src/common/components/KPI.jsx +++ b/src/common/components/KPI.jsx @@ -12,7 +12,7 @@ function KPI({ label, icon, value, unit, max, variation, variationColor, style, changeWithColor, valueUnit, unstyled, chart, - fontSize, iconSize, labelSize, + fontSize, iconSize, labelSize, textProps, ...rest }) { const verifiVariation = () => { if (variation.includes('+')) return 'up'; @@ -52,7 +52,7 @@ function KPI({ const numberColors = getNumberColor(); return ( - + {chart !== null ? ( {label && ( @@ -66,7 +66,7 @@ function KPI({ ) : ( - + {label} )} @@ -76,7 +76,7 @@ function KPI({ )} {chart === null && ( - + {unit} {value} {/* {value.toString().length >= 3 @@ -130,6 +130,7 @@ KPI.propTypes = { fontSize: PropTypes.string, iconSize: PropTypes.string, labelSize: PropTypes.string, + textProps: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), // variationUnit: PropTypes.string.isRequired, }; @@ -148,6 +149,7 @@ KPI.defaultProps = { fontSize: 'l', iconSize: '26px', labelSize: '15px', + textProps: {}, // variationUnit: '', }; diff --git a/src/common/components/MarkDownParser/ContentHeading.jsx b/src/common/components/MarkDownParser/ContentHeading.jsx index 0b4d6015f..9ef9598d7 100644 --- a/src/common/components/MarkDownParser/ContentHeading.jsx +++ b/src/common/components/MarkDownParser/ContentHeading.jsx @@ -1,12 +1,14 @@ import PropTypes from 'prop-types'; import { Box, useColorModeValue } from '@chakra-ui/react'; +import useStyle from '../../hooks/useStyle'; import Heading from '../Heading'; import Text from '../Text'; import Icon from '../Icon'; function ContentHeading({ - content, children, callToAction, titleRightSide, + content, children, callToAction, titleRightSide, isGuidedExperience, }) { + const { backgroundColor4 } = useStyle(); const { title, subtitle, assetType } = content; const assetTypeIcons = { LESSON: 'book', @@ -15,16 +17,34 @@ function ContentHeading({ QUIZ: 'answer', }; + const guidedExperienceStyles = () => { + if (!isGuidedExperience) return {}; + + return { + background: backgroundColor4, + margin: { base: '0px -10px', md: '0px -2rem' }, + borderRadius: '11px 11px 0 0', + padding: '15px', + borderBottom: '1px solid #BBE5FE', + }; + }; + return content && Object.keys(content).length !== 0 && ( - + - + {title} @@ -47,11 +67,13 @@ ContentHeading.propTypes = { children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, callToAction: PropTypes.node, titleRightSide: PropTypes.node, + isGuidedExperience: PropTypes.bool, }; ContentHeading.defaultProps = { content: {}, callToAction: null, titleRightSide: null, + isGuidedExperience: false, }; export default ContentHeading; diff --git a/src/common/components/MarkDownParser/MDComponents/index.jsx b/src/common/components/MarkDownParser/MDComponents/index.jsx index de8a3dd84..2338ac19e 100644 --- a/src/common/components/MarkDownParser/MDComponents/index.jsx +++ b/src/common/components/MarkDownParser/MDComponents/index.jsx @@ -441,7 +441,7 @@ export function DOMComponent({ children }) { } export function MDCheckbox({ - index, children, subTasks, subTasksLoaded, subTasksProps, setSubTasksProps, updateSubTask, + index, children, subTasks, subTasksLoaded, newSubTasks, setNewSubTasks, updateSubTask, }) { const childrenData = children[1]?.props?.children || children; const [isChecked, setIsChecked] = useState(false); @@ -463,15 +463,15 @@ export function MDCheckbox({ const text = renderToStringClient(); const slug = typeof text === 'string' && slugify(text); - const currentSubTask = subTasks.length > 0 && subTasks.filter((task) => task?.id === slug); - const taskChecked = subTasks && subTasks.filter((task) => task?.id === slug && task?.status !== 'PENDING').length > 0; + const currentSubTask = subTasks.find((task) => task?.id === slug); useEffect(() => { // load checked tasks + const taskChecked = subTasks.some((task) => task?.id === slug && task?.status !== 'PENDING'); if (taskChecked) { setIsChecked(true); } - }, [taskChecked]); + }, [subTasks]); const taskStatus = { true: 'DONE', @@ -481,35 +481,25 @@ export function MDCheckbox({ useEffect(() => { if (subTasksLoaded) { if ( - subTasksProps?.length > 0 && subTasksProps.find((l) => l?.id === slug) + newSubTasks?.length > 0 && newSubTasks.find((l) => l?.id === slug) ) { return () => {}; } - if (currentSubTask.length > 0) { - setSubTasksProps((prev) => { - if (prev.length > 0) { - return [...prev, currentSubTask[0]]; - } - return [currentSubTask[0]]; + if (currentSubTask) { + setNewSubTasks((prev) => { + const content = [...prev]; + if (!content.some((subTask) => subTask.id === currentSubTask.id)) content.push(currentSubTask); + return content; }); } else { - setSubTasksProps((prev) => { - if (prev?.length > 0) { - return [ - ...prev, - { - id: slug, - status: 'PENDING', - label: text, - }, - ]; - } - return [ - { - id: slug, - status: 'PENDING', - label: text, - }, - ]; + setNewSubTasks((prev) => { + const task = { + id: slug, + status: 'PENDING', + label: text, + }; + const content = [...prev]; + if (!content.some((subTask) => subTask.id === task.id)) content.push(task); + return content; }); } } @@ -543,7 +533,7 @@ export function MDCheckbox({ } export function OnlyForBanner({ - children, permission, include, exclude, cohortSession, profile, + children, permission, include, exclude, profile, }) { const allCapabilities = permission.split(',').concat(include.split(',').concat(exclude.split(','))); log('md_permissions:', allCapabilities); @@ -553,7 +543,6 @@ export function OnlyForBanner({ onlyMember withBanner profile={profile} - cohortSession={cohortSession} capabilities={allCapabilities} > {children} @@ -602,16 +591,16 @@ MDCheckbox.propTypes = { index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), subTasks: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)), subTasksLoaded: PropTypes.bool, - subTasksProps: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)), - setSubTasksProps: PropTypes.func, + newSubTasks: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)), + setNewSubTasks: PropTypes.func, updateSubTask: PropTypes.func, }; MDCheckbox.defaultProps = { index: 0, subTasks: [], subTasksLoaded: false, - subTasksProps: [], - setSubTasksProps: () => {}, + newSubTasks: [], + setNewSubTasks: () => {}, updateSubTask: () => {}, }; @@ -632,14 +621,12 @@ MDText.propTypes = { OnlyForBanner.propTypes = { children: PropTypes.node.isRequired, permission: PropTypes.string, - cohortSession: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), profile: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), include: PropTypes.string, exclude: PropTypes.string, }; OnlyForBanner.defaultProps = { permission: '', - cohortSession: {}, profile: {}, include: '', exclude: '', diff --git a/src/common/components/MarkDownParser/SubTasks.jsx b/src/common/components/MarkDownParser/SubTasks.jsx index 09a20d07d..ee9651c28 100644 --- a/src/common/components/MarkDownParser/SubTasks.jsx +++ b/src/common/components/MarkDownParser/SubTasks.jsx @@ -7,7 +7,7 @@ import Text from '../Text'; import { toCapitalize } from '../../../utils'; function SubTasks({ - subTasks, title, description, assetType, + subTasks, title, description, assetType, variant, ...rest }) { const { t } = useTranslation('common'); @@ -15,14 +15,39 @@ function SubTasks({ const taskPercent = Math.round((tasksDone.length / subTasks.length) * 100); const assetValue = t(`common:asset-type-pronouns.${assetType.toLowerCase()}`); + const variantContainerStyles = { + square: { + flexDirection: 'column', + maxWidth: { base: 'none', sm: '50%' }, + }, + }; + + const variantTextStyles = { + square: { + textAlign: 'center', + p: '0px', + }, + }; + return subTasks.length > 0 && ( - + - + {title || toCapitalize(t('subtasks.title', { count: subTasks.length, asset_type: assetValue }))} - + {description || t('subtasks.description')} @@ -35,6 +60,7 @@ SubTasks.propTypes = { title: PropTypes.string, description: PropTypes.string, assetType: PropTypes.string, + variant: PropTypes.string, }; SubTasks.defaultProps = { @@ -42,6 +68,7 @@ SubTasks.defaultProps = { title: '', description: '', assetType: 'lesson', + variant: '', }; export default SubTasks; diff --git a/src/common/components/MarkDownParser/index.jsx b/src/common/components/MarkDownParser/index.jsx index bc7707f53..a2ab5e5c4 100644 --- a/src/common/components/MarkDownParser/index.jsx +++ b/src/common/components/MarkDownParser/index.jsx @@ -18,6 +18,7 @@ import { } from './MDComponents'; import { usePersistent } from '../../hooks/usePersistent'; import useCohortHandler from '../../hooks/useCohortHandler'; +import useModuleHandler from '../../hooks/useModuleHandler'; import Toc from './toc'; import ContentHeading from './ContentHeading'; import CallToAction from '../CallToAction'; @@ -66,8 +67,8 @@ function HrComponent() { function IframeComponent({ src, title, width, height }) { return (); } -function OnlyForComponent({ cohortSession, profile, ...props }) { - return (); +function OnlyForComponent({ profile, ...props }) { + return (); } function CodeViewerComponent(props) { @@ -118,7 +119,7 @@ function MdCallToAction({ assetData }) { ); } -function ListComponent({ subTasksLoaded, subTasksProps, setSubTasksProps, subTasks, updateSubTask, ...props }) { +function ListComponent({ subTasksLoaded, newSubTasks, setNewSubTasks, subTasks, updateSubTask, ...props }) { const childrenExists = props?.children?.length >= 0; const type = childrenExists && props?.children[0]?.props && props.children[0].props.type; const type2 = childrenExists && props?.children[1]?.props && props.children[1]?.props.node?.children[0]?.properties?.type; @@ -127,8 +128,8 @@ function ListComponent({ subTasksLoaded, subTasksProps, setSubTasksProps, subTas className="MDCheckbox" {...props} subTasksLoaded={subTasksLoaded} - subTasksProps={subTasksProps} - setSubTasksProps={setSubTasksProps} + newSubTasks={newSubTasks} + setNewSubTasks={setNewSubTasks} subTasks={subTasks} updateSubTask={updateSubTask} /> @@ -139,14 +140,14 @@ function ListComponent({ subTasksLoaded, subTasksProps, setSubTasksProps, subTas function MarkDownParser({ content, callToActionProps, withToc, frontMatter, titleRightSide, currentTask, isPublic, currentData, - showLineNumbers, showInlineLineNumbers, assetData, alerMessage, + showLineNumbers, showInlineLineNumbers, assetData, alerMessage, isGuidedExperience, showContentHeading, }) { const { t, lang } = useTranslation('common'); - const [subTasks, setSubTasks] = useState([]); const [subTasksLoaded, setSubTasksLoaded] = useState(false); - const [subTasksProps, setSubTasksProps] = useState([]); + const [newSubTasks, setNewSubTasks] = useState([]); const [learnpackActions, setLearnpackActions] = useState([]); const [fileContext, setFileContext] = useState(''); + const { subTasks, setSubTasks } = useModuleHandler(); const { state } = useCohortHandler(); const { cohortSession } = state; const [profile] = usePersistent('profile', {}); @@ -183,12 +184,12 @@ function MarkDownParser({ const createSubTasksIfNotExists = async () => { // const cleanedSubTasks = subTasks.filter((task) => task.id !== currentTask.id); - if (currentTask?.id && subTasksProps.length > 0) { + if (currentTask?.id && newSubTasks.length > 0) { const resp = await bc.todo().subtask().update( currentTask?.id, [ // ...cleanedSubTasks, - ...subTasksProps, + ...newSubTasks, ], ); if (resp.status >= 200 && resp.status < 400) { @@ -200,8 +201,10 @@ function MarkDownParser({ // Create subTasks if not exists useEffect(() => { - createSubTasksIfNotExists(); - }, [subTasksProps]); + if (subTasksLoaded && subTasks.length === 0) { + createSubTasksIfNotExists(); + } + }, [subTasksLoaded, subTasks, newSubTasks]); const { token, assetSlug, gitpod, interactive, @@ -286,7 +289,7 @@ function MarkDownParser({ <> { setShowCloneModal(false); @@ -309,34 +312,38 @@ function MarkDownParser({ showLineNumbers={false} /> - - )} - content={frontMatter} - > - {withToc && ( - - )} - {alerMessage && alerMessage} + {showContentHeading && ( + + )} + content={frontMatter} + currentData={currentData} + > + {withToc && ( + + )} + {alerMessage && alerMessage} - {Array.isArray(subTasks) && subTasks?.length > 0 && ( - - )} - + {Array.isArray(subTasks) && subTasks?.length > 0 && ( + + )} + + )} {isPublic && withToc && ( )} @@ -364,12 +371,12 @@ function MarkDownParser({ // table: { // component: MDTable, // }, - onlyfor: ({ ...props }) => OnlyForComponent({ ...props, cohortSession, profile }), + onlyfor: ({ ...props }) => OnlyForComponent({ ...props, profile }), codeviewer: ({ ...props }) => CodeViewerComponent({ ...props, preParsedContent, fileContext }), calltoaction: ({ ...props }) => MdCallToAction({ ...props, assetData }), // Component for list of checkbox // children[1].props.node.children[0].properties.type - li: ({ ...props }) => ListComponent({ subTasksLoaded, subTasksProps, setSubTasksProps, subTasks, updateSubTask, ...props }), + li: ({ ...props }) => ListComponent({ subTasksLoaded, newSubTasks, setNewSubTasks, subTasks, updateSubTask, ...props }), quote: Quote, }} > @@ -392,6 +399,8 @@ MarkDownParser.propTypes = { showInlineLineNumbers: PropTypes.bool, assetData: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.object])), alerMessage: PropTypes.node, + isGuidedExperience: PropTypes.bool, + showContentHeading: PropTypes.bool, }; MarkDownParser.defaultProps = { content: '', @@ -406,6 +415,8 @@ MarkDownParser.defaultProps = { showInlineLineNumbers: true, assetData: null, alerMessage: null, + isGuidedExperience: false, + showContentHeading: true, }; export default MarkDownParser; diff --git a/src/common/components/MktSideRecommendations.jsx b/src/common/components/MktSideRecommendations.jsx index b9385b5d6..6b2d075e5 100644 --- a/src/common/components/MktSideRecommendations.jsx +++ b/src/common/components/MktSideRecommendations.jsx @@ -135,10 +135,7 @@ function MktSideRecommendations({ title, endpoint, technologies, containerPaddin if (recom?.color) { return recom.color; } - if (recom?.icon_url) { - return 'green.400'; - } - return 'gray.100'; + return 'blue.50'; }; useEffect(() => { @@ -155,7 +152,7 @@ function MktSideRecommendations({ title, endpoint, technologies, containerPaddin return ( <> - + {recom?.course_translation?.title || recom.title} @@ -183,6 +180,8 @@ function MktSideRecommendations({ title, endpoint, technologies, containerPaddin background="white" gridGap="10px" width="100%" + _hover="none" + marginTop="10px" > {t('learn-more')} @@ -207,13 +206,13 @@ function MktSideRecommendations({ title, endpoint, technologies, containerPaddin const tags = []; return ( - + - + - - + + {recom?.course_translation?.title || recom.title} @@ -237,7 +236,7 @@ function MktSideRecommendations({ title, endpoint, technologies, containerPaddin href={link} alignItems="center" display="flex" - colorScheme={{ base: 'default', md: 'success' }} + colorScheme={{ base: 'default', md: 'blue.400' }} width="auto" color={{ base: 'green.light', md: 'white' }} gridGap="10px" diff --git a/src/common/components/OnlyFor.jsx b/src/common/components/OnlyFor.jsx index 96adf28b2..0fe403e1e 100644 --- a/src/common/components/OnlyFor.jsx +++ b/src/common/components/OnlyFor.jsx @@ -39,19 +39,21 @@ function Component({ withBanner, children }) { } function OnlyFor({ - cohortSession, academy, capabilities, children, onlyMember, onlyTeachers, withBanner, profile, + academy, capabilities, children, onlyMember, onlyTeachers, withBanner, profile, cohort, }) { const academyNumber = Math.floor(academy); const teachers = ['TEACHER', 'ASSISTANT', 'REVIEWER']; const commonUser = ['TEACHER', 'ASSISTANT', 'STUDENT', 'REVIEWER']; const { state } = useCohortHandler(); - const { userCapabilities: cohortCapabilities } = state; + const { userCapabilities: cohortCapabilities, cohortSession } = state; + + const currentCohort = cohort || cohortSession; const profileCapabilities = profile?.permissionsSlug || []; const userCapabilities = [...new Set([...cohortCapabilities, ...profileCapabilities])]; const profileRole = profile?.roles?.length > 0 && profile?.roles[0]?.role?.toUpperCase(); - const cohortRole = cohortSession?.cohort_role?.toUpperCase() || profileRole || 'NONE'; - const isCapableAcademy = cohortSession && cohortSession.academy?.id === academyNumber; + const cohortRole = currentCohort?.cohort_role?.toUpperCase() || profileRole || 'NONE'; + const isCapableAcademy = currentCohort && currentCohort.academy?.id === academyNumber; const isMember = commonUser.includes(cohortRole); const isTeacher = teachers.includes(cohortRole); const capabilitiesNotExists = capabilities.length <= 0 || capabilities.includes(''); @@ -60,7 +62,7 @@ function OnlyFor({ ).includes(true); const haveRequiredCapabilities = () => { - if (!cohortSession) return false; + if (!currentCohort) return false; if (onlyTeachers && isTeacher) { if (isCapableRole) return true; if (capabilitiesNotExists) return true; @@ -85,13 +87,13 @@ function OnlyFor({ } OnlyFor.propTypes = { - cohortSession: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired, academy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), capabilities: PropTypes.arrayOf(PropTypes.string), children: PropTypes.node.isRequired, onlyMember: PropTypes.bool, onlyTeachers: PropTypes.bool, profile: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), + cohort: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), withBanner: PropTypes.bool, }; @@ -101,6 +103,7 @@ OnlyFor.defaultProps = { onlyMember: false, onlyTeachers: false, profile: {}, + cohort: null, withBanner: false, }; diff --git a/src/common/components/PopoverTaskHandler.jsx b/src/common/components/PopoverTaskHandler.jsx index b5055be7b..fc99a1409 100644 --- a/src/common/components/PopoverTaskHandler.jsx +++ b/src/common/components/PopoverTaskHandler.jsx @@ -1,4 +1,6 @@ -import { Box, PopoverArrow, Text, PopoverBody, PopoverCloseButton, PopoverContent, PopoverHeader, Button, FormErrorMessage, FormControl, Input, useColorModeValue, useToast, Popover, PopoverTrigger } from '@chakra-ui/react'; +/* eslint-disable react/prop-types */ +/* eslint-disable no-unused-vars */ +import { Box, PopoverArrow, Text, PopoverBody, PopoverCloseButton, PopoverContent, PopoverHeader, Button, FormErrorMessage, FormControl, Input, useColorModeValue, useToast, Popover, PopoverTrigger, Tooltip } from '@chakra-ui/react'; import * as Yup from 'yup'; import { Field, Form, Formik } from 'formik'; import PropTypes from 'prop-types'; @@ -13,56 +15,72 @@ import useStyle from '../hooks/useStyle'; import useCohortHandler from '../hooks/useCohortHandler'; import { formatBytes } from '../../utils'; -export function TextByTaskStatus({ currentTask, t }) { +export function textByTaskStatus(currentTask) { + const { t } = useTranslation('dashboard'); + const { hexColor } = useStyle(); const taskIsApproved = currentTask?.revision_status === 'APPROVED'; // task project status if (currentTask && currentTask.task_type === 'PROJECT' && currentTask.task_status) { if (currentTask.task_status === 'DONE' && currentTask.revision_status === 'PENDING') { - return ( - <> - - {t('common:taskStatus.update-project-delivery')} - - ); + return { + icon: { + icon: 'checked', + color: '#FFB718', + width: '20px', + height: '20px', + }, + text: t('common:taskStatus.update-project-delivery'), + }; } if (currentTask.revision_status === 'APPROVED') { - return ( - <> - - {t('common:taskStatus.project-approved')} - - ); + return { + icon: { + icon: 'verified', + color: '#606060', + width: '20px', + }, + text: t('common:taskStatus.project-approved'), + }; } if (currentTask.revision_status === 'REJECTED') { - return ( - <> - - {t('common:taskStatus.update-project-delivery')} - - ); + return { + icon: { + icon: 'checked', + color: '#FF4433', + width: '20px', + }, + text: t('common:taskStatus.update-project-delivery'), + }; } - return ( - <> - - {t('common:taskStatus.send-project')} - - ); + return { + icon: { + icon: 'longArrowRight', + color: 'white', + width: '20px', + }, + text: t('common:taskStatus.send-project'), + }; } // common task status if (currentTask && currentTask.task_type !== 'PROJECT' && currentTask.task_status === 'DONE') { - return ( - <> - - {t('common:taskStatus.mark-as-not-done')} - - ); + return { + icon: { + icon: 'close', + color: '#FFFFFF', + width: '12px', + }, + text: t('common:taskStatus.mark-as-not-done'), + }; } - return ( - <> - - {t('common:taskStatus.mark-as-done')} - - ); + + return { + icon: { + icon: 'checked2', + color: taskIsApproved ? '#606060' : '#FFFFFF', + width: '14px', + }, + text: t('common:taskStatus.mark-as-done'), + }; } export function IconByTaskStatus({ currentTask, noDeliveryFormat }) { @@ -90,17 +108,12 @@ export function IconByTaskStatus({ currentTask, noDeliveryFormat }) { return ; } -function PopoverTaskHandler({ - isLoading, +function PopoverCustomContent({ currentAssetData, currentTask, sendProject, onClickHandler, - settingsOpen, - allowText, closeSettings, - toggleSettings, - buttonChildren, }) { const { t } = useTranslation('dashboard'); const { state } = useCohortHandler(); @@ -116,7 +129,6 @@ function PopoverTaskHandler({ const commonInputActiveColor = useColorModeValue('gray.800', 'gray.350'); const toast = useToast(); - const taskIsApproved = allowText && currentTask?.revision_status === 'APPROVED'; const deliveryFormatExists = typeof currentAssetData?.delivery_formats === 'string'; const noDeliveryFormat = deliveryFormatExists && currentAssetData?.delivery_formats.includes('no_delivery'); const deliveryFormatIsUrl = deliveryFormatExists && currentAssetData?.delivery_formats.includes('url'); @@ -125,7 +137,6 @@ function PopoverTaskHandler({ const howToSendProjectUrl = 'https://4geeksacademy.notion.site/How-to-deliver-a-project-e1db0a8b1e2e4fbda361fc2f5457c0de'; const maxFileSize = 1048576 * 10; // 10mb const fileErrorExists = fileProps.some((file) => file.formatError) || fileProps.some((file) => file.sizeError); - const isButtonDisabled = currentTask === null || taskIsApproved; const customUrlValidation = Yup.object().shape({ githubUrl: Yup.string().matches( @@ -217,6 +228,288 @@ function PopoverTaskHandler({ closeSettings(); }; + return ( + + + {t('deliverProject.title')} + + + {noDeliveryFormat ? ( + + + {t('deliverProject.no-delivery-needed')} + + + + + ) : ( + <> + {typeof currentAssetData === 'object' && deliveryFormatIsUrl ? ( + { + setIsSubmitting(true); + if (githubUrl !== '') { + sendProject({ task: currentTask, githubUrl, taskStatus: 'DONE' }); + setIsSubmitting(false); + onClickHandler(); + } + }} + validationSchema={regexUrlExists ? customUrlValidation : githubUrlValidation} + > + {() => ( +
+ + {({ field, form }) => { + setGithubUrl(form.values.githubUrl); + return ( + + + + {form.errors.githubUrl} + + + ); + }} + + + {currentAssetData?.delivery_instructions?.length > 2 ? ( + + + + ) : ( + + )} + + +
+ )} +
+ ) : ( + + {currentAssetData?.delivery_instructions?.length > 2 ? ( + + + + ) : ( + + {t('deliverProject.file-upload')} +
{currentAssetData?.delivery_formats?.replaceAll(',', ', ').replaceAll('.', '').toUpperCase()} + + )} + + + + + + + + setDragOver(true)} + onDragLeave={() => setDragOver(false)} + /> + + + {fileProps.some((file) => typeof file?.type === 'string') && ( + <> + + {fileProps.map((file) => { + const errorExists = file.formatError || file.sizeError; + const extension = file.name.split('.').pop(); + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg']; + const isImage = imageExtensions.includes(extension); + const icon = iconDict.includes(extension) ? extension : 'file'; + return ( + + + + + 20}> + {file.name} + + + {errorExists ? ( + <> + + {file.formatError + ? t('deliverProject.error-file-format') + : file.sizeError && t('deliverProject.error-file-size')} + + ) : formatBytes(file.size)} + + + + { + handleRemoveFileInList(file.name); + }} + cursor="pointer" + > + + + + ); + })} + + + {t('deliverProject.total-size', { size: formatBytes(fileSumSize) })} + + + )} + + + + + + )} + + )} + + + ); +} + +function PopoverTaskHandler({ + isGuidedExperience, + isLoading, + currentAssetData, + currentTask, + sendProject, + onClickHandler, + settingsOpen, + allowText, + closeSettings, + toggleSettings, + buttonChildren, +}) { + const { hexColor } = useStyle(); + const taskIsApproved = allowText && currentTask?.revision_status === 'APPROVED'; + const isButtonDisabled = currentTask === null || taskIsApproved; + + const handleCloseFile = () => { + closeSettings(); + }; + + const textAndIcon = textByTaskStatus(currentTask || {}); + + if (isGuidedExperience) { + return ( + + + + + + + + + + ); + } + return ( {allowText ? ( - + <> + + {textAndIcon.text} + ) : ( )} @@ -255,221 +551,13 @@ function PopoverTaskHandler({ - - - {t('deliverProject.title')} - - - {noDeliveryFormat ? ( - - - {t('deliverProject.no-delivery-needed')} - - - - - ) : ( - <> - {typeof currentAssetData === 'object' && deliveryFormatIsUrl ? ( - { - setIsSubmitting(true); - if (githubUrl !== '') { - sendProject({ task: currentTask, githubUrl, taskStatus: 'DONE' }); - setIsSubmitting(false); - onClickHandler(); - } - }} - validationSchema={regexUrlExists ? customUrlValidation : githubUrlValidation} - > - {() => ( -
- - {({ field, form }) => { - setGithubUrl(form.values.githubUrl); - return ( - - - - {form.errors.githubUrl} - - - ); - }} - - - {currentAssetData?.delivery_instructions?.length > 2 ? ( - - - - ) : ( - - )} - - - - )} -
- ) : ( - - {currentAssetData?.delivery_instructions?.length > 2 ? ( - - - - ) : ( - - {t('deliverProject.file-upload')} - {currentAssetData?.delivery_formats?.replaceAll(',', ', ').replaceAll('.', '').toUpperCase()} - - )} - - - - - - - - setDragOver(true)} - onDragLeave={() => setDragOver(false)} - /> - - - {fileProps.some((file) => typeof file?.type === 'string') && ( - <> - - {fileProps.map((file) => { - const errorExists = file.formatError || file.sizeError; - const extension = file.name.split('.').pop(); - const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg']; - const isImage = imageExtensions.includes(extension); - const icon = iconDict.includes(extension) ? extension : 'file'; - return ( - - - - - 20}> - {file.name} - - - {errorExists ? ( - <> - - {file.formatError - ? t('deliverProject.error-file-format') - : file.sizeError && t('deliverProject.error-file-size')} - - ) : formatBytes(file.size)} - - - - { - handleRemoveFileInList(file.name); - }} - cursor="pointer" - > - - - - ); - })} - - - {t('deliverProject.total-size', { size: formatBytes(fileSumSize) })} - - - )} - - - - - - )} - - )} -
-
+
); } @@ -485,6 +573,7 @@ PopoverTaskHandler.propTypes = { allowText: PropTypes.bool, toggleSettings: PropTypes.func, buttonChildren: PropTypes.node, + isGuidedExperience: PropTypes.bool, }; PopoverTaskHandler.defaultProps = { @@ -498,15 +587,15 @@ PopoverTaskHandler.defaultProps = { allowText: false, toggleSettings: () => {}, buttonChildren: null, + isGuidedExperience: false, }; -TextByTaskStatus.propTypes = { - currentTask: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), - t: PropTypes.func.isRequired, -}; -TextByTaskStatus.defaultProps = { - currentTask: {}, -}; +// TextByTaskStatus.propTypes = { +// currentTask: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), +// }; +// TextByTaskStatus.defaultProps = { +// currentTask: {}, +// }; IconByTaskStatus.propTypes = { currentTask: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), noDeliveryFormat: PropTypes.bool, diff --git a/src/common/components/ReactPlayerV2.jsx b/src/common/components/ReactPlayerV2.jsx index c606e3f37..5404301f1 100644 --- a/src/common/components/ReactPlayerV2.jsx +++ b/src/common/components/ReactPlayerV2.jsx @@ -7,7 +7,7 @@ import Icon from './Icon'; import useStyle from '../hooks/useStyle'; function ReactPlayerV2({ - url, thumbnail, controls, closeOnOverlayClick, className, withThumbnail, iframeStyle, thumbnailStyle, title, withModal, ...rest + url, thumbnail, controls, closeOnOverlayClick, className, withThumbnail, iframeStyle, thumbnailStyle, title, withModal, containerStyle, ...rest }) { const { lang } = useTranslation('exercises'); const isVideoFromDrive = url && url.includes('drive.google.com'); @@ -105,7 +105,7 @@ function ReactPlayerV2({ ) : ( <> {url && !isExternalVideoProvider && ( - + {contentList.map((item) => { - const isLesson = getAssetPath(item) === 'lesson'; - const isExercise = getAssetPath(item) === 'interactive-exercise'; - const isProject = getAssetPath(item) === 'interactive-coding-tutorial'; - const isHowTo = getAssetPath(item) === 'how-to'; + const assetPath = getAssetPath(item); + const prefixLang = item?.lang === 'us' ? '' : `/${item?.lang}`; const date = new Date(item?.published_at); const dateCreated = isValidDate(item?.published_at) ? { @@ -69,18 +74,7 @@ function RelatedContent({ slug, type, extraQuerys, technologies, pathWithDifficu } : {}; const getLink = () => { - if (isLesson) { - return `${prefixLang}/lesson/${item.slug}`; - } - if (isExercise) { - return `${prefixLang}/interactive-exercise/${item.slug}`; - } - if (isProject) { - return `${prefixLang}/interactive-coding-tutorial/${item.slug}`; - } - if (isHowTo) { - return `${prefixLang}/how-to/${item.slug}`; - } + if (assetTypePaths.includes(assetPath)) return `${prefixLang}/${assetPath}/${item.slug}`; return `/${projectPath}${checkIsPathDifficulty(item.difficulty)}/${item.slug}`; }; return ( diff --git a/src/common/components/ReviewModal/CodeRevisionsList.jsx b/src/common/components/ReviewModal/CodeRevisionsList.jsx new file mode 100644 index 000000000..97fb80759 --- /dev/null +++ b/src/common/components/ReviewModal/CodeRevisionsList.jsx @@ -0,0 +1,88 @@ +import { Box, Button, Flex } from '@chakra-ui/react'; +import PropTypes from 'prop-types'; +import useTranslation from 'next-translate/useTranslation'; +import useStyle from '../../hooks/useStyle'; +import Icon from '../Icon'; +import Text from '../Text'; + +function CodeRevisionsList({ codeRevisions, revisionContent, selectCodeRevision, ...rest }) { + const { fontColor, borderColor, hexColor, featuredLight } = useStyle(); + const { t } = useTranslation('assignments'); + + return ( + + {codeRevisions.map((commit) => { + const isSelected = revisionContent?.id === commit?.id; + const hasBeenReviewed = typeof commit?.is_good === 'boolean'; + const dataStruct = { + ...commit, + revision_rating: commit?.revision_rating, + hasBeenReviewed, + }; + return ( + selectCodeRevision(dataStruct)} _hover={{ background: featuredLight }} border="1px solid" borderColor={isSelected ? 'blue.default' : borderColor} justifyContent="space-between" alignItems="center" height="48px" padding="4px 8px" borderRadius="8px"> + + + + + + {commit?.file?.name} + + + {`${commit?.file?.commit_hash?.slice(0, 10)}...`} + + + {commit?.committer?.github_username && ( + + {commit?.committer?.github_username} + + )} + + + + + {hasBeenReviewed ? ( + + + + ) : ( + + + + + + + )} + + + + ); + })} + + ); +} +CodeRevisionsList.propTypes = { + selectCodeRevision: PropTypes.func, + revisionContent: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), + codeRevisions: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.any])), +}; +CodeRevisionsList.defaultProps = { + selectCodeRevision: () => {}, + revisionContent: null, + codeRevisions: [], +}; + +export default CodeRevisionsList; diff --git a/src/common/components/ReviewModal/ReviewCodeRevision.jsx b/src/common/components/ReviewModal/ReviewCodeRevision.jsx index 40b70add2..8c3b4a0d7 100644 --- a/src/common/components/ReviewModal/ReviewCodeRevision.jsx +++ b/src/common/components/ReviewModal/ReviewCodeRevision.jsx @@ -2,6 +2,7 @@ import { Box, Button, Divider, Flex, Textarea } from '@chakra-ui/react'; import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; import useTranslation from 'next-translate/useTranslation'; +import CodeRevisionsList from './CodeRevisionsList'; import Heading from '../Heading'; import useStyle from '../../hooks/useStyle'; import bc from '../../services/breathecode'; @@ -19,7 +20,7 @@ const defaultReviewRateData = { revision_rating: null, }; function ReviewCodeRevision({ contextData, setContextData, stages, setStage, disableRate }) { - const { fontColor, borderColor, lightColor, hexColor, featuredLight } = useStyle(); + const { lightColor, hexColor, featuredLight } = useStyle(); const [reviewRateData, setReviewRateData] = useState(defaultReviewRateData); const { t } = useTranslation('assignments'); @@ -149,62 +150,7 @@ function ReviewCodeRevision({ contextData, setContextData, stages, setStage, dis {t('code-review.filename')} {t('code-review.feedback-status')} - - {codeRevisions?.length > 0 && codeRevisions.map((commit) => { - const isSelected = revisionContent?.id === commit?.id; - const hasBeenReviewed = typeof commit?.is_good === 'boolean'; - const dataStruct = { - ...commit, - revision_rating: commit?.revision_rating, - hasBeenReviewed, - }; - return ( - selectCodeRevision(dataStruct)} _hover={{ background: featuredLight }} border="1px solid" borderColor={isSelected ? 'blue.default' : borderColor} justifyContent="space-between" alignItems="center" height="48px" padding="4px 8px" borderRadius="8px"> - - - - - - {commit?.file?.name} - - - {`${commit?.file?.commit_hash?.slice(0, 10)}...`} - - - {commit?.committer?.github_username && ( - - {commit?.committer?.github_username} - - )} - - - - - - - - - - - - - - ); - })} - + {revisionContent?.id && ( diff --git a/src/common/components/ReviewModal/index.jsx b/src/common/components/ReviewModal/index.jsx index 22eaed58c..d7a61941d 100644 --- a/src/common/components/ReviewModal/index.jsx +++ b/src/common/components/ReviewModal/index.jsx @@ -18,8 +18,7 @@ import ReviewCodeRevision from './ReviewCodeRevision'; import { usePersistent } from '../../hooks/usePersistent'; import useCohortHandler from '../../hooks/useCohortHandler'; import PopoverTaskHandler from '../PopoverTaskHandler'; -import { updateAssignment } from '../../hooks/useModuleHandler'; -import useModuleMap from '../../store/actions/moduleMapAction'; +import useModuleHandler from '../../hooks/useModuleHandler'; import iconDict from '../../utils/iconDict.json'; import UndoApprovalModal from '../UndoApprovalModal'; import useAuth from '../../hooks/useAuth'; @@ -56,13 +55,13 @@ function ReviewModal({ isExternal, externalFiles, isOpen, isStudent, externalDat }); const [profile] = usePersistent('profile', {}); const [comment, setComment] = useState(''); + const { updateAssignment } = useModuleHandler(); const { state } = useCohortHandler(); const { cohortSession } = state; const [currentAssetData, setCurrentAssetData] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [openUndoApproval, setOpenUndoApproval] = useState(false); const [fileData, setFileData] = useState(); - const { contextState, setContextState } = useModuleMap(); const [reviewStatus, setReviewStatus] = useState(''); const [contextData, setContextData] = useState({ commitFiles: { @@ -427,7 +426,7 @@ function ReviewModal({ isExternal, externalFiles, isOpen, isStudent, externalDat }; const sendProject = async ({ task, githubUrl, taskStatus: newTaskStatus }) => { await updateAssignment({ - t, task, closeSettings, toast, githubUrl, taskStatus: newTaskStatus, contextState, setContextState, + task, closeSettings, githubUrl, taskStatus: newTaskStatus, }); }; diff --git a/src/common/components/TeacherSidebar.jsx b/src/common/components/TeacherSidebar.jsx index b32ee0475..ba980866d 100644 --- a/src/common/components/TeacherSidebar.jsx +++ b/src/common/components/TeacherSidebar.jsx @@ -11,6 +11,7 @@ import Icon from './Icon'; import Text from './Text'; import AttendanceModal from './AttendanceModal'; import useCohortHandler from '../hooks/useCohortHandler'; +import useAuth from '../hooks/useAuth'; import { isValidDate, isWindow } from '../../utils'; function ItemText({ text }) { @@ -50,9 +51,10 @@ function ItemButton({ } function TeacherSidebar({ - title, user, students, width, sortedAssignments, + title, students, width, }) { const { t } = useTranslation('dashboard'); + const { user } = useAuth(); const { colorMode } = useColorMode(); const [openAttendance, setOpenAttendance] = useState(false); const { state } = useCohortHandler(); @@ -163,7 +165,6 @@ function TeacherSidebar({ setOpenAttendance(false)} title={t('attendance-modal.start-today-class')} // title="Start your today's class" @@ -178,18 +179,14 @@ function TeacherSidebar({ TeacherSidebar.propTypes = { title: PropTypes.string, - user: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), students: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.any])), width: PropTypes.string, - sortedAssignments: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))), }; TeacherSidebar.defaultProps = { title: 'Actions', - user: {}, students: [], width: '100%', - sortedAssignments: [], }; ItemText.propTypes = { diff --git a/src/common/components/Timeline.jsx b/src/common/components/Timeline.jsx index f5b287112..2d7a7c086 100644 --- a/src/common/components/Timeline.jsx +++ b/src/common/components/Timeline.jsx @@ -4,28 +4,26 @@ import React, { import PropTypes from 'prop-types'; import { useRouter } from 'next/router'; import { - Box, Flex, useColorMode, useColorModeValue, + Box, + Flex, + useColorModeValue, } from '@chakra-ui/react'; import useTranslation from 'next-translate/useTranslation'; +import useStyle from '../hooks/useStyle'; import Icon from './Icon'; import Text from './Text'; -const color = { - light: 'blue.light', - dark: 'featuredDark', -}; - function Timeline({ - title, assignments, technologies, width, onClickAssignment, showPendingTasks, + title, assignments, technologies, width, onClickAssignment, showPendingTasks, variant, }) { const { t, lang } = useTranslation('syllabus'); - const { colorMode } = useColorMode(); const router = useRouter(); + const { hexColor, fontColor, backgroundColor, backgroundColor3 } = useStyle(); const { lessonSlug } = router.query; const [currentAssignment, setCurrentAssignment] = useState(null); - const [currentDefaultSlug, setCurrentDefaultSlug] = useState(null); const fontColor1 = useColorModeValue('gray.dark', 'white'); const fontColor2 = useColorModeValue('gray.dark', 'gray.light'); + const bgColor = useColorModeValue('blue.light', 'featuredDark'); // scroll scrollIntoView for id when lessonSlug changes const scrollIntoView = (id) => { @@ -38,29 +36,93 @@ function Timeline({ } }; + const scrollTop = () => { + const element = document.getElementById('main-container'); + if (element) { + element.scrollTo({ + behavior: 'smooth', + top: 0, + }); + } + }; + useEffect(() => { if (assignments?.length > 0) { const assignmentFound = assignments.find((item) => ( item?.translations?.us?.slug === lessonSlug || item?.translations?.es?.slug === lessonSlug + || item?.slug === lessonSlug )); - const slug = currentAssignment?.translations?.us?.slug || currentAssignment?.translations?.en?.slug; - setCurrentDefaultSlug(slug); setCurrentAssignment(assignmentFound); } }, [lessonSlug, assignments]); useEffect(() => { - if (currentDefaultSlug) { - scrollIntoView(currentDefaultSlug); + if (currentAssignment?.slug) { + scrollIntoView(currentAssignment.slug); } - }, [currentDefaultSlug]); + }, [currentAssignment]); const handleClick = (e, item) => { e.preventDefault(); e.stopPropagation(); onClickAssignment(e, item); + scrollTop(); }; + const getAssignmentTitle = (item) => { + if (!item?.translations) return item?.title; + + return lang === 'en' ? (item?.translations?.en?.title || item?.translations?.us?.title) + : (item?.translations?.[lang]?.title || item?.title); + }; + + if (variant === 'guided-experience') { + return ( + + {assignments.length > 0 ? assignments.map((item, index) => { + const mapIndex = index; + const muted = item?.slug !== currentAssignment?.slug; + const assignmentTitle = getAssignmentTitle(item); + + return ( + handleClick(e, item)} + width="100%" + borderRadius="0px 8px 8px 0px " + bg={!muted ? backgroundColor : 'none'} + borderLeft={!muted && `4px solid ${hexColor.blueDefault}`} + padding="16px" + display="flex" + alignItems="center" + justifyContent="space-between" + gap="5px" + _hover={{ background: backgroundColor3 }} + > + + + {index + 1} + + {item.type} + + {assignmentTitle} + + {item.task_status === 'DONE' && ( + + )} + + ); + }) : ( + + {showPendingTasks ? t('no-modules-to-show') : t('module-not-started')} + + )} + + ); + } + return ( <> @@ -97,10 +159,8 @@ function Timeline({ {assignments.length > 0 ? assignments.map((item, index) => { const mapIndex = index; - const muted = item?.slug !== currentDefaultSlug; - const assignmentTitle = lang === 'en' - ? (item?.translations?.en?.title || item?.translations?.us?.title) - : (item?.translations?.[lang]?.title || item?.title); + const muted = item?.slug !== currentAssignment?.slug; + const assignmentTitle = getAssignmentTitle(item); return ( - handleClick(e, item)} width="100%" borderRadius="17px" bg={!muted ? color[colorMode] : 'none'} paddingY="10.5px" paddingX="12px"> + handleClick(e, item)} width="100%" borderRadius="17px" bg={!muted ? bgColor : 'none'} paddingY="10.5px" paddingX="12px"> @@ -147,6 +207,7 @@ Timeline.propTypes = { width: PropTypes.string, onClickAssignment: PropTypes.func, showPendingTasks: PropTypes.bool, + variant: PropTypes.string, }; Timeline.defaultProps = { @@ -156,6 +217,7 @@ Timeline.defaultProps = { width: '100%', onClickAssignment: () => {}, showPendingTasks: false, + variant: '', }; export default memo(Timeline); diff --git a/src/common/handlers/cohorts.js b/src/common/handlers/cohorts.js index 085f84104..203288b7b 100644 --- a/src/common/handlers/cohorts.js +++ b/src/common/handlers/cohorts.js @@ -121,18 +121,12 @@ export const processRelatedAssignments = (syllabusData = {}, taskTodo = []) => { const content = [...updatedRead, ...updatedPractice, ...updatedProject, ...updatedAnswer]; - const includesDailyTask = (module) => { - const getModules = taskTodo.some((task) => task.associated_slug === module.slug); - return getModules; - }; + const includesDailyTask = (module) => taskTodo.some((task) => task.associated_slug === module.slug); - const includesStatusPending = (module) => { - const getModules = module.task_status === 'PENDING' && module.revision_status !== 'APPROVED'; - return getModules; - }; + const includesStatusPending = (module) => module.task_status === 'PENDING' && module.revision_status !== 'APPROVED'; - const filteredContent = content.filter((module) => includesDailyTask(module)); - const filteredContentByPending = content.filter((module) => includesStatusPending(module)); + const filteredContent = content.filter(includesDailyTask); + const filteredContentByPending = content.filter(includesStatusPending); return { filteredContent, @@ -195,3 +189,17 @@ export const generateCohortSyllabusModules = async (id) => { return {}; } }; + +export const getTechonologies = (cohortDays) => { + let technologyTags = []; + + for (let i = 0; i < cohortDays.length; i += 1) { + if (typeof cohortDays[i].technologies === 'string') technologyTags.push(cohortDays[i].technologies); + if (Array.isArray(cohortDays[i].technologies)) { + technologyTags = technologyTags.concat(cohortDays[i].technologies); + } + } + technologyTags = [...new Set(technologyTags)]; + + return technologyTags; +}; diff --git a/src/common/handlers/index.js b/src/common/handlers/index.js index d26e40b85..d9cf65fa5 100644 --- a/src/common/handlers/index.js +++ b/src/common/handlers/index.js @@ -279,36 +279,38 @@ const handlers = { quiz: [], }; - modules?.forEach((module) => { - const { - assignments = [], - lessons = [], - replits = [], - quizzes = [], - } = module; - - const exercisesCount = replits.length; - const lessonsCount = lessons.length; - const projectCount = assignments.length; - const quizzesCount = quizzes.length; - - const assignmentsRecopilatedObj = { - exercisesCount, - lessonsCount, - projectCount, - quizzesCount, - }; - const replitsCompletedFromTask = getCompletedTasksFromModule(replits, taskTodo); - const quizzesCompletedFromTask = getCompletedTasksFromModule(quizzes, taskTodo); - const lessonsCompletedFromTask = getCompletedTasksFromModule(lessons, taskTodo); - const assignmentsCompletedFromTask = getCompletedTasksFromModule(assignments, taskTodo); - assetsCompleted.exercise.push(...replitsCompletedFromTask); - assetsCompleted.lesson.push(...lessonsCompletedFromTask); - assetsCompleted.project.push(...assignmentsCompletedFromTask); - assetsCompleted.quiz.push(...quizzesCompletedFromTask); - - assignmentsRecopilated.push(assignmentsRecopilatedObj); - }); + if (Array.isArray(modules)) { + modules?.forEach((module) => { + const { + assignments = [], + lessons = [], + replits = [], + quizzes = [], + } = module; + + const exercisesCount = replits.length; + const lessonsCount = lessons.length; + const projectCount = assignments.length; + const quizzesCount = quizzes.length; + + const assignmentsRecopilatedObj = { + exercisesCount, + lessonsCount, + projectCount, + quizzesCount, + }; + const replitsCompletedFromTask = getCompletedTasksFromModule(replits, taskTodo); + const quizzesCompletedFromTask = getCompletedTasksFromModule(quizzes, taskTodo); + const lessonsCompletedFromTask = getCompletedTasksFromModule(lessons, taskTodo); + const assignmentsCompletedFromTask = getCompletedTasksFromModule(assignments, taskTodo); + assetsCompleted.exercise.push(...replitsCompletedFromTask); + assetsCompleted.lesson.push(...lessonsCompletedFromTask); + assetsCompleted.project.push(...assignmentsCompletedFromTask); + assetsCompleted.quiz.push(...quizzesCompletedFromTask); + + assignmentsRecopilated.push(assignmentsRecopilatedObj); + }); + } const assignmentsRecopilatedObj = { exercise: 0, diff --git a/src/common/hooks/useCohortHandler.js b/src/common/hooks/useCohortHandler.js index 35417c396..931b78702 100644 --- a/src/common/hooks/useCohortHandler.js +++ b/src/common/hooks/useCohortHandler.js @@ -5,7 +5,9 @@ import useTranslation from 'next-translate/useTranslation'; import { useRouter } from 'next/router'; import useAuth from './useAuth'; import { devLog, getStorageItem } from '../../utils'; -import useAssignments from '../store/actions/cohortAction'; +import useCohortAction from '../store/actions/cohortAction'; +import useModuleHandler from './useModuleHandler'; +import { processRelatedAssignments } from '../handlers/cohorts'; import bc from '../services/breathecode'; import { BREATHECODE_HOST, DOMAIN_NAME } from '../../utils/variables'; @@ -13,7 +15,8 @@ function useCohortHandler() { const router = useRouter(); const { user } = useAuth(); const { t, lang } = useTranslation('dashboard'); - const { setCohortSession, setTaskCohortNull, setSortedAssignments, setUserCapabilities, state } = useAssignments(); + const { setCohortSession, setTaskCohortNull, setSortedAssignments, setUserCapabilities, setMyCohorts, state } = useCohortAction(); + const { cohortProgram, taskTodo, setCohortProgram, setTaskTodo } = useModuleHandler(); const { cohortSession, @@ -49,29 +52,26 @@ function useCohortHandler() { } }; - const getCohortAssignments = ({ - setContextState, slug, cohort, + const getCohortAssignments = async ({ + slug, cohort, }) => { if (user) { - const academyId = cohort.academy.id; - const { version } = cohort.syllabus_version; - const syllabusSlug = cohort?.syllabus_version.slug || slug; + const academyId = cohort?.academy.id; + const version = cohort?.syllabus_version?.version; + const syllabusSlug = cohort?.syllabus_version?.slug || slug; const currentAcademy = user.roles.find((role) => role.academy.id === academyId); if (currentAcademy) { - // Fetch cohortProgram and TaskTodo then apply to contextState (useModuleMap - action) - Promise.all([ - bc.todo({ cohort: cohort.id, limit: 1000 }).getTaskByStudent(), // Tasks with cohort id - bc.syllabus().get(academyId, syllabusSlug, version), // cohortProgram - bc.auth().getRoles(currentAcademy?.role), // Roles - ]).then(( - [taskTodoData, programData, userRoles], - ) => { + // Fetch cohortProgram and TaskTodo then apply to moduleMap store + try { + const [taskTodoData, programData, userRoles] = await Promise.all([ + bc.todo({ cohort: cohort.id, limit: 1000 }).getTaskByStudent(), // Tasks with cohort id + bc.syllabus().get(academyId, syllabusSlug, version), // cohortProgram + bc.auth().getRoles(currentAcademy?.role), // Roles + ]); setUserCapabilities(userRoles.data.capabilities); - setContextState({ - taskTodo: taskTodoData.data.results, - cohortProgram: programData.data, - }); - }).catch((err) => { + setTaskTodo(taskTodoData.data.results); + setCohortProgram(programData.data); + } catch (err) { console.log(err); toast({ position: 'top', @@ -82,98 +82,91 @@ function useCohortHandler() { isClosable: true, }); router.push('/choose-program'); - }); + } } } }; - const handleRedirectToPublicPage = () => { - axios.get(`${BREATHECODE_HOST}/v1/registry/asset/${assetSlug}`) - .then((response) => { - if (response?.data?.asset_type) { - redirectToPublicPage(response.data); - } - }) - .catch(() => { - router.push('/404'); - }); + const handleRedirectToPublicPage = async () => { + try { + const response = await axios.get(`${BREATHECODE_HOST}/v1/registry/asset/${assetSlug}`); + if (response?.data?.asset_type) { + redirectToPublicPage(response.data); + } + } catch (e) { + router.push('/404'); + } }; - const getCohortData = ({ + const getCohortData = async ({ cohortSlug, - }) => new Promise((resolve, reject) => { - // Fetch cohort data with pathName structure - if (cohortSlug && accessToken) { - bc.admissions().me().then(({ data }) => { + }) => { + try { + // Fetch cohort data with pathName structure + if (cohortSlug && accessToken) { + const { data } = await bc.admissions().me(); if (!data) throw new Error('No data'); const { cohorts } = data; + + const parsedCohorts = cohorts.map(((elem) => { + const { cohort, ...cohort_user } = elem; + const { syllabus_version } = cohort; + return { + ...cohort, + selectedProgramSlug: `/cohort/${cohort.slug}/${syllabus_version.slug}/v${syllabus_version.version}`, + cohort_role: elem.role, + cohort_user, + }; + })); + // find cohort with current slug - const findCohort = cohorts.find((c) => c.cohort.slug === cohortSlug); - const currentCohort = findCohort?.cohort; + const currentCohort = parsedCohorts.find((c) => c.slug === cohortSlug); - if (assetSlug && (!currentCohort)) { - handleRedirectToPublicPage(); - } - if (currentCohort) { - const { syllabus_version } = currentCohort; - setCohortSession({ - ...cohortSession, - ...currentCohort, - selectedProgramSlug: `/cohort/${currentCohort.slug}/${syllabus_version.slug}/v${syllabus_version.version}`, - cohort_role: findCohort.role, - cohort_user: { - created_at: findCohort.created_at, - educational_status: findCohort.educational_status, - finantial_status: findCohort.finantial_status, - role: findCohort.role, - }, - }); - resolve(currentCohort); + if (!currentCohort) { + if (assetSlug) return handleRedirectToPublicPage(); + + return router.push('/choose-program'); } - }).catch((error) => { - handleRedirectToPublicPage(); - toast({ - position: 'top', - title: t('alert-message:invalid-cohort-slug'), - // title: 'Invalid cohort slug', - status: 'error', - duration: 7000, - isClosable: true, - }); - reject(error); - setTimeout(() => { - localStorage.removeItem('cohortSession'); - }, 4000); - }); - } else { + + setCohortSession(currentCohort); + setMyCohorts(parsedCohorts); + return currentCohort; + } + + return handleRedirectToPublicPage(); + } catch (error) { handleRedirectToPublicPage(); + toast({ + position: 'top', + title: t('alert-message:invalid-cohort-slug'), + status: 'error', + duration: 7000, + isClosable: true, + }); + return localStorage.removeItem('cohortSession'); } - }); + }; // Sort all data fetched in order of taskTodo - const prepareTasks = ({ - cohortProgram, contextState, nestAssignments, - }) => { - const moduleData = cohortProgram.json?.days || cohortProgram.json?.modules; - const cohort = cohortProgram.json ? moduleData : []; + const prepareTasks = () => { + const moduleData = cohortProgram?.json?.days || cohortProgram?.json?.modules || []; const assignmentsRecopilated = []; devLog('json.days:', moduleData); - if (contextState.cohortProgram.json && contextState.taskTodo) { - cohort.map((assignment) => { + if (cohortProgram?.json && taskTodo) { + moduleData.forEach((assignment) => { const { id, label, description, lessons, replits, assignments, quizzes, } = assignment; if (lessons && replits && assignments && quizzes) { - const nestedAssignments = nestAssignments({ - id, - read: lessons, - practice: replits, - project: assignments, - answer: quizzes, - taskTodo: contextState.taskTodo, - }); - const { modules, filteredModules, filteredModulesByPending } = nestedAssignments; + const nestedAssignments = processRelatedAssignments(assignment, taskTodo); + + // this properties name's reassignment is done to keep compatibility with deprecated functions + const { + content: modules, + filteredContent: filteredModules, + filteredContentByPending: filteredModulesByPending, + } = nestedAssignments; // Data to be sent to [sortedAssignments] = state const assignmentsStruct = { @@ -201,14 +194,12 @@ function useCohortHandler() { ...assignmentsStruct, }); } - - const filterNotEmptyModules = assignmentsRecopilated.filter( - (l) => l.modules.length > 0, - ); - return setSortedAssignments(filterNotEmptyModules); } - return null; }); + const filterNotEmptyModules = assignmentsRecopilated.filter( + (l) => l.modules.length > 0, + ); + setSortedAssignments(filterNotEmptyModules); } }; diff --git a/src/common/hooks/useModuleHandler.js b/src/common/hooks/useModuleHandler.js index 7419fb237..0537f0bbe 100644 --- a/src/common/hooks/useModuleHandler.js +++ b/src/common/hooks/useModuleHandler.js @@ -1,270 +1,166 @@ -import { differenceInDays } from 'date-fns'; +import { useToast } from '@chakra-ui/react'; +import useTranslation from 'next-translate/useTranslation'; +import useModuleMap from '../store/actions/moduleMapAction'; import bc from '../services/breathecode'; import { reportDatalayer } from '../../utils/requests'; -export const updateAssignment = async ({ - t, task, closeSettings, toast, githubUrl, contextState, setContextState, taskStatus, -}) => { - // Task case - const toggleStatus = (task.task_status === undefined || task.task_status === 'PENDING') ? 'DONE' : 'PENDING'; - if (task.task_type && task.task_type !== 'PROJECT') { - const taskToUpdate = { - ...task, - id: task.id, - task_status: toggleStatus, - }; - - try { - await bc.todo({}).update(taskToUpdate); - const keyIndex = contextState.taskTodo.findIndex((x) => x.id === task.id); - setContextState({ - ...contextState, - taskTodo: [ - ...contextState.taskTodo.slice(0, keyIndex), // before keyIndex (inclusive) +function useModuleHandler() { + const { t } = useTranslation('alert-message'); + const toast = useToast(); + const { setTaskTodo, setCohortProgram, state, setCurrentTask, setSubTasks, setNextModule, setPrevModule } = useModuleMap(); + const { taskTodo } = state; + + const updateAssignment = async ({ + task, closeSettings, githubUrl, taskStatus, + }) => { + // Task case + const toggleStatus = (task.task_status === undefined || task.task_status === 'PENDING') ? 'DONE' : 'PENDING'; + if (task.task_type && task.task_type !== 'PROJECT') { + const taskToUpdate = { + ...task, + id: task.id, + task_status: toggleStatus, + }; + + try { + await bc.todo({}).update(taskToUpdate); + const keyIndex = taskTodo.findIndex((x) => x.id === task.id); + setTaskTodo([ + ...taskTodo.slice(0, keyIndex), // before keyIndex (inclusive) taskToUpdate, // key item (updated) - ...contextState.taskTodo.slice(keyIndex + 1), // after keyIndex (exclusive) - ], - }); - toast({ - position: 'top', - title: t('alert-message:assignment-updated'), - status: 'success', - duration: 6000, - isClosable: true, - }); - - closeSettings(); - } catch (error) { - console.log(error); - toast({ - position: 'top', - title: t('alert-message:assignment-update-error'), - status: 'errror', - duration: 5000, - isClosable: true, - }); - closeSettings(); - } - } else { - // Project case - const getProjectUrl = () => { - if (githubUrl) { - return githubUrl; - } - return ''; - }; - - const projectUrl = getProjectUrl(); - - const isDelivering = projectUrl !== ''; - // const linkIsRemoved = task.task_type === 'PROJECT' && !isDelivering; - const taskToUpdate = { - ...task, - task_status: taskStatus || toggleStatus, - github_url: projectUrl, - revision_status: 'PENDING', - delivered_at: new Date(), - }; - - try { - const response = await bc.todo({}).update(taskToUpdate); - // verify if form is equal to the response - if (response.data.github_url === projectUrl) { - const keyIndex = contextState.taskTodo.findIndex((x) => x.id === task.id); - setContextState({ - ...contextState, - taskTodo: [ - ...contextState.taskTodo.slice(0, keyIndex), // before keyIndex (inclusive) - taskToUpdate, // key item (updated) - ...contextState.taskTodo.slice(keyIndex + 1), // after keyIndex (exclusive) - ], - }); - reportDatalayer({ - dataLayer: { - event: 'assignment_status_updated', - task_status: taskStatus, - task_id: task.id, - task_title: task.title, - task_associated_slug: task.associated_slug, - task_type: task.task_type, - task_revision_status: task.revision_status, - }, - }); + ...taskTodo.slice(keyIndex + 1), // after keyIndex (exclusive) + ]); toast({ position: 'top', - // title: `"${res.data.title}" has been updated successfully`, - title: isDelivering - ? t('alert-message:delivery-success') - : t('alert-message:delivery-removed'), + title: t('alert-message:assignment-updated'), status: 'success', duration: 6000, isClosable: true, }); closeSettings(); + } catch (error) { + console.log(error); + toast({ + position: 'top', + title: t('alert-message:assignment-update-error'), + status: 'error', + duration: 5000, + isClosable: true, + }); + closeSettings(); + } + } else { + // Project case + const getProjectUrl = () => { + if (githubUrl) { + return githubUrl; + } + return ''; + }; + + const projectUrl = getProjectUrl(); + + const isDelivering = projectUrl !== ''; + // const linkIsRemoved = task.task_type === 'PROJECT' && !isDelivering; + const taskToUpdate = { + ...task, + task_status: taskStatus || toggleStatus, + github_url: projectUrl, + revision_status: 'PENDING', + delivered_at: new Date(), + }; + + try { + const response = await bc.todo({}).update(taskToUpdate); + // verify if form is equal to the response + if (response.data.github_url === projectUrl) { + const keyIndex = taskTodo.findIndex((x) => x.id === task.id); + setTaskTodo([ + ...taskTodo.slice(0, keyIndex), // before keyIndex (inclusive) + taskToUpdate, // key item (updated) + ...taskTodo.slice(keyIndex + 1), // after keyIndex (exclusive) + ]); + reportDatalayer({ + dataLayer: { + event: 'assignment_status_updated', + task_status: taskStatus, + task_id: task.id, + task_title: task.title, + task_associated_slug: task.associated_slug, + task_type: task.task_type, + task_revision_status: task.revision_status, + }, + }); + toast({ + position: 'top', + title: isDelivering + ? t('alert-message:delivery-success') + : t('alert-message:delivery-removed'), + status: 'success', + duration: 6000, + isClosable: true, + }); + closeSettings(); + } + } catch (error) { + console.log(error); + toast({ + position: 'top', + title: t('alert-message:delivery-error'), + status: 'error', + duration: 5000, + isClosable: true, + }); + closeSettings(); } - } catch (error) { - console.log(error); - toast({ - position: 'top', - title: t('alert-message:delivery-error'), - status: 'errror', - duration: 5000, - isClosable: true, - }); - closeSettings(); } - } -}; + }; -export const startDay = async ({ - t, newTasks, label, contextState, setContextState, toast, customHandler = () => {}, -}) => { - try { - const response = await bc.todo({}).add(newTasks); + const startDay = async ({ + newTasks, label, customHandler = () => {}, + }) => { + try { + const response = await bc.todo({}).add(newTasks); - if (response.status < 400) { + if (response.status < 400) { + toast({ + position: 'top', + title: label + ? t('alert-message:module-started', { title: label }) + : t('alert-message:module-sync-success'), + status: 'success', + duration: 6000, + isClosable: true, + }); + setTaskTodo([ + ...taskTodo, + ...response.data, + ]); + customHandler(); + } + } catch (err) { + console.log('error_ADD_TASK 🔴 ', err); toast({ position: 'top', - title: label - ? t('alert-message:module-started', { title: label }) - : t('alert-message:module-sync-success'), - // title: `Module ${label ? `${label}started` : 'synchronized'} successfully`, - status: 'success', + title: t('alert-message:module-start-error'), + status: 'error', duration: 6000, isClosable: true, }); - setContextState({ - ...contextState, - taskTodo: [ - ...contextState.taskTodo, - ...response.data, - ], - }); - customHandler(); } - } catch (err) { - console.log('error_ADD_TASK 🔴 ', err); - toast({ - position: 'top', - title: t('alert-message:module-start-error'), - status: 'error', - duration: 6000, - isClosable: true, - }); - } -}; - -export const nestAssignments = ({ - id, label = '', read, practice, project, answer, taskTodo = [], -}) => { - const getTaskProps = (slug) => taskTodo.find( - (task) => task.associated_slug === slug, - ); - const currentDate = new Date(); - - const updatedRead = read?.map((el) => ({ - ...el, - id, - label, - task_status: getTaskProps(el.slug)?.task_status || '', - revision_status: getTaskProps(el.slug)?.revision_status || '', - created_at: getTaskProps(el.slug)?.created_at || '', - position: el.position, - type: 'Read', - icon: 'book', - task_type: 'LESSON', - })).sort((a, b) => b.position - a.position); - - const updatedPractice = practice?.map((el) => ({ - ...el, - id, - label, - task_status: getTaskProps(el.slug)?.task_status || '', - revision_status: getTaskProps(el.slug)?.revision_status || '', - created_at: getTaskProps(el.slug)?.created_at || '', - position: el.position, - type: 'Practice', - icon: 'strength', - task_type: 'EXERCISE', - })).sort((a, b) => b.position - a.position); - - const updatedProject = project?.map((el) => { - const taskProps = getTaskProps(el?.slug?.slug || el?.slug); - - return ({ - ...el, - id, - label, - slug: el?.slug?.slug || el?.slug, - task_status: taskProps?.task_status || '', - revision_status: taskProps?.revision_status || '', - created_at: taskProps?.created_at || '', - daysDiff: taskProps?.created_at ? differenceInDays(currentDate, new Date(taskProps?.created_at)) : '', - position: el.position, - mandatory: el.mandatory, - type: 'Project', - icon: 'code', - task_type: 'PROJECT', - }); - }).sort((a, b) => b.position - a.position); - - const updatedAnswer = answer?.map((el) => ({ - ...el, - id, - label, - task_status: getTaskProps(el.slug)?.task_status || '', - revision_status: getTaskProps(el.slug)?.revision_status || '', - created_at: getTaskProps(el.slug)?.created_at || '', - position: el.position, - type: 'Answer', - icon: 'answer', - task_type: 'QUIZ', - })).sort((a, b) => b.position - a.position); - - const modules = [...updatedRead, ...updatedPractice, ...updatedProject, ...updatedAnswer]; - - const includesDailyTask = (module) => { - const getModules = taskTodo.some((task) => task.associated_slug === module.slug); - return getModules; - }; - - const includesStatusPending = (module) => { - const getModules = module.task_status === 'PENDING' && module.revision_status !== 'APPROVED'; - return getModules; }; - const filteredModules = modules.filter((module) => includesDailyTask(module)); - const filteredModulesByPending = modules.filter((module) => includesStatusPending(module)); - return { - filteredModules, - modules, - filteredModulesByPending, + updateAssignment, + startDay, + setTaskTodo, + setCohortProgram, + setCurrentTask, + setSubTasks, + setNextModule, + setPrevModule, + ...state, }; - /* - example: - filteredModules: [{...}, {...}, {...}] - filteredModules: [{...}, {...}] - filteredModules: [] - filteredModules: [] - filteredModules: [{...}] - -------------------------------------------------- - modules: [{...}, {...}, {...}] - modules: [{...}, {...}] - modules: [{...}, {...}, {...}, {...}, {...}] - */ -}; - -export const getTechonologies = (cohortDays) => { - let technologyTags = []; - - for (let i = 0; i < cohortDays.length; i += 1) { - if (typeof cohortDays[i].technologies === 'string') technologyTags.push(cohortDays[i].technologies); - if (Array.isArray(cohortDays[i].technologies)) { - technologyTags = technologyTags.concat(cohortDays[i].technologies); - } - } - technologyTags = [...new Set(technologyTags)]; +} - return technologyTags; -}; +export default useModuleHandler; diff --git a/src/common/hooks/useStyle.js b/src/common/hooks/useStyle.js index b28983673..57d9a0183 100644 --- a/src/common/hooks/useStyle.js +++ b/src/common/hooks/useStyle.js @@ -5,6 +5,7 @@ const useStyle = () => { const backgroundColor = useColorModeValue('white', 'darkTheme'); const backgroundColor2 = useColorModeValue('white', 'gray.700'); const backgroundColor3 = useColorModeValue('gray.light2', 'gray.800'); + const backgroundColor4 = useColorModeValue('#F4FAFF', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); const borderColor2 = useColorModeValue('gray.200', 'featuredDark'); const borderColorStrong = useColorModeValue('gray.400', 'gray.500'); @@ -47,6 +48,7 @@ const useStyle = () => { lightColor: useColorModeValue('#F5F5F5', '#4A5568'), lightColor2: useColorModeValue('#F5F5F5', '#283340'), lightColor3: useColorModeValue('#F5F5F5', '#17202A'), + lightColor4: useColorModeValue('#F0F2F5', '#4A5568'), white2: useColorModeValue('#ffffff', '#283340'), danger: useColorModeValue('#CD0000', '#e26161'), blueDefault: '#0097CD', @@ -54,6 +56,7 @@ const useStyle = () => { green: '#38A56A', greenLight: '#25BF6C', greenLight2: '#A4FFBD', + greenLight3: '#D7FFE2', fontColor2: useColorModeValue('#3A3A3A', '#EBEBEB'), successLight: useColorModeValue('#e9ffef', '#A4FFBD'), }; @@ -72,6 +75,7 @@ const useStyle = () => { backgroundColor, backgroundColor2, backgroundColor3, + backgroundColor4, borderColor, borderColor2, borderColorStrong, diff --git a/src/common/services/breathecode.js b/src/common/services/breathecode.js index a83f1463b..761f04fdc 100644 --- a/src/common/services/breathecode.js +++ b/src/common/services/breathecode.js @@ -162,6 +162,7 @@ const breathecode = { // getTaskByStudent: (cohortId) => axios.get(`${url}/user/me/task?cohort=${cohortId}`), getTaskByStudent: () => axios.get(`${url}/user/me/task${qs}`), add: (args) => axios.post(`${url}/user/me/task`, args), + postCompletionJob: (taskId) => axios.post(`${url}/completion_job/${taskId}`), // delete: (id, args) => axios.delete(`${url}/user/${id}/task/${args.id}`, args), update: (args) => axios.put(`${url}/task/${args.id}`, args), updateBulk: (args) => axios.put(`${url}/user/me/task`, args), @@ -365,6 +366,7 @@ const breathecode = { const qs = parseQuerys(query); return { completionJob: (data) => axios.post(`${url}/prompting/completion/43${qs}`, data), + meToken: (token) => axios.get(`${url}/auth/me/token?breathecode_token=${token}`), }; }, }; diff --git a/src/common/store/actions/cohortAction.js b/src/common/store/actions/cohortAction.js index 60352200e..1f188bcff 100644 --- a/src/common/store/actions/cohortAction.js +++ b/src/common/store/actions/cohortAction.js @@ -1,5 +1,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { + SET_MY_COHORTS, SET_COHORT_SESSION, SET_SORTED_ASSIGNMENTS, SET_TASK_COHORT_NULL, @@ -7,11 +8,20 @@ import { } from '../types'; import { usePersistent } from '../../hooks/usePersistent'; -const useCohort = () => { +const useCohortAction = () => { const dispatch = useDispatch(); const [, persistCohortSession] = usePersistent('cohortSession', {}); const state = useSelector((reducerState) => reducerState.cohortReducer); + const setMyCohorts = (payload) => { + dispatch({ + type: SET_MY_COHORTS, + payload: { + myCohorts: payload, + }, + }); + }; + const setCohortSession = (payload) => { dispatch({ type: SET_COHORT_SESSION, @@ -51,6 +61,7 @@ const useCohort = () => { return { state, + setMyCohorts, setCohortSession, setTaskCohortNull, setSortedAssignments, @@ -58,4 +69,4 @@ const useCohort = () => { }; }; -export default useCohort; +export default useCohortAction; diff --git a/src/common/store/actions/moduleMapAction.js b/src/common/store/actions/moduleMapAction.js index 45ee687dd..f89770730 100644 --- a/src/common/store/actions/moduleMapAction.js +++ b/src/common/store/actions/moduleMapAction.js @@ -2,42 +2,58 @@ import { useDispatch, useSelector } from 'react-redux'; const useModuleMap = () => { const dispatch = useDispatch(); - const modules = useSelector((state) => state.moduleMapReducer.modules); - const contextState = useSelector((state) => state.moduleMapReducer.contextState); - const updateModuleStatus = (module) => { - const changedModules = modules.map((m, index) => { - if (index === module.index) { - return { - ...m, status: module.status, - }; - } - return m; + const state = useSelector((reducerState) => reducerState.moduleMapReducer); + + const setTaskTodo = (newState) => { + dispatch({ + type: 'CHANGE_TASK_TO_DO', + payload: newState, }); + }; + + const setCohortProgram = (newState) => { dispatch({ - type: 'CHANGE_STATUS', - payload: changedModules, + type: 'CHANGE_COHORT_PROGRAM', + payload: newState, }); }; - const setContextState = (newState) => { + const setCurrentTask = (newState) => { dispatch({ - type: 'CHANGE_CONTEXT_STATE', + type: 'CHANGE_CURRENT_TASK', payload: newState, }); }; - // const changeSingleTask = (newState) => { - // dispatch({ - // type: 'CHANGE_SINGLE_TASK_STATUS', - // payload: newState, - // }); - // }; + const setSubTasks = (newState) => { + dispatch({ + type: 'CHANGE_SUB_TASKS', + payload: newState, + }); + }; + + const setNextModule = (payload) => { + dispatch({ + type: 'CHANGE_NEXT_MODULE', + payload, + }); + }; + + const setPrevModule = (payload) => { + dispatch({ + type: 'CHANGE_PREV_MODULE', + payload, + }); + }; + return { - modules, - contextState, - setContextState, - updateModuleStatus, - // changeSingleTask, + setTaskTodo, + setCohortProgram, + setCurrentTask, + setSubTasks, + setNextModule, + setPrevModule, + state, }; }; diff --git a/src/common/store/reducers/cohortReducer.js b/src/common/store/reducers/cohortReducer.js index 02a92cd34..05a430257 100644 --- a/src/common/store/reducers/cohortReducer.js +++ b/src/common/store/reducers/cohortReducer.js @@ -1,4 +1,5 @@ import { + SET_MY_COHORTS, SET_COHORT_SESSION, SET_SORTED_ASSIGNMENTS, SET_TASK_COHORT_NULL, @@ -6,6 +7,7 @@ import { } from '../types'; const initialState = { + myCohorts: [], cohortSession: {}, sortedAssignments: [], taskCohortNull: [], @@ -14,6 +16,13 @@ const initialState = { const cohortHandlerReducer = (state = initialState, action) => { switch (action.type) { + case SET_MY_COHORTS: { + const { myCohorts } = action.payload; + return { + ...state, + myCohorts, + }; + } case SET_COHORT_SESSION: { const { cohortSession } = action.payload; return { diff --git a/src/common/store/reducers/moduleMapReducer.js b/src/common/store/reducers/moduleMapReducer.js index 0b0b6d79f..5a036bc36 100644 --- a/src/common/store/reducers/moduleMapReducer.js +++ b/src/common/store/reducers/moduleMapReducer.js @@ -1,41 +1,43 @@ const initialState = { - modules: [ - { - title: 'Read', - text: 'Introduction to the pre-work', - icon: 'verified', - status: 'inactive', - }, - { - title: 'Practice', - text: 'Practice pre-work', - icon: 'book', - status: 'active', - }, - { - title: 'Practice', - text: 'Star wars', - icon: 'verified', - status: 'finished', - }, - ], - contextState: { - cohortProgram: [], - taskTodo: [], - }, + cohortProgram: {}, + taskTodo: [], + currentTask: null, + subTasks: [], + nextModule: null, + prevModule: null, }; const moduleMapReducer = (state = initialState, action) => { switch (action.type) { - case 'CHANGE_STATUS': + case 'CHANGE_TASK_TO_DO': return { ...state, - modules: action.payload, + taskTodo: action.payload, }; - case 'CHANGE_CONTEXT_STATE': + case 'CHANGE_COHORT_PROGRAM': return { ...state, - contextState: action.payload, + cohortProgram: action.payload, + }; + case 'CHANGE_CURRENT_TASK': + return { + ...state, + currentTask: action.payload, + }; + case 'CHANGE_SUB_TASKS': + return { + ...state, + subTasks: action.payload, + }; + case 'CHANGE_NEXT_MODULE': + return { + ...state, + nextModule: action.payload, + }; + case 'CHANGE_PREV_MODULE': + return { + ...state, + prevModule: action.payload, }; default: return state; diff --git a/src/common/store/types/index.js b/src/common/store/types/index.js index 6dc448b9c..ddc6451a9 100644 --- a/src/common/store/types/index.js +++ b/src/common/store/types/index.js @@ -22,6 +22,7 @@ const SET_PAYMENT_STATUS = 'SET_PAYMENT_STATUS'; const SET_SUBMITTING_CARD = 'SET_SUBMITTING_CARD'; const SET_SUBMITTING_PAYMENT = 'SET_SUBMITTING_PAYMENT'; const SET_SELF_APPLIED_COUPON = 'SET_SELF_APPLIED_COUPON'; +const SET_MY_COHORTS = 'SET_MY_COHORTS'; const SET_COHORT_SESSION = 'SET_COHORT_SESSION'; const SET_SORTED_ASSIGNMENTS = 'SET_SORTED_ASSIGNMENTS'; const SET_TASK_COHORT_NULL = 'SET_TASK_COHORT_NULL'; @@ -55,6 +56,7 @@ export { SET_SERVICE_PROPS, SET_SELECTED_SERVICE, SET_PAYMENT_METHODS, + SET_MY_COHORTS, SET_COHORT_SESSION, SET_SORTED_ASSIGNMENTS, SET_TASK_COHORT_NULL, diff --git a/src/js_modules/chooseProgram/Programs.jsx b/src/js_modules/chooseProgram/Programs.jsx index 7ed69ff3d..69c084d28 100644 --- a/src/js_modules/chooseProgram/Programs.jsx +++ b/src/js_modules/chooseProgram/Programs.jsx @@ -8,11 +8,10 @@ import axios from '../../axios'; import useProgramList from '../../common/store/actions/programListAction'; function Programs({ item, onOpenModal, setLateModalProps }) { - const { state, setCohortSession } = useCohortHandler(); - const { cohortSession } = state; + const { setCohortSession } = useCohortHandler(); const [isLoadingPageContent, setIsLoadingPageContent] = useState(false); const { programsList } = useProgramList(); - const { cohort } = item; + const { cohort, ...cohortUser } = item; const signInDate = item.created_at; const { version, slug } = cohort.syllabus_version; const currentCohortProps = programsList[cohort.slug]; @@ -44,7 +43,8 @@ function Programs({ item, onOpenModal, setLateModalProps }) { axios.defaults.headers.common.Academy = cohort.academy.id; setCohortSession({ ...cohort, - ...cohortSession, + cohort_role: cohortUser?.role, + cohort_user: cohortUser, selectedProgramSlug: `/cohort/${cohort?.slug}/${slug}/v${version}`, }); router.push(`/cohort/${cohort?.slug}/${slug}/v${version}`); diff --git a/src/js_modules/moduleMap/taskHandler.jsx b/src/js_modules/moduleMap/ButtonHandlerByTaskStatus.jsx similarity index 52% rename from src/js_modules/moduleMap/taskHandler.jsx rename to src/js_modules/moduleMap/ButtonHandlerByTaskStatus.jsx index 740cf6464..c446a181b 100644 --- a/src/js_modules/moduleMap/taskHandler.jsx +++ b/src/js_modules/moduleMap/ButtonHandlerByTaskStatus.jsx @@ -1,20 +1,17 @@ -/* eslint-disable react/no-unstable-nested-components */ import { - Button, + Button, Tooltip, } from '@chakra-ui/react'; -import useTranslation from 'next-translate/useTranslation'; import PropTypes from 'prop-types'; import { useState } from 'react'; import useStyle from '../../common/hooks/useStyle'; import ReviewModal from '../../common/components/ReviewModal'; import Icon from '../../common/components/Icon'; -import PopoverTaskHandler, { IconByTaskStatus, TextByTaskStatus } from '../../common/components/PopoverTaskHandler'; +import PopoverTaskHandler, { IconByTaskStatus, textByTaskStatus } from '../../common/components/PopoverTaskHandler'; export function ButtonHandlerByTaskStatus({ onlyPopoverDialog, currentTask, sendProject, changeStatusAssignment, toggleSettings, closeSettings, - settingsOpen, allowText, onClickHandler, currentAssetData, fileData, handleOpen, + settingsOpen, allowText, onClickHandler, currentAssetData, fileData, handleOpen, isGuidedExperience, }) { - const { t } = useTranslation('dashboard'); const { hexColor } = useStyle(); const [isReviewModalOpen, setIsReviewModalOpen] = useState(false); const [loaders, setLoaders] = useState({ @@ -29,47 +26,6 @@ export function ButtonHandlerByTaskStatus({ const noDeliveryFormat = deliveryFormatExists && currentAssetData?.delivery_formats.includes('no_delivery'); const isButtonDisabled = currentTask === null || taskIsApproved; - function TaskButton() { - return ( - - ); - } - const openAssignmentFeedbackModal = () => { setIsReviewModalOpen(true); setLoaders((prevState) => ({ @@ -78,12 +34,48 @@ export function ButtonHandlerByTaskStatus({ })); }; - function OpenModalButton() { - return ( - <> - {currentTask?.description && ( + const handleTaskButton = (event) => { + if (currentTask) { + setLoaders((prevState) => ({ + ...prevState, + isChangingTaskStatus: true, + })); + changeStatusAssignment(event, currentTask) + .finally(() => { + setLoaders((prevState) => ({ + ...prevState, + isChangingTaskStatus: false, + })); + onClickHandler(); + }); + } + }; + + const textAndIcon = textByTaskStatus(currentTask || {}); + + // PRROJECT CASE + if (currentTask && currentTask.task_type === 'PROJECT' && currentTask.task_status) { + if ((currentTask.task_status === 'DONE' || taskIsApprovedOrRejected) && !onlyPopoverDialog && !isGuidedExperience) { + return ( + <> + {currentTask?.description && ( + + )} - )} - - - ); - } - - // PRROJECT CASE - if (currentTask && currentTask.task_type === 'PROJECT' && currentTask.task_status) { - if ((currentTask.task_status === 'DONE' || taskIsApprovedOrRejected) && !onlyPopoverDialog) { - return ( - <> - setIsReviewModalOpen(false)} @@ -153,6 +122,7 @@ export function ButtonHandlerByTaskStatus({ } return ( ); } + + if (isGuidedExperience) { + return ( + + + + ); + } + return ( - + ); } @@ -182,6 +197,7 @@ ButtonHandlerByTaskStatus.propTypes = { currentAssetData: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), fileData: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), onlyPopoverDialog: PropTypes.bool, + isGuidedExperience: PropTypes.bool, }; ButtonHandlerByTaskStatus.defaultProps = { currentTask: null, @@ -192,4 +208,5 @@ ButtonHandlerByTaskStatus.defaultProps = { toggleSettings: () => {}, handleOpen: () => {}, onlyPopoverDialog: false, + isGuidedExperience: false, }; diff --git a/src/js_modules/moduleMap/index.jsx b/src/js_modules/moduleMap/index.jsx index 5477cde93..8038fa0f0 100644 --- a/src/js_modules/moduleMap/index.jsx +++ b/src/js_modules/moduleMap/index.jsx @@ -1,22 +1,25 @@ import { memo } from 'react'; import { - Box, Button, Heading, useColorModeValue, useToast, + Box, Button, Heading, useColorModeValue, } from '@chakra-ui/react'; import useTranslation from 'next-translate/useTranslation'; import PropTypes from 'prop-types'; import Text from '../../common/components/Text'; import Module from './module'; -import { startDay } from '../../common/hooks/useModuleHandler'; +import useModuleHandler from '../../common/hooks/useModuleHandler'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; import Icon from '../../common/components/Icon'; import { reportDatalayer } from '../../utils/requests'; function ModuleMap({ - index, userId, contextState, setContextState, slug, modules, filteredModules, - title, description, taskTodo, cohortData, taskCohortNull, filteredModulesByPending, + index, slug, modules, filteredModules, + title, description, cohortData, filteredModulesByPending, showPendingTasks, searchValue, existsActivities, }) { const { t } = useTranslation('dashboard'); - const toast = useToast(); + const { startDay } = useModuleHandler(); + const { state } = useCohortHandler(); + const { taskCohortNull } = state; const commonBorderColor = useColorModeValue('gray.200', 'gray.900'); const currentModules = showPendingTasks ? filteredModulesByPending : filteredModules; const cohortId = cohortData?.id || cohortData?.cohort_id; @@ -37,12 +40,7 @@ function ModuleMap({ }, }); startDay({ - t, - id: userId, newTasks: updatedTasks, - contextState, - setContextState, - toast, }); }; @@ -115,7 +113,6 @@ function ModuleMap({ key={`${module.title}-${cheatedIndex}`} currIndex={i} data={module} - taskTodo={taskTodo} /> ); }) : ( @@ -155,17 +152,12 @@ function ModuleMap({ ModuleMap.propTypes = { index: PropTypes.number.isRequired, - userId: PropTypes.number.isRequired, - contextState: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired, - setContextState: PropTypes.func.isRequired, title: PropTypes.string, slug: PropTypes.string, modules: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))), filteredModules: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))), description: PropTypes.string, - taskTodo: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))), cohortData: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), - taskCohortNull: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))), filteredModulesByPending: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))), showPendingTasks: PropTypes.bool, searchValue: PropTypes.string, @@ -177,9 +169,7 @@ ModuleMap.defaultProps = { title: 'HTML/CSS/Bootstrap', slug: 'html-css-bootstrap', description: '', - taskTodo: [], cohortData: {}, - taskCohortNull: [], filteredModulesByPending: [], showPendingTasks: false, searchValue: '', diff --git a/src/js_modules/moduleMap/module.jsx b/src/js_modules/moduleMap/module.jsx index e94b2c8cd..ca7cd89fc 100644 --- a/src/js_modules/moduleMap/module.jsx +++ b/src/js_modules/moduleMap/module.jsx @@ -7,12 +7,11 @@ import { import useTranslation from 'next-translate/useTranslation'; import PropTypes from 'prop-types'; import { useState, memo } from 'react'; -import { updateAssignment } from '../../common/hooks/useModuleHandler'; +import useModuleHandler from '../../common/hooks/useModuleHandler'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; import useStyle from '../../common/hooks/useStyle'; -import useModuleMap from '../../common/store/actions/moduleMapAction'; -import { ButtonHandlerByTaskStatus } from './taskHandler'; +import { ButtonHandlerByTaskStatus } from './ButtonHandlerByTaskStatus'; import ModuleComponent from '../../common/components/Module'; -import { isWindow } from '../../utils/index'; import bc from '../../common/services/breathecode'; import ShareButton from '../../common/components/ShareButton'; import Icon from '../../common/components/Icon'; @@ -20,11 +19,13 @@ import { reportDatalayer } from '../../utils/requests'; // import { usePersistent } from '../../common/hooks/usePersistent'; function Module({ - data, taskTodo, currIndex, isDisabled, onDisabledClick, variant, + data, currIndex, isDisabled, onDisabledClick, variant, }) { const { t, lang } = useTranslation('dashboard'); const [settingsOpen, setSettingsOpen] = useState(false); - const { contextState, setContextState } = useModuleMap(); + const { taskTodo, updateAssignment } = useModuleHandler(); + const { state } = useCohortHandler(); + const { cohortSession } = state; const [currentAssetData, setCurrentAssetData] = useState(null); const [fileData, setFileData] = useState(null); const [, setUpdatedTask] = useState(null); @@ -79,8 +80,6 @@ function Module({ }, ]; - const cohortSession = isWindow ? JSON.parse(localStorage.getItem('cohortSession') || '{}') : {}; - const closeSettings = () => { setSettingsOpen(false); }; @@ -151,7 +150,7 @@ function Module({ ...task, }); await updateAssignment({ - t, task, taskStatus, closeSettings, toast, contextState, setContextState, + task, taskStatus, closeSettings, }); } }; @@ -161,7 +160,7 @@ function Module({ }) => { setShowModal(true); await updateAssignment({ - t, task, closeSettings, toast, githubUrl, taskStatus, contextState, setContextState, + task, closeSettings, githubUrl, taskStatus, }); }; @@ -251,7 +250,6 @@ function Module({ Module.propTypes = { data: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), currIndex: PropTypes.number, - taskTodo: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any]))).isRequired, isDisabled: PropTypes.bool, onDisabledClick: PropTypes.func, variant: PropTypes.string, diff --git a/src/js_modules/navbar/MobileNav.jsx b/src/js_modules/navbar/MobileNav.jsx index d38713eee..484c6d9f9 100644 --- a/src/js_modules/navbar/MobileNav.jsx +++ b/src/js_modules/navbar/MobileNav.jsx @@ -8,14 +8,13 @@ import { import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; import useTranslation from 'next-translate/useTranslation'; +import { useRouter } from 'next/router'; import Icon from '../../common/components/Icon'; import MobileItem from './MobileItem'; import LanguageSelector from '../../common/components/LanguageSelector'; -// import syllabusList from '../../../public/syllabus.json'; import NextChakraLink from '../../common/components/NextChakraLink'; -// import UpgradeExperience from '../../common/components/UpgradeExperience'; import useStyle from '../../common/hooks/useStyle'; -// import UpgradeExperience from '../../common/components/UpgradeExperience'; +import { setStorageItem } from '../../utils'; function MobileNav({ // eslint-disable-next-line no-unused-vars @@ -24,6 +23,7 @@ function MobileNav({ const [privateItems, setPrivateItems] = useState([]); const { colorMode, toggleColorMode } = useColorMode(); const { t } = useTranslation('navbar'); + const router = useRouter(); const commonColors = useColorModeValue('white', 'gray.800'); // const readSyllabus = JSON.parse(syllabusList); const prismicRef = process.env.PRISMIC_REF; @@ -100,6 +100,7 @@ function MobileNav({ setStorageItem('redirect', router?.asPath)} fontSize="16px" lineHeight="22px" margin="0" diff --git a/src/js_modules/projects/ProjectList.jsx b/src/js_modules/projects/ProjectList.jsx index ce0b6e172..297711194 100644 --- a/src/js_modules/projects/ProjectList.jsx +++ b/src/js_modules/projects/ProjectList.jsx @@ -36,8 +36,8 @@ const ProjectList = forwardRef(({ const breakpointColumnsObj = { default: 3, 1100: 3, - 700: 2, - 500: 1, + 880: 2, + 590: 1, }; return ( diff --git a/src/js_modules/syllabus/ExerciseGuidedExperience.jsx b/src/js_modules/syllabus/ExerciseGuidedExperience.jsx new file mode 100644 index 000000000..4c6886fa9 --- /dev/null +++ b/src/js_modules/syllabus/ExerciseGuidedExperience.jsx @@ -0,0 +1,246 @@ +import { useState, useEffect } from 'react'; +import { + Box, Button, +} from '@chakra-ui/react'; +import useTranslation from 'next-translate/useTranslation'; +import PropTypes from 'prop-types'; +import { intervalToDuration } from 'date-fns'; +import modifyEnv from '../../../modifyEnv'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; +import useStyle from '../../common/hooks/useStyle'; +import ReactPlayerV2 from '../../common/components/ReactPlayerV2'; +import KPI from '../../common/components/KPI'; +import MarkDownParser from '../../common/components/MarkDownParser'; +import NextChakraLink from '../../common/components/NextChakraLink'; +import Heading from '../../common/components/Heading'; +import Text from '../../common/components/Text'; +import SimpleModal from '../../common/components/SimpleModal'; +import Icon from '../../common/components/Icon'; +import { intervalToHours } from '../../utils'; + +function ExerciseGuidedExperience({ currentTask, currentAsset }) { + const { t } = useTranslation('syllabus'); + const { colorMode } = useStyle(); + const { state } = useCohortHandler(); + const { cohortSession } = state; + const [showCloneModal, setShowCloneModal] = useState(false); + const [telemetryReport, setTelemetryReport] = useState([]); + const BREATHECODE_HOST = modifyEnv({ queryString: 'host', env: process.env.BREATHECODE_HOST }); + + const isExerciseStated = !!currentTask?.assignment_telemetry; + + useEffect(() => { + if (isExerciseStated) { + const { steps, workout_session: workoutSession, last_interaction_at: lastInteractionAt } = currentTask.assignment_telemetry; + const completedSteps = steps.reduce((acum, elem) => { + if (elem.completed_at) return acum + 1; + return acum; + }, 0); + + const compilations = []; + const tests = []; + let successfulCompilations = 0; + let successfulTests = 0; + let errors = 0; + + steps.forEach((step) => { + compilations.push(...step.compilations); + tests.push(...step.tests); + }); + + compilations.forEach((comp) => { + if (comp > 0) errors += 1; + else successfulCompilations += 1; + }); + + tests.forEach((comp) => { + if (comp > 0) errors += 1; + else successfulTests += 1; + }); + + const completionPercentage = (completedSteps * 100) / steps.length; + const roundedPercentage = Math.round((completionPercentage + Number.EPSILON) * 100) / 100; + + const totalHours = workoutSession.reduce((acum, elem) => { + const startedAt = elem.started_at; + const endedAt = elem.ended_at || lastInteractionAt; + + const duration = intervalToDuration({ + start: new Date(startedAt), + end: new Date(endedAt), + }); + + const hours = intervalToHours(duration); + + return acum + hours; + }, 0); + + const roundedHours = Math.round((totalHours + Number.EPSILON) * 100) / 100; + setTelemetryReport([{ + label: t('completion-percentage'), + icon: 'graph-up', + value: `${roundedPercentage}%`, + }, { + label: t('total-steps'), + icon: 'list', + value: `${completedSteps}/${steps.length}`, + }, { + label: t('total-time'), + icon: 'clock', + value: `${roundedHours} hs`, + }, { + label: t('successful-compiles'), + icon: 'documentVerified', + value: successfulCompilations, + }, { + label: t('successful-tests'), + icon: 'sync-success', + value: successfulTests, + }, { + label: t('total-errors'), + icon: 'sync-error', + value: errors, + }]); + } + }, [currentTask]); + + const token = localStorage.getItem('accessToken'); + + const newWorkspace = `${BREATHECODE_HOST}/v1/provisioning/me/container/new?token=${token}&cohort=${cohortSession?.id}&repo=${currentAsset?.url}`; + const continueWorkSpace = `${BREATHECODE_HOST}/v1/provisioning/me/workspaces?token=${token}&cohort=${cohortSession?.id}&repo=${currentAsset?.url}`; + + const urlToClone = currentAsset?.url || currentAsset?.readme_url?.split('/blob')?.[0]; + const repoName = urlToClone?.split('/')?.pop(); + + return ( + + + + + + {currentAsset?.title} + + + {currentAsset?.description} + + + + + + + {isExerciseStated && ( + + {telemetryReport.map((elem) => ( + + ))} + + )} + + + + + + + {t('common:learnpack.title')} + + + + + + + + {t('common:learnpack.open-in-learnpack-button.text')} + + + + + { + setShowCloneModal(false); + }} + headerStyles={{ + textAlign: 'center', + textTransform: 'uppercase', + }} + bodyStyles={{ + className: 'markdown-body', + padding: { base: '10px 30px' }, + }} + > + + + + ); +} + +ExerciseGuidedExperience.propTypes = { + currentTask: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), + currentAsset: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), +}; + +ExerciseGuidedExperience.defaultProps = { + currentTask: null, + currentAsset: null, +}; + +export default ExerciseGuidedExperience; diff --git a/src/js_modules/syllabus/GuidedExperienceSidebar.jsx b/src/js_modules/syllabus/GuidedExperienceSidebar.jsx new file mode 100644 index 000000000..144f33641 --- /dev/null +++ b/src/js_modules/syllabus/GuidedExperienceSidebar.jsx @@ -0,0 +1,182 @@ +/* eslint-disable no-unused-vars */ +import { useState } from 'react'; +import { + Box, + Button, + Img, + Spinner, + useColorModeValue, + Divider, +} from '@chakra-ui/react'; +import PropTypes from 'prop-types'; +import useTranslation from 'next-translate/useTranslation'; +import Heading from '../../common/components/Heading'; +import { Config, getSlideProps } from './config'; +import Timeline from '../../common/components/Timeline'; +import NextChakraLink from '../../common/components/NextChakraLink'; +import Text from '../../common/components/Text'; +import Icon from '../../common/components/Icon'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; +import useStyle from '../../common/hooks/useStyle'; + +function GuidedExperienceSidebar({ onClickAssignment, isOpen, onToggle, currentModuleIndex, handleStartDay }) { + const { t } = useTranslation('syllabus'); + const [moduleLoading, setModuleLoading] = useState(false); + const { state } = useCohortHandler(); + const { cohortSession, sortedAssignments } = state; + const background = useColorModeValue('#E4E8EE', '#283340'); + + const Open = !isOpen; + const { height, display, position, zIndex, ...slideStyles } = getSlideProps(Open); + const { + currentThemeValue, + } = Config(); + const { hexColor } = useStyle(); + + const currentModule = sortedAssignments[currentModuleIndex]; + const nextModule = sortedAssignments[currentModuleIndex + 1]; + + const openNextModule = async () => { + try { + const nextAssignments = nextModule.filteredModules; + if (nextAssignments.length === 0) { + setModuleLoading(true); + await handleStartDay(nextModule, true); + setModuleLoading(false); + } + const assignment = nextModule.modules[0]; + onClickAssignment(null, assignment); + } catch (e) { + console.log(e); + setModuleLoading(false); + } + }; + + return ( + <> + + + {cohortSession?.syllabus_version && ( + + {cohortSession?.syllabus_version?.logo && ( + + )} + {cohortSession.syllabus_version?.name} + + )} + + + + + {currentModule ? ( + <> + + + + {' '} + {currentModule.label && ( + + {currentModule.label.toUpperCase()} + + )} + + + + + {nextModule && ( + + )} + + ) : ( + + + + )} + + + + ); +} + +GuidedExperienceSidebar.propTypes = { + onClickAssignment: PropTypes.func, + isOpen: PropTypes.bool, + onToggle: PropTypes.func, + currentModuleIndex: PropTypes.number, + handleStartDay: PropTypes.func.isRequired, +}; +GuidedExperienceSidebar.defaultProps = { + onClickAssignment: () => {}, + isOpen: false, + onToggle: () => {}, + currentModuleIndex: null, +}; + +export default GuidedExperienceSidebar; diff --git a/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx b/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx new file mode 100644 index 000000000..bbb9dad22 --- /dev/null +++ b/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx @@ -0,0 +1,236 @@ +import React, { useState, useRef, useEffect } from 'react'; +import useTranslation from 'next-translate/useTranslation'; +import PropTypes from 'prop-types'; +import { Box, Button, useColorModeValue } from '@chakra-ui/react'; +import TaskCodeRevisions from './TaskCodeRevisions'; +import useModuleHandler from '../../common/hooks/useModuleHandler'; +import useStyle from '../../common/hooks/useStyle'; +import SubTasks from '../../common/components/MarkDownParser/SubTasks'; +import ReactPlayerV2 from '../../common/components/ReactPlayerV2'; +import Heading from '../../common/components/Heading'; +import Text from '../../common/components/Text'; +import Icon from '../../common/components/Icon'; + +function ProjectHeading({ currentAsset, isDelivered }) { + const { backgroundColor4, hexColor } = useStyle(); + const { subTasks } = useModuleHandler(); + + const title = currentAsset?.title; + const assetType = currentAsset?.asset_type; + const assetTypeIcons = { + LESSON: 'book', + EXERCISE: 'strength', + PROJECT: 'code', + QUIZ: 'answer', + }; + + return ( + <> + + + + + + + {title} + + + {currentAsset?.description && ( + + {currentAsset.description} + + )} + + + {Array.isArray(subTasks) && subTasks?.length > 0 && ( + + )} + {isDelivered && ( + + + + )} + + + {!isDelivered && ( + + )} + + + ); +} + +function ProjectBoardGuidedExperience({ currentAsset }) { + const { t } = useTranslation('syllabus'); + const { currentTask } = useModuleHandler(); + const headerRef = useRef(null); + const [isHeaderVisible, setIsHeaderVisible] = useState(true); + const { backgroundColor4, hexColor, backgroundColor, featuredLight } = useStyle(); + + const title = currentAsset?.title; + const assetType = currentAsset?.asset_type; + + const isDelivered = currentTask?.task_status === 'DONE'; + + const assetTypeIcons = { + LESSON: 'book', + EXERCISE: 'strength', + PROJECT: 'code', + QUIZ: 'answer', + }; + + const scrollTop = () => { + const markdownBody = document.getElementById('main-container'); + markdownBody.scroll({ + top: 0, + behavior: 'smooth', + }); + }; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + setIsHeaderVisible(entry.isIntersecting); + }, + { + root: document.querySelector('.scrollable-container'), + threshold: 0, + }, + ); + + if (headerRef.current) { + observer.observe(headerRef.current); + } + + return () => { + if (headerRef.current) { + observer.unobserve(headerRef.current); + } + }; + }, []); + + return ( + <> + + + + {isDelivered && ( + + + {t('teachers-feedback')} + + {currentTask.description ? ( + + + {currentTask.description} + + + ) : ( + <> + + {t('no-feedback')} + + + {t('task-notification')} + + + )} + + )} + + {isDelivered && ( + + )} + + + + + + {title} + + + + + + ); +} + +ProjectBoardGuidedExperience.propTypes = { + currentAsset: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), +}; +ProjectBoardGuidedExperience.defaultProps = { + currentAsset: null, +}; + +ProjectHeading.propTypes = { + currentAsset: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), + isDelivered: PropTypes.bool, +}; +ProjectHeading.defaultProps = { + currentAsset: null, + isDelivered: false, +}; + +export default ProjectBoardGuidedExperience; diff --git a/src/js_modules/syllabus/SyllabusMarkdownComponent.jsx b/src/js_modules/syllabus/SyllabusMarkdownComponent.jsx index def1c4697..ddc40ac45 100644 --- a/src/js_modules/syllabus/SyllabusMarkdownComponent.jsx +++ b/src/js_modules/syllabus/SyllabusMarkdownComponent.jsx @@ -5,7 +5,7 @@ import { MDSkeleton } from '../../common/components/Skeleton'; function SyllabusMarkdownComponent({ ipynbHtmlUrl, readme, currentBlankProps, callToActionProps, currentData, lesson, - quizSlug, lessonSlug, currentTask, alerMessage, + quizSlug, lessonSlug, currentTask, alerMessage, isGuidedExperience, }) { const { t } = useTranslation('syllabus'); const blankText = t('blank-page', { url: currentBlankProps?.url }); @@ -16,6 +16,8 @@ function SyllabusMarkdownComponent({ content={readme.content} callToActionProps={callToActionProps} withToc={lesson?.toLowerCase() === 'read'} + showContentHeading={!(currentData.asset_type === 'PROJECT' && isGuidedExperience)} + isGuidedExperience={isGuidedExperience} frontMatter={{ title: currentData.title, // subtitle: currentData.description, @@ -33,6 +35,7 @@ function SyllabusMarkdownComponent({ content={blankText} callToActionProps={callToActionProps} withToc={lesson?.toLowerCase() === 'read'} + isGuidedExperience={isGuidedExperience} frontMatter={{ title: currentBlankProps?.title, // subtitle: currentBlankProps.description, diff --git a/src/js_modules/syllabus/TaskCodeRevisions.jsx b/src/js_modules/syllabus/TaskCodeRevisions.jsx new file mode 100644 index 000000000..6dbaceac2 --- /dev/null +++ b/src/js_modules/syllabus/TaskCodeRevisions.jsx @@ -0,0 +1,306 @@ +import { Box, Button, Divider, Flex, Textarea, useToast } from '@chakra-ui/react'; +import { useEffect, useState } from 'react'; +import useTranslation from 'next-translate/useTranslation'; +import useAuth from '../../common/hooks/useAuth'; +import useModuleHandler from '../../common/hooks/useModuleHandler'; +import useStyle from '../../common/hooks/useStyle'; +import bc from '../../common/services/breathecode'; +import CodeRevisionsList from '../../common/components/ReviewModal/CodeRevisionsList'; +import Icon from '../../common/components/Icon'; +import Heading from '../../common/components/Heading'; +import Text from '../../common/components/Text'; +import MarkDownParser from '../../common/components/MarkDownParser'; +import { error } from '../../utils/logging'; + +const inputReviewRateCommentLimit = 100; +const defaultReviewRateData = { + status: null, + comment: '', + isSubmitting: false, + submited: false, +}; +function TaskCodeRevisions() { + const { t } = useTranslation('syllabus'); + const { currentTask } = useModuleHandler(); + const { featuredLight, hexColor, backgroundColor, backgroundColor4 } = useStyle(); + const { isAuthenticatedWithRigobot } = useAuth(); + const toast = useToast(); + const [contextData, setContextData] = useState({ + code_revisions: [], + revision_content: {}, + }); + const [reviewRateData, setReviewRateData] = useState(defaultReviewRateData); + + const reviewRateStatus = reviewRateData?.status; + const codeRevisions = contextData?.code_revisions || []; + const revisionContent = contextData?.revision_content; + const hasRevision = revisionContent !== undefined; + const resetView = () => { + setReviewRateData({ + status: null, + comment: '', + isSubmitting: false, + submited: false, + }); + }; + + const selectCodeRevision = (revision) => { + const content = revision?.original_code; + const decodedReviewCodeContent = atob(content); + + setContextData((prevState) => ({ + ...prevState, + revision_content: { + path: revision?.file?.name, + ...revision, + code: decodedReviewCodeContent, + }, + })); + + if (revision.is_good || revision.revision_rating_comments) { + setReviewRateData((prev) => ({ + ...prev, + submited: true, + status: revision.is_good ? 'like' : 'dislike', + comment: revision.revision_rating_comments, + })); + } + }; + + const getCodeRevisions = async () => { + try { + if (!isAuthenticatedWithRigobot || !currentTask.github_url) return; + const response = await bc.assignments().getPersonalCodeRevisionsByTask(currentTask.id); + const data = await response.json(); + + if (response.ok) { + const codeRevisionsSortedByDate = data.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + setContextData((prev) => ({ + ...prev, + code_revisions: codeRevisionsSortedByDate, + })); + } else { + toast({ + title: t('alert-message:something-went-wrong'), + description: `Cannot get code revisions: ${data?.detail}`, + status: 'error', + duration: 5000, + position: 'top', + isClosable: true, + }); + } + } catch (errorMsg) { + error('Error fetching code revisions:', errorMsg); + } + }; + + useEffect(() => { + if (currentTask) { + getCodeRevisions(); + } + }, [currentTask?.id]); + + const handleSelectReviewRate = (status) => { + setReviewRateData((prev) => ({ ...prev, status })); + }; + + const onChangeRateComment = (e) => { + if (e.target.value.length <= inputReviewRateCommentLimit) { + setReviewRateData((prev) => ({ ...prev, comment: e.target.value })); + } + }; + + const submitReviewRate = async (type) => { + try { + setReviewRateData((prev) => ({ ...prev, isSubmitting: true })); + const argsData = { + send: { + is_good: reviewRateData.status === 'like', + comment: reviewRateData.comment, + }, + skip: { + is_good: reviewRateData.status === 'like', + comment: null, + }, + }; + const { data } = await bc.assignments().rateCodeRevision(revisionContent?.id, argsData[type]); + + setReviewRateData((prev) => ({ ...prev, submited: true })); + const updatedRevisionContent = { + ...data, + is_good: typeof data?.is_good === 'string' ? data?.is_good === 'True' : data?.is_good, + hasBeenReviewed: true, + }; + const updateCodeRevisions = contextData.code_revisions.map((revision) => { + if (revision.id === revisionContent.id) { + return updatedRevisionContent; + } + return revision; + }); + selectCodeRevision(updatedRevisionContent); + setContextData((prevState) => ({ + ...prevState, + code_revisions: updateCodeRevisions, + })); + } finally { + setReviewRateData((prev) => ({ ...prev, isSubmitting: false })); + } + }; + + return ( + + {revisionContent?.id ? ( + + + + {revisionContent?.file?.name} + + + ) : ( + + {t('code-reviews')} + + )} + {codeRevisions?.length > 0 ? ( + <> + {!revisionContent?.id ? ( + + ) : ( + <> + + + {hasRevision && ( + + + + {revisionContent?.comment} + + + + + + + {reviewRateStatus !== null && ( + <> + + + + + + {t(reviewRateStatus)} + + + + )} + + )} + {hasRevision && reviewRateData.submited && ( + + + + {`${t('you')}:`} + + + {reviewRateData?.comment} + + + + )} + + + {!reviewRateStatus ? ( + <> + + {t('rate-comment')} + + + + + + + ) : ( + + +