From 466245307936ca76c2a975f0d59f9ae3d10d6a72 Mon Sep 17 00:00:00 2001 From: Gabriele Antonini Date: Tue, 9 May 2023 15:56:37 +0200 Subject: [PATCH 1/6] chore: display empty circle indicator if unenrolled --- .../src/components/sidebar-course-status.tsx | 1 + .../src/components/sidebar-topic-status.tsx | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx index 9a80f8e9c5..aa916f086b 100644 --- a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx +++ b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx @@ -25,6 +25,7 @@ export const StatusIndicator = (props: StatusIndicatorProps) => { case 'completed': return ; case 'inProgress': + case 'notEnrolled': return ; default: return ; diff --git a/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx b/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx index af1ac2f161..6b82251b8f 100644 --- a/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx +++ b/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { CheckActiveIcon, CircleIcon } from '@commercetools-uikit/icons'; import { useFetchCourseDetails, @@ -10,6 +10,7 @@ import ConfigContext, { } from './config-context'; import { designSystem } from '@commercetools-docs/ui-kit'; import styled from '@emotion/styled'; +import { useAuth0 } from '@auth0/auth0-react'; const UnknownStateSpacer = styled.div` width: ${designSystem.dimensions.spacings.m}; @@ -25,6 +26,7 @@ export const StatusIndicator = (props: StatusIndicatorProps) => { case 'completed': return ; case 'notCompleted': + case 'notAvailable': return ; default: return ; @@ -37,17 +39,26 @@ type PageTopicStatusProps = { }; const SidebarTopicStatus = (props: PageTopicStatusProps) => { + const { isAuthenticated } = useAuth0(); + const [topicStatus, setTopicStatus] = useState(); const { data } = useFetchCourseDetails(props.courseId); const { features } = useContext(ConfigContext); - // CourseStatus feature flag - if (!isFeatureEnabled(EFeatureFlag.CourseStatus, features)) { - return null; - } + useEffect(() => { + if ( + // CourseStatus feature flag + isFeatureEnabled(EFeatureFlag.CourseStatus, features) && + isAuthenticated + ) { + setTopicStatus( + data?.result?.topics + ? getTopicStatusByPageTitle(data.result.topics, props.pageTitle) + : 'notAvailable' + ); + } - const topicStatus = data?.result?.topics - ? getTopicStatusByPageTitle(data.result.topics, props.pageTitle) - : undefined; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [features, data, isAuthenticated]); return props.courseId && ; }; From d6a51d17392eeb003ccd52753ac7e57ec6d99df7 Mon Sep 17 00:00:00 2001 From: Gabriele Antonini Date: Tue, 9 May 2023 15:58:03 +0200 Subject: [PATCH 2/6] chore: changeset --- .changeset/clean-guests-join.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-guests-join.md diff --git a/.changeset/clean-guests-join.md b/.changeset/clean-guests-join.md new file mode 100644 index 0000000000..f37d124343 --- /dev/null +++ b/.changeset/clean-guests-join.md @@ -0,0 +1,5 @@ +--- +'@commercetools-docs/gatsby-theme-learning': patch +--- + +If the user is not enrolled into a self-learning course, the sidebar will indicate an empty circle as status indicator From 226fb5eeef94a7abd12743faea395a0f82979e8b Mon Sep 17 00:00:00 2001 From: Gabriele Antonini Date: Thu, 11 May 2023 11:02:35 +0200 Subject: [PATCH 3/6] chore: handle api error for users not enrolled --- .../src/components/sidebar-course-status.tsx | 29 ++++++++++++++----- .../src/hooks/hooks.utils.ts | 19 ++++++++++-- .../src/hooks/use-course-pages.ts | 2 +- .../src/hooks/use-course-status.ts | 4 +-- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx index fe374a6f3c..2c06265518 100644 --- a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx +++ b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx @@ -1,8 +1,9 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { CircleIcon, VerifiedIcon } from '@commercetools-uikit/icons'; import { getCourseStatusByCourseId, useFetchCourses, + ClientCourseStatus, } from '../hooks/use-course-status'; import ConfigContext, { isFeatureEnabled, @@ -38,15 +39,27 @@ type SidebarCourseStatusProps = { const SidebarCourseStatus = (props: SidebarCourseStatusProps) => { const { data } = useFetchCourses(); const { features } = useContext(ConfigContext); + const [courseStatus, setCourseStatus] = useState< + ClientCourseStatus | undefined + >(); - // CourseStatus feature flag - if (!isFeatureEnabled(EFeatureFlag.CourseStatus, features)) { - return null; - } + useEffect(() => { + // CourseStatus feature flag + if (!isFeatureEnabled(EFeatureFlag.CourseStatus, features)) { + setCourseStatus(undefined); + } else { + setCourseStatus( + data?.result?.enrolledCourses + ? getCourseStatusByCourseId( + data.result.enrolledCourses, + props.courseId + ) + : undefined + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, props.courseId]); - const courseStatus = data?.result?.enrolledCourses - ? getCourseStatusByCourseId(data.result.enrolledCourses, props.courseId) - : undefined; return <>{props.courseId && }; }; diff --git a/packages/gatsby-theme-learning/src/hooks/hooks.utils.ts b/packages/gatsby-theme-learning/src/hooks/hooks.utils.ts index a20320d3c2..c5bfbd26b6 100644 --- a/packages/gatsby-theme-learning/src/hooks/hooks.utils.ts +++ b/packages/gatsby-theme-learning/src/hooks/hooks.utils.ts @@ -2,8 +2,11 @@ import type { ApiCallResult, EnrolledCourses, CourseWithDetails, + ApiCallResultError, } from '../external-types'; +const ERR_TYPE_NOT_ENROLLED = 'errorcoursecontextnotvalid'; + class FetchDataError extends Error { status: number | undefined; info: object | undefined; @@ -17,6 +20,13 @@ class FetchDataError extends Error { } } +/** + * Checks if the errors array contains a specific code indicating that the + * user is not yet enrolled into the course + */ +const isUserNotUnenrolledError = (errors: ApiCallResultError[]) => + !!errors.find((error) => error.type === ERR_TYPE_NOT_ENROLLED); + export const fetcherWithToken = async ( url: string, getAuthToken: () => Promise, @@ -36,9 +46,12 @@ export const fetcherWithToken = async ( EnrolledCourses | CourseWithDetails >; if (jsonResponse.errors) { - const msg = `Error: "${jsonResponse.errors[0].message}" while fetching ${url}`; - console.error(msg); - throw new FetchDataError(msg); + // any errors different than 'unenrolled' throws an exception + if (!isUserNotUnenrolledError(jsonResponse.errors)) { + const msg = `Error: "${jsonResponse.errors[0].message}" while fetching ${url}`; + console.error(msg); + throw new FetchDataError(msg); + } } return jsonResponse; }; diff --git a/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts b/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts index 82490d5652..ad43e9971d 100644 --- a/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts +++ b/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts @@ -121,7 +121,7 @@ export const useOrderedCoursesInfo = (): OrderedCoursesInfo[] => { courseId, pages: navData.pages, status: data?.result.enrolledCourses - ? getCourseStatusByCourseId(data?.result.enrolledCourses, courseId) + ? getCourseStatusByCourseId(data.result.enrolledCourses, courseId) : '', }; }); diff --git a/packages/gatsby-theme-learning/src/hooks/use-course-status.ts b/packages/gatsby-theme-learning/src/hooks/use-course-status.ts index 3a4634ca7f..b0b46416c2 100644 --- a/packages/gatsby-theme-learning/src/hooks/use-course-status.ts +++ b/packages/gatsby-theme-learning/src/hooks/use-course-status.ts @@ -62,8 +62,8 @@ export const getCourseStatusByCourseId = ( courses: Course[], courseId: number ): ClientCourseStatus => { - if (!courses || !courseId) { - console.warn('getCourseStatusByCourseId expects courses list and courseId'); + if (!courseId) { + console.warn('getCourseStatusByCourseId expects courseId'); return 'notAvailable'; } const filteredCourse = courses.find( From aa500c352a1cf208b1d45ce2d8552a0aabb43c43 Mon Sep 17 00:00:00 2001 From: Gabriele Antonini Date: Thu, 11 May 2023 14:02:27 +0200 Subject: [PATCH 4/6] chore: display empty circle in any cases different than completed --- .../src/components/sidebar-course-status.tsx | 28 ++++++++++++------- .../src/components/sidebar-topic-status.tsx | 25 +++++++++++------ .../src/hooks/use-course-details.ts | 7 +++-- .../src/hooks/use-course-status.ts | 6 ++-- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx index 2c06265518..679ff8756f 100644 --- a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx +++ b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx @@ -10,6 +10,7 @@ import ConfigContext, { EFeatureFlag, } from './config-context'; import styled from '@emotion/styled'; +import { useAuth0 } from '@auth0/auth0-react'; const UnknownStateSpacer = styled.div` min-width: 21px; @@ -17,7 +18,12 @@ const UnknownStateSpacer = styled.div` `; type StatusIndicatorProps = { - status?: string; + status?: + | 'completed' // course completed, we display a green tick icon + | 'inProgress' // user enrolled but course not completed/passed, we display an empty circle icon + | 'notEnrolled' // user not enrolled into course completed/passed, we display an empty circle icon + | 'notAvailable' // error during fetching operation, we display an empty circle icon + | undefined; // user is not logged in, we display an empty placeholder }; export const StatusIndicator = (props: StatusIndicatorProps) => { @@ -37,28 +43,30 @@ type SidebarCourseStatusProps = { }; const SidebarCourseStatus = (props: SidebarCourseStatusProps) => { + const { isAuthenticated } = useAuth0(); const { data } = useFetchCourses(); const { features } = useContext(ConfigContext); const [courseStatus, setCourseStatus] = useState< ClientCourseStatus | undefined >(); + // If status-indicator feature flag is disable OR the user is logged out + // it will pass undefined to StatusIndicator which in turn will render an empty spacer... useEffect(() => { - // CourseStatus feature flag - if (!isFeatureEnabled(EFeatureFlag.CourseStatus, features)) { + if ( + !isFeatureEnabled(EFeatureFlag.CourseStatus, features) || + !isAuthenticated + ) { setCourseStatus(undefined); } else { + //... otherwise getCourseStatusByCourseId helper will return the proper status prop to the StatusIndicator component setCourseStatus( - data?.result?.enrolledCourses - ? getCourseStatusByCourseId( - data.result.enrolledCourses, - props.courseId - ) - : undefined + getCourseStatusByCourseId(data?.result?.enrolledCourses, props.courseId) ); } + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, props.courseId]); + }, [data, props.courseId, isAuthenticated]); return <>{props.courseId && }; }; diff --git a/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx b/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx index 307b326a55..59d4f22a67 100644 --- a/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx +++ b/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx @@ -17,15 +17,21 @@ const UnknownStateSpacer = styled.div` margin-right: 5px; `; +type TopicStatus = + | 'completed' // course completed, we display a green tick icon + | 'inProgress' // user enrolled but course not completed/passed, we display an empty circle icon + | 'notAvailable' // error during fetching operation, we display an empty circle icon + | undefined; // user is not logged in, we display an empty placeholder + type StatusIndicatorProps = { - status?: string; + status?: TopicStatus; }; export const StatusIndicator = (props: StatusIndicatorProps) => { switch (props.status) { case 'completed': return ; - case 'notCompleted': + case 'inProgress': case 'notAvailable': return ; default: @@ -40,20 +46,21 @@ type PageTopicStatusProps = { const SidebarTopicStatus = (props: PageTopicStatusProps) => { const { isAuthenticated } = useAuth0(); - const [topicStatus, setTopicStatus] = useState(); + const [topicStatus, setTopicStatus] = useState(); const { data } = useFetchCourseDetails(props.courseId); const { features } = useContext(ConfigContext); + // If status-indicator feature flag is disable OR the user is logged out + // it will pass undefined to StatusIndicator which in turn will render an empty spacer... useEffect(() => { if ( - // CourseStatus feature flag - isFeatureEnabled(EFeatureFlag.CourseStatus, features) && - isAuthenticated + !isFeatureEnabled(EFeatureFlag.CourseStatus, features) || + !isAuthenticated ) { + setTopicStatus(undefined); + } else { setTopicStatus( - data?.result?.topics - ? getTopicStatusByPageTitle(data.result.topics, props.pageTitle) - : 'notAvailable' + getTopicStatusByPageTitle(data?.result?.topics, props.pageTitle) ); } diff --git a/packages/gatsby-theme-learning/src/hooks/use-course-details.ts b/packages/gatsby-theme-learning/src/hooks/use-course-details.ts index 5b0b99f657..57db783531 100644 --- a/packages/gatsby-theme-learning/src/hooks/use-course-details.ts +++ b/packages/gatsby-theme-learning/src/hooks/use-course-details.ts @@ -56,15 +56,18 @@ export const useFetchCourseDetails = ( }; export const getTopicStatusByPageTitle = ( - topics: CourseTopic[], + topics: CourseTopic[] | undefined, pageTitle: string ) => { + if (!topics || !pageTitle) { + return 'notAvailable'; + } const matchingTopic = topics.find( (topic) => topic.name.trim().toLowerCase() === pageTitle.trim().toLowerCase() ); if (matchingTopic) { - return matchingTopic.completed ? 'completed' : 'notCompleted'; + return matchingTopic.completed ? 'completed' : 'inProgress'; } return 'notAvailable'; }; diff --git a/packages/gatsby-theme-learning/src/hooks/use-course-status.ts b/packages/gatsby-theme-learning/src/hooks/use-course-status.ts index b0b46416c2..3f43a842a4 100644 --- a/packages/gatsby-theme-learning/src/hooks/use-course-status.ts +++ b/packages/gatsby-theme-learning/src/hooks/use-course-status.ts @@ -59,11 +59,11 @@ export const useFetchCourses = (): { }; export const getCourseStatusByCourseId = ( - courses: Course[], + courses: Course[] | undefined, courseId: number ): ClientCourseStatus => { - if (!courseId) { - console.warn('getCourseStatusByCourseId expects courseId'); + if (!courseId || !courses) { + console.warn('getCourseStatusByCourseId expects courses && courseId'); return 'notAvailable'; } const filteredCourse = courses.find( From a742218e835be011f5e2a14384426eda9a4856ab Mon Sep 17 00:00:00 2001 From: Gabriele Antonini Date: Thu, 11 May 2023 17:08:44 +0200 Subject: [PATCH 5/6] chore: handle loading state --- .../src/components/sidebar-course-status.tsx | 23 ++++++++++++------- .../src/components/sidebar-topic-status.tsx | 7 +++++- .../src/hooks/use-course-status.ts | 6 ++++- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx index 679ff8756f..3cd476f622 100644 --- a/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx +++ b/packages/gatsby-theme-learning/src/components/sidebar-course-status.tsx @@ -17,13 +17,16 @@ const UnknownStateSpacer = styled.div` margin-right: 5px; `; +/** + * 'completed' // course completed, we display a green tick icon + * 'inProgress' // user enrolled but course not completed/passed, we display an empty circle icon + * 'notEnrolled' // user not enrolled into course completed/passed, we display an empty circle icon + * 'notAvailable' // error during fetching operation, we display an empty circle icon + * 'isLoading' // API request in progress, we display an empty circle icon + * undefined; // user is not logged in, we display an empty placeholder + */ type StatusIndicatorProps = { - status?: - | 'completed' // course completed, we display a green tick icon - | 'inProgress' // user enrolled but course not completed/passed, we display an empty circle icon - | 'notEnrolled' // user not enrolled into course completed/passed, we display an empty circle icon - | 'notAvailable' // error during fetching operation, we display an empty circle icon - | undefined; // user is not logged in, we display an empty placeholder + status?: ClientCourseStatus; }; export const StatusIndicator = (props: StatusIndicatorProps) => { @@ -32,6 +35,7 @@ export const StatusIndicator = (props: StatusIndicatorProps) => { return ; case 'inProgress': case 'notEnrolled': + case 'isLoading': return ; default: return ; @@ -44,7 +48,7 @@ type SidebarCourseStatusProps = { const SidebarCourseStatus = (props: SidebarCourseStatusProps) => { const { isAuthenticated } = useAuth0(); - const { data } = useFetchCourses(); + const { data, isLoading } = useFetchCourses(); const { features } = useContext(ConfigContext); const [courseStatus, setCourseStatus] = useState< ClientCourseStatus | undefined @@ -58,6 +62,9 @@ const SidebarCourseStatus = (props: SidebarCourseStatusProps) => { !isAuthenticated ) { setCourseStatus(undefined); + } else if (isLoading) { + // ...if data is loading, set the `isLoading` status + setCourseStatus('isLoading'); } else { //... otherwise getCourseStatusByCourseId helper will return the proper status prop to the StatusIndicator component setCourseStatus( @@ -66,7 +73,7 @@ const SidebarCourseStatus = (props: SidebarCourseStatusProps) => { } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, props.courseId, isAuthenticated]); + }, [data, props.courseId, isAuthenticated, isLoading]); return <>{props.courseId && }; }; diff --git a/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx b/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx index 59d4f22a67..64f9ee759e 100644 --- a/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx +++ b/packages/gatsby-theme-learning/src/components/sidebar-topic-status.tsx @@ -21,6 +21,7 @@ type TopicStatus = | 'completed' // course completed, we display a green tick icon | 'inProgress' // user enrolled but course not completed/passed, we display an empty circle icon | 'notAvailable' // error during fetching operation, we display an empty circle icon + | 'isLoading' // API fetch in progress, we display an empty circle icon | undefined; // user is not logged in, we display an empty placeholder type StatusIndicatorProps = { @@ -33,6 +34,7 @@ export const StatusIndicator = (props: StatusIndicatorProps) => { return ; case 'inProgress': case 'notAvailable': + case 'isLoading': return ; default: return ; @@ -47,7 +49,7 @@ type PageTopicStatusProps = { const SidebarTopicStatus = (props: PageTopicStatusProps) => { const { isAuthenticated } = useAuth0(); const [topicStatus, setTopicStatus] = useState(); - const { data } = useFetchCourseDetails(props.courseId); + const { data, isLoading } = useFetchCourseDetails(props.courseId); const { features } = useContext(ConfigContext); // If status-indicator feature flag is disable OR the user is logged out @@ -58,6 +60,9 @@ const SidebarTopicStatus = (props: PageTopicStatusProps) => { !isAuthenticated ) { setTopicStatus(undefined); + } else if (isLoading) { + // ...if data is loading, set the `isLoading` status + setTopicStatus('isLoading'); } else { setTopicStatus( getTopicStatusByPageTitle(data?.result?.topics, props.pageTitle) diff --git a/packages/gatsby-theme-learning/src/hooks/use-course-status.ts b/packages/gatsby-theme-learning/src/hooks/use-course-status.ts index 3f43a842a4..1319f292e5 100644 --- a/packages/gatsby-theme-learning/src/hooks/use-course-status.ts +++ b/packages/gatsby-theme-learning/src/hooks/use-course-status.ts @@ -19,7 +19,11 @@ import { useAuthToken } from './use-auth-token'; * - notEnrolled: when a course exists on the platform but the user is not enrolled * - notAvailable: when any unexpected situation happens */ -export type ClientCourseStatus = CourseStatus | 'notEnrolled' | 'notAvailable'; +export type ClientCourseStatus = + | CourseStatus + | 'notEnrolled' + | 'notAvailable' + | 'isLoading'; type UseFetchCoursesResponse = { data: ApiCallResult | undefined; error: string; From 0a107b8eefddf1122cb1111e660c92c85f678c20 Mon Sep 17 00:00:00 2001 From: Gabriele Antonini Date: Fri, 12 May 2023 11:58:32 +0200 Subject: [PATCH 6/6] chore: reduce content flickering on hard page refresh --- .../src/components/course-complete-modal.tsx | 9 +++- .../src/components/if-learning-path.tsx | 14 +++--- .../src/components/if-user-logged.tsx | 2 + .../src/hooks/use-course-pages.ts | 44 ++++++++++++------- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/packages/gatsby-theme-learning/src/components/course-complete-modal.tsx b/packages/gatsby-theme-learning/src/components/course-complete-modal.tsx index 821693ffc2..e0f3d17573 100644 --- a/packages/gatsby-theme-learning/src/components/course-complete-modal.tsx +++ b/packages/gatsby-theme-learning/src/components/course-complete-modal.tsx @@ -57,8 +57,14 @@ const CourseCompleteModal = (props: CourseCompleteModalProps) => { const { isModalOpen, openModal, closeModal } = useModalState(); const { data } = useFetchCourseDetails(props.courseId); const { isClientSide } = useIsClientSide(); + const [goToUrl, setGoToUrl] = useState('/'); const courseInfo = useOrderedCoursesInfo(); - const goToUrl = getNextUnfinishedCoursePath(courseInfo, props.courseId); + + useEffect(() => { + if (courseInfo) { + setGoToUrl(getNextUnfinishedCoursePath(courseInfo, props.courseId)); + } + }, [courseInfo, props.courseId]); useEffect(() => { if (goToUrl === '/') { @@ -89,7 +95,6 @@ const CourseCompleteModal = (props: CourseCompleteModalProps) => { const onConfirmHandler = (e: SyntheticEvent) => { e.preventDefault(); - const goToUrl = getNextUnfinishedCoursePath(courseInfo, props.courseId); closeModal(); navigate(goToUrl); }; diff --git a/packages/gatsby-theme-learning/src/components/if-learning-path.tsx b/packages/gatsby-theme-learning/src/components/if-learning-path.tsx index a209152e67..5731881999 100644 --- a/packages/gatsby-theme-learning/src/components/if-learning-path.tsx +++ b/packages/gatsby-theme-learning/src/components/if-learning-path.tsx @@ -15,33 +15,33 @@ type IfLearningPathCompleteProps = { }; export const IfLearningPathComplete = (props: IfLearningPathCompleteProps) => { - const { isAuthenticated } = useAuth0(); + const { isAuthenticated, isLoading } = useAuth0(); const [isVisible, setIsVisible] = useState(false); const courseInfo = useOrderedCoursesInfo(); useEffect(() => { - if (isAuthenticated && courseInfo) { + if (!isLoading && isAuthenticated && courseInfo) { const incompleteCourse = courseInfo.find( (course) => course.status !== 'completed' ); !incompleteCourse ? setIsVisible(true) : setIsVisible(false); } - }, [isAuthenticated, courseInfo]); + }, [isAuthenticated, courseInfo, isLoading]); return isAuthenticated && isVisible ? content(props.children) : null; }; export const IfLearningPathNotComplete = ( props: IfLearningPathCompleteProps ) => { - const { isAuthenticated } = useAuth0(); - const [isVisible, setIsVisible] = useState(true); + const { isAuthenticated, isLoading } = useAuth0(); + const [isVisible, setIsVisible] = useState(false); const courseInfo = useOrderedCoursesInfo(); useEffect(() => { - if (isAuthenticated && courseInfo) { + if (!isLoading && isAuthenticated && courseInfo) { const incompleteCourse = courseInfo.find( (course) => course.status !== 'completed' ); !incompleteCourse ? setIsVisible(false) : setIsVisible(true); } - }, [isAuthenticated, courseInfo]); + }, [isAuthenticated, courseInfo, isLoading]); return isAuthenticated && isVisible ? content(props.children) : null; }; diff --git a/packages/gatsby-theme-learning/src/components/if-user-logged.tsx b/packages/gatsby-theme-learning/src/components/if-user-logged.tsx index 0ec99927a2..25fdf9b299 100644 --- a/packages/gatsby-theme-learning/src/components/if-user-logged.tsx +++ b/packages/gatsby-theme-learning/src/components/if-user-logged.tsx @@ -32,6 +32,8 @@ export const IfUserLoggedOut = (props: IsLoggedOutProps) => { const { isAuthenticated, isLoading } = useAuth0(); if (isLoading && props.assumeTrue) { return content(props.children); + } else if (isLoading) { + return null; } else { return !isAuthenticated ? content(props.children) : null; } diff --git a/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts b/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts index ad43e9971d..b4e0e3c5a7 100644 --- a/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts +++ b/packages/gatsby-theme-learning/src/hooks/use-course-pages.ts @@ -3,6 +3,7 @@ import { useFetchCourses, getCourseStatusByCourseId, } from './use-course-status'; +import { useEffect, useState } from 'react'; type CoursePageNode = { slug: string; @@ -101,12 +102,15 @@ export type OrderedCoursesInfo = { /** * Returns an array of objects matching the course order defined in the navigation. - * Each object has 2 properties: courseId and pages (the list of topics pages with the same order as - * defined in navigation.yaml) + * Each object has 3 properties: courseId, pages (the list of topics pages with the same order as + * defined in navigation.yaml) and status */ -export const useOrderedCoursesInfo = (): OrderedCoursesInfo[] => { +export const useOrderedCoursesInfo = (): OrderedCoursesInfo[] | undefined => { const coursePageData = useCoursePages(); - const { data } = useFetchCourses(); + const { data, isLoading } = useFetchCourses(); + const [orderedCoursesInfo, setOrderedCourseInfo] = useState< + OrderedCoursesInfo[] | undefined + >(); courseMapToPages(coursePageData); const navigationData = coursePageData.allNavigationYaml; const coursesOnlyNavData = navigationData.nodes.filter((navElement) => { @@ -114,16 +118,24 @@ export const useOrderedCoursesInfo = (): OrderedCoursesInfo[] => { coursePageMap.has(navElementPage.path) ); }); - const orderedCorusesInfo = coursesOnlyNavData.map((navData) => { - const firstPageSlug = navData.pages[0].path; - const courseId = coursePageMap.get(firstPageSlug)?.courseId || 0; - return { - courseId, - pages: navData.pages, - status: data?.result.enrolledCourses - ? getCourseStatusByCourseId(data.result.enrolledCourses, courseId) - : '', - }; - }); - return orderedCorusesInfo; + useEffect(() => { + if (!isLoading && data) { + setOrderedCourseInfo( + coursesOnlyNavData.map((navData) => { + const firstPageSlug = navData.pages[0].path; + const courseId = coursePageMap.get(firstPageSlug)?.courseId || 0; + return { + courseId, + pages: navData.pages, + status: getCourseStatusByCourseId( + data.result.enrolledCourses, + courseId + ), + }; + }) + ); + } + }, [data, isLoading, coursesOnlyNavData]); + + return orderedCoursesInfo; };