diff --git a/bun.lockb b/bun.lockb index 05d15e7a6..9c7a61636 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/i18n.js b/i18n.js index baba9084f..8d004e07d 100644 --- a/i18n.js +++ b/i18n.js @@ -22,6 +22,7 @@ module.exports = { '/interactive-coding-tutorials': ['projects'], '/interactive-exercise/[slug]': ['exercises', 'workshops'], '/choose-program': ['choose-program', 'dashboard', 'profile', 'assignments'], + '/main-cohort/[mainCohortSlug]/syllabus/[cohortSlug]/[lesson]/[lessonSlug]': ['syllabus', 'dashboard', 'projects', 'assignments'], '/syllabus/[cohortSlug]/[lesson]/[lessonSlug]': ['syllabus', 'dashboard', 'projects', 'assignments'], '/survey/[surveyId]': ['survey'], '/mentorship': ['mentorship'], diff --git a/package-lock.json b/package-lock.json index fee068f51..06cecd83a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "react-player": "^2.12.0", "react-plx": "^2.1.2", "react-redux": "8.0.5", + "react-rewards": "^2.0.4", "react-select": "^5.7.3", "react-syntax-highlighter": "15.5.0", "react-tagsinput": "^3.20.1", @@ -28732,6 +28733,15 @@ } } }, + "node_modules/react-rewards": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-rewards/-/react-rewards-2.0.4.tgz", + "integrity": "sha512-Lw7gIhD8yPDzC6boaVmcXwuTHRLSLAdqB3kZc+29YWvdHWsuc3fdAZlxI8Cm8fvD8fhP+3JkZBtzX224czw15w==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-select": { "version": "5.7.5", "license": "MIT", diff --git a/package.json b/package.json index ea1cc4bfe..42fc8fb93 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "react-player": "^2.12.0", "react-plx": "^2.1.2", "react-redux": "8.0.5", + "react-rewards": "^2.0.4", "react-select": "^5.7.3", "react-syntax-highlighter": "15.5.0", "react-tagsinput": "^3.20.1", diff --git a/public/locales/en/alert-message.json b/public/locales/en/alert-message.json index 7c0d686eb..dc1f2f950 100644 --- a/public/locales/en/alert-message.json +++ b/public/locales/en/alert-message.json @@ -8,6 +8,7 @@ "content-not-found2": "The endpoint could not access any content of this {{lesson}}", "default-version-not-found": "Default version could not access any content of this {{lesson}}", "invalid-cohort-slug": "Invalid cohort slug", + "error-fetching-syllabus": "There was a problem while fetching the syllabus data", "no-cohort-modules-found": "No cohort modules found, first choose a valid cohort", "language-not-found": "Data for language \"{{currentLanguageLabel}}\" not found, showing the english version", "task-cant-sync-with-cohort": "Some Tasks cannot sync with current cohort", diff --git a/public/locales/en/choose-program.json b/public/locales/en/choose-program.json index 1668489db..c203d7fa1 100644 --- a/public/locales/en/choose-program.json +++ b/public/locales/en/choose-program.json @@ -4,6 +4,10 @@ }, "title": "Your Programs", "welcome-back-user": "Welcome, {{name}}", + "hello-user": "Hello, {{name}}", + "rigo-chat": { + "welcome-message": "Hi {{firstName}}! I see you are on the course {{cohortName}}. Is there anything you would like to know about it?" + }, "welcome": "Welcome", "your-active-programs": "Your active programs", "join-our-community": "Join our community", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index d1dbcc91b..c7e0eb153 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,11 +1,15 @@ { "change-language": "Change language", "connect-with-github": "Connect with Github", + "connect-with-rigobot": "Connect with Rigobot", + "get-help-rigobot": "Get help from Rigobot", "see-financing-options": "See financing options", "your-tutors-in-this-cohort": "Your tutors:", "main-instructor": "Main Instructor", "teacher-assistant": "Teacher Assistant", "rigo": "Rigo", + "see-workshops": "See all workshops", + "schedule-mentoring": "Schedule a mentoring session", "rigobot-bubble": { "greeting": "Hi!" }, diff --git a/public/locales/en/dashboard.json b/public/locales/en/dashboard.json index b27b8bb98..5a5fddfb8 100644 --- a/public/locales/en/dashboard.json +++ b/public/locales/en/dashboard.json @@ -4,8 +4,26 @@ }, "title": "Your News", "moduleMap": "Module map", + "module": "Module", + "modules-count": "{{count}} Modules", + "path-to-claim": "Path to claim your certificate", + "open": "Open", + "completed": "Completed!", + "hours-worked": "{{hours}} Hours worked", + "issued-on": "Issued on {{date}}", + "share": "Share", + "hide-content": "Hide content", + "show-content": "Show content", + "start-course": "Start course", "backToChooseProgram": "Back to choose program", "progressText": "progress in the program", + "students-modal": { + "students-course": "Students in this course", + "select-student": "Select a student to see their full report", + "filter-by-name": "Filter by name or email", + "student": "Student", + "no-students": "No students found" + }, "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!", "intro-video-title": "Welcome!", @@ -198,6 +216,7 @@ "take-attendancy": "Take attendance", "review-attendancy": "Review attendance", "assignments": "Assignments", + "student-progress": "Student Progress", "teacher-tutorial": "Teacher tutorial", "no-instructions": ">:warning: No available instruction found for this module" }, diff --git a/public/locales/es/alert-message.json b/public/locales/es/alert-message.json index e78fb9f32..60913881f 100644 --- a/public/locales/es/alert-message.json +++ b/public/locales/es/alert-message.json @@ -8,6 +8,7 @@ "content-not-found2": "El endpoint no pudo acceder a ningún contenido de esta {{lesson}}", "default-version-not-found": "La versión predeterminada no pudo acceder a ningún contenido de este {{lesson}}", "invalid-cohort-slug": "Slug de cohorte no válido", + "error-fetching-syllabus": "Hubo un problema al mostrar la información del plan de estudios", "no-cohort-modules-found": "No se encontraron módulos de cohorte, primero elija una cohorte válida", "language-not-found": "No se encontró información para el idioma \"{{currentLanguageLabel}}\", mostrando la versión en inglés", "task-cant-sync-with-cohort": "Algunas tareas no se pueden sincronizar con la cohorte actual", diff --git a/public/locales/es/choose-program.json b/public/locales/es/choose-program.json index 6a8d62e62..7e25e7825 100644 --- a/public/locales/es/choose-program.json +++ b/public/locales/es/choose-program.json @@ -4,6 +4,10 @@ }, "title": "Tus Programas", "welcome-back-user": "Bienvenido, {{name}}", + "hello-user": "Hola, {{name}}", + "rigo-chat": { + "welcome-message": "¡Hola {{firstName}}! Veo que estas leyendo haciendo el curso {{cohortName}}. ¿Hay algo que pueda hacer para ayudarte?" + }, "welcome": "Bienvenido", "your-active-programs": "Tus programas activos", "join-our-community": "Únete a nuestra comunidad", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index a0ebad0f0..7a1be183a 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -1,11 +1,18 @@ { "change-language": "Cambiar idioma", "connect-with-github": "Conéctate con Github", + "connect-with-rigobot": "Conéctate con Rigobot", + "get-help-rigobot": "Pide ayuda a Rigobot", "see-financing-options": "Ver opciones de financiamiento", "your-tutors-in-this-cohort": "Tus tutores:", "main-instructor": "Instructor Principal", "teacher-assistant": "Asistente de Profesor", "rigo": "Rigo", + "see-workshops": "Ver todos los workshops", + "schedule-mentoring": "Agenda una sesión de mentoria", + "rigobot-bubble": { + "greeting": "Hola!" + }, "ai-tutor": "Tutor IA", "clone-modal": { "title": "¿Cómo clonar un proyecto?" diff --git a/public/locales/es/dashboard.json b/public/locales/es/dashboard.json index 33ef3e7f8..5c7a6e248 100644 --- a/public/locales/es/dashboard.json +++ b/public/locales/es/dashboard.json @@ -4,8 +4,26 @@ }, "title": "Tus noticias", "moduleMap": "Mapa de módulos", + "module": "Módulo", + "modules-count": "{{count}} Módulos", + "path-to-claim": "Vía para reclamar tu certificado", + "open": "Abrir", + "completed": "¡Completed!", + "hours-worked": "{{hours}} Horas trabajadas", + "issued-on": "Emitido en {{date}}", + "share": "Compartir", + "hide-content": "Ocultar contenido", + "show-content": "Mostrar contenido", + "start-course": "Empezar el curso", "backToChooseProgram": "Volver a elegir programa", "progressText": "Progreso en el programa", + "students-modal": { + "students-course": "Estudiantes en este curso", + "select-student": "Selecciona un estudiante para ver el reporte de su progreso", + "filter-by-name": "Filtrar por nombre o email", + "student": "Estudiante", + "no-students": "No se encontraron estudiantes" + }, "whiteLabeledText": "Este curso es traído a ti gracias a nuestra alianza con esta universidad.", "free-trial-msg": "Actualmente se encuentra en una prueba gratuita, algunas funciones pueden ser limitadas. ¡Actualiza tu plan para tener acceso ilimitado!", "intro-video-title": "Bienvenido!", @@ -199,6 +217,7 @@ "take-attendancy": "Tomar asistencia", "review-attendancy": "Revisar asistencia", "assignments": "Tareas", + "student-progress": "Progreso del Estudiante", "teacher-tutorial": "Tutorial de profesor", "no-instructions": ">:warning: No se encontró instrucción disponible para este módulo" }, diff --git a/src/common/components/AttendanceModal/index.jsx b/src/common/components/AttendanceModal/index.jsx index 4c0b28248..f47470a29 100644 --- a/src/common/components/AttendanceModal/index.jsx +++ b/src/common/components/AttendanceModal/index.jsx @@ -20,8 +20,7 @@ function AttendanceModal({ title, message, isOpen, onClose, students, }) { const { t } = useTranslation('dashboard'); - const { state, setCohortSession } = useCohortHandler(); - const { cohortSession, sortedAssignments } = state; + const { setCohortSession, cohortSession, sortedAssignments } = useCohortHandler(); const [historyLog, setHistoryLog] = useState(); const [day, setDay] = useState(cohortSession.current_day); const [attendanceTaken, setAttendanceTaken] = useState({}); diff --git a/src/common/components/FooterTC.jsx b/src/common/components/FooterTC.jsx index c72db4237..8217869f7 100644 --- a/src/common/components/FooterTC.jsx +++ b/src/common/components/FooterTC.jsx @@ -21,6 +21,7 @@ function FooterTC({ pageProps }) { const noFooterRoutes = [ '/cohort/[cohortSlug]/[slug]/[version]', '/syllabus/[cohortSlug]/[lesson]/[lessonSlug]', + '/main-cohort/[mainCohortSlug]/syllabus/[cohortSlug]/[lesson]/[lessonSlug]', '/mentorship/schedule', ]; diff --git a/src/common/components/Icon/set/badge.jsx b/src/common/components/Icon/set/badge.jsx new file mode 100644 index 000000000..3d6686172 --- /dev/null +++ b/src/common/components/Icon/set/badge.jsx @@ -0,0 +1,23 @@ +const badge = ({ + width, height, style, color, +}) => ( + + + + +); + +export default badge; diff --git a/src/common/components/Icon/set/certificate-2.jsx b/src/common/components/Icon/set/certificate-2.jsx new file mode 100644 index 000000000..e3399ab24 --- /dev/null +++ b/src/common/components/Icon/set/certificate-2.jsx @@ -0,0 +1,78 @@ +const certificate2 = ({ + width, height, style, color, color2, +}) => ( + + + + + + + + + + + + + + + + + + + + + + + +); + +export default certificate2; diff --git a/src/common/components/Icon/set/certificate-small.jsx b/src/common/components/Icon/set/certificate-small.jsx new file mode 100644 index 000000000..95c7bbe6a --- /dev/null +++ b/src/common/components/Icon/set/certificate-small.jsx @@ -0,0 +1,27 @@ +const certificateSmall = ({ + width, height, style, color, +}) => ( + + + + + +); + +export default certificateSmall; diff --git a/src/common/components/Icon/set/dots.jsx b/src/common/components/Icon/set/dots.jsx new file mode 100644 index 000000000..3733b32d1 --- /dev/null +++ b/src/common/components/Icon/set/dots.jsx @@ -0,0 +1,27 @@ +const send = ({ + width, height, style, color, +}) => ( + + + + + +); + +export default send; diff --git a/src/common/components/Icon/set/party-popper-off.jsx b/src/common/components/Icon/set/party-popper-off.jsx new file mode 100644 index 000000000..3264bd1b8 --- /dev/null +++ b/src/common/components/Icon/set/party-popper-off.jsx @@ -0,0 +1,35 @@ +const partyPopperOff = ({ + width, height, style, color, color2, +}) => ( + + + + + + + +); + +export default partyPopperOff; diff --git a/src/common/components/Icon/set/party-popper.jsx b/src/common/components/Icon/set/party-popper.jsx new file mode 100644 index 000000000..1b962a6c1 --- /dev/null +++ b/src/common/components/Icon/set/party-popper.jsx @@ -0,0 +1,40 @@ +const partyPopperOff = ({ + width, height, style, +}) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default partyPopperOff; diff --git a/src/common/components/Icon/set/share.jsx b/src/common/components/Icon/set/share.jsx new file mode 100644 index 000000000..74df4abb9 --- /dev/null +++ b/src/common/components/Icon/set/share.jsx @@ -0,0 +1,20 @@ +const send = ({ + width, height, style, color, +}) => ( + + + + +); + +export default send; diff --git a/src/common/components/LiveEvent/MainEvent.jsx b/src/common/components/LiveEvent/MainEvent.jsx index b798934a2..98bc85eec 100644 --- a/src/common/components/LiveEvent/MainEvent.jsx +++ b/src/common/components/LiveEvent/MainEvent.jsx @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /* eslint-disable react/jsx-no-useless-fragment */ import { Box, Divider, Tag, TagLabel } from '@chakra-ui/react'; import PropTypes from 'prop-types'; @@ -26,7 +27,7 @@ function MainEvent({ const liveStartsAtDate = new Date(event?.starting_at); const liveEndsAtDate = new Date(event?.ended_at || event?.ending_at); - const isTeacher = cohorts.some(({ cohort, role }) => cohort.slug === event.cohort?.slug && ['TEACHER', 'ASSISTANT'].includes(role)); + const isTeacher = cohorts.some(({ slug, cohort_user }) => slug === event.cohort?.slug && ['TEACHER', 'ASSISTANT'].includes(cohort_user.role)); const joinMessage = () => (isTeacher ? t('start-class') : event?.cohort?.name); return ( diff --git a/src/common/components/Navbar/index.jsx b/src/common/components/Navbar/index.jsx index 83741d5a4..a77bcb295 100644 --- a/src/common/components/Navbar/index.jsx +++ b/src/common/components/Navbar/index.jsx @@ -44,7 +44,6 @@ function NavbarWithSubNavigation({ translations, pageProps }) { const isUtmMediumAcademy = userSession?.utm_medium === 'academy'; const { isAuthenticated, isLoading, user, logout } = useAuth(); const [ITEMS, setITEMS] = useState([]); - const [allSubscriptions, setAllSubscriptions] = useState([]); const [mktCourses, setMktCourses] = useState([]); const [userCohorts, setUserCohorts] = useState([]); const { state } = useCohortHandler(); @@ -78,60 +77,13 @@ function NavbarWithSubNavigation({ translations, pageProps }) { } = navbarTR[locale]; const translationsPropsExists = translations?.length > 0; - const { selectedProgramSlug } = cohortSession; - - const programSlug = cohortSession?.selectedProgramSlug || '/choose-program'; + const programSlug = '/choose-program'; const whiteLabelitems = t('white-label-version-items', { selectedProgramSlug: '/choose-program', }, { returnObjects: true }); - useEffect(() => { - if (cohortSession?.available_as_saas) { - bc.payment({ - status: 'ACTIVE,FREE_TRIAL,FULLY_PAID,CANCELLED,PAYMENT_ISSUE,EXPIRED,ERROR', - }).subscriptions() - .then(async ({ data }) => { - const planFinancings = data?.plan_financings?.length > 0 ? data?.plan_financings : []; - const subscriptions = data?.subscriptions?.length > 0 ? data?.subscriptions : []; - - setAllSubscriptions([...planFinancings, ...subscriptions]); - }); - } - }, [cohortSession]); - - const allowNavigation = () => { - const getAdditionalInfo = () => { - if (allSubscriptions) { - const currentSessionSubs = allSubscriptions?.filter((sub) => sub.academy?.id === cohortSession?.academy?.id); - const cohortSubscriptions = currentSessionSubs?.filter((sub) => sub.selected_cohort_set?.cohorts.some((cohort) => cohort.id === cohortSession.id)); - - if (cohortSubscriptions.length === 0) { - return false; - } - - const expiredCourse = cohortSubscriptions.find((sub) => sub.status === 'EXPIRED' || sub.status === 'ERROR'); - if (expiredCourse) return false; - - const fullyPaidSub = cohortSubscriptions.find((sub) => sub.status === 'FULLY_PAID' || sub.status === 'ACTIVE'); - if (fullyPaidSub) return true; - - const freeTrialSub = cohortSubscriptions.find((sub) => sub.status === 'FREE_TRIAL'); - const freeTrialExpDate = new Date(freeTrialSub?.valid_until); - const todayDate = new Date(); - - if (todayDate > freeTrialExpDate) return false; - return true; - } - return false; - }; - - if (cohortSession?.available_as_saas === true && cohortSession.cohort_role === 'STUDENT') return getAdditionalInfo(); - if (Object.keys(cohortSession).length > 0 && (cohortSession.cohort_role !== 'STUDENT' || cohortSession.available_as_saas === false)) return true; - return false; - }; - - const items = t('ITEMS', { selectedProgramSlug: allowNavigation() ? selectedProgramSlug : '/choose-program' }, { returnObjects: true }); + const items = t('ITEMS', { selectedProgramSlug: '/choose-program' }, { returnObjects: true }); axios.defaults.headers.common['Accept-Language'] = locale; @@ -215,7 +167,7 @@ function NavbarWithSubNavigation({ translations, pageProps }) { setITEMS(preFilteredItems.filter((item) => item.disabled !== true)); } } - }, [user, userCohorts, isLoading, selectedProgramSlug, mktCourses, router.locale, location]); + }, [user, userCohorts, isLoading, cohortSession, mktCourses, router.locale, location]); const closeSettings = () => { setSettingsOpen(false); diff --git a/src/common/components/ProgressBar/Progress.jsx b/src/common/components/ProgressBar/Progress.jsx index e734b2bd1..4a69501c3 100644 --- a/src/common/components/ProgressBar/Progress.jsx +++ b/src/common/components/ProgressBar/Progress.jsx @@ -13,6 +13,7 @@ function Progress({ baseColor, borderRadius, widthSize, + width, }) { const [barWidth, setBarWidth] = useState(0); const [initialized, setInitialized] = useState(false); @@ -53,7 +54,7 @@ function Progress({ const baseColorDefault = useColorModeValue('gray.100', 'whiteAlpha.300'); return ( - + 100 ? 100 : percentage; const taskPercentageLimited = taskCount?.percentage > 100 ? 100 : taskCount?.percentage; diff --git a/src/common/components/ReviewModal/index.jsx b/src/common/components/ReviewModal/index.jsx index 6a249a900..4fc33c3ca 100644 --- a/src/common/components/ReviewModal/index.jsx +++ b/src/common/components/ReviewModal/index.jsx @@ -17,7 +17,6 @@ import LoaderScreen from '../LoaderScreen'; import ReviewCodeRevision from './ReviewCodeRevision'; import useCohortHandler from '../../hooks/useCohortHandler'; import PopoverTaskHandler from '../PopoverTaskHandler'; -import useModuleHandler from '../../hooks/useModuleHandler'; import iconDict from '../../utils/iconDict.json'; import UndoApprovalModal from '../UndoApprovalModal'; import useAuth from '../../hooks/useAuth'; @@ -53,8 +52,7 @@ function ReviewModal({ isExternal, externalFiles, isOpen, isStudent, externalDat isApprovingOrRejecting: false, }); const [comment, setComment] = useState(''); - const { updateAssignment } = useModuleHandler(); - const { state } = useCohortHandler(); + const { updateAssignment, state } = useCohortHandler(); const { cohortSession } = state; const [currentAssetData, setCurrentAssetData] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); diff --git a/src/common/components/ShareButton.jsx b/src/common/components/ShareButton.jsx index 77a5e4ab3..0ebbf0d1d 100644 --- a/src/common/components/ShareButton.jsx +++ b/src/common/components/ShareButton.jsx @@ -14,7 +14,7 @@ import Link from './NextChakraLink'; import useStyle from '../hooks/useStyle'; function ShareButton({ - variant, title, shareText, message, link, socials, withParty, onlyModal, currentTask, + variant, title, shareText, message, link, socials, withParty, onlyModal, currentTask, onClose, }) { const { t } = useTranslation('profile'); const [party, setParty] = useState(true); @@ -80,6 +80,7 @@ function ShareButton({ onClose={() => { setIsOpen(false); setParty(true); + onClose(); }} size="xl" > @@ -210,6 +211,7 @@ ShareButton.propTypes = { shareText: PropTypes.string, message: PropTypes.string, withParty: PropTypes.bool, + onClose: PropTypes.func, }; ShareButton.defaultProps = { @@ -221,6 +223,7 @@ ShareButton.defaultProps = { shareText: '', message: '', withParty: false, + onClose: () => {}, }; export default memo(ShareButton); diff --git a/src/common/components/SupportSidebar/MentoringConsumables.jsx b/src/common/components/SupportSidebar/MentoringConsumables.jsx index 48484aeaf..77883a8fa 100644 --- a/src/common/components/SupportSidebar/MentoringConsumables.jsx +++ b/src/common/components/SupportSidebar/MentoringConsumables.jsx @@ -62,7 +62,7 @@ function NoConsumablesCard({ t, setMentoryProps, handleGetMoreMentorships, mento } function ProfilesSection({ - profiles, + profiles, size, }) { const { usersConnected } = useOnline(); @@ -73,8 +73,8 @@ function ProfilesSection({ const isOnline = usersConnected?.includes(c.user.id); return ( list.filter((service) => { if (allCohorts.length > 0) { return allCohorts.some((elem) => { - if (elem?.cohort?.academy?.id === service?.academy?.id && (elem?.finantial_status === 'LATE' || elem?.educational_status === 'SUSPENDED')) { + if (elem?.academy?.id === service?.academy?.id && (elem?.cohort_user.finantial_status === 'LATE' || elem?.cohort_user.educational_status === 'SUSPENDED')) { return false; } return true; diff --git a/src/common/components/TeacherSidebar.jsx b/src/common/components/TeacherSidebar.jsx index ba980866d..f3e86e0a6 100644 --- a/src/common/components/TeacherSidebar.jsx +++ b/src/common/components/TeacherSidebar.jsx @@ -101,7 +101,7 @@ function TeacherSidebar({ {/* Start attendance */} - {cohortSession.ending_date && ( + {cohortSession?.ending_date && ( setOpenAttendance(true)}> @@ -111,7 +111,7 @@ function TeacherSidebar({ )} {/* Review attendance */} - {cohortSession.ending_date && ( + {cohortSession?.ending_date && ( { window.open(`/cohort/${cohortSlug}/attendance`, '_blank'); diff --git a/src/common/context/AuthContext.jsx b/src/common/context/AuthContext.jsx index 0753a7160..aa6bb5b95 100644 --- a/src/common/context/AuthContext.jsx +++ b/src/common/context/AuthContext.jsx @@ -16,6 +16,7 @@ import Text from '../components/Text'; import { SILENT_CODE } from '../../lib/types'; import { warn } from '../../utils/logging'; import { generateUserContext } from '../../utils/rigobotContext'; +import useCohortAction from '../store/actions/cohortAction'; const initialState = { isLoading: true, @@ -123,6 +124,7 @@ export const AuthContext = createContext({ function AuthProvider({ children, pageProps }) { const router = useRouter(); + const { setMyCohorts } = useCohortAction(); const { t, lang } = useTranslation('footer'); const toast = useToast(); const { rigo, isRigoInitialized } = useRigo(); @@ -371,6 +373,7 @@ function AuthProvider({ children, pageProps }) { localStorage.removeItem('showGithubWarning'); localStorage.removeItem('redirect'); dispatch({ type: 'LOGOUT' }); + setMyCohorts([]); }; const updateProfile = async (payload) => { diff --git a/src/common/handlers/index.js b/src/common/handlers/index.js index d9cf65fa5..54bde8b9b 100644 --- a/src/common/handlers/index.js +++ b/src/common/handlers/index.js @@ -168,24 +168,24 @@ const handlers = { date: formatedDate, }; }, - getCohortsFinished: (cohorts) => cohorts.filter((program) => { - const educationalStatus = program?.educational_status?.toUpperCase(); - const programCohortStage = program?.cohort?.stage?.toUpperCase(); + getCohortsFinished: (cohorts) => cohorts.filter((cohort) => { + const educationalStatus = cohort.cohort_user.educational_status.toUpperCase(); + const programCohortStage = cohort.stage.toUpperCase(); const hasEnded = ['ENDED'].includes(programCohortStage); const isGraduated = educationalStatus === 'GRADUATED'; const showStudent = ['GRADUATED', 'POSTPONED', 'ACTIVE'].includes(educationalStatus); const isNotHiddenOnPrework = programCohortStage === 'PREWORK' - && program?.cohort?.is_hidden_on_prework === false + && cohort.is_hidden_on_prework === false && hasEnded; return (isGraduated || hasEnded || isNotHiddenOnPrework) && showStudent; }), - getActiveCohorts: (cohorts) => cohorts.filter((program) => { - const educationalStatus = program?.educational_status?.toUpperCase(); - const programRole = program?.role?.toUpperCase(); - const programCohortStage = program?.cohort?.stage?.toUpperCase(); + getActiveCohorts: (cohorts) => cohorts.filter((cohort) => { + const educationalStatus = cohort.cohort_user.educational_status?.toUpperCase(); + const programRole = cohort.cohort_user.role?.toUpperCase(); + const programCohortStage = cohort.stage.toUpperCase(); const isGraduated = educationalStatus === 'GRADUATED'; const visibleForTeacher = programRole !== 'STUDENT'; @@ -201,7 +201,7 @@ const handlers = { const cohortIsAvailable = showCohort && !hasEnded; const isNotHiddenOnPrework = programCohortStage === 'PREWORK' - && program?.cohort?.is_hidden_on_prework === false + && cohort.is_hidden_on_prework === false && !hasEnded; const showStudent = ['ACTIVE'].includes(educationalStatus) && programRole === 'STUDENT'; diff --git a/src/common/hooks/useCohortHandler.js b/src/common/hooks/useCohortHandler.js index 16b0bd4a0..618e2f45b 100644 --- a/src/common/hooks/useCohortHandler.js +++ b/src/common/hooks/useCohortHandler.js @@ -1,13 +1,14 @@ /* eslint-disable camelcase */ +import { useMemo } from 'react'; import axios from 'axios'; import { useToast } from '@chakra-ui/react'; import useTranslation from 'next-translate/useTranslation'; import { useRouter } from 'next/router'; import useAuth from './useAuth'; -import { devLog, getStorageItem } from '../../utils'; +import { getStorageItem } from '../../utils'; import useCohortAction from '../store/actions/cohortAction'; -import useModuleHandler from './useModuleHandler'; import { processRelatedAssignments } from '../handlers/cohorts'; +import { reportDatalayer } from '../../utils/requests'; import bc from '../services/breathecode'; import { BREATHECODE_HOST, DOMAIN_NAME } from '../../utils/variables'; @@ -15,13 +16,20 @@ function useCohortHandler() { const router = useRouter(); const { user } = useAuth(); const { t, lang } = useTranslation('dashboard'); - const { setCohortSession, setTaskCohortNull, setSortedAssignments, setUserCapabilities, setMyCohorts, state } = useCohortAction(); - const { cohortProgram, taskTodo, setCohortProgram, setTaskTodo } = useModuleHandler(); + const { + setCohortSession, + setTaskCohortNull, + setUserCapabilities, + setMyCohorts, + setCohortsAssingments, + state, + } = useCohortAction(); const { cohortSession, - sortedAssignments, userCapabilities, + cohortsAssignments, + myCohorts, } = state; const toast = useToast(); const accessToken = getStorageItem('accessToken'); @@ -52,25 +60,127 @@ function useCohortHandler() { } }; - const getCohortAssignments = async ({ - slug, cohort, updatedUser = undefined, + const serializeModulesMap = (moduleData, tasks) => { + const assignmentsRecopilated = []; + moduleData.forEach((module) => { + const { + id, label, description, lessons, replits, assignments, quizzes, + } = module; + if (lessons && replits && assignments && quizzes) { + const nestedAssignments = processRelatedAssignments(module, tasks); + + // this properties name's reassignment is done to keep compatibility with deprecated functions + const { + content, + filteredContent, + filteredContentByPending, + } = nestedAssignments; + + // Data to be sent to [sortedAssignments] = state + const assignmentsStruct = { + id, + label, + description, + content, + exists_activities: content?.length > 0, + filteredContent, + filteredContentByPending: content?.length > 0 ? filteredContentByPending : null, + duration_in_days: module?.duration_in_days || null, + teacherInstructions: module.teacher_instructions, + extendedInstructions: module.extended_instructions || `${t('teacher-sidebar.no-instructions')}`, + keyConcepts: module['key-concepts'], + }; + + if (content.length > 0) { + // prevent duplicates when a new module has been started (added to sortedAssignments array) + const keyIndex = assignmentsRecopilated.findIndex((x) => x.id === id); + if (keyIndex > -1) { + assignmentsRecopilated.splice(keyIndex, 1, { + ...assignmentsStruct, + }); + } else { + assignmentsRecopilated.push({ + ...assignmentsStruct, + }); + } + } + } + }); + + return assignmentsRecopilated; + }; + + const getCohortsModules = async (cohorts) => { + try { + const assignmentsMap = {}; + + const preFechedCohorts = cohorts.reduce((acum, curr) => { + if (curr.slug in cohortsAssignments) return [...acum, curr]; + return acum; + }, []); + + const cohortsToFetch = cohorts.filter((cohort) => !preFechedCohorts.some(({ slug }) => slug === cohort.slug)); + + const syllabusPromises = cohortsToFetch.map((cohort) => bc.syllabus().get(cohort.academy.id, cohort.syllabus_version.slug, cohort.syllabus_version.version).then((res) => ({ cohort: cohort.id, ...res }))); + const tasksPromises = cohortsToFetch.map((cohort) => bc.todo({ cohort: cohort.id, limit: 1000 }).getTaskByStudent().then((res) => ({ cohort: cohort.id, ...res }))); + const allResults = await Promise.all([ + ...syllabusPromises, + ...tasksPromises, + ]); + + preFechedCohorts.forEach(({ slug }) => { + assignmentsMap[slug] = { ...cohortsAssignments[slug] }; + }); + + cohortsToFetch.forEach((cohort) => { + const cohortResults = allResults.filter((elem) => elem.cohort === cohort.id); + + let syllabus = null; + let tasks = []; + + cohortResults.forEach((elem) => { + const { data } = elem; + if ('json' in data) syllabus = data; + else tasks = data.results; + }); + const cohortModules = serializeModulesMap(syllabus.json.days, tasks); + + assignmentsMap[cohort.slug] = { + modules: cohortModules, + syllabus, + tasks, + }; + }); + + setCohortsAssingments({ ...cohortsAssignments, ...assignmentsMap }); + + return assignmentsMap; + } catch (e) { + console.log(e); + toast({ + position: 'top', + title: t('alert-message:error-fetching-syllabus'), + status: 'error', + duration: 7000, + isClosable: true, + }); + + return {}; + } + }; + + const getCohortUserCapabilities = async ({ + cohort, updatedUser = undefined, }) => { if (user) { - const academyId = cohort?.academy.id; - const version = cohort?.syllabus_version?.version; - const syllabusSlug = cohort?.syllabus_version?.slug || slug; + const academyId = cohort?.academy?.id; const currentAcademy = user.roles.find((role) => role.academy.id === academyId) || updatedUser?.roles.find((role) => role.academy.id === academyId); if (currentAcademy) { // 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 - ]); + const userRoles = await bc.auth().getRoles(currentAcademy?.role); // Roles + setUserCapabilities(userRoles.data.capabilities); - setTaskTodo(taskTodoData.data.results); - setCohortProgram(programData.data); } catch (err) { console.log(err); toast({ @@ -98,29 +208,37 @@ function useCohortHandler() { } }; + const parseCohort = (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, + }; + }; + const getCohortData = async ({ cohortSlug, }) => { 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 currentCohort = parsedCohorts.find((c) => c.slug === cohortSlug); + let parsedCohorts = myCohorts.map((cohort) => ({ ...cohort })); + let currentCohort = myCohorts.find((c) => c.slug === cohortSlug); + + //we make sure that we have already loaded the data of the cohort and its micro cohorts + if (!currentCohort || (currentCohort.micro_cohorts.length > 0 && currentCohort.micro_cohorts.every((cohort) => myCohorts.some(({ slug }) => cohort.slug === slug)))) { + const { data } = await bc.admissions().me(); + if (!data) throw new Error('No data'); + const { cohorts } = data; + + parsedCohorts = cohorts.map(parseCohort); + + currentCohort = parsedCohorts.find((c) => c.slug === cohortSlug); + } if (!currentCohort) { if (assetSlug) return handleRedirectToPublicPage(); @@ -128,6 +246,10 @@ function useCohortHandler() { return router.push('/choose-program'); } + const cohorts = currentCohort.micro_cohorts.length > 0 ? parsedCohorts.filter((c) => currentCohort.micro_cohorts.some((elem) => elem.slug === c.slug)) : [currentCohort]; + + await getCohortsModules(cohorts); + setCohortSession(currentCohort); setMyCohorts(parsedCohorts); return currentCohort; @@ -147,71 +269,189 @@ function useCohortHandler() { } }; - // Sort all data fetched in order of taskTodo - const prepareTasks = () => { - const moduleData = cohortProgram?.json?.days || cohortProgram?.json?.modules || []; - const assignmentsRecopilated = []; - devLog('json.days:', moduleData); + const taskTodo = useMemo(() => { + if (cohortSession && cohortSession.slug in cohortsAssignments) { + return cohortsAssignments[cohortSession.slug].tasks; + } + return []; + }, [cohortsAssignments, cohortSession]); - if (cohortProgram?.json && taskTodo) { - moduleData.forEach((assignment) => { - const { - id, label, description, lessons, replits, assignments, quizzes, - } = assignment; - if (lessons && replits && assignments && quizzes) { - 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 = { - id, - label, - description, - modules, - exists_activities: modules?.length > 0, - filteredModules, - filteredModulesByPending: modules?.length > 0 ? filteredModulesByPending : null, - duration_in_days: assignment?.duration_in_days || null, - teacherInstructions: assignment.teacher_instructions, - extendedInstructions: assignment.extended_instructions || `${t('teacher-sidebar.no-instructions')}`, - keyConcepts: assignment['key-concepts'], - }; + const cohortProgram = useMemo(() => { + if (cohortSession && cohortSession.slug in cohortsAssignments) { + return cohortsAssignments[cohortSession.slug].syllabus; + } + return null; + }, [cohortsAssignments, cohortSession]); - // prevent duplicates when a new module has been started (added to sortedAssignments array) - const keyIndex = assignmentsRecopilated.findIndex((x) => x.id === id); - if (keyIndex > -1) { - assignmentsRecopilated.splice(keyIndex, 1, { - ...assignmentsStruct, - }); - } else { - assignmentsRecopilated.push({ - ...assignmentsStruct, - }); - } + const sortedAssignments = useMemo(() => { + if (cohortSession?.slug in cohortsAssignments) return cohortsAssignments[cohortSession.slug].modules; + return []; + }, [cohortsAssignments, cohortSession]); + + const updateTask = (task, cohort) => { + const { id, name, slug } = cohort; + const cohortData = cohortsAssignments[slug]; + + const keyIndex = cohortData.tasks.findIndex((x) => x.id === task.id); + + const newTasks = [ + ...cohortData.tasks.slice(0, keyIndex), // before keyIndex (inclusive) + { ...task, cohort: { id, name, slug } }, // key item (updated) + ...cohortData.tasks.slice(keyIndex + 1), // after keyIndex (exclusive) + ]; + + setCohortsAssingments({ + ...cohortsAssignments, + [slug]: { + ...cohortData, + tasks: newTasks, + modules: serializeModulesMap(cohortData.syllabus.json.days, newTasks), + }, + }); + }; + + const addTasks = (tasks, cohort) => { + const { id, slug, name } = cohort; + const cohortData = cohortsAssignments[cohort.slug]; + + const newTasks = [ + ...cohortData.tasks, + ...tasks.map((task) => ({ ...task, cohort: { id, slug, name } })), + ]; + + setCohortsAssingments({ + ...cohortsAssignments, + [cohort.slug]: { + ...cohortData, + tasks: newTasks, + modules: serializeModulesMap(cohortData.syllabus.json.days, newTasks), + }, + }); + }; + + const updateAssignment = async ({ + task, closeSettings, githubUrl, taskStatus, + }) => { + // Task case + const { cohort, ...taskData } = task; + const toggleStatus = (task.task_status === undefined || task.task_status === 'PENDING') ? 'DONE' : 'PENDING'; + const isProject = task.task_type && task.task_type === 'PROJECT'; + + try { + const projectUrl = githubUrl || ''; + + const isDelivering = projectUrl !== ''; + + let taskToUpdate; + + const toastMessage = () => { + if (!isProject) return t('alert-message:assignment-updated'); + return isDelivering ? t('alert-message:delivery-success') : t('alert-message:delivery-removed'); + }; + + if (isProject) { + taskToUpdate = { + ...taskData, + task_status: taskStatus || toggleStatus, + github_url: projectUrl, + revision_status: 'PENDING', + delivered_at: new Date(), + }; + } else { + taskToUpdate = { + ...taskData, + id: task.id, + task_status: toggleStatus, + }; + } + + const response = await bc.todo().update(taskToUpdate); + if (response.status < 400) { + updateTask(taskToUpdate, cohort); + 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: toastMessage(), + status: 'success', + duration: 6000, + isClosable: true, + }); + closeSettings(); + } else { + toast({ + position: 'top', + title: isProject ? t('alert-message:delivery-error') : t('alert-message:assignment-update-error'), + status: 'error', + duration: 5000, + isClosable: true, + }); + closeSettings(); + } + } catch (error) { + console.log(error); + toast({ + position: 'top', + title: isProject ? t('alert-message:delivery-error') : t('alert-message:assignment-update-error'), + status: 'error', + duration: 5000, + isClosable: true, + }); + closeSettings(); + } + }; + + const startDay = async ({ + newTasks, cohort, label, customHandler = () => {}, updateContext = true, + }) => { + try { + const response = await bc.todo().add(newTasks); + + 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, + }); + if (updateContext) { + addTasks(response.data, cohort); } + customHandler(response.data); + } + } catch (err) { + console.log('error_ADD_TASK 🔴 ', err); + toast({ + position: 'top', + title: t('alert-message:module-start-error'), + status: 'error', + duration: 6000, + isClosable: true, }); - const filterNotEmptyModules = assignmentsRecopilated.filter( - (l) => l.modules.length > 0, - ); - setSortedAssignments(filterNotEmptyModules); } }; const getTasksWithoutCohort = ({ setModalIsOpen }) => { // Tasks with cohort null - if (router.asPath === cohortSession.selectedProgramSlug) { + if (router.asPath === cohortSession?.selectedProgramSlug) { bc.todo({ cohort: null }).getTaskByStudent() .then(({ data }) => { const filteredUnsyncedCohortTasks = sortedAssignments.flatMap( - (assignment) => data.filter( - (task) => assignment.modules.some( - (module) => task.associated_slug === module.slug, + (module) => data.filter( + (task) => module.content.some( + (assignment) => task.associated_slug === assignment.slug, ), ), ); @@ -233,7 +473,7 @@ function useCohortHandler() { let lastDoneTaskModule = null; sortedAssignments.forEach( (module) => { - if (module.modules.some((task) => task.task_status === 'DONE')) lastDoneTaskModule = module; + if (module.content.some((task) => task.task_status === 'DONE')) lastDoneTaskModule = module; }, ); return lastDoneTaskModule; @@ -241,7 +481,7 @@ function useCohortHandler() { const getMandatoryProjects = () => { const mandatoryProjects = sortedAssignments.flatMap( - (assignment) => assignment.filteredModules.filter( + (module) => module.filteredContent.filter( (l) => { const isMandatoryTimeOut = l.task_type === 'PROJECT' && l.task_status === 'PENDING' && l.mandatory === true && l.daysDiff >= 14; // exceeds 2 weeks @@ -255,16 +495,27 @@ function useCohortHandler() { return { setCohortSession, - setSortedAssignments, - getCohortAssignments, + setMyCohorts, + getCohortUserCapabilities, getCohortData, - prepareTasks, getDailyModuleData, getLastDoneTaskModuleData, getMandatoryProjects, getTasksWithoutCohort, userCapabilities, state, + setCohortsAssingments, + serializeModulesMap, + parseCohort, + taskTodo, + cohortProgram, + addTasks, + updateTask, + updateAssignment, + startDay, + getCohortsModules, + sortedAssignments, + ...state, }; } diff --git a/src/common/hooks/useModuleHandler.js b/src/common/hooks/useModuleHandler.js index 83f5c9224..4cf70e653 100644 --- a/src/common/hooks/useModuleHandler.js +++ b/src/common/hooks/useModuleHandler.js @@ -1,161 +1,9 @@ -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'; 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 { cohort, ...taskData } = task; - const toggleStatus = (task.task_status === undefined || task.task_status === 'PENDING') ? 'DONE' : 'PENDING'; - if (task.task_type && task.task_type !== 'PROJECT') { - const taskToUpdate = { - ...taskData, - id: taskData.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) - ...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: '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 = { - ...taskData, - 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(); - } - } - }; - - const startDay = async ({ - newTasks, label, customHandler = () => {}, - }) => { - try { - const response = await bc.todo({}).add(newTasks); - - 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: t('alert-message:module-start-error'), - status: 'error', - duration: 6000, - isClosable: true, - }); - } - }; + const { state, setCurrentTask, setSubTasks, setNextModule, setPrevModule } = useModuleMap(); return { - updateAssignment, - startDay, - setTaskTodo, - setCohortProgram, setCurrentTask, setSubTasks, setNextModule, diff --git a/src/common/store/actions/cohortAction.js b/src/common/store/actions/cohortAction.js index 1f188bcff..a635838c8 100644 --- a/src/common/store/actions/cohortAction.js +++ b/src/common/store/actions/cohortAction.js @@ -2,9 +2,9 @@ import { useDispatch, useSelector } from 'react-redux'; import { SET_MY_COHORTS, SET_COHORT_SESSION, - SET_SORTED_ASSIGNMENTS, SET_TASK_COHORT_NULL, SET_USER_CAPABILITIES, + SET_COHORTS_ASSIGNMENTS, } from '../types'; import { usePersistent } from '../../hooks/usePersistent'; @@ -41,20 +41,20 @@ const useCohortAction = () => { }); }; - const setSortedAssignments = (payload) => { + const setUserCapabilities = (paylaod) => { dispatch({ - type: SET_SORTED_ASSIGNMENTS, + type: SET_USER_CAPABILITIES, payload: { - sortedAssignments: payload, + userCapabilities: paylaod, }, }); }; - const setUserCapabilities = (paylaod) => { + const setCohortsAssingments = (paylaod) => { dispatch({ - type: SET_USER_CAPABILITIES, + type: SET_COHORTS_ASSIGNMENTS, payload: { - userCapabilities: paylaod, + cohortsAssignments: paylaod, }, }); }; @@ -64,8 +64,8 @@ const useCohortAction = () => { setMyCohorts, setCohortSession, setTaskCohortNull, - setSortedAssignments, setUserCapabilities, + setCohortsAssingments, }; }; diff --git a/src/common/store/actions/moduleMapAction.js b/src/common/store/actions/moduleMapAction.js index f89770730..5236278d7 100644 --- a/src/common/store/actions/moduleMapAction.js +++ b/src/common/store/actions/moduleMapAction.js @@ -4,20 +4,6 @@ const useModuleMap = () => { const dispatch = useDispatch(); const state = useSelector((reducerState) => reducerState.moduleMapReducer); - const setTaskTodo = (newState) => { - dispatch({ - type: 'CHANGE_TASK_TO_DO', - payload: newState, - }); - }; - - const setCohortProgram = (newState) => { - dispatch({ - type: 'CHANGE_COHORT_PROGRAM', - payload: newState, - }); - }; - const setCurrentTask = (newState) => { dispatch({ type: 'CHANGE_CURRENT_TASK', @@ -47,8 +33,6 @@ const useModuleMap = () => { }; return { - setTaskTodo, - setCohortProgram, setCurrentTask, setSubTasks, setNextModule, diff --git a/src/common/store/reducers/cohortReducer.js b/src/common/store/reducers/cohortReducer.js index 05a430257..6d0f9af55 100644 --- a/src/common/store/reducers/cohortReducer.js +++ b/src/common/store/reducers/cohortReducer.js @@ -1,15 +1,15 @@ import { SET_MY_COHORTS, SET_COHORT_SESSION, - SET_SORTED_ASSIGNMENTS, SET_TASK_COHORT_NULL, SET_USER_CAPABILITIES, + SET_COHORTS_ASSIGNMENTS, } from '../types'; const initialState = { myCohorts: [], - cohortSession: {}, - sortedAssignments: [], + cohortSession: null, + cohortsAssignments: {}, taskCohortNull: [], userCapabilities: [], }; @@ -30,13 +30,6 @@ const cohortHandlerReducer = (state = initialState, action) => { cohortSession, }; } - case SET_SORTED_ASSIGNMENTS: { - const { sortedAssignments } = action.payload; - return { - ...state, - sortedAssignments, - }; - } case SET_TASK_COHORT_NULL: { const { taskCohortNull } = action.payload; return { @@ -51,6 +44,13 @@ const cohortHandlerReducer = (state = initialState, action) => { userCapabilities, }; } + case SET_COHORTS_ASSIGNMENTS: { + const { cohortsAssignments } = action.payload; + return { + ...state, + cohortsAssignments, + }; + } default: { return state; } diff --git a/src/common/store/reducers/moduleMapReducer.js b/src/common/store/reducers/moduleMapReducer.js index 5a036bc36..459c8be3c 100644 --- a/src/common/store/reducers/moduleMapReducer.js +++ b/src/common/store/reducers/moduleMapReducer.js @@ -1,6 +1,4 @@ const initialState = { - cohortProgram: {}, - taskTodo: [], currentTask: null, subTasks: [], nextModule: null, @@ -9,16 +7,6 @@ const initialState = { const moduleMapReducer = (state = initialState, action) => { switch (action.type) { - case 'CHANGE_TASK_TO_DO': - return { - ...state, - taskTodo: action.payload, - }; - case 'CHANGE_COHORT_PROGRAM': - return { - ...state, - cohortProgram: action.payload, - }; case 'CHANGE_CURRENT_TASK': return { ...state, diff --git a/src/common/store/types/index.js b/src/common/store/types/index.js index e2d5526d3..f3b2c7253 100644 --- a/src/common/store/types/index.js +++ b/src/common/store/types/index.js @@ -22,8 +22,8 @@ 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_COHORTS_ASSIGNMENTS = 'SET_COHORTS_ASSIGNMENTS'; const SET_COHORT_SESSION = 'SET_COHORT_SESSION'; -const SET_SORTED_ASSIGNMENTS = 'SET_SORTED_ASSIGNMENTS'; const SET_TASK_COHORT_NULL = 'SET_TASK_COHORT_NULL'; const SET_USER_CAPABILITIES = 'SET_USER_CAPABILITIES'; @@ -55,8 +55,8 @@ export { SET_SELECTED_SERVICE, SET_PAYMENT_METHODS, SET_MY_COHORTS, + SET_COHORTS_ASSIGNMENTS, SET_COHORT_SESSION, - SET_SORTED_ASSIGNMENTS, SET_TASK_COHORT_NULL, SET_USER_CAPABILITIES, SET_SELF_APPLIED_COUPON, diff --git a/src/js_modules/Cohort/CohortModules.jsx b/src/js_modules/Cohort/CohortModules.jsx new file mode 100644 index 000000000..2a718a05b --- /dev/null +++ b/src/js_modules/Cohort/CohortModules.jsx @@ -0,0 +1,480 @@ +/* eslint-disable no-unused-vars */ +import React, { useRef, useMemo, useState, useEffect } from 'react'; +import { + Box, + Button, + useColorMode, + Spinner, + CircularProgress, + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, +} from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import useTranslation from 'next-translate/useTranslation'; +import PropTypes from 'prop-types'; +import { format } from 'date-fns'; +import { es, en } from 'date-fns/locale'; +import { useReward } from 'react-rewards'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; +import useStyle from '../../common/hooks/useStyle'; +import Heading from '../../common/components/Heading'; +import ShareButton from '../../common/components/ShareButton'; +import Text from '../../common/components/Text'; +import Icon from '../../common/components/Icon'; +import Progress from '../../common/components/ProgressBar/Progress'; + +const locales = { es, en }; + +function CohortModules({ cohort, modules, mainCohort, certificate, openByDefault }) { + const containerRef = useRef(null); + const { reward } = useReward(cohort.slug, 'confetti', { + lifetime: 50, + zIndex: 100, + spread: 50, + position: 'absolute', + }); + const { t, lang } = useTranslation('dashboard'); + const langDict = { + en: 'us', + us: 'us', + }; + const [loadingStartCourse, setLoadingStartCourse] = useState(false); + const [loadingModule, setLoadingModule] = useState(null); + const [shareModal, setShareModal] = useState(false); + const router = useRouter(); + const { backgroundColor, hexColor } = useStyle(); + const { colorMode } = useColorMode(); + const { serializeModulesMap, startDay, cohortsAssignments, setCohortsAssingments } = useCohortHandler(); + + const cohortColor = cohort.color || hexColor.blueDefault; + const isGraduated = !!certificate; + + const getModuleLabel = (module) => { + if (typeof module.label === 'string') return module.label; + if (lang in module.label) return module.label[lang]; + return module.label[langDict[lang]]; + }; + + const getModulesProgress = (acc, curr) => { + if (!(curr.task_type in acc)) { + acc[curr.task_type] = { + total: 1, + icon: curr.icon, + done: curr.task_status === 'DONE' ? 1 : 0, + }; + } else { + acc[curr.task_type].total += 1; + if (curr.task_status === 'DONE') acc[curr.task_type].done += 1; + } + return acc; + }; + + const modulesProgress = useMemo(() => { + if (!modules || !Array.isArray(modules)) return null; + + const modulesDict = {}; + modules.forEach((module) => { + const assignmentsCount = module.content.reduce(getModulesProgress, {}); + + const typesPerModule = Object.keys(assignmentsCount); + const moduleTotalAssignments = typesPerModule.reduce((acc, curr) => assignmentsCount[curr].total + acc, 0); + const moduleDoneAssignments = typesPerModule.reduce((acc, curr) => assignmentsCount[curr].done + acc, 0); + const isStarted = module.filteredContent.length > 0; + modulesDict[module.id] = { + moduleTotalAssignments, + moduleDoneAssignments, + assignmentsCount, + isStarted, + }; + }); + + return modulesDict; + }, [modules]); + + const cohortProgress = useMemo(() => { + if (!modulesProgress) return null; + + const allModules = Object.values(modulesProgress); + const totalAssignments = allModules.reduce((acc, curr) => curr.moduleTotalAssignments + acc, 0); + const doneAssignments = allModules.reduce((acc, curr) => curr.moduleDoneAssignments + acc, 0); + + const percentage = cohort.cohort_user.educational_status === 'GRADUATED' ? 100 : Math.floor((doneAssignments * 100) / 100); + + const isCohortStarted = allModules.some((module) => module.isStarted); + + return { + totalAssignments, + doneAssignments, + percentage, + isCohortStarted, + }; + }, [modulesProgress]); + + const updateMicroCohortModules = (tasks) => { + const cohortModulesUpdated = serializeModulesMap(cohortsAssignments[cohort.slug].syllabus.json.days, tasks); + const allMicroCohortAssignments = { + ...cohortsAssignments, + [cohort.slug]: { + ...cohortsAssignments[cohort.slug], + modules: cohortModulesUpdated, + tasks: [...cohortsAssignments[cohort.slug].tasks, tasks], + }, + }; + + setCohortsAssingments(allMicroCohortAssignments); + }; + + const startCourse = async () => { + try { + const firstModule = modules[0]; + + const moduleToUpdate = firstModule?.content; + const newTasks = moduleToUpdate?.map((l) => ({ + ...l, + associated_slug: l.slug, + cohort: cohort.id, + })); + + setLoadingStartCourse(true); + await startDay({ newTasks, cohort }); + setLoadingStartCourse(false); + } catch (e) { + console.log(e); + setLoadingStartCourse(false); + } + }; + + const getColorVariations = (colorHex) => { + if (!colorHex) return {}; + const lightRange = [0.2, 0.3, 0.5, 0.8, 0.9]; + const darkRange = [0.2, 0.3, 0.4, 0.7, 0.8]; + const r = parseInt(colorHex.slice(1, 3), 16); // r = 102 + const g = parseInt(colorHex.slice(3, 5), 16); // g = 51 + const b = parseInt(colorHex.slice(5, 7), 16); // b = 153 + + const [light1, light2, light3, light4, light5] = lightRange.map((variation) => { + const tintR = Math.round(Math.min(255, r + (255 - r) * variation)); // 117 + const tintG = Math.round(Math.min(255, g + (255 - g) * variation)); // 71 + const tintB = Math.round(Math.min(255, b + (255 - b) * variation)); // 163 + + return `#${ + [tintR, tintG, tintB] + .map((x) => x.toString(16).padStart(2, '0')) + .join('')}`; + }); + + const [dark1, dark2, dark3, dark4, dark5] = darkRange.map((variation) => { + const shadeR = Math.round(Math.max(0, r - r * variation)); // 92 + const shadeG = Math.round(Math.max(0, g - g * variation)); // 46 + const shadeB = Math.round(Math.max(0, b - b * variation)); // 138 + + return `#${ + [shadeR, shadeG, shadeB] + .map((x) => x.toString(16).padStart(2, '0')) + .join('')}`; + }); + + return { + light: { + mode1: light1, mode2: light2, mode3: light3, mode4: light4, mode5: light5, + }, + dark: { + mode1: dark1, mode2: dark2, mode3: dark3, mode4: dark4, mode5: dark5, + }, + }; + }; + + const colorVariations = getColorVariations(cohortColor); + + const redirectToModule = async (module) => { + try { + const { isStarted } = modulesProgress[module.id]; + //start module + if (!isStarted) { + const moduleToUpdate = module?.content; + const newTasks = moduleToUpdate?.map((l) => ({ + ...l, + associated_slug: l.slug, + cohort: cohort.id, + })); + + setLoadingModule(module.id); + await startDay({ newTasks, cohort }); + setLoadingModule(null); + } + + const moduleFirstAssignment = module?.content[0]; + + let syllabusRoute = `/syllabus/${cohort.slug}/${moduleFirstAssignment.type.toLowerCase()}/${moduleFirstAssignment.slug}`; + if (mainCohort) syllabusRoute = `/main-cohort/${mainCohort.slug}/${syllabusRoute}`; + + router.push(syllabusRoute); + } catch (e) { + console.log(e); + setLoadingModule(null); + } + }; + + const progressBoxStyles = () => { + if (!isGraduated) { + return { + paddingY: '8px', + background: colorVariations[colorMode].mode4, + }; + } + + return { + display: 'flex', + flexDirection: 'column', + width: '100%', + justifyContent: 'space-between', + }; + }; + + const certfToken = certificate?.preview_url && certificate.preview_url?.split('/')?.pop(); + const certfLink = certfToken ? `https://certificate.4geeks.com/${certfToken}` : '#'; + const profession = certificate?.specialty.name; + const socials = t('profile:share-certificate.socials', { certfLink, profession }, { returnObjects: true }); + + const share = (e) => { + e.stopPropagation(); + setShareModal(true); + }; + + const showCertificate = (e) => { + e.stopPropagation(); + window.open(certfLink); + }; + + useEffect(() => { + if (certificate) { + setTimeout(() => { + reward(); + }, 1500); + } + }, [certificate]); + + return ( + + + {({ isExpanded }) => ( + <> + + + + + + + {cohort.name} + + + + {isGraduated && ( + + + {t('completed')} + + + )} + + + {t('modules-count', { count: modules?.length })} + + + + + {cohortProgress?.isCohortStarted && ( + + + {t(isExpanded ? 'hide-content' : 'show-content')} + + + + )} + + + {isGraduated && ( + + + {t('completed')} + + + )} + + + {t('modules-count', { count: modules?.length })} + + + + + {isGraduated && ( + + + + {t('open')} + + + )} + {cohortProgress?.isCohortStarted && ( + + + + + {t('path-to-claim')} + + + + {cohortProgress.percentage !== 100 ? ( + + + + ) : ( + + + + )} + + + {`${cohortProgress.percentage}%`} + + + + {isGraduated && ( + + + + + {t('hours-worked', { hours: cohort.syllabus_version.duration_in_hours })} + + + + + + {t('issued-on', { date: format(new Date(certificate.issued_at), 'MMMM d y', { + locale: locales[lang], + }) })} + + + {shareModal && ( + setShareModal(false)} + /> + )} + + + )} + + )} + + + {cohortProgress?.isCohortStarted ? ( + + {modules?.map((module) => { + const assignmentsCount = modulesProgress?.[module.id].assignmentsCount; + const moduleTotalAssignments = modulesProgress?.[module.id].moduleTotalAssignments; + const moduleDoneAssignments = modulesProgress?.[module.id].moduleDoneAssignments; + + const typesPerModule = Object.keys(assignmentsCount); + + return ( + redirectToModule(module)} background={backgroundColor} cursor="pointer" _hover={{ opacity: 0.7 }} display="flex" alignItems="center" justifyContent="space-between" padding="8px" borderRadius="8px"> + + {loadingModule === module.id ? ( + + ) : ( + <> + {moduleTotalAssignments === moduleDoneAssignments ? ( + + ) : ( + + )} + + )} + + {getModuleLabel(module)} + + + + {typesPerModule.map((taskType) => { + const { icon, total, done } = assignmentsCount[taskType]; + return ( + + + + {`${done}/`} + {total} + + {done === total && } + + ); + })} + + + ); + })} + + ) : ( + + )} + + )} + + + ); +} + +export default CohortModules; + +CohortModules.propTypes = { + cohort: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired, + modules: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.any])), + mainCohort: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), + certificate: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])), + openByDefault: PropTypes.bool, +}; + +CohortModules.defaultProps = { + mainCohort: null, + modules: null, + certificate: null, + openByDefault: false, +}; diff --git a/src/js_modules/Cohort/Header.jsx b/src/js_modules/Cohort/Header.jsx new file mode 100644 index 000000000..4799229a7 --- /dev/null +++ b/src/js_modules/Cohort/Header.jsx @@ -0,0 +1,175 @@ +/* eslint-disable no-unused-vars */ +import React, { useState, useEffect } from 'react'; +import { + Flex, Box, Container, +} from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import useTranslation from 'next-translate/useTranslation'; +import bc from '../../common/services/breathecode'; +import useAuth from '../../common/hooks/useAuth'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; +import useStyle from '../../common/hooks/useStyle'; +import useRigo from '../../common/hooks/useRigo'; +import { SimpleSkeleton } from '../../common/components/Skeleton'; +import Heading from '../../common/components/Heading'; +import Text from '../../common/components/Text'; +import Icon from '../../common/components/Icon'; +import StudentsModal from './StudentsModal'; +import { ProfilesSection } from '../../common/components/SupportSidebar/MentoringConsumables'; +import { BREATHECODE_HOST } from '../../utils/variables'; +import { getStorageItem } from '../../utils'; + +// eslint-disable-next-line react/prop-types +function CustomButton({ children, ...props }) { + const { backgroundColor, backgroundColor4 } = useStyle(); + return ( + + {children} + + ); +} + +function Header() { + const { t } = useTranslation('choose-program'); + const router = useRouter(); + const { user, isAuthenticatedWithRigobot, conntectToRigobot } = useAuth(); + const { rigo, isRigoInitialized } = useRigo(); + const { featuredLight, hexColor } = useStyle(); + const { cohortSession } = useCohortHandler(); + const [mentors, setMentors] = useState([]); + const [showStudentsModal, setShowStudentsModal] = useState(false); + + const fetchServices = async () => { + try { + const { data } = await bc.mentorship({ + status: 'ACTIVE', + academy: cohortSession?.academy?.id, + }).getMentor(); + setMentors(data); + } catch (e) { + console.log(e); + } + }; + + useEffect(() => { + if (cohortSession && cohortSession.cohort_role === 'STUDENT') fetchServices(); + }, [cohortSession]); + + const hasGithub = user?.github && user.github.username !== ''; + + const getRigobotButtonText = () => { + if (!hasGithub) return t('common:connect-with-github'); + if (!isAuthenticatedWithRigobot) return t('common:connect-with-tigobot'); + + return t('common:get-help-rigobot'); + }; + + const rigobotMessage = () => { + if (!hasGithub) { + const accessToken = getStorageItem('accessToken'); + window.location.href = `${BREATHECODE_HOST}/v1/auth/github/${accessToken}?url=${window.location.href}`; + } else if (!isAuthenticatedWithRigobot) { + conntectToRigobot(); + } else { + rigo.updateOptions({ + showBubble: true, + // highlight: true, + welcomeMessage: t('rigo-chat.welcome-message', { firstName: user?.first_name, cohortName: cohortSession?.name }), + collapsed: false, + purposeSlug: '4geekscom-public-agent', + }); + } + }; + + return ( + + {cohortSession ? ( + + + + {t('hello-user', { name: user?.first_name })} + + + {t('read-to-start-learning')} + + + + {cohortSession.cohort_role === 'STUDENT' ? ( + <> + router.push('/workshops')}> + + + {t('common:see-workshops')} + + + + router.push('/mentorship/schedule')}> + + + {t('common:schedule-mentoring')} + + + + {isRigoInitialized && ( + + + + {getRigobotButtonText()} + + + )} + + ) : ( + <> + setShowStudentsModal(true)}> + + + {t('dashboard:teacher-sidebar.student-progress')} + + + window.open(`/cohort/${cohortSession?.slug}/assignments?academy=${cohortSession?.academy?.id}`, '_blank')}> + + + {t('dashboard:teacher-sidebar.assignments')} + + + window.open('https://www.notion.so/4geeksacademy/Mentor-training-433451eb9dac4dc680b7c5dae1796519', '_blank')}> + + + {t('dashboard:teacher-sidebar.teacher-tutorial')} + + + + )} + + + ) : ( + + )} + {cohortSession && cohortSession.cohort_role !== 'STUDENT' && ( + setShowStudentsModal(false)} /> + )} + + ); +} + +export default Header; diff --git a/src/js_modules/Cohort/StudentsModal.jsx b/src/js_modules/Cohort/StudentsModal.jsx new file mode 100644 index 000000000..4635c408a --- /dev/null +++ b/src/js_modules/Cohort/StudentsModal.jsx @@ -0,0 +1,154 @@ +import React, { useEffect, useState } from 'react'; +import useTranslation from 'next-translate/useTranslation'; +import PropTypes from 'prop-types'; +import { + Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, Button, Box, + FormControl, FormLabel, Input, Flex, Grid, Avatar, Spinner, + useColorMode, ModalCloseButton, +} from '@chakra-ui/react'; +import Text from '../../common/components/Text'; +import bc from '../../common/services/breathecode'; +import useStyle from '../../common/hooks/useStyle'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; + +function StudentsModal({ + isOpen, onClose, +}) { + const { t } = useTranslation('dashboard'); + const { state } = useCohortHandler(); + const { cohortSession } = state; + const [students, setStudents] = useState([]); + const [studentsCount, setStudentsCount] = useState(0); + const [filterStudent, setFilterStudent] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const { colorMode } = useColorMode(); + + const { hexColor, lightColor, borderColor } = useStyle(); + + const loadStudents = async (offset, append = false, like) => { + try { + setIsLoading(true); + const { data } = await bc.cohort({ offset, limit: 10, like }).getStudents(cohortSession.slug); + + const { count, results } = data; + setStudentsCount(parseInt(count, 10)); + if (append) setStudents((prev) => [...prev, ...results]); + else setStudents(results); + } catch (e) { + console.log(e); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (cohortSession?.cohort_role !== 'STUDENT' && students.length === 0) loadStudents(); + }, [cohortSession]); + + const handleFilterChange = (e) => { + setFilterStudent(e.target.value); + }; + + useEffect(() => { + let timeoutId; + if (!filterStudent) loadStudents(0, false); + else timeoutId = setTimeout(() => loadStudents(0, false, filterStudent), 1000); + return () => clearTimeout(timeoutId); + }, [filterStudent]); + + return ( + + + + + {t('dashboard:students-modal.students-course')} + + + + + {t('dashboard:students-modal.select-student')} + + + + {t('dashboard:students-modal.student')} + + + + + + + {students.map(({ user }) => ( + { + window.open(`/cohort/${cohortSession?.slug}/student/${user.id}?academy=${cohortSession?.academy?.id}`, '_blank'); + }} + > + + + + {`${user.first_name} ${user.last_name}`} + + + + ))} + + {!isLoading && students.length === 0 && ( + + + {t('dashboard:students-modal.no-students')} + + + )} + {isLoading && ( + + + + )} + + {!isLoading && studentsCount !== students.length && ( + + )} + + + + {t('attendance-modal.showing-students-with-active-educational-status')} + + + + + ); +} + +StudentsModal.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func, +}; +StudentsModal.defaultProps = { + isOpen: true, + onClose: () => { }, +}; + +export default StudentsModal; diff --git a/src/js_modules/checkout/PaymentInfo.jsx b/src/js_modules/checkout/PaymentInfo.jsx index c02621477..f9312c676 100644 --- a/src/js_modules/checkout/PaymentInfo.jsx +++ b/src/js_modules/checkout/PaymentInfo.jsx @@ -14,7 +14,6 @@ import useAuth from '../../common/hooks/useAuth'; import { reportDatalayer } from '../../utils/requests'; import { getQueryString, getStorageItem } from '../../utils'; import useCohortHandler from '../../common/hooks/useCohortHandler'; -import useModuleHandler from '../../common/hooks/useModuleHandler'; import { getCohort } from '../../common/handlers/cohorts'; import axiosInstance from '../../axios'; import { getAllMySubscriptions } from '../../common/handlers/subscriptions'; @@ -36,9 +35,7 @@ function PaymentInfo() { const { checkoutData, selectedPlanCheckoutData, cohortPlans, paymentMethods, loader, isSubmittingPayment, paymentStatus, } = state; - const { state: cohortState, setCohortSession, getCohortAssignments, prepareTasks } = useCohortHandler(); - const { sortedAssignments } = cohortState; - const { cohortProgram, taskTodo, startDay } = useModuleHandler(); + const { cohortsAssignments, getCohortsModules, startDay, setCohortSession } = useCohortHandler(); const [readyToRedirect, setReadyToRedirect] = useState(false); const [isRedirecting, setIsRedirecting] = useState(false); const [updatedUser, setUpdatedUser] = useState(undefined); @@ -63,11 +60,14 @@ function PaymentInfo() { const openSyllabusAndRedirect = () => { const langLink = lang !== 'en' ? `/${lang}` : ''; - const firstAssigmentSlug = sortedAssignments[0].modules[0].slug; - const firstAssigmentType = sortedAssignments[0].modules[0].type.toLowerCase(); + + const modules = cohortsAssignments[cohortFound.slug]?.modules; + + const firstAssigmentSlug = modules[0].content[0].slug; + const firstAssigmentType = modules[0].content[0].type.toLowerCase(); const syllabusRedirectURL = `${langLink}/syllabus/${cohortFound?.slug}/${firstAssigmentType}/${firstAssigmentSlug}`; - const updatedTasks = (sortedAssignments[0].modules || [])?.map((l) => ({ + const updatedTasks = (modules[0].content || [])?.map((l) => ({ ...l, title: l.title, associated_slug: l?.slug?.slug || l.slug, @@ -83,6 +83,7 @@ function PaymentInfo() { }, }); startDay({ + cohort: cohortFound, newTasks: updatedTasks, }); @@ -105,7 +106,7 @@ function PaymentInfo() { selectedProgramSlug: cohortDashboardLink, }); - if (!sortedAssignments.length > 0) { + if (cohortFound?.micro_cohorts?.length > 0 || !(cohortFound.slug in cohortsAssignments)) { router.push(cohortDashboardLink); return; } @@ -114,23 +115,19 @@ function PaymentInfo() { }; useEffect(() => { - if (!(sortedAssignments.length > 0)) return undefined; + if (!cohortFound || (cohortFound.micro_cohorts.length === 0 && !(cohortFound.slug in cohortsAssignments))) return undefined; const timer = setTimeout(() => { setReadyToRedirect(true); }, 1000); return () => clearTimeout(timer); - }, [sortedAssignments]); - - useEffect(() => { - prepareTasks(); - }, [taskTodo, cohortProgram]); + }, [cohortsAssignments, cohortFound]); useEffect(() => { - getCohortAssignments( - { slug: cohortFound?.syllabus_version?.slug, cohort: cohortFound, updatedUser }, - ); + if (cohortFound?.micro_cohorts.length === 0) { + getCohortsModules([cohortFound]); + } }, [updatedUser]); useEffect(() => { @@ -147,37 +144,36 @@ function PaymentInfo() { fetchMyCohorts(); }, [cohortFound]); - const joinCohort = (cohort) => { - reportDatalayer({ - dataLayer: { - event: 'join_cohort', - cohort_id: cohort?.id, - }, - }); - bc.cohort().join(cohort?.id) - .then(async (resp) => { - const dataRequested = await resp.json(); - if (resp.status >= 400) { - toast({ - position: 'top', - title: dataRequested?.detail, - status: 'info', - duration: 5000, - isClosable: true, - }); - setReadyToRefetch(false); - } - if (dataRequested?.status === 'ACTIVE') { - setCohortFound(cohort); - } - }) - .catch((error) => { - console.error('Error al unirse a la cohorte:', error); - setIsSubmittingPayment(false); - setTimeout(() => { - setReadyToRefetch(false); - }, 600); + const joinCohort = async (cohort) => { + try { + reportDatalayer({ + dataLayer: { + event: 'join_cohort', + cohort_id: cohort?.id, + }, }); + const resp = await bc.cohort().join(cohort?.id); + const dataRequested = await resp.json(); + if (resp.status >= 400) { + toast({ + position: 'top', + title: dataRequested?.detail, + status: 'info', + duration: 5000, + isClosable: true, + }); + setReadyToRefetch(false); + } + if (dataRequested?.status === 'ACTIVE') { + setCohortFound(cohort); + } + } catch (error) { + console.error('Error al unirse a la cohorte:', error); + setIsSubmittingPayment(false); + setTimeout(() => { + setReadyToRefetch(false); + }, 600); + } }; useEffect(() => { @@ -194,7 +190,7 @@ function PaymentInfo() { if (readyToRefetch && timeElapsed < 10 && isPaymentSuccess) { interval = setInterval(() => { getAllMySubscriptions() - .then((subscriptions) => { + .then(async (subscriptions) => { const currentSubscription = subscriptions?.find( (subscription) => checkoutData?.plans[0]?.plan_slug === subscription.plans[0]?.slug, ); @@ -202,19 +198,16 @@ function PaymentInfo() { (subscription) => checkoutData?.plans[0]?.plan_slug === subscription.plans[0]?.slug, ); const cohortsForSubscription = currentSubscription?.selected_cohort_set.cohorts; - const findedCohort = cohortsForSubscription?.length > 0 ? cohortsForSubscription.find( + const foundCohort = cohortsForSubscription?.find( (cohort) => cohort?.id === cohortId, - ) : {}; + ); if (isPurchasedPlanFound) { - if (findedCohort?.id) { - getCohort(findedCohort?.id) - .then((cohort) => { - joinCohort(cohort); - }) - .finally(() => { - clearInterval(interval); - }); + if (foundCohort?.id) { + const cohort = await getCohort(foundCohort?.id); + joinCohort(cohort); + + clearInterval(interval); setReadyToRefetch(false); } else { clearInterval(interval); diff --git a/src/js_modules/checkout/Summary.jsx b/src/js_modules/checkout/Summary.jsx index 6be91f929..88b774bc6 100644 --- a/src/js_modules/checkout/Summary.jsx +++ b/src/js_modules/checkout/Summary.jsx @@ -16,14 +16,11 @@ import { getAllMySubscriptions } from '../../common/handlers/subscriptions'; import { SILENT_CODE } from '../../lib/types'; import axiosInstance from '../../axios'; import useCohortHandler from '../../common/hooks/useCohortHandler'; -import useModuleHandler from '../../common/hooks/useModuleHandler'; import { getCohort } from '../../common/handlers/cohorts'; function Summary() { const { t, lang } = useTranslation('signup'); - const { state: cohortState, setCohortSession, getCohortAssignments, prepareTasks } = useCohortHandler(); - const { sortedAssignments } = cohortState; - const { cohortProgram, taskTodo, startDay } = useModuleHandler(); + const { cohortsAssignments, startDay, setCohortSession, getCohortsModules } = useCohortHandler(); const [readyToRedirect, setReadyToRedirect] = useState(false); const [isRedirecting, setIsRedirecting] = useState(false); const [updatedUser, setUpdatedUser] = useState(undefined); @@ -96,11 +93,14 @@ function Summary() { const openSyllabusAndRedirect = () => { const langLink = lang !== 'en' ? `/${lang}` : ''; - const firstAssigmentSlug = sortedAssignments[0].modules[0].slug; - const firstAssigmentType = sortedAssignments[0].modules[0].type.toLowerCase(); + + const modules = cohortsAssignments[cohortFound.slug]?.modules; + + const firstAssigmentSlug = modules[0].content[0].slug; + const firstAssigmentType = modules[0].content[0].type.toLowerCase(); const syllabusRedirectURL = `${langLink}/syllabus/${cohortFound?.slug}/${firstAssigmentType}/${firstAssigmentSlug}`; - const updatedTasks = (sortedAssignments[0].modules || [])?.map((l) => ({ + const updatedTasks = (modules[0].content || [])?.map((l) => ({ ...l, title: l.title, associated_slug: l?.slug?.slug || l.slug, @@ -116,6 +116,7 @@ function Summary() { }, }); startDay({ + cohort: cohortFound, newTasks: updatedTasks, }); @@ -134,7 +135,7 @@ function Summary() { selectedProgramSlug: cohortDashboardLink, }); - if (!sortedAssignments.length > 0) { + if (cohortFound?.micro_cohorts?.length > 0 || !(cohortFound.slug in cohortsAssignments)) { router.push(cohortDashboardLink); return; } @@ -143,23 +144,19 @@ function Summary() { }; useEffect(() => { - if (!(sortedAssignments.length > 0)) return undefined; + if (!cohortFound || (cohortFound.micro_cohorts.length === 0 && !(cohortFound.slug in cohortsAssignments))) return undefined; const timer = setTimeout(() => { setReadyToRedirect(true); }, 1000); return () => clearTimeout(timer); - }, [sortedAssignments]); + }, [cohortsAssignments, cohortFound]); useEffect(() => { - prepareTasks(); - }, [taskTodo, cohortProgram]); - - useEffect(() => { - getCohortAssignments( - { slug: cohortFound?.syllabus_version?.slug, cohort: cohortFound, updatedUser }, - ); + if (cohortFound?.micro_cohorts.length === 0) { + getCohortsModules([cohortFound]); + } }, [updatedUser]); useEffect(() => { diff --git a/src/js_modules/chooseProgram/Programs.jsx b/src/js_modules/chooseProgram/Programs.jsx index 69c084d28..56ca85753 100644 --- a/src/js_modules/chooseProgram/Programs.jsx +++ b/src/js_modules/chooseProgram/Programs.jsx @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import PropTypes from 'prop-types'; import { useRouter } from 'next/router'; import { subMinutes } from 'date-fns'; @@ -11,7 +12,7 @@ function Programs({ item, onOpenModal, setLateModalProps }) { const { setCohortSession } = useCohortHandler(); const [isLoadingPageContent, setIsLoadingPageContent] = useState(false); const { programsList } = useProgramList(); - const { cohort, ...cohortUser } = item; + const { cohort_user: cohortUser, ...cohort } = item; const signInDate = item.created_at; const { version, slug } = cohort.syllabus_version; const currentCohortProps = programsList[cohort.slug]; @@ -97,8 +98,8 @@ function Programs({ item, onOpenModal, setLateModalProps }) { // isBought={!isFreeTrial} isLoadingPageContent={isLoadingPageContent} isLoading={currentCohortProps === undefined} - startsIn={item?.cohort?.kickoff_date} - endsAt={item?.cohort?.ending_date} + startsIn={item?.kickoff_date} + endsAt={item?.ending_date} signInDate={signInDate} icon="coding" subscription={subscription || {}} diff --git a/src/js_modules/chooseProgram/index.jsx b/src/js_modules/chooseProgram/index.jsx index 4eba9cac1..f990484ff 100644 --- a/src/js_modules/chooseProgram/index.jsx +++ b/src/js_modules/chooseProgram/index.jsx @@ -28,15 +28,13 @@ function ChooseProgram({ chooseList, handleChoose, setLateModalProps }) { const cardColumnSize = 'repeat(auto-fill, minmax(17rem, 1fr))'; const finishedCohorts = handlers.getCohortsFinished(chooseList); - const activeCohorts = handlers.getActiveCohorts(chooseList).map((item) => { - const cohort = item?.cohort; + const activeCohorts = handlers.getActiveCohorts(chooseList).map((cohort) => { + const { cohort_user: cohortUser } = cohort; const currentCohortProps = programsList[cohort.slug]; return ({ - ...item, - cohort: { - ...cohort, - available_as_saas: item?.role === 'TEACHER' ? false : cohort?.available_as_saas, - }, + ...cohort, + available_as_saas: cohortUser?.role === 'TEACHER' ? false : cohort.available_as_saas, + cohort_user: { ...cohortUser }, subscription: currentCohortProps?.subscription, plan_financing: currentCohortProps?.plan_financing, all_subscriptions: currentCohortProps?.all_subscriptions, @@ -44,11 +42,11 @@ function ChooseProgram({ chooseList, handleChoose, setLateModalProps }) { }); }); - const hasNonSaasCourse = chooseList.some(({ cohort }) => !cohort.available_as_saas); + const hasNonSaasCourse = chooseList.some((cohort) => !cohort.available_as_saas); const marketingCourses = marketingCursesList.filter( (item) => !activeCohorts.some( - ({ cohort }) => cohort.slug === item?.cohort?.slug, + (cohort) => cohort.slug === item?.cohort?.slug, ) && item?.course_translation?.title, ); diff --git a/src/js_modules/moduleMap/index.jsx b/src/js_modules/moduleMap/index.jsx index a25a65c6d..1edfa9742 100644 --- a/src/js_modules/moduleMap/index.jsx +++ b/src/js_modules/moduleMap/index.jsx @@ -8,25 +8,23 @@ import { reportDatalayer } from '../../utils/requests'; import { languageFix } from '../../utils'; import Text from '../../common/components/Text'; import Module from './module'; -import useModuleHandler from '../../common/hooks/useModuleHandler'; import useCohortHandler from '../../common/hooks/useCohortHandler'; import Icon from '../../common/components/Icon'; function ModuleMap({ - index, slug, modules, filteredModules, - title, description, cohortData, filteredModulesByPending, + index, slug, content, filteredContent, + title, description, cohortData, filteredContentByPending, showPendingTasks, searchValue, existsActivities, }) { const { t, lang } = useTranslation('dashboard'); - const { startDay } = useModuleHandler(); - const { state } = useCohortHandler(); + const { state, startDay } = useCohortHandler(); const { taskCohortNull } = state; const commonBorderColor = useColorModeValue('gray.200', 'gray.900'); - const currentModules = showPendingTasks ? filteredModulesByPending : filteredModules; + const currentModules = showPendingTasks ? filteredContentByPending : filteredContent; const cohortId = cohortData?.id || cohortData?.cohort_id; const handleStartDay = () => { - const updatedTasks = (modules || [])?.map((l) => ({ + const updatedTasks = (content || [])?.map((l) => ({ ...l, title: l.title, associated_slug: l?.slug?.slug || l.slug, @@ -42,25 +40,26 @@ function ModuleMap({ }, }); startDay({ + cohort: cohortData, newTasks: updatedTasks, }); }; const taskCohortNullExistsInModules = taskCohortNull.some((el) => { - const task = modules.find((l) => l.slug === el.associated_slug); + const task = content.find((l) => l.slug === el.associated_slug); return task; }); const isAvailableToSync = () => { if (!taskCohortNullExistsInModules - && filteredModules.length > 0 + && filteredContent.length > 0 && searchValue.length === 0 - && modules.length !== filteredModules.length + && content.length !== filteredContent.length ) return true; return false; }; - return ((showPendingTasks && filteredModulesByPending !== null) || (showPendingTasks === false)) && ( + return ((showPendingTasks && filteredContentByPending !== null) || (showPendingTasks === false)) && ( - {t('modules.activitiesLength', { count: filteredModules.length })} + {t('modules.activitiesLength', { count: filteredContent.length })} @@ -88,7 +87,7 @@ function ModuleMap({ {isAvailableToSync() && ( - {t('modules.newActivities.title', { tasksLength: (modules.length - filteredModules.length) })} + {t('modules.newActivities.title', { tasksLength: (content.length - filteredContent.length) })}