diff --git a/i18n.js b/i18n.js index c81e33453..bb5fb1bab 100644 --- a/i18n.js +++ b/i18n.js @@ -25,6 +25,7 @@ module.exports = { '/syllabus/[cohortSlug]/[lesson]/[lessonSlug]': ['syllabus', 'dashboard', 'projects', 'assignments'], '/survey/[surveyId]': ['survey'], '/mentorship': ['mentorship'], + '/mentorship/schedule': ['mentorship', 'dashboard', 'signup', 'common'], '/how-to': ['how-to'], '/pricing': ['pricing', 'signup'], '/how-to/[slug]': ['how-to'], 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/assignments.json b/public/locales/en/assignments.json index 9aa97c70d..91b2c6c7d 100644 --- a/public/locales/en/assignments.json +++ b/public/locales/en/assignments.json @@ -8,6 +8,12 @@ "no-upload-students": "The following students are still pending to upload their final project information.", "see-students": "See students", "no-information": "It is possible that the student never opened this assignment. No information was found about this student assignment", + "sync-cohort": "You can synchronize all the students tasks in the cohort", + "sync": "Synchronize", + "sync-cohort-title": "Synchronize cohort", + "sync-warning": "WARNING: This operation should only be performed on cohorts with serious synchronization problems", + "cancel": "Cancel", + "error-msg": "Something went wrong while synchronizing the cohort", "educational-status": "Educational Status", "delivered-percentage": "% delivered", "last-deliver": "Last delivery: {{date}} ago", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 350c61dd5..89b5547b3 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": { @@ -234,6 +236,8 @@ "logout-and-switch-user": "Logout and switch user", "free": "Free", "login": "Login", + "terms-and-conditions": "Terms and conditions", + "terms-and-conditions-link": "/terms-and-conditions", "privacy-policy": "Privacy policy", "privacy-policy-link": "/privacy-policy", "bootcamp": { diff --git a/public/locales/en/dashboard.json b/public/locales/en/dashboard.json index 2eba43f76..a5357150a 100644 --- a/public/locales/en/dashboard.json +++ b/public/locales/en/dashboard.json @@ -3,8 +3,8 @@ "title": "Dashboard" }, "title": "Your News", - "backToChooseProgram": "Back to choose program", "moduleMap": "Module map", + "backToChooseProgram": "Back to choose program", "progressText": "progress in the program", "whiteLabeledText": "This course is brought to you thanks to our parnership with this university", "free-trial-msg": "You are currently on a free trial, some features might be limited. Upgrade your plan to have unlimited access!", @@ -14,9 +14,9 @@ "cta-description": "Your current plan does not include access to this cohort, please upgrade to access the content.", "cta-cohort-not-found": "This cohort does not exist or you don't have access to join.", "cta-button": "Review plan", - "join-next-cohort":"Join next cohort", - "start-course":"Start this course", - "join-more":"Join more than 100 people taking this course right now", + "join-next-cohort": "Join next cohort", + "start-course": "Start this course", + "join-more": "Join more than 100 people taking this course right now", "preview-description": "You are reviewing this cohort dashboard on \"preview mode\", in order to start learning and interact with the materials please join the cohort." }, "already-have-this-cohort": "You are already a member of this cohort", @@ -84,6 +84,8 @@ "get-more-mentorships": "Get more mentorships", "schedule-button": "Schedule a mentoring session", "mentors-available": "+{{count}} Mentors available", + "service-not-found": "couldn't find service", + "mentor-not-found": "couldn't find mentor", "actionButtons": [ { "name": "mentoring", @@ -104,7 +106,15 @@ "get-unlimited-mentorship": "and get unlimited mentorship", "you-have": "You have", "available-sessions": "available sessions", - "tooltip": "Mentorships reload every week" + "tooltip": "Mentorships reload every week", + "mentor-sessions-available": "mentoring sessions available", + "action": "Schedule session", + "no-available": "No services available" + }, + "schedule-steps": { + "select-mentorship": "First select a type of mentorship", + "select-mentor": "Now search for a mentor", + "schedule": "Schedule the session" }, "deliverProject": { "title": "Deliver assignment", @@ -194,4 +204,4 @@ "title": "Welcome to 4Geeks!", "description": "Watch this short video that explains how to get the most out of 4Geeks and enhance your learning experience" } -} +} \ No newline at end of file diff --git a/public/locales/en/signup.json b/public/locales/en/signup.json index 0bcbaeecd..b5b074e0b 100644 --- a/public/locales/en/signup.json +++ b/public/locales/en/signup.json @@ -146,9 +146,10 @@ "email-required": "Email is required", "confirm-email-required": "Confirm Email is required", "confirm-email-not-match": "Emails don't match", - "termns-and-conditions-required": "I agree to receive information in my email about coding workshops, events, courses, and other marketing materials. We'll never share your email," + "receive-information": "I agree to receive information in my email, WhatsApp, and/or other channels about coding workshops, events, courses, and other marketing materials. We'll never share your contact information and you can easily opt out at any moment. " }, "no-date-available": "We currently have no dates available for the location entered", + "agree-terms-and-conditions": "By signing up, you agree to the", "alert-message": { "title": "Already a member?", "description": "It seems that you already have an account in 4geeks.com", diff --git a/public/locales/en/syllabus.json b/public/locales/en/syllabus.json index 26908a980..994eea724 100644 --- a/public/locales/en/syllabus.json +++ b/public/locales/en/syllabus.json @@ -3,13 +3,32 @@ "module-not-started": "You haven't started this module yet", "no-modules-to-show": "No modules to show", "edit-page": "Edit in GitHub", + "solution-video": "Solution video", + "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 +50,12 @@ "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", - "click-to-review": "Click here to review the 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.", + "share-social-message": "I just finished coding {{title}} at 4geeks.com" } diff --git a/public/locales/en/workshops.json b/public/locales/en/workshops.json index 6515cfd43..7960726b3 100644 --- a/public/locales/en/workshops.json +++ b/public/locales/en/workshops.json @@ -14,6 +14,11 @@ "in-person-confirm": "This is an in-person event that will take place in {{address}}. Do you still want to confirm your attendance?", "confirm-attendance": "Yes, I will be there", "deny-attendance" : "No, I will not attend", + "denny-access":{ + "description": "This is a private event, only students with access to", + "button": "You don't have access to this event", + "can-join": "can join" + }, "form": { "title": "Join this event", "description": "Sign in to join other coders live solving technical or career challenges.", 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/assignments.json b/public/locales/es/assignments.json index 10d4e5e33..90126b4c5 100644 --- a/public/locales/es/assignments.json +++ b/public/locales/es/assignments.json @@ -8,6 +8,11 @@ "no-upload-students": "Los siguientes estudiantes aún están pendientes de subir la información de su proyecto final.", "see-students": "Ver estudiantes", "no-information": "Es posible que el estudiante nunca haya abierto esta tarea. No se encontró información sobre la tarea de este estudiante.", + "sync-cohort": "Puedes sincronizar todas las asignaciones de los estudiantes de la cohorte", + "sync": "Sincronizar", + "sync-cohort-title": "Sincronizar la cohorte", + "sync-warning": "ADVERTENCIA: Esta operación solo debe realizarse en cohortes con problemas graves de sincronización.", + "cancel": "Cancelar", "educational-status": "Estatus eduacional", "delivered-percentage": "% de entregado", "last-deliver": "Última entrega: hace {{date}}", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 16947e566..25615d4c6 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": { @@ -233,6 +235,8 @@ "logout-and-switch-user": "Cerrar sesión e iniciar como otro usuario", "free": "Gratis", "login": "Iniciar sesión", + "terms-and-conditions": "Términos y condiciones", + "terms-and-conditions-link": "/es/terminos-y-condiciones", "privacy-policy": "Política de privacidad", "privacy-policy-link": "/es/politicas-de-privacidad", "bootcamp": { diff --git a/public/locales/es/dashboard.json b/public/locales/es/dashboard.json index 0a4b24808..e7897dc8f 100644 --- a/public/locales/es/dashboard.json +++ b/public/locales/es/dashboard.json @@ -15,9 +15,9 @@ "cta-description": "Tu plan actual no incluye acceso a esta cohorte, por favor actualiza para acceder al contenido.", "cta-cohort-not-found": "Esta cohorte no existe o no tienes acceso para unirte.", "cta-button": "Revisar plan", - "join-next-cohort":"Únete a la cohorte", - "start-course":"Empezar este curso", - "join-more":"Únete a más de 100 personas que toman este curso ahora mismo", + "join-next-cohort": "Únete a la cohorte", + "start-course": "Empezar este curso", + "join-more": "Únete a más de 100 personas que toman este curso ahora mismo", "preview-description": "Estás revisando esta cohorte en \"modo de vista previa\". Para comenzar a aprender e interactuar con el material, únete a la cohorte." }, "modules": { @@ -72,18 +72,21 @@ "no-mentors": "No hay mentores disponibles", "no-mentor-link": "No hay enlace para crear sesión con este mentor", "search-mentor": "Buscar mentor por nombre", - "start-mentorship": "Para comenzar, seleccione un tipo de tutoría", + "start-mentorship": "Para comenzar, seleccione un tipo de mentoría", "mentoring-label": "Mentoría", - "mentoring": "Programe una sesión de tutoría 1-1.", - "select-type": "Seleccione un tipo de tutoría", - "mentoring-available": "Tutorías disponibles", - "no-mentoring-available": "No tienes tutorías disponibles.", + "mentoring": "Programe una sesión de mentoría 1-1.", + "select-type": "Seleccione un tipo de mentoría", + "mentoring-available": "Mentorías disponibles", + "no-mentoring-available": "No tienes mentorías disponibles.", "create-session-text": "Agendar ahora", "learn-more": "Aprender mas.", "learn-more-link": "https://4geeks.com/es/docs/knowledge-base-4geeks/sesiones-de-tutoria", "get-more-mentorships": "Consigue más mentorías", - "schedule-button": "Programar una sesión de tutoría", + "schedule-button": "Programar una sesión de mentoría", "mentors-available": "+{{count}} Mentores disponibles", + "for": "para", + "service-not-found": "no se ha encontrado servicio", + "mentor-not-found": "no se ha encontrado el mentor", "actionButtons": [ { "name": "mentoring", @@ -100,11 +103,19 @@ ] }, "mentorship": { - "no-mentorship": "Se ha quedado sin tutorías, pero no se preocupe, puede obtener más con unos pocos clics.", - "get-unlimited-mentorship": "y obtén tutoría ilimitada", + "no-mentorship": "Se ha quedado sin mentorías, pero no se preocupe, puede obtener más con unos pocos clics.", + "get-unlimited-mentorship": "y obtén mentorías ilimitadas", "you-have": "Tienes", "available-sessions": "sesiones disponibles", - "tooltip": "Las tutorías se recargan cada semana" + "tooltip": "Las mentorías se recargan cada semana", + "mentor-sessions-available": "sesiones de mentoria disponibles", + "action": "Reservar mentoria", + "no-available": "No hay servicios disponibles" + }, + "schedule-steps": { + "select-mentorship": "Primero selecciona un tipo de mentoría", + "select-mentor": "Ahora busca un mentor", + "schedule": "Agenda la sesión" }, "deliverProject": { "title": "Entregar tarea", 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/signup.json b/public/locales/es/signup.json index 7732374cb..d60bee094 100644 --- a/public/locales/es/signup.json +++ b/public/locales/es/signup.json @@ -146,9 +146,10 @@ "email-required": "Correo electronico es requerido", "confirm-email-required": "Confirmar Correo electrónico es requerido", "confirm-email-not-match": "Los correos electrónicos no coinciden", - "termns-and-conditions-required": "Acepto recibir información a mi correo electrónico sobre talleres de programación, eventos, cursos y otros materiales de marketing. Nunca compartiremos tu correo electrónico," + "receive-information": "Acepto recibir información a través de mi correo electrónico, WhatsApp y/o otros canales sobre talleres de programación, eventos, cursos y otros materiales promocionales. Nunca compartiremos tu información de contacto y podrás darte de baja fácilmente en cualquier momento." }, "no-date-available": "Actualmente no tenemos fechas disponibles para la ubicación ingresada", + "agree-terms-and-conditions": "Al registrarte estás aceptando nuestros", "alert-message": { "title": "¿Ya eres usuario?", "description": "Parece que ya tienes una cuenta", diff --git a/public/locales/es/syllabus.json b/public/locales/es/syllabus.json index 16bb47cf6..fd2c92777 100644 --- a/public/locales/es/syllabus.json +++ b/public/locales/es/syllabus.json @@ -3,13 +3,32 @@ "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", + "solution-video": "Ver solución", + "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 +50,12 @@ "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", - "click-to-review": "Pincha aquí para revisar la 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.", + "share-social-message": "Acabo de terminar de programar {{title}} en 4geeks.com" } diff --git a/public/locales/es/workshops.json b/public/locales/es/workshops.json index c0b1f1305..1c8ccb3a3 100644 --- a/public/locales/es/workshops.json +++ b/public/locales/es/workshops.json @@ -14,6 +14,11 @@ "in-person-confirm": "Este es un evento presencial que se llevará a cabo en {{address}}. ¿Aún quieres confirmar tu asistencia?", "confirm-attendance": "Si, si asistiré", "deny-attendance" : "No, no asistiré", + "denny-access":{ + "description": "Esto es un evento privado, solo estudiantes con acceso a", + "button": "No tienes acceso a este evento.", + "can-join": "pueden entrar" + }, "form": { "title": "Únete a este evento", "description": "Inicia sesión para unirte a otros programadores en vivo resolviendo desafíos técnicos o profesionales.", diff --git a/public/sitemap.xml b/public/sitemap.xml index 52b3d365d..3390c017d 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -2,26 +2,26 @@ https://4geeks.com/pages-sitemap.xml - 2024-08-06T16:07:22.641Z + 2024-09-18T15:38:55.380Z https://4geeks.com/howto-sitemap.xml - 2024-08-06T16:07:22.641Z + 2024-09-18T15:38:55.380Z https://4geeks.com/lessons-sitemap.xml - 2024-08-06T16:07:22.641Z + 2024-09-18T15:38:55.380Z https://4geeks.com/projects-sitemap.xml - 2024-08-06T16:07:22.641Z + 2024-09-18T15:38:55.380Z https://4geeks.com/exercises-sitemap.xml - 2024-08-06T16:07:22.641Z + 2024-09-18T15:38:55.380Z https://4geeks.com/technologies-sitemap.xml - 2024-08-06T16:07:22.641Z + 2024-09-18T15:38:55.380Z \ No newline at end of file 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/CallToAction.jsx b/src/common/components/CallToAction.jsx index 201a405c8..c8c3a1154 100644 --- a/src/common/components/CallToAction.jsx +++ b/src/common/components/CallToAction.jsx @@ -16,7 +16,7 @@ import Icon from './Icon'; function CallToAction({ background, imageSrc, icon, href, styleContainer, isExternalLink, title, text, buttonText, onClick, margin, buttonsData, buttonStyle, fontSizeOfTitle, - isLoading, reverseButtons, + isLoading, reverseButtons, buttonsContainerStyles, }) { return ( {buttonText && !buttonsData?.length > 0 && ( - - {form.errors.password} - - )} - - - {({ field, form }) => ( - - - Repeat Password - - - - - - {form.errors.passwordConfirmation} - - )} - */} - setIsChecked(!isChecked)}> - {t('signup:validators.termns-and-conditions-required')} + {t('signup:validators.receive-information')} {' '} {t('common:privacy-policy')} diff --git a/src/common/components/Forms/Signup.jsx b/src/common/components/Forms/Signup.jsx index a7b2c0424..ce51ba63e 100644 --- a/src/common/components/Forms/Signup.jsx +++ b/src/common/components/Forms/Signup.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import * as Yup from 'yup'; -import { Avatar, Box, Button, Checkbox, useToast, +import { + Avatar, Box, Button, Checkbox, useToast, Spinner, InputGroup, InputRightElement, @@ -312,12 +313,7 @@ function SignupForm({ setIsChecked(!isChecked)}> - {t('validators.termns-and-conditions-required')} - {' '} - - {t('common:privacy-policy')} - - . + {t('validators.receive-information')} {!invertHandlerPosition && showLoginLink && ( @@ -328,17 +324,33 @@ function SignupForm({ )} - + + + + {t('agree-terms-and-conditions')} + {' '} + + {t('common:terms-and-conditions')} + + {' '} + {t('common:word-connector.and')} + {' '} + + {t('common:privacy-policy')} + + . + + {invertHandlerPosition && showLoginLink && ( {t('already-have-account')} @@ -449,7 +461,7 @@ SignupForm.propTypes = { formContainerStyle: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), }; SignupForm.defaultProps = { - onHandleSubmit: () => {}, + onHandleSubmit: () => { }, planSlug: null, courseChoosed: '', showVerifyEmail: true, 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..b32cb24a1 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,37 @@ 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 +370,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 +398,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 +414,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/DeliverModalContent.jsx b/src/common/components/ReviewModal/DeliverModalContent.jsx index c7a892f90..740376934 100644 --- a/src/common/components/ReviewModal/DeliverModalContent.jsx +++ b/src/common/components/ReviewModal/DeliverModalContent.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react/no-unstable-nested-components */ import { useEffect, useState, useRef } from 'react'; import { Box, @@ -24,7 +23,7 @@ import bc from '../../services/breathecode'; import useStyle from '../../hooks/useStyle'; import Icon from '../Icon'; -function DeliverModal({ +function DeliverModalContent({ isStudent, currentTask, projectLink, @@ -215,7 +214,7 @@ function DeliverModal({ ); } -DeliverModal.propTypes = { +DeliverModalContent.propTypes = { isStudent: PropTypes.bool, currentTask: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired, projectLink: PropTypes.string.isRequired, @@ -229,7 +228,7 @@ DeliverModal.propTypes = { loaders: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), proceedToCommitFiles: PropTypes.func, }; -DeliverModal.defaultProps = { +DeliverModalContent.defaultProps = { isStudent: false, readOnly: false, showCodeReviews: false, @@ -239,4 +238,4 @@ DeliverModal.defaultProps = { proceedToCommitFiles: () => {}, }; -export default DeliverModal; +export default DeliverModalContent; 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..417296764 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: { @@ -85,7 +84,7 @@ function ReviewModal({ isExternal, externalFiles, isOpen, isStudent, externalDat const hasNotBeenReviewed = revisionStatus === PENDING; const hasBeenApproved = revisionStatus === APPROVED; const hasBeenRejected = revisionStatus === REJECTED; - const noFilesToReview = !hasBeenApproved && contextData?.commitFiles?.fileList?.length === 0; + const noFilesToReview = !hasBeenApproved && (contextData?.commitFiles?.fileList?.length === 0 || !('commitFiles' in contextData)); const codeRevisionsNotExists = typeof contextData?.code_revisions === 'undefined'; const hasFilesToReview = contextData?.code_revisions?.length > 0 || !isStudent; // Used to show rigobot files content const stage = stageHistory?.current; @@ -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, }); }; @@ -627,7 +626,7 @@ function ReviewModal({ isExternal, externalFiles, isOpen, isStudent, externalDat )} - {(!isAuthenticatedWithRigobot || !noFilesToReview) && hasFilesToReview && !disableRate && ( + {(!isAuthenticatedWithRigobot || !noFilesToReview) && hasFilesToReview && !disableRate && contextData?.commitFiles?.fileList?.length > 0 && ( diff --git a/src/common/components/SupportSidebar/Mentoring.jsx b/src/common/components/SupportSidebar/Mentoring.jsx index a02592404..57fc097c0 100644 --- a/src/common/components/SupportSidebar/Mentoring.jsx +++ b/src/common/components/SupportSidebar/Mentoring.jsx @@ -7,12 +7,9 @@ import { // useToast, } from '@chakra-ui/react'; import PropTypes from 'prop-types'; -import { format } from 'date-fns'; -import { es } from 'date-fns/locale'; import { useRouter } from 'next/router'; import useTranslation from 'next-translate/useTranslation'; import bc from '../../services/breathecode'; -import MentoringFree from './MentoringFree'; import MentoringConsumables from './MentoringConsumables'; import useAuth from '../../hooks/useAuth'; import useCohortHandler from '../../hooks/useCohortHandler'; @@ -20,28 +17,19 @@ import useCohortHandler from '../../hooks/useCohortHandler'; function Mentoring({ width, allCohorts, allSyllabus, programServices, subscriptions, subscriptionData, }) { + // const toast = useToast(); const { t } = useTranslation('dashboard'); - const [savedChanges, setSavedChanges] = useState({}); + const router = useRouter(); + const { isLoading, user } = useAuth(); + const { slug } = router.query; const { state } = useCohortHandler(); const { cohortSession } = state; - const router = useRouter(); - const [consumables, setConsumables] = useState({}); + const [consumables, setConsumables] = useState([]); const [mentoryProps, setMentoryProps] = useState({}); const [allMentorsAvailable, setAllMentorsAvailable] = useState([]); const [programMentors, setProgramMentors] = useState([]); - const [isAvailableForConsumables, setIsAvailableForConsumables] = useState(true); - const { isLoading, user } = useAuth(); - // const toast = useToast(); - const { slug } = router.query; - - const [searchProps, setSearchProps] = useState({ - serviceSearch: '', - mentorSearch: '', - }); - - const servicesFiltered = programServices.list.filter( - (l) => l.name.toLowerCase().includes(searchProps.serviceSearch), - ); + const [cohortSessionIsSaaS, setCohortSessionIsSaaS] = useState(true); + const [searchProps, setSearchProps] = useState({ serviceSearch: '', mentorSearch: '' }); const filterServices = () => { if (subscriptionData?.selected_mentorship_service_set?.mentorship_services?.length > 0) { @@ -62,38 +50,13 @@ function Mentoring({ const mentorsFiltered = programMentors.filter( (mentor) => { const fullName = `${mentor.user.first_name} ${mentor.user.last_name}`.toLowerCase(); - const mentorServices = fullName.includes(searchProps.mentorSearch) && mentor.services.some((sv) => sv.status === 'ACTIVE' - && sv.slug === mentoryProps?.service?.slug); - return mentorServices; + return ( + fullName.includes(searchProps.mentorSearch) + && mentor.services.some((sv) => sv.status === 'ACTIVE' && sv.slug === mentoryProps?.service?.slug) + ); }, ); - const dateFormated = { - en: mentoryProps?.date && format(new Date(mentoryProps.date), 'MMMM dd'), - es: mentoryProps?.date && format(new Date(mentoryProps.date), "dd 'de' MMMM", { locale: es }), - }; - - const dateFormated2 = { - en: mentoryProps?.date && format(new Date(mentoryProps.date), 'MMMM dd, yyyy'), - es: mentoryProps?.date && format(new Date(mentoryProps.date), "dd 'de' MMMM, yyyy", { locale: es }), - }; - - useEffect(() => { - if (mentoryProps?.time) { - const [hours, minutes] = mentoryProps.time.split(':'); - - const nDate = mentoryProps?.date - && new Date(mentoryProps.date); - - nDate.setHours(+hours, +minutes, 0, 0); // set hours/minute; - setMentoryProps({ ...mentoryProps, date: nDate }); - setSavedChanges({ ...mentoryProps, date: nDate }); - } - }, [mentoryProps?.time]); - - const step1 = !mentoryProps?.service; - const step2 = mentoryProps?.service && !mentoryProps?.date; - const getAllMentorsAvailable = async () => { const servicesSlugs = programServices.list.map((service) => service?.slug); @@ -106,7 +69,6 @@ function Mentoring({ academies[academy.id].services.push(restOfService); }); - // Convert the object to an array of academies with their services const academyData = Object.entries(academies).map(([academy, values]) => ({ id: Number(academy), services: values.services, @@ -130,6 +92,21 @@ function Mentoring({ return []; }; + const sortByConsumptionAvailability = (allConsumables) => allConsumables.sort((a, b) => { + const balanceA = a?.balance?.unit; + const balanceB = b?.balance?.unit; + + if (balanceA === -1 && balanceB !== -1) return -1; + if (balanceA !== -1 && balanceB === -1) return 1; + + if (balanceA > 0 && balanceB <= 0) return -1; + if (balanceA <= 0 && balanceB > 0) return 1; + + if (balanceA > 0 && balanceB > 0) return balanceB - balanceA; + + return 0; + }); + const getMentorsAndConsumables = async () => { const mentors = await getAllMentorsAvailable(); const reqConsumables = await bc.payment().service().consumable() @@ -141,7 +118,8 @@ function Mentoring({ })))); const allConsumables = await Promise.all(reqConsumables); - setConsumables(allConsumables); + const sortedConsumables = sortByConsumptionAvailability(allConsumables); + setConsumables(sortedConsumables); setAllMentorsAvailable(mentors); }; @@ -153,73 +131,34 @@ function Mentoring({ useEffect(() => { const existsCohortSession = typeof cohortSession?.available_as_saas === 'boolean'; - if (existsCohortSession) { - setIsAvailableForConsumables(cohortSession?.available_as_saas); - } - if (!existsCohortSession) { - if (allCohorts.length > 0) { - setIsAvailableForConsumables(allCohorts?.some((c) => c.cohort?.available_as_saas === true)); - } + setCohortSessionIsSaaS(cohortSession?.available_as_saas); } }, [allCohorts]); - const mentorshipService = consumables?.mentorship_service_sets?.find( - (c) => c?.slug.toLowerCase() === subscriptionData?.selected_mentorship_service_set?.slug.toLowerCase(), - ); - return !isLoading && user?.id && ( {t('supportSideBar.mentoring-label')} - {isAvailableForConsumables ? ( - 0 ? programServices.list : subscriptionData?.selected_mentorship_service_set?.mentorship_services, - servicesFiltered: suscriptionServicesFiltered, - dateFormated, - searchProps, - setSearchProps, - setProgramMentors, - savedChanges, - setSavedChanges, - mentorsFiltered, - step1, - step2, - dateFormated2, - allMentorsAvailable, - subscriptionData, - allSubscriptions: subscriptions, - }} - /> - ) : ( - - )} + 0 ? programServices.list : subscriptionData?.selected_mentorship_service_set?.mentorship_services, + servicesFiltered: suscriptionServicesFiltered, + searchProps, + setSearchProps, + setProgramMentors, + mentorsFiltered, + allMentorsAvailable, + subscriptionData, + cohortSessionIsSaaS, + allSubscriptions: subscriptions, + }} + /> ); } diff --git a/src/common/components/SupportSidebar/MentoringConsumables.jsx b/src/common/components/SupportSidebar/MentoringConsumables.jsx index 3f8eb7479..86667d45b 100644 --- a/src/common/components/SupportSidebar/MentoringConsumables.jsx +++ b/src/common/components/SupportSidebar/MentoringConsumables.jsx @@ -18,7 +18,7 @@ import Text from '../Text'; import { AvatarSkeletonWrapped } from '../Skeleton'; import modifyEnv from '../../../../modifyEnv'; import { validatePlanExistence } from '../../handlers/subscriptions'; -import { getStorageItem, isDevMode } from '../../../utils'; +import { getStorageItem } from '../../../utils'; import { reportDatalayer } from '../../../utils/requests'; function NoConsumablesCard({ t, setMentoryProps, handleGetMoreMentorships, mentoryProps, subscriptionData, disableBackButton = false, ...rest }) { @@ -53,9 +53,9 @@ function NoConsumablesCard({ t, setMentoryProps, handleGetMoreMentorships, mento {!disableBackButton && ( - + )} ); @@ -90,13 +90,12 @@ function ProfilesSection({ } function MentoringConsumables({ - mentoryProps, width, consumables, mentorshipService, setMentoryProps, - programServices, dateFormated, servicesFiltered, searchProps, - setSearchProps, setProgramMentors, savedChanges, setSavedChanges, - mentorsFiltered, dateFormated2, allMentorsAvailable, subscriptionData, allSubscriptions, + mentoryProps, width, consumables, cohortSessionIsSaaS, setMentoryProps, + programServices, servicesFiltered, searchProps, setSearchProps, setProgramMentors, + mentorsFiltered, allMentorsAvailable, subscriptionData, allSubscriptions, + queryService, queryMentor, titleSize, }) { const { t } = useTranslation('dashboard'); - const { user } = useAuth(); const BREATHECODE_HOST = modifyEnv({ queryString: 'host', env: process.env.BREATHECODE_HOST }); const commonBackground = useColorModeValue('white', 'rgba(255, 255, 255, 0.1)'); @@ -108,29 +107,32 @@ function MentoringConsumables({ const [isFetchingDataForModal, setIsFetchingDataForModal] = useState(false); const [dataToGetAccessModal, setDataToGetAccessModal] = useState({}); const [consumableOfService, setConsumableOfService] = useState({}); + const [servicesWithMentorsAvailable, setServicesWithMentorsAvailable] = useState([]); + const [hasReset, setHasReset] = useState(false); + const [notifyError, setNotifyError] = useState(true); + const [shouldHandleService, setShouldHandleService] = useState(true); const router = useRouter(); - const toast = useToast(); const { slug } = router.query; - - const mentorshipBalance = mentorshipService?.balance?.unit || mentorshipService?.balance || consumableOfService?.balance?.unit; + const toast = useToast(); + const mentorshipBalance = consumableOfService?.balance?.unit; const currentBalance = Number(mentorshipBalance && mentorshipBalance); const calculateExistenceOfConsumable = () => { if (consumableOfService.available_as_saas === false) return true; if (consumableOfService?.balance) return consumableOfService?.balance?.unit > 0 || consumableOfService?.balance?.unit === -1; - return consumables?.mentorship_service_sets?.length > 0 && Object.values(mentorshipService).length > 0 && (currentBalance > 0 || currentBalance === -1); + return consumables?.mentorship_service_sets?.length > 0 && (currentBalance > 0 || currentBalance === -1); }; const existConsumablesOnCurrentService = calculateExistenceOfConsumable(); const getMostRecentPaidAt = (invoices) => invoices.reduce((latest, invoice) => { const paidAtDate = new Date(invoice.paid_at); return paidAtDate > latest ? paidAtDate : latest; - }, new Date(0)); // Initialize with a very old date + }, new Date(0)); const sortByMostRecentInvoice = (a, b) => { const latestA = getMostRecentPaidAt(a.invoices); const latestB = getMostRecentPaidAt(b.invoices); - return latestB - latestA; // Descending order + return latestB - latestA; }; const currentServiceSubscription = Array.isArray(allSubscriptions) && allSubscriptions.sort(sortByMostRecentInvoice).find((subscription) => subscription.selected_mentorship_service_set.mentorship_services.some((service) => service.slug === mentoryProps?.service?.slug)); @@ -144,7 +146,7 @@ function MentoringConsumables({ } }, [allMentorsAvailable]); - const manageMentorsData = (service, data) => { + const manageMentorsData = (service, mentors) => { reportDatalayer({ dataLayer: { event: 'select_mentorship_service', @@ -153,18 +155,18 @@ function MentoringConsumables({ mentorship_service: service?.slug, }, }); - const relatedConsumables = consumables.find((consumable) => consumable?.mentorship_services?.some((c) => c?.slug === service?.slug)); - setProgramMentors(data); + const relatedConsumable = consumables.find((consumable) => consumable?.mentorship_services?.some((c) => c?.slug === service?.slug)); + setProgramMentors(mentors); setConsumableOfService({ - ...relatedConsumables, + ...relatedConsumable, balance: { - unit: service?.academy?.available_as_saas === false ? -1 : relatedConsumables?.balance?.unit, + unit: (service?.academy?.available_as_saas === false || cohortSessionIsSaaS === false) ? -1 : relatedConsumable?.balance?.unit, }, available_as_saas: service?.academy?.available_as_saas, }); + setTimeout(() => { setMentoryProps({ ...mentoryProps, service }); - setSavedChanges({ ...savedChanges, service }); }, 50); }; @@ -194,7 +196,71 @@ function MentoringConsumables({ } }; - const servicesWithMentorsAvailable = servicesFiltered.filter((service) => allMentorsAvailable.some((mentor) => mentor.services.some((mentServ) => mentServ.slug === service.slug))); + useEffect(() => { + const getAllServicesWithMentors = () => servicesFiltered.filter((service) => allMentorsAvailable.some((ment) => ment.services.some((mentServ) => mentServ.slug === service.slug))); + const getServicesWithMentor = (mentor) => servicesFiltered.filter((service) => mentor.services.some((mentServ) => mentServ.slug === service.slug)); + + const showErrorToast = () => { + toast({ + position: 'top', + title: 'Error', + description: `${t('supportSideBar.mentor-not-found')} "${queryMentor}"`, + status: 'error', + duration: 7000, + isClosable: true, + }); + }; + + let servWithMentorsAvailable = getAllServicesWithMentors(); + + if (queryMentor && allMentorsAvailable.length > 0 && !hasReset) { + const mentorFound = allMentorsAvailable.find((ment) => ment.slug === queryMentor); + + if (!mentorFound && notifyError) { + showErrorToast(); + setNotifyError(false); + } + if (mentorFound) { + servWithMentorsAvailable = getServicesWithMentor(mentorFound); + setProgramMentors([mentorFound]); + } + } + + setServicesWithMentorsAvailable(servWithMentorsAvailable); + + if (!hasReset && queryMentor) { + setOpen(true); + } + }, [servicesFiltered, queryMentor, hasReset]); + + useEffect(() => { + if (queryService && servicesWithMentorsAvailable?.length > 0 && shouldHandleService && !hasReset) { + const serviceFound = servicesWithMentorsAvailable.find((service) => service.slug === queryService); + + if (!serviceFound && notifyError) { + toast({ + position: 'top', + title: 'Error', + description: `${t('supportSideBar.service-not-found')} "${queryService}" ${queryMentor ? `${t('common:word-connector.for')} "${queryMentor}"` : ''}`, + status: 'error', + duration: 7000, + isClosable: true, + }); + setNotifyError(false); + return; + } + + handleService(serviceFound); + setOpen(true); + setShouldHandleService(false); + } + }, [hasReset, servicesWithMentorsAvailable]); + + const reset = () => { + if (mentoryProps?.service) setMentoryProps({}); + else setOpen(false); + setHasReset(true); + }; const handleGetMoreMentorships = () => { setIsFetchingDataForModal(true); @@ -212,16 +278,17 @@ function MentoringConsumables({ }) .finally(() => setIsFetchingDataForModal(false)); }; - const reportBookMentor = () => { + + const reportBookMentor = (mentorSelected) => { reportDatalayer({ dataLayer: { event: 'book_mentorship_session', path: router.pathname, consumables_amount: currentBalance, mentorship_service: mentoryProps?.service?.slug, - mentor_name: `${mentoryProps.mentor.user.first_name} ${mentoryProps.mentor.user.last_name}`, - mentor_id: mentoryProps.mentor.slug, - mentor_booking_url: mentoryProps.mentor.booking_url, + mentor_name: `${mentorSelected.user.first_name} ${mentorSelected.user.last_name}`, + mentor_id: mentorSelected.slug, + mentor_booking_url: mentorSelected.booking_url, }, }); }; @@ -236,12 +303,12 @@ function MentoringConsumables({ > {open && mentoryProps?.service && ( - setMentoryProps({})} cursor="pointer"> + )} {open && !mentoryProps?.service && ( - setOpen(false)} cursor="pointer"> + )} @@ -249,7 +316,7 @@ function MentoringConsumables({ {!mentoryProps?.service && (consumables?.mentorship_service_sets?.length !== 0 || currentBalance !== 0) && ( <> - + {t('supportSideBar.mentoring')}
@@ -285,11 +352,10 @@ function MentoringConsumables({ )} )} - - {t('supportSideBar.mentors-available', { count: 3 })} + + {t('supportSideBar.mentors-available', { count: allMentorsAvailable.length })}
- {/* Schedule event */} - )} - {mentoryProps?.confirm && ( - + {mentoryProps?.service && !mentoryProps?.mentor && ( + <> + + setSearchProps({ ...searchProps, mentorSearch: e.target.value?.toLowerCase() })} background={commonBackground} borderBottomRadius="0" border="0" placeholder={t('supportSideBar.search-mentor')} /> + + + + + + {mentorsFiltered.length > 0 ? mentorsFiltered.map((mentor, i) => ( + + {i !== 0 && ( + )} - {!mentoryProps?.confirm && ( - setMentoryProps({ ...mentoryProps, time: null })} className="link" width="fit-content" margin="0 auto"> - Go back + + {`${mentor?.user?.first_name} + + + {`${mentor.user.first_name} ${mentor.user.last_name}`} + + + + {(mentor.one_line_bio && mentor.one_line_bio !== '') ? `${mentor.one_line_bio} ` : ''} + {mentor?.booking_url ? ( + reportBookMentor(mentor)} + href={`${BREATHECODE_HOST}/mentor/${mentor?.slug}?utm_campaign=${mentoryProps?.service?.slug}&utm_source=4geeks&salesforce_uuid=${user?.id}&token=${accessToken}`} + target="_blank" + rel="noopener noreferrer" + > + {t('supportSideBar.create-session-text')} + + ) : ( + + {t('supportSideBar.no-mentor-link')} + + )} + - )} + + + )) : ( + + {t('supportSideBar.no-mentors')} - - )} + )} +
)} @@ -519,30 +507,30 @@ function MentoringConsumables({ MentoringConsumables.propTypes = { mentoryProps: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any, PropTypes.string])), width: PropTypes.string, + titleSize: PropTypes.string, + queryService: PropTypes.string, + queryMentor: PropTypes.string, consumables: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), - mentorshipService: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), setMentoryProps: PropTypes.func.isRequired, programServices: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.any, PropTypes.string])), - dateFormated: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any, PropTypes.string])).isRequired, servicesFiltered: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.any, PropTypes.string])).isRequired, searchProps: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any, PropTypes.string])).isRequired, setSearchProps: PropTypes.func.isRequired, - savedChanges: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any, PropTypes.string])).isRequired, - setSavedChanges: PropTypes.func.isRequired, setProgramMentors: PropTypes.func, mentorsFiltered: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.any])).isRequired, - dateFormated2: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired, subscriptionData: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), allSubscriptions: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.any])), }; MentoringConsumables.defaultProps = { + queryService: undefined, + queryMentor: undefined, + titleSize: undefined, mentoryProps: [], width: '100%', consumables: {}, - mentorshipService: {}, programServices: [], - setProgramMentors: () => {}, + setProgramMentors: () => { }, subscriptionData: {}, allSubscriptions: [], }; 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/context/AuthContext.jsx b/src/common/context/AuthContext.jsx index 45a5f2b04..2dc5262ef 100644 --- a/src/common/context/AuthContext.jsx +++ b/src/common/context/AuthContext.jsx @@ -196,7 +196,7 @@ function AuthProvider({ children, pageProps }) { method: 'native', user_id: data.id, email: data.email, - // is_saas: data.roles.filter(r => r.role.toLowerCase() == "student" && r.) + is_academy_legacy: data.roles.some((r) => r.academy.id === 6), first_name: data.first_name, last_name: data.last_name, avatar_url: data.profile?.avatar_url || data.github?.avatar_url, 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 2e2612ecd..20b22aebc 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), @@ -259,6 +260,7 @@ const breathecode = { personalFiles: (taskId) => breathecode.get(`${url}/me/task/${taskId}/commitfile${qs}`), personalFile: (commitId) => breathecode.get(`${url}/me/commitfile/${commitId}${qs}`), rateCodeRevision: (coderevisionId, data) => axios.post(`${url}/me/coderevision/${coderevisionId}/rate`, data), + syncCohort: (cohortId) => axios.get(`${url}/academy/cohort/${cohortId}/synctasks`), }; }, feedback: () => { @@ -286,6 +288,7 @@ const breathecode = { const qs = parseQuerys(query); return { lead: (data) => axios.post(`${url}/lead${qs}`, data), + courses: () => axios.get(`${url}/course${qs}`), }; }, @@ -324,7 +327,6 @@ const breathecode = { return { checking: (data) => axios.put(`${url}/checking${qs}`, data), subscriptions: () => axios.get(`${url}/me/subscription${qs}`), - courses: () => axios.get(`${host}/marketing/course${qs}`), pay: (data) => breathecode.post(`${url}/pay${qs}`, data), addCard: (data) => breathecode.post(`${url}/card${qs}`, data), cancelSubscription: (id) => axios.put(`${url}/subscription/${id}/cancel${qs}`), @@ -365,6 +367,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/common/views/StudentAssignments.jsx b/src/common/views/StudentAssignments.jsx index 814b43fdd..2b691e97e 100644 --- a/src/common/views/StudentAssignments.jsx +++ b/src/common/views/StudentAssignments.jsx @@ -195,6 +195,7 @@ function StudentAssignments({ currentStudentList, updpateAssignment, syllabusDat setCurrentTask(null)} + selectedCohort={selectedCohort} /> { + try { + const resp = await bc.assignments().syncCohort(selectedCohort.id); + if (resp.status >= 400) throw new Error('Sync error'); + + const { message } = resp.data; + + toast({ + position: 'top', + title: 'Success', + description: message, + status: 'success', + duration: 5000, + }); + } catch (e) { + console.log(e); + toast({ + position: 'top', + title: t('error-msg'), + status: 'error', + duration: 6000, + isClosable: true, + }); + } finally { + setIsSyncOpen(false); + onClose(); + } + }; + return ( @@ -227,8 +260,33 @@ export function NoInfoModal({ isOpen, onClose }) { - - {t('no-information')} + + {t('no-information')} + {selectedCohort && ( + <> + {t('sync-cohort')} + + + )} + + {t('sync-warning')} + + + + + @@ -393,6 +451,11 @@ ReviewModal.defaultProps = { NoInfoModal.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, + selectedCohort: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), +}; + +NoInfoModal.defaultProps = { + selectedCohort: null, }; DetailsModal.propTypes = { 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/chooseProgram/index.jsx b/src/js_modules/chooseProgram/index.jsx index 0b04be4e0..4eba9cac1 100644 --- a/src/js_modules/chooseProgram/index.jsx +++ b/src/js_modules/chooseProgram/index.jsx @@ -23,13 +23,12 @@ function ChooseProgram({ chooseList, handleChoose, setLateModalProps }) { const [marketingCursesList, setMarketingCursesList] = useState([]); const [showFinished, setShowFinished] = useState(false); const [upgradeModalIsOpen, setUpgradeModalIsOpen] = useState(false); - const activeCohorts = handlers.getActiveCohorts(chooseList); - const finishedCohorts = handlers.getCohortsFinished(chooseList); const { featuredColor } = useStyle(); const router = useRouter(); - const cardColumnSize = 'repeat(auto-fill, minmax(17rem, 1fr))'; - const activeSubscriptionCohorts = activeCohorts?.length > 0 ? activeCohorts.map((item) => { + + const finishedCohorts = handlers.getCohortsFinished(chooseList); + const activeCohorts = handlers.getActiveCohorts(chooseList).map((item) => { const cohort = item?.cohort; const currentCohortProps = programsList[cohort.slug]; return ({ @@ -43,18 +42,14 @@ function ChooseProgram({ chooseList, handleChoose, setLateModalProps }) { all_subscriptions: currentCohortProps?.all_subscriptions, subscription_exists: currentCohortProps?.subscription !== null || currentCohortProps?.plan_financing !== null, }); - }) : []; + }); - const marketingCourses = marketingCursesList?.length > 0 ? marketingCursesList.filter( - (item) => !activeSubscriptionCohorts.some( - (activeCohort) => activeCohort?.all_subscriptions?.some( - (sb) => sb?.selected_cohort_set?.slug === item?.slug, - ), - ) && item?.course_translation?.title, - ) : []; + const hasNonSaasCourse = chooseList.some(({ cohort }) => !cohort.available_as_saas); - const isNotAvailableForMktCourses = activeSubscriptionCohorts.length > 0 && activeSubscriptionCohorts.some( - (item) => item?.cohort?.available_as_saas === false, + const marketingCourses = marketingCursesList.filter( + (item) => !activeCohorts.some( + ({ cohort }) => cohort.slug === item?.cohort?.slug, + ) && item?.course_translation?.title, ); useEffect(() => { @@ -62,46 +57,52 @@ function ChooseProgram({ chooseList, handleChoose, setLateModalProps }) { }, [router.locale]); useEffect(() => { - bc.payment({ academy: WHITE_LABEL_ACADEMY }).courses() + bc.marketing({ academy: WHITE_LABEL_ACADEMY }).courses() .then(({ data }) => { setMarketingCursesList(data); }); }, [router?.locale]); + const filterForNonSaasStudents = (course) => { + if (!hasNonSaasCourse) return true; + + return course.plan_slug === process.env.BASE_PLAN; + }; + return ( <> - {activeSubscriptionCohorts.length > 0 && ( - - - {t('your-active-programs')} - - - + {activeCohorts.length > 0 && ( + <> + + + {t('your-active-programs')} + + + + 1 ? cardColumnSize : '', md: cardColumnSize }} + height="auto" + gridGap="4rem" + > + {activeCohorts.map((item) => ( + setUpgradeModalIsOpen(true)} + setLateModalProps={setLateModalProps} + /> + ))} + + )} setUpgradeModalIsOpen(false)} /> - {activeSubscriptionCohorts.length > 0 && ( - 1 ? cardColumnSize : '', md: cardColumnSize }} - height="auto" - gridGap="4rem" - > - {activeSubscriptionCohorts.map((item) => ( - setUpgradeModalIsOpen(true)} - setLateModalProps={setLateModalProps} - /> - ))} - - )} - {!isNotAvailableForMktCourses && marketingCourses?.length > 0 && marketingCourses.some((l) => l?.course_translation?.title) && ( + {marketingCourses?.length > 0 && ( <> @@ -115,7 +116,7 @@ function ChooseProgram({ chooseList, handleChoose, setLateModalProps }) { height="auto" gridGap="4rem" > - {marketingCourses.map((item) => ( + {marketingCourses.filter(filterForNonSaasStudents).map((item) => ( )} - { - finishedCohorts.length > 0 && ( - <> - 0 && ( + <> + + - - {isPlural(finishedCohorts) - ? t('finished.plural', { finishedCohorts: finishedCohorts.length }) - : t('finished.singular', { finishedCohorts: finishedCohorts.length })} - - setShowFinished(!showFinished)} - > - {showFinished ? t('finished.hide') : t('finished.show')} - - - - + setShowFinished(!showFinished)} > - {showFinished && finishedCohorts.map((item) => ( - setUpgradeModalIsOpen(true)} - /> - ))} - - - ) - } + {showFinished ? t('finished.hide') : t('finished.show')} + + + + + {showFinished && finishedCohorts.map((item) => ( + setUpgradeModalIsOpen(true)} + /> + ))} + + + )} ); } 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..3cf489655 --- /dev/null +++ b/src/js_modules/syllabus/ExerciseGuidedExperience.jsx @@ -0,0 +1,218 @@ +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 ModalToCloneProject from './ModalToCloneProject'; +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 NextChakraLink from '../../common/components/NextChakraLink'; +import Heading from '../../common/components/Heading'; +import Text from '../../common/components/Text'; +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}`; + + return ( + + + + + + {currentAsset?.title} + + + {currentAsset?.description} + + + + + + + {isExerciseStated && ( + + {telemetryReport.map((elem) => ( + + ))} + + )} + + + + + + + {t('common:learnpack.title')} + + + + + + + + {t('common:learnpack.open-in-learnpack-button.text')} + + + + + + + ); +} + +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/ModalToCloneProject.jsx b/src/js_modules/syllabus/ModalToCloneProject.jsx new file mode 100644 index 000000000..1c303d436 --- /dev/null +++ b/src/js_modules/syllabus/ModalToCloneProject.jsx @@ -0,0 +1,51 @@ +import useTranslation from 'next-translate/useTranslation'; +import PropTypes from 'prop-types'; +import MarkDownParser from '../../common/components/MarkDownParser'; +import SimpleModal from '../../common/components/SimpleModal'; + +function ModalToCloneProject({ isOpen, onClose, currentAsset }) { + const { t } = useTranslation('syllabus'); + + const urlToClone = currentAsset?.url || currentAsset?.readme_url?.split('/blob')?.[0]; + const repoName = urlToClone?.split('/')?.pop(); + + return ( + + + + ); +} + +ModalToCloneProject.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func, + currentAsset: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), +}; + +ModalToCloneProject.defaultProps = { + isOpen: false, + onClose: () => {}, + currentAsset: null, +}; + +export default ModalToCloneProject; diff --git a/src/js_modules/syllabus/OpenWithLearnpackCTA.jsx b/src/js_modules/syllabus/OpenWithLearnpackCTA.jsx new file mode 100644 index 000000000..922c6b558 --- /dev/null +++ b/src/js_modules/syllabus/OpenWithLearnpackCTA.jsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import useTranslation from 'next-translate/useTranslation'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; +import CallToAction from '../../common/components/CallToAction'; +import modifyEnv from '../../../modifyEnv'; +import ModalToCloneProject from './ModalToCloneProject'; + +function OpenWithLearnpackCTA({ currentAsset }) { + const { t, lang } = useTranslation('common'); + const [learnpackActions, setLearnpackActions] = useState([]); + const { state } = useCohortHandler(); + const { cohortSession } = state; + const [showCloneModal, setShowCloneModal] = useState(false); + const BREATHECODE_HOST = modifyEnv({ queryString: 'host', env: process.env.BREATHECODE_HOST }); + + const accessToken = localStorage.getItem('accessToken'); + + const provisioningLinks = [{ + title: t('learnpack.new-exercise'), + link: `${BREATHECODE_HOST}/v1/provisioning/me/container/new?token=${accessToken}&cohort=${cohortSession?.id}&repo=${currentAsset?.url}`, + isExternalLink: true, + }, + { + title: t('learnpack.continue-exercise'), + link: `${BREATHECODE_HOST}/v1/provisioning/me/workspaces?token=${accessToken}&cohort=${cohortSession?.id}&repo=${currentAsset?.url}`, + isExternalLink: true, + }]; + + useEffect(() => { + const openInLearnpackAction = t('learnpack.open-in-learnpack-button', {}, { returnObjects: true }); + const localhostAction = { + text: `${t('learnpack.open-locally')}${cohortSession?.available_as_saas ? ` (${t('learnpack.recommended')})` : ''}`, + type: 'button', + onClick: () => { + setShowCloneModal(true); + }, + }; + const cloudActions = { + ...openInLearnpackAction, + text: `${openInLearnpackAction.text}${cohortSession?.available_as_saas === false ? ` (${t('learnpack.recommended')})` : ''}`, + links: provisioningLinks, + }; + if (cohortSession?.id) { + if (!currentAsset?.gitpod) setLearnpackActions([localhostAction]); + else if (cohortSession.available_as_saas) setLearnpackActions([localhostAction, cloudActions]); + else setLearnpackActions([cloudActions, localhostAction]); + } + }, [lang, cohortSession?.id, currentAsset?.url]); + + return ( + <> + + + + ); +} + +OpenWithLearnpackCTA.propTypes = { + currentAsset: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.array])), +}; +OpenWithLearnpackCTA.defaultProps = { + currentAsset: null, +}; + +export default OpenWithLearnpackCTA; diff --git a/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx b/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx new file mode 100644 index 000000000..5111a3331 --- /dev/null +++ b/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx @@ -0,0 +1,237 @@ +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' && currentAsset?.delivery_formats !== 'no_delivery'; + + 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..052e9ada3 --- /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')} + + + + + + + ) : ( + + +