diff --git a/i18n.js b/i18n.js index bb5fb1bab..baba9084f 100644 --- a/i18n.js +++ b/i18n.js @@ -35,7 +35,7 @@ module.exports = { '/workshops/[event_slug]': ['workshops', 'signup'], '/join/cohort/[id]': ['dashboard', 'signup'], '/podcast': ['podcast'], - '/accept-invite': ['accept-invite'], + '/accept-invite': ['accept-invite', 'signup'], '/bootcamp/[course_slug]': ['course', 'dashboard'], }, locales: ['en', 'es'], diff --git a/public/locales/en/accept-invite.json b/public/locales/en/accept-invite.json index 0bee0ffd3..569a0774e 100644 --- a/public/locales/en/accept-invite.json +++ b/public/locales/en/accept-invite.json @@ -1,4 +1,10 @@ { "heading": "You have been invited to {{name}}, please fill out the following information to start learning", - "error": "Something went wrong" + "error": "Something went wrong", + "no-token": "You must include an invite token to see the invite", + "already-accepted": "The invite has been accepted already", + "new-invite": "You have been invited to {{course}}", + "invalid-user": "This invitation belongs to another user", + "accept-and-learn": "Accept and start learning", + "belongs-to-other-user": "This invitation belongs to another user" } \ No newline at end of file diff --git a/public/locales/en/technologies.json b/public/locales/en/technologies.json index 91781b5ff..609ed4dd9 100644 --- a/public/locales/en/technologies.json +++ b/public/locales/en/technologies.json @@ -14,13 +14,14 @@ "loading-workshops": "error loading workshops", "no-data": "There are no assets for this technology" }, + "start-learning": "Start learning {{technology}}", "backToLessons": "Back to Lessons", "title": "{{technology}}", "search": "Search lesson", "description": "Browse our list of curated database of projects, exercises, and lessons to learn {{technology}}", "tech-materials": "{{tech}} learning materials", "tech-workshops": "{{tech}} workshops", - "request-mentorship": "Request a mentorship", + "request-mentorship": "Get {{tech}} mentorship", "popular-exercises": "Popular exercises", "lessons-section": "Lessons section", "exercises-section": "Exercises section", diff --git a/public/locales/es/accept-invite.json b/public/locales/es/accept-invite.json index 6147a5e63..09c0a5cce 100644 --- a/public/locales/es/accept-invite.json +++ b/public/locales/es/accept-invite.json @@ -1,4 +1,10 @@ { "heading": "Has sido invitado a {{name}}, por favor completa la siguiente información para comenzar a aprender", - "error": "Algo ha salido mal" + "error": "Algo ha salido mal", + "no-token": "Debes incluir un token de invitación para ver la invitación", + "already-accepted": "La invitacion ya fue aceptada", + "new-invite": "Has sido invitado a {{course}}", + "invalid-user": "Esta invitacion pertenece a otro usuario", + "accept-and-learn": "Acepta y empieza a aprender", + "belongs-to-other-user": "Esta invitacion pertenece a otro usuario" } diff --git a/public/locales/es/technologies.json b/public/locales/es/technologies.json index 3b31352ab..8f2aa5624 100644 --- a/public/locales/es/technologies.json +++ b/public/locales/es/technologies.json @@ -14,13 +14,14 @@ "loading-workshops": "error cargando los workshops", "no-data": "No se encontró assets para esta tecnología" }, + "start-learning": "Empezar a aprender {{technology}}", "backToLessons": "Regresar a lecciones", "title": "{{technology}}", "search": "Buscar lección", "description": "Explore nuestra lista seleccionada de proyectos, ejercicios y lecciones para aprender {{technology}}", "tech-materials": "Materiales de aprendizaje de {{tech}}", "tech-workshops": "{{tech}} workshops", - "request-mentorship": "Solicitar una mentoria", + "request-mentorship": "Obtener mentoria de {{tech}}", "popular-exercises": "Ejercicios populares", "lessons-section": "Sección de lecciones", "exercises-section": "Sección de ejercicios", diff --git a/src/common/components/Icon/set/arrowLeft3.jsx b/src/common/components/Icon/set/arrowLeft3.jsx new file mode 100644 index 000000000..1e47145fe --- /dev/null +++ b/src/common/components/Icon/set/arrowLeft3.jsx @@ -0,0 +1,23 @@ +const arrowLeft3 = ({ + style, width, height, color, +}) => ( + + + +); + +export default arrowLeft3; diff --git a/src/common/components/MarkDownParser/ArticleMarkdown.jsx b/src/common/components/MarkDownParser/ArticleMarkdown.jsx index 3f97078b7..1c338347a 100644 --- a/src/common/components/MarkDownParser/ArticleMarkdown.jsx +++ b/src/common/components/MarkDownParser/ArticleMarkdown.jsx @@ -5,7 +5,7 @@ import Toc from './toc'; import ContentHeading from './ContentHeading'; import SubTasks from './SubTasks'; import MarkDownParser from './index'; -import OpenWithLearnpackCTA from '../../../js_modules/syllabus/OpenWithLearnpackCTA'; +import ProjectInstructions from '../../../js_modules/syllabus/ProjectInstructions'; function ArticleMarkdown({ content, withToc, frontMatter, titleRightSide, currentTask, currentData, @@ -20,8 +20,8 @@ function ArticleMarkdown({ {!isGuidedExperience && ( + callToAction={(currentData?.interactive || currentData?.template_url) && ( + )} content={frontMatter} currentData={currentData} diff --git a/src/common/components/MarkDownParser/MDComponents/index.jsx b/src/common/components/MarkDownParser/MDComponents/index.jsx index 6e050e7ac..385606b1b 100644 --- a/src/common/components/MarkDownParser/MDComponents/index.jsx +++ b/src/common/components/MarkDownParser/MDComponents/index.jsx @@ -286,11 +286,7 @@ function QuoteVersion4({ ...props }) { ); } export function Quote({ children }) { - const [version, setVersion] = useState(2); - - useEffect(() => { - setVersion(Math.floor(Math.random() * 4) + 1); - }, []); + const version = 1; if (version === 1 && children.length > 0) { return ( diff --git a/src/common/components/Navbar/index.jsx b/src/common/components/Navbar/index.jsx index 364a3307c..1b9b0bf6d 100644 --- a/src/common/components/Navbar/index.jsx +++ b/src/common/components/Navbar/index.jsx @@ -44,6 +44,7 @@ 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(); @@ -85,9 +86,53 @@ function NavbarWithSubNavigation({ translations, pageProps }) { selectedProgramSlug: '/choose-program', }, { returnObjects: true }); - const items = t('ITEMS', { - selectedProgramSlug: selectedProgramSlug || '/choose-program', - }, { returnObjects: true }); + useEffect(() => { + if (cohortSession?.available_as_saas) { + bc.payment({ + status: 'ACTIVE,FREE_TRIAL,FULLY_PAID,CANCELLED,PAYMENT_ISSUE', + }).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 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 }); axios.defaults.headers.common['Accept-Language'] = locale; @@ -165,7 +210,7 @@ function NavbarWithSubNavigation({ translations, pageProps }) { setITEMS( preFilteredItems .filter((item) => (item.disabled !== true && item.hide_on_auth !== true) - && (item.id !== 'bootcamps' || !isBootcampStudent)), + && (item.id !== 'bootcamps' || !isBootcampStudent)), ); } else { setITEMS(preFilteredItems.filter((item) => item.disabled !== true)); diff --git a/src/common/hooks/useModuleHandler.js b/src/common/hooks/useModuleHandler.js index 0537f0bbe..262e1235f 100644 --- a/src/common/hooks/useModuleHandler.js +++ b/src/common/hooks/useModuleHandler.js @@ -23,7 +23,8 @@ function useModuleHandler() { }; try { - await bc.todo({}).update(taskToUpdate); + const { cohort, ...taskData } = taskToUpdate; + await bc.todo().update(taskData); const keyIndex = taskTodo.findIndex((x) => x.id === task.id); setTaskTodo([ ...taskTodo.slice(0, keyIndex), // before keyIndex (inclusive) diff --git a/src/js_modules/syllabus/ExerciseGuidedExperience.jsx b/src/js_modules/syllabus/ExerciseGuidedExperience.jsx index e6af549cb..a3ac766a6 100644 --- a/src/js_modules/syllabus/ExerciseGuidedExperience.jsx +++ b/src/js_modules/syllabus/ExerciseGuidedExperience.jsx @@ -4,7 +4,7 @@ import useTranslation from 'next-translate/useTranslation'; import PropTypes from 'prop-types'; import { intervalToDuration } from 'date-fns'; import { intervalToHours } from '../../utils'; -import OpenWithLearnpackCTA from './OpenWithLearnpackCTA'; +import ProjectInstructions from './ProjectInstructions'; import useStyle from '../../common/hooks/useStyle'; import ReactPlayerV2 from '../../common/components/ReactPlayerV2'; import KPI from '../../common/components/KPI'; @@ -168,7 +168,7 @@ function ExerciseGuidedExperience({ currentTask, currentAsset, handleStartLearnp )} - + )} diff --git a/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx b/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx index 5347db152..7640f42e6 100644 --- a/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx +++ b/src/js_modules/syllabus/ProjectBoardGuidedExperience.jsx @@ -7,7 +7,7 @@ import SubtasksPill from './SubtasksPill'; import StatusPill from './StatusPill'; import Topbar from './Topbar'; import TaskCodeRevisions from './TaskCodeRevisions'; -import OpenWithLearnpackCTA from './OpenWithLearnpackCTA'; +import ProjectInstructions from './ProjectInstructions'; import useModuleHandler from '../../common/hooks/useModuleHandler'; import useStyle from '../../common/hooks/useStyle'; import ReactPlayerV2 from '../../common/components/ReactPlayerV2'; @@ -59,7 +59,7 @@ function ProjectHeading({ currentAsset, isDelivered, handleStartLearnpack }) { )} - + diff --git a/src/js_modules/syllabus/OpenWithLearnpackCTA.jsx b/src/js_modules/syllabus/ProjectInstructions.jsx similarity index 83% rename from src/js_modules/syllabus/OpenWithLearnpackCTA.jsx rename to src/js_modules/syllabus/ProjectInstructions.jsx index 00d95bb16..146d86554 100644 --- a/src/js_modules/syllabus/OpenWithLearnpackCTA.jsx +++ b/src/js_modules/syllabus/ProjectInstructions.jsx @@ -65,19 +65,12 @@ function ProvisioningPopover({ openInLearnpackAction, provisioningLinks }) { ); } -function ButtonsHandler({ currentAsset, setShowCloneModal, vendors, handleStartLearnpack }) { +function ButtonsHandler({ currentAsset, setShowCloneModal, vendors, handleStartLearnpack, isForOpenLocaly, startWithLearnpack, variant }) { const { t } = useTranslation('common'); const { state } = useCohortHandler(); const { cohortSession } = state; const openInLearnpackAction = t('learnpack.open-in-learnpack-button', {}, { returnObjects: true }); - const learnpackDeployUrl = currentAsset?.learnpack_deploy_url; - const templateUrl = currentAsset?.template_url; - // const templateUrl = 'https://google.com'; - const isInteractive = currentAsset?.interactive; - // const isInteractive = false; - const noLearnpackIncluded = noLearnpackAssets['no-learnpack']; - const accessToken = localStorage.getItem('accessToken'); const provisioningLinks = [{ @@ -96,14 +89,12 @@ function ButtonsHandler({ currentAsset, setShowCloneModal, vendors, handleStartL markdownBody.scrollIntoView({ block: 'start', behavior: 'smooth' }); }; - const startWithLearnpack = learnpackDeployUrl && cohortSession.available_as_saas && !noLearnpackIncluded.includes(currentAsset.slug); const showProvisioningLinks = vendors.length > 0 && currentAsset?.gitpod && !cohortSession.available_as_saas; const isExternalExercise = currentAsset.external && currentAsset.asset_type === 'EXERCISE'; - const isToOpenLocaly = cohortSession.available_as_saas && (isInteractive || templateUrl); if (isExternalExercise) { return ( - ); @@ -114,7 +105,7 @@ function ButtonsHandler({ currentAsset, setShowCloneModal, vendors, handleStartL {showProvisioningLinks && ( - @@ -122,36 +113,39 @@ function ButtonsHandler({ currentAsset, setShowCloneModal, vendors, handleStartL )} {startWithLearnpack ? ( - ) : ( )} ); } -function OpenWithLearnpackCTA({ currentAsset, variant, handleStartLearnpack }) { +function ProjectInstructions({ currentAsset, variant, handleStartLearnpack }) { const { t } = useTranslation('common'); const { currentTask } = useModuleHandler(); const { state } = useCohortHandler(); const { cohortSession } = state; const [vendors, setVendors] = useState([]); const [showCloneModal, setShowCloneModal] = useState(false); + const noLearnpackIncluded = noLearnpackAssets['no-learnpack']; const fetchProvisioningVendors = async () => { try { @@ -168,11 +162,20 @@ function OpenWithLearnpackCTA({ currentAsset, variant, handleStartLearnpack }) { } }, [cohortSession]); + const templateUrl = currentAsset?.template_url; + const isInteractive = currentAsset?.interactive; + const isForOpenLocaly = isInteractive || templateUrl; + const learnpackDeployUrl = currentAsset?.learnpack_deploy_url; + + const startWithLearnpack = learnpackDeployUrl && cohortSession.available_as_saas && !noLearnpackIncluded.includes(currentAsset.slug); + if (variant === 'small') { return ( <> - + {(!isForOpenLocaly || startWithLearnpack) && ( + + )} {t('learnpack.choose-open')} @@ -191,6 +194,9 @@ function OpenWithLearnpackCTA({ currentAsset, variant, handleStartLearnpack }) { handleStartLearnpack={handleStartLearnpack} setShowCloneModal={setShowCloneModal} vendors={vendors} + isForOpenLocaly={isForOpenLocaly} + startWithLearnpack={startWithLearnpack} + variant={variant} /> @@ -212,7 +218,7 @@ function OpenWithLearnpackCTA({ currentAsset, variant, handleStartLearnpack }) { @@ -229,7 +235,10 @@ function OpenWithLearnpackCTA({ currentAsset, variant, handleStartLearnpack }) { currentAsset={currentAsset} handleStartLearnpack={handleStartLearnpack} setShowCloneModal={setShowCloneModal} + startWithLearnpack={startWithLearnpack} + isForOpenLocaly={isForOpenLocaly} vendors={vendors} + variant={variant} /> @@ -238,12 +247,12 @@ function OpenWithLearnpackCTA({ currentAsset, variant, handleStartLearnpack }) { ); } -OpenWithLearnpackCTA.propTypes = { +ProjectInstructions.propTypes = { variant: PropTypes.string, handleStartLearnpack: PropTypes.func.isRequired, currentAsset: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.array])), }; -OpenWithLearnpackCTA.defaultProps = { +ProjectInstructions.defaultProps = { variant: null, currentAsset: null, }; @@ -253,4 +262,4 @@ ProvisioningPopover.propTypes = { provisioningLinks: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired, }; -export default OpenWithLearnpackCTA; +export default ProjectInstructions; diff --git a/src/middleware.js b/src/middleware.js index 0534239ae..9c7569bde 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -17,32 +17,41 @@ export const config = { }; async function middleware(req) { - const url = await req.nextUrl.clone(); + const url = req.nextUrl.clone(); const { href, origin } = url; - const fullPath = href.replace(origin, ''); + let fullPath = href.replace(origin, ''); + + const localeMatch = fullPath.match(/^\/(es|en|us)(\/|$)/); + const localePrefix = localeMatch ? localeMatch[1] : null; + + if (localePrefix) { + fullPath = fullPath.replace(`/${localePrefix}`, ''); + } const aliasAndLessonRedirects = [...aliasRedirects, ...redirectsFromApi]; + const currentProject = aliasAndLessonRedirects.find((item) => { - const destinationIsNotEqualToSource = item?.source !== item?.destination; + const normalizedSource = item.source.endsWith('/') ? item.source.slice(0, -1) : item.source; + const normalizedPath = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath; - if (item?.source === fullPath && destinationIsNotEqualToSource) return true; - return false; + return ( + normalizedSource === normalizedPath || normalizedSource === `/${localePrefix}${normalizedPath}` + ) && item.source !== item.destination; }); - const conditionalResult = () => { - if (currentProject?.type === 'PROJECT-REROUTE' && currentProject?.source) { - return true; + // Evitar redirecciones infinitas + if (currentProject) { + const destinationUrl = `${origin}${currentProject.destination}`; + if (destinationUrl === href) { + log('Middleware: already on destination URL, skipping redirect.'); + return NextResponse.next(); } - if (currentProject?.source) { - return true; - } - return false; - }; - if (conditionalResult()) { - log(`Middleware: redirecting from ${fullPath} → ${currentProject?.destination}`); - return NextResponse.redirect(`${origin}${currentProject?.destination || ''}`); + log(`Middleware: redirecting from ${href} → ${currentProject.destination}`); + return NextResponse.redirect(destinationUrl, 301); } + return NextResponse.next(); } + export default middleware; diff --git a/src/pages/accept-invite.jsx b/src/pages/accept-invite.jsx index 75484ddf8..d41899ef0 100644 --- a/src/pages/accept-invite.jsx +++ b/src/pages/accept-invite.jsx @@ -1,65 +1,142 @@ import { useState, useEffect } from 'react'; -import { - Box, - Image, - Flex, - Button, - FormControl, - FormLabel, - Input, - Checkbox, - useToast, - FormErrorMessage, -} from '@chakra-ui/react'; +import { Box, Image, Flex, Button, FormControl, FormLabel, Input, Checkbox, useToast, FormErrorMessage } from '@chakra-ui/react'; import { Form, Formik, Field } from 'formik'; -import useTranslation from 'next-translate/useTranslation'; import { useRouter } from 'next/router'; +import PropTypes from 'prop-types'; +import useTranslation from 'next-translate/useTranslation'; +import { BREATHECODE_HOST } from '../utils/variables'; +import bc from '../common/services/breathecode'; +import useAuth from '../common/hooks/useAuth'; +import LoaderScreen from '../common/components/LoaderScreen'; import validationSchema from '../common/components/Forms/validationSchemas'; import NextChakraLink from '../common/components/NextChakraLink'; -import LoaderScreen from '../common/components/LoaderScreen'; import Text from '../common/components/Text'; import Heading from '../common/components/Heading'; -import { BREATHECODE_HOST } from '../utils/variables'; + +function FormField({ name, label, type = 'text', isReadOnly = false, placeholder }) { + return ( + + {({ field, form }) => ( + + + {label} + + + {form.errors[name]} + + )} + + ); +} function AcceptInvite() { - const { t, lang } = useTranslation('accept-invite'); - const [isChecked, setIsChecked] = useState(false); - const [invite, setInvite] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(false); const toast = useToast(); const router = useRouter(); + const { t, lang } = useTranslation('accept-invite'); + const { isAuthenticated, user, isLoading, login } = useAuth(); const { query } = router; const { inviteToken } = query; + const [incorrectUser, setIncorrectUser] = useState(false); + const [userNewInvite, setUserNewInvite] = useState(false); + const [isChecked, setIsChecked] = useState(false); + const [invite, setInvite] = useState(null); + const [pageLoader, setPageLoader] = useState(true); + const [isLogging, setIsLogging] = useState(false); + const [isAccepted, setIsAccepted] = useState(false); - const getInvite = async () => { - try { - const resp = await fetch(`${BREATHECODE_HOST}/v1/auth/member/invite/${inviteToken}`, { - headers: { - 'Content-Type': 'application/json', - }, - }); + useEffect(() => { + const initialize = async () => { + if (!inviteToken) { + setPageLoader(false); + return; + } + + try { + const resp = await fetch(`${BREATHECODE_HOST}/v1/auth/member/invite/${inviteToken}`, { + headers: { 'Content-Type': 'application/json' }, + }); + const data = await resp.json(); + if (data.status_code > 300) throw new Error(data.detail); + if (user && user.email !== data.email) setIncorrectUser(true); + if (user && user.email === data.email) setUserNewInvite(true); - const data = await resp.json(); - setInvite(data); + setInvite(data); + } catch (error) { + setIsAccepted(true); + } finally { + setPageLoader(false); + } + }; - setIsLoading(false); + if (!isLoading) { + initialize(); + } + }, [inviteToken, user, isLoading]); + + const acceptInvite = async ({ id, cohort }) => { + try { + setPageLoader(true); + const res = await bc.auth().invites().accept(id); + const { status } = res; + if (status >= 200 && status < 400) { + toast({ + title: t('alert-message:invitation-accepted-cohort', { cohortName: cohort.name }), + position: 'top', + status: 'success', + duration: 9000, + isClosable: true, + }); + router.push('choose-program'); + setPageLoader(false); + } else { + throw new Error('Invitation error'); + } } catch (e) { - console.log(e.message); - setIsLoading(false); - setError(true); + console.log(e); toast({ - title: e?.message || t('error'), + title: t('alert-message:invitation-error'), + position: 'top', status: 'error', - duration: 9000, + duration: 5000, isClosable: true, }); } }; - useEffect(() => { - getInvite(); - }, []); + const handleLogin = (values, actions) => { + setIsLogging(true); + login(values) + .then((data) => { + actions.setSubmitting(false); + if (data.status === 200) { + toast({ + position: 'top', + title: t('alert-message:welcome'), + status: 'success', + duration: 9000, + isClosable: true, + }); + } + router.push('/choose-program'); + setIsLogging(true); + }) + .catch(() => { + actions.setSubmitting(false); + toast({ + position: 'top', + title: t('alert-message:account-not-found'), + status: 'error', + duration: 9000, + isClosable: true, + }); + }); + }; const putInvite = async (values, actions) => { try { @@ -90,8 +167,9 @@ function AcceptInvite() { status: 'success', duration: 9000, isClosable: true, + position: 'top', }); - router.push('/login'); + handleLogin(values, actions); } actions.setSubmitting(false); } catch (e) { @@ -106,7 +184,7 @@ function AcceptInvite() { } }; - if (isLoading) { + if (pageLoader) { return ( - - {t('error')} - - - ); - } - return ( - - {invite?.academy.name} - - {t('heading', { name: invite?.academy.name })} - + <> + {!inviteToken + && ( + + + {t('no-token')} + + + {t('signup:consumables.back-to-dashboard')} + + + )} + {isAccepted && !isLogging + && ( + + + {t('already-accepted')} + + + {t('signup:consumables.back-to-dashboard')} + + + )} + {incorrectUser && user + && ( + + + {t('invalid-user')} + + + {t('signup:consumables.back-to-dashboard')} + + + )} + {user && userNewInvite + && ( + + + {t('new-invite', { course: invite?.cohort?.name })} + + + + )} + {((invite && inviteToken && !isAccepted && !incorrectUser && !userNewInvite) || (isLogging)) + && ( + + {invite?.academy.name} + + {t('heading', { name: invite?.academy.name })} + + {!incorrectUser && ( + + {({ isSubmitting }) => ( +
+ + + + - - {({ isSubmitting }) => ( - - - - {({ field, form }) => ( - - - {t('common:first-name')} - - - {form.errors.first_name} - - )} - - - {({ field, form }) => ( - - - {t('common:last-name')} - - - {form.errors.last_name} - - )} - - - - - {({ field, form }) => ( - - - {t('common:phone')} - - - {form.errors.phone} - - {t('signup:phone-info')} - - - )} - - - - - {({ field, form }) => ( - - - {t('common:email')} - - - {form.errors.email} - - )} - - - - - {({ field, form }) => ( - - - {t('Choose your password')} - - - {form.errors.password} - - )} - - - - - {({ field, form }) => ( - - - {t('Repeat your password')} - - - {form.errors.passwordConfirmation} - - )} - - + + + + {t('signup:phone-info')} + + - - setIsChecked(!isChecked)} - /> - - {t('signup:validators.receive-information')} - {' '} - - {t('common:privacy-policy')} - - - + + + - - + + + + + + + + + + setIsChecked(!isChecked)} /> + + + {' '} + {t('signup:validators.receive-information')} + {' '} + + {t('common:privacy-policy')} + + + + + )} +
+ )} +
)} - -
+ ); } +FormField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + type: PropTypes.string, + isReadOnly: PropTypes.bool, + placeholder: PropTypes.string, +}; + +FormField.defaultProps = { + type: 'text', + isReadOnly: false, + placeholder: '', +}; + export default AcceptInvite; diff --git a/src/pages/cohort/[cohortSlug]/[slug]/[version]/index.jsx b/src/pages/cohort/[cohortSlug]/[slug]/[version]/index.jsx index 5ab120fed..616145970 100644 --- a/src/pages/cohort/[cohortSlug]/[slug]/[version]/index.jsx +++ b/src/pages/cohort/[cohortSlug]/[slug]/[version]/index.jsx @@ -37,6 +37,7 @@ import Heading from '../../../../../common/components/Heading'; import asPrivate from '../../../../../common/context/PrivateRouteWrapper'; import useAuth from '../../../../../common/hooks/useAuth'; import { ModuleMapSkeleton, SimpleSkeleton } from '../../../../../common/components/Skeleton'; +import { parseQuerys } from '../../../../../utils/url'; import bc from '../../../../../common/services/breathecode'; import axios from '../../../../../axios'; @@ -183,7 +184,11 @@ function Dashboard() { }; const checkNavigationAvailability = () => { - const showToast = () => { + const showToastAndRedirect = (programSlug) => { + const querys = parseQuerys({ + plan: programSlug, + }); + router.push(`/${lang}/checkout${querys}`); toast({ position: 'top', title: t('alert-message:access-denied'), @@ -196,9 +201,10 @@ function Dashboard() { 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)); + const currentCohortSlug = cohortSubscriptions[0]?.selected_cohort_set?.slug; + if (cohortSubscriptions.length === 0) { - router.push('/choose-program'); - showToast(); + showToastAndRedirect(currentCohortSlug); return; } @@ -213,8 +219,7 @@ function Dashboard() { const todayDate = new Date(); if (todayDate > freeTrialExpDate) { - router.push('/choose-program'); - showToast(); + showToastAndRedirect(currentCohortSlug); return; } @@ -226,7 +231,7 @@ function Dashboard() { if (cohortSession?.available_as_saas === true && cohortSession.cohort_role === 'STUDENT') { checkNavigationAvailability(); } - if (cohortSession.cohort_role !== 'STUDENT' || cohortSession?.available_as_saas === false) setGrantAccess(true); + if (Object.keys(cohortSession).length > 0 && (cohortSession.cohort_role !== 'STUDENT' || cohortSession.available_as_saas === false)) setGrantAccess(true); }, [cohortSession, allSubscriptions]); useEffect(() => { diff --git a/src/pages/syllabus/[cohortSlug]/[lesson]/[lessonSlug]/index.jsx b/src/pages/syllabus/[cohortSlug]/[lesson]/[lessonSlug]/index.jsx index 4bf83ee08..41a7fea24 100644 --- a/src/pages/syllabus/[cohortSlug]/[lesson]/[lessonSlug]/index.jsx +++ b/src/pages/syllabus/[cohortSlug]/[lesson]/[lessonSlug]/index.jsx @@ -8,7 +8,7 @@ import { import useTranslation from 'next-translate/useTranslation'; import { useRouter } from 'next/router'; import Head from 'next/head'; -import { isWindow, assetTypeValues, getExtensionName, getStorageItem } from '../../../../../utils'; +import { isWindow, assetTypeValues, getExtensionName, getStorageItem, languageFix } from '../../../../../utils'; import asPrivate from '../../../../../common/context/PrivateRouteWrapper'; import Heading from '../../../../../common/components/Heading'; import useModuleHandler from '../../../../../common/hooks/useModuleHandler'; @@ -40,6 +40,7 @@ import useStyle from '../../../../../common/hooks/useStyle'; import { ORIGIN_HOST, BREATHECODE_HOST } from '../../../../../utils/variables'; import useSession from '../../../../../common/hooks/useSession'; import { log } from '../../../../../utils/logging'; +import { parseQuerys } from '../../../../../utils/url'; import completions from './completion-jobs.json'; import { generateUserContext } from '../../../../../utils/rigobotContext'; @@ -209,7 +210,7 @@ function SyllabusContent() { const updateOpenedAt = async () => { try { - const result = await bc.todo().update({ ...currentTask, opened_at: new Date() }); + const result = await bc.todo().update({ id: currentTask.id, opened_at: new Date() }); if (result.data) { const updateTasks = taskTodo.map((task) => ({ ...task })); const index = updateTasks.findIndex((el) => el.task_type === assetTypeValues[lesson] && el.associated_slug === lessonSlug); @@ -253,7 +254,7 @@ function SyllabusContent() { }, [currentTask]); useEffect(() => { - const assetSlug = currentAsset?.translations?.us || currentAsset?.translations?.en || lessonSlug; + const assetSlug = currentAsset?.translations[lang] || currentAsset?.translations?.us || currentAsset?.translations?.en || lessonSlug; if (taskTodo.length > 0) { setCurrentTask(taskTodo.find((el) => el.task_type === assetTypeValues[lesson] && (el.associated_slug === assetSlug || currentAsset?.aliases?.includes(el.associated_slug)))); @@ -283,7 +284,11 @@ function SyllabusContent() { }); }, []); - const showToast = () => { + const showToastAndRedirect = (programSlug) => { + const querys = parseQuerys({ + plan: programSlug, + }); + router.push(`/${lang}/checkout${querys}`); toast({ position: 'top', title: t('alert-message:access-denied'), @@ -297,9 +302,10 @@ function SyllabusContent() { if (allSubscriptions && cohortSession && cohortSession.available_as_saas === true && cohortSession.cohort_role === 'STUDENT') { 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)); + const currentCohortSlug = cohortSubscriptions[0]?.selected_cohort_set?.slug; + if (!(cohortSubscriptions.length > 0)) { - router.push('/choose-program'); - showToast(); + showToastAndRedirect(currentCohortSlug); return; } @@ -314,14 +320,13 @@ function SyllabusContent() { const todayDate = new Date(); if (todayDate > freeTrialExpDate) { - router.push('/choose-program'); - showToast(); + showToastAndRedirect(currentCohortSlug); return; } setGrantAccess(true); } - if (cohortSession.cohort_role !== 'STUDENT' || cohortSession.available_as_saas === false) setGrantAccess(true); + if (Object.keys(cohortSession).length > 0 && (cohortSession.cohort_role !== 'STUDENT' || cohortSession.available_as_saas === false)) setGrantAccess(true); }, [cohortSession, allSubscriptions]); const toggleSettings = () => { @@ -1375,9 +1380,11 @@ function SyllabusContent() { - - {t('reached-the-end-of-the-module', { label, nextModuleLabel: nextModule?.label })} - + {label && nextModule.label && ( + + {t('reached-the-end-of-the-module', { label: languageFix(label, lang), nextModuleLabel: languageFix(nextModule.label, lang) })} + + )} + + {!isAtStart && ( + + )} + - {techsShown.map((tech, index) => ( + {techsBySortPriority.map((tech) => ( handleMouseUp(tech)} + cursor="pointer" + borderBottom="2px solid" + borderColor={tech.slug === technologyData.slug ? 'blue.1000' : 'transparent'} + _hover={tech.slug !== technologyData.slug && { borderColor: 'gray.200' }} > {tech?.icon_url && ( <> {`${tech.title}`} handleMouseUp(tech)} + filter={tech.slug !== technologyData.slug && 'grayscale(100%)'} /> - {index === 0 && {technologyData.title}} + + + {tech.title} + + )} ))} - + + {!isAtEnd && ( + + )} - { - marketingInfoExist ? ( - <> - - - - {marketingInfo.title ? languageFix(marketingInfo.title, lang) : t('landing-technology.title', { technology: toCapitalize(technologyData?.title) })} - - - {marketingInfo.description ? languageFix(marketingInfo.description, lang) : t('landing-technology.defaultDescription')} - - - {coursesAvailable - && ( - - - - )} - - - - + + {marketingInfoExist ? ( + <> + + + + {marketingInfo.title ? languageFix(marketingInfo.title, lang) : t('landing-technology.title', { technology: toCapitalize(technologyData?.title) })} + + + {marketingInfo.description ? languageFix(marketingInfo.description, lang) : t('landing-technology.defaultDescription')} + + + {coursesAvailable + && ( + + + + )} + + + + - - {marketingInfo?.video ? ( - + {marketingInfo?.video ? ( + + ) : ( + + python image related - ) : ( - - python image related - - )} + + )} + + + {exercises.length > 0 && ( + + + {t('popular-exercises')} + + + + + + )} + {workShopsForTech?.length > 0 && ( + + + - {exercises.length > 0 && ( - - - {t('popular-exercises')} - - - - - - )} - {workShopsForTech?.length > 0 && ( - - - - - - )} - {lessonMaterials?.length > 0 && ( - - - {t('tech-materials', { tech: technologyData?.title })} - - - - - - )} - - ) : ( - - ) - } + )} + {lessonMaterials?.length > 0 && ( + + + {t('tech-materials', { tech: technologyData?.title })} + + + + + + )} + + ) : ( + + )} ) : (