diff --git a/bun.lockb b/bun.lockb index fe7c4344b..f479aac6b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package-lock.json b/package-lock.json index 3a335a312..fee068f51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33578,6 +33578,126 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.3.tgz", + "integrity": "sha512-UpBKxu2ob9scbpJyEq/xPgpdrgBgN3aLYlxyGqlYX5/KnwpJpFuIHU2lx8upQQ7L+MEmz+fA1XSgesoK92ppwQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.3.tgz", + "integrity": "sha512-5AzM7Yx1Ky+oLY6pHs7tjONTF22JirDPd5Jw/3/NazJ73uGB05NqhGhB4SbeCchg7SlVYVBeRMrMSZwJwq/xoA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.3.tgz", + "integrity": "sha512-A/C1shbyUhj7wRtokmn73eBksjTM7fFQoY2v/0rTM5wehpkjQRLOXI8WJsag2uLhnZ4ii5OzR1rFPwoD9cvOgA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.3.tgz", + "integrity": "sha512-FubPuw/Boz8tKkk+5eOuDHOpk36F80rbgxlx4+xty/U71e3wZZxVYHfZXmf0IRToBn1Crb8WvLM9OYj/Ur815g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.3.tgz", + "integrity": "sha512-DPw8nFuM1uEpbX47tM3wiXIR0Qa+atSzs9Q3peY1urkhofx44o7E1svnq+a5Q0r8lAcssLrwiM+OyJJgV/oj7g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.3.tgz", + "integrity": "sha512-zBPSP8cHL51Gub/YV8UUePW7AVGukp2D8JU93IHbVDu2qmhFAn9LWXiOOLKplZQKxnIPUkJTQAJDCWBWU4UWUA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.3.tgz", + "integrity": "sha512-ONcL/lYyGUj4W37D4I2I450SZtSenmFAvapkJQNIJhrPMhzDU/AdfLkW98NvH1D2+7FXwe7yclf3+B7v28uzBQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.3.tgz", + "integrity": "sha512-2Vz2tYWaLqJvLcWbbTlJ5k9AN6JD7a5CN2pAeIzpbecK8ZF/yobA39cXtv6e+Z8c5UJuVOmaTldEAIxvsIux/Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 7d5c49ede..f16ceda2d 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -72,6 +72,7 @@ }, "subscription": { "what-you-will-get": "What you will get:", + "bootcamp-mentorships": "You have unlimited access to those mentorship services with this icon ∞ because you belong to the bootcamp", "mentoring-available": "Mentoring sessions available", "your-mentoring-available": "Your mentoring services available", "workshop-available": "Workshop sessions available", diff --git a/public/locales/es/profile.json b/public/locales/es/profile.json index 3378fa00c..4724802a8 100644 --- a/public/locales/es/profile.json +++ b/public/locales/es/profile.json @@ -72,6 +72,7 @@ }, "subscription": { "what-you-will-get": "Lo que obtendrás:", + "bootcamp-mentorships": "Tienes acceso ilimitado a los servicios de mentoría con este ícono ∞ porque perteneces al bootcamp", "mentoring-available": "Sesiones de mentorías disponibles", "your-mentoring-available": "Tus servicios de mentorías disponibles", "workshop-available": "Sesiones de workshops disponibles", diff --git a/src/common/components/ProfileForm.jsx b/src/common/components/ProfileForm.jsx index 1be515b07..a8e1a1fd5 100644 --- a/src/common/components/ProfileForm.jsx +++ b/src/common/components/ProfileForm.jsx @@ -25,10 +25,10 @@ function ProfileForm() { useEffect(() => { const userSchema = { - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - phone: user.phone, + first_name: user?.first_name, + last_name: user?.last_name, + email: user?.email, + phone: user?.phone, }; setUserInfo(userSchema); setDefaultUserInfo(userSchema); diff --git a/src/common/hooks/useCohortHandler.js b/src/common/hooks/useCohortHandler.js index 16b0bd4a0..b2ee9f26e 100644 --- a/src/common/hooks/useCohortHandler.js +++ b/src/common/hooks/useCohortHandler.js @@ -265,6 +265,7 @@ function useCohortHandler() { getTasksWithoutCohort, userCapabilities, state, + setMyCohorts, }; } diff --git a/src/js_modules/profile/Subscriptions/index.jsx b/src/js_modules/profile/Subscriptions/index.jsx index 77defd8a1..bb0afa44e 100644 --- a/src/js_modules/profile/Subscriptions/index.jsx +++ b/src/js_modules/profile/Subscriptions/index.jsx @@ -1,5 +1,5 @@ +/* eslint-disable camelcase */ /* eslint-disable no-unsafe-optional-chaining */ -/* eslint-disable react/jsx-no-useless-fragment */ import { Box, Flex, @@ -64,6 +64,18 @@ function Subscriptions({ cohorts }) { const getConsumables = async () => { try { + const nonSaasCohorts = cohorts.filter(({ available_as_saas }) => !available_as_saas); + const academies = [...new Set(nonSaasCohorts.map(({ academy }) => academy.id))]; + + const allServices = { + mentorships: [], + workshops: [], + }; + + const cohortsServices = academies.map((academy) => bc.mentorship({ academy }, true).getService()); + const responseServices = await Promise.all(cohortsServices); + const nonSaasServices = responseServices.flatMap(({ data }) => data).map((elem) => ({ ...elem, nonSaasAcademy: true })); + const res = await bc.payment().service().consumable(); if (res.status === 200) { const { data } = res; @@ -71,7 +83,7 @@ function Subscriptions({ cohorts }) { const promiseMentorship = data.mentorship_service_sets.map(async (elem) => { const mentRes = await bc.mentorship().getServiceSet(elem.id); - return mentRes.data.mentorship_services; + return mentRes.data.mentorship_services.map((service) => ({ ...service, unit: elem.balance.unit })); }); const promiseEvents = data.event_type_sets.map(async (elem) => { @@ -81,11 +93,11 @@ function Subscriptions({ cohorts }) { }); const resMentorships = await Promise.all(promiseMentorship); const resWorkshops = await Promise.all(promiseEvents); - setServices({ - mentorships: resMentorships.flat(), - workshops: resWorkshops.flat(), - }); + allServices.mentorships = [...resMentorships.flat(), ...nonSaasServices]; + allServices.workshops = resWorkshops.flat(); } + + setServices(allServices); setLoadingServices(false); } catch (e) { setLoadingServices(false); @@ -109,7 +121,7 @@ function Subscriptions({ cohorts }) { const allSubscriptions = subscriptionData?.subscriptions && subscriptionData?.plan_financings - && [...subscriptionData?.subscriptions, ...subscriptionData?.plan_financings] + && [...subscriptionData.subscriptions, ...subscriptionData.plan_financings] .filter((subscription) => subscription?.plans?.[0]?.slug !== undefined); const prioritizeStatus = ['fully_paid', 'active', 'payment_issue', 'expired', 'cancelled', 'error']; @@ -156,7 +168,7 @@ function Subscriptions({ cohorts }) { const totalMentorshipsAvailable = consumables.mentorship_service_sets.reduce((acum, service) => acum + service.balance.unit, 0); const totalWorkshopsAvailable = consumables.event_type_sets.reduce((acum, service) => acum + service.balance.unit, 0); - const existsNoAvailableAsSaas = cohorts.some((c) => c?.cohort?.available_as_saas === false); + const existsNoAvailableAsSaas = cohorts.some((c) => c.available_as_saas === false); return ( <> @@ -165,59 +177,57 @@ function Subscriptions({ cohorts }) { {t('my-subscriptions')} )} - {!existsNoAvailableAsSaas && ( - - {loadingServices ? ( - <> - - - - ) : ( - <> - - - {t('subscription.mentoring-available')} - - - - - {totalMentorshipsAvailable >= 0 ? ( - - {totalMentorshipsAvailable} - - ) : ( - - )} - - + + {loadingServices ? ( + <> + + + + ) : ( + <> + + + {t('subscription.mentoring-available')} + + + + + {(totalMentorshipsAvailable < 0 || existsNoAvailableAsSaas) ? ( + + ) : ( + + {totalMentorshipsAvailable} + + )} + - - - {t('subscription.workshop-available')} - - - - - {totalWorkshopsAvailable >= 0 ? ( - - {totalWorkshopsAvailable} - - ) : ( - - )} - - + + + + {t('subscription.workshop-available')} + + + + + {totalWorkshopsAvailable >= 0 ? ( + + {totalWorkshopsAvailable} + + ) : ( + + )} + - - )} - - )} + + + )} + @@ -231,12 +241,27 @@ function Subscriptions({ cohorts }) { {services[servicesModal].map((service) => { const logo = service.logo_url || service.icon_url; return ( - - - {logo && Service logo} - - {service.name} - + + + + {logo && Service logo} + + {service.name} + + + {servicesModal === 'mentorships' && ( + <> + {service.nonSaasAcademy ? ( + + ) : ( + + + {service.unit} + + + )} + + )} {service.description} @@ -244,6 +269,11 @@ function Subscriptions({ cohorts }) { ); })} + {servicesModal === 'mentorships' && existsNoAvailableAsSaas && ( + + {t('subscription.bootcamp-mentorships')} + + )} )} diff --git a/src/pages/pricing.jsx b/src/pages/pricing.jsx index ecc97665d..9bb2f291a 100644 --- a/src/pages/pricing.jsx +++ b/src/pages/pricing.jsx @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import { Box, Flex, Container, Button, Img, Link, Image } from '@chakra-ui/react'; import { useEffect, useState } from 'react'; import useTranslation from 'next-translate/useTranslation'; @@ -8,8 +9,9 @@ import Heading from '../common/components/Heading'; import Text from '../common/components/Text'; import Faq from '../common/components/Faq'; import useStyle from '../common/hooks/useStyle'; -import bc from '../common/services/breathecode'; import useAuth from '../common/hooks/useAuth'; +import useCohortHandler from '../common/hooks/useCohortHandler'; +import bc from '../common/services/breathecode'; import PricingCard from '../common/components/PricingCard'; import useSignup from '../common/store/actions/signupAction'; import LoaderScreen from '../common/components/LoaderScreen'; @@ -40,6 +42,8 @@ const getYearlyPlans = (originalPlans, suggestedPlans, allFeaturedPlans) => { function PricingView() { const { t, lang } = useTranslation('pricing'); + const { state, setMyCohorts } = useCohortHandler(); + const { myCohorts } = state; const { getSelfAppliedCoupon } = useSignup(); const [activeType, setActiveType] = useState('monthly'); const { isAuthenticated } = useAuth(); @@ -169,6 +173,36 @@ function PricingView() { staleTime: Infinity, }); + const initializeCohorts = async () => { + try { + 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, + }; + })); + setMyCohorts(parsedCohorts); + } catch (e) { + console.log(e); + } + }; + + useEffect(() => { + const hasActiveBootcamp = myCohorts.some((cohort) => !cohort.available_as_saas + && cohort.ending_date && new Date(cohort.ending_date) > new Date() + && cohort.cohort_user.educational_status === 'ACTIVE'); + + if (hasActiveBootcamp) router.push('/choose-program'); + }, [myCohorts]); + useEffect(() => { const mktQueryString = parseQuerys({ featured: true, @@ -239,6 +273,7 @@ function PricingView() { useEffect(() => { if (isAuthenticated) { fetchMySubscriptions(); + initializeCohorts(); } }, [isAuthenticated, allFeaturedPlansSelected]); diff --git a/src/pages/profile/[slug].jsx b/src/pages/profile/[slug].jsx index 81cf33065..2f47c796d 100644 --- a/src/pages/profile/[slug].jsx +++ b/src/pages/profile/[slug].jsx @@ -1,5 +1,6 @@ +/* eslint-disable camelcase */ import { - Tab, TabList, TabPanel, TabPanels, Tabs, + Tab, TabList, TabPanel, TabPanels, Tabs, Box, } from '@chakra-ui/react'; import useTranslation from 'next-translate/useTranslation'; import { @@ -8,9 +9,11 @@ import { import { useRouter } from 'next/router'; import Heading from '../../common/components/Heading'; import useAuth from '../../common/hooks/useAuth'; +import useCohortHandler from '../../common/hooks/useCohortHandler'; import asPrivate from '../../common/context/PrivateRouteWrapper'; import bc from '../../common/services/breathecode'; import { cleanQueryStrings } from '../../utils'; +import { ModuleMapSkeleton } from '../../common/components/Skeleton'; import AlertMessage from '../../common/components/AlertMessage'; import GridContainer from '../../common/components/GridContainer'; import Subscriptions from '../../js_modules/profile/Subscriptions'; @@ -25,8 +28,10 @@ function Profile() { const { asPath } = router; const [currentTabIndex, setCurrentTabIndex] = useState(0); const [certificates, setCertificates] = useState([]); - const [myCohorts, setMyCohorts] = useState([]); + const [isLoadingCohorts, setIsLoadingCohorts] = useState(true); const [isAvailableToShowModalMessage, setIsAvailableToShowModalMessage] = useState([]); + const { state: cohortsState, setMyCohorts } = useCohortHandler(); + const { myCohorts } = cohortsState; const tabListMenu = t('tabList', {}, { returnObjects: true }); const tabPosition = { @@ -50,18 +55,38 @@ function Profile() { }); }, []); + const fetchCohorts = async () => { + try { + 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, + }; + })); + setMyCohorts(parsedCohorts); + + const isToShowGithubMessage = cohorts?.some( + (l) => l?.educational_status === 'ACTIVE' && l.cohort.available_as_saas === false, + ); + setIsAvailableToShowModalMessage(isToShowGithubMessage); + } catch (e) { + console.log(e); + } finally { + setIsLoadingCohorts(false); + } + }; + useEffect(() => { if (isAuthenticated) { - bc.admissions().me() - .then((resp) => { - const data = resp?.data; - const cohorts = data?.cohorts; - setMyCohorts(cohorts); - const isToShowGithubMessage = cohorts?.some( - (l) => l?.educational_status === 'ACTIVE' && l.cohort.available_as_saas === false, - ); - setIsAvailableToShowModalMessage(isToShowGithubMessage); - }); + fetchCohorts(); } }, [isAuthenticated]); @@ -110,17 +135,23 @@ function Profile() { ))} - - - - - - - - - - - + {!isLoadingCohorts ? ( + + + + + + + + + + + + ) : ( + + + + )}