From afade3ffe87416a01347ba1890ab7913bb23fb2b Mon Sep 17 00:00:00 2001 From: Jonas Carlsen Date: Tue, 7 Jan 2025 14:26:16 +0100 Subject: [PATCH] feat: add learningpath preview --- src/App.tsx | 6 + src/components/Learningpath/Learningpath.tsx | 313 ++++++++++-------- .../Learningpath/LearningpathMenu.tsx | 54 ++- .../Learningpath/learningpathUtils.ts | 9 + .../LearningpathPage/LearningpathPage.tsx | 21 +- .../Learningpath/PreviewLearningpathPage.tsx | 125 +++++++ .../PlainLearningpathContainer.tsx | 17 +- src/graphqlTypes.ts | 14 + src/messages/messagesEN.ts | 5 + src/messages/messagesNB.ts | 5 + src/messages/messagesNN.ts | 5 + src/messages/messagesSE.ts | 5 + src/routeHelpers.tsx | 5 +- 13 files changed, 421 insertions(+), 163 deletions(-) create mode 100644 src/components/Learningpath/learningpathUtils.ts create mode 100644 src/containers/MyNdla/Learningpath/PreviewLearningpathPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 85aacba48..fe1e22c4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,6 +42,7 @@ import { EditLearningpathTitlePage } from "./containers/MyNdla/Learningpath/Edit import { LearningpathCheck } from "./containers/MyNdla/Learningpath/LearningpathCheck"; import LearningpathPage from "./containers/MyNdla/Learningpath/LearningpathPage"; import { NewLearningpathPage } from "./containers/MyNdla/Learningpath/NewLearningpathPage"; +import { PreviewLearningpathPage } from "./containers/MyNdla/Learningpath/PreviewLearningpathPage"; import { SaveLearningpathPage } from "./containers/MyNdla/Learningpath/SaveLearningpathPage"; import MyNdlaLayout from "./containers/MyNdla/MyNdlaLayout"; import MyNdlaPage from "./containers/MyNdla/MyNdlaPage"; @@ -224,6 +225,11 @@ const AppRoutes = ({ base }: AppProps) => { path=":learningpathId/save" element={} />} /> + + + } />} /> + } />} /> + } />} /> )} diff --git a/src/components/Learningpath/Learningpath.tsx b/src/components/Learningpath/Learningpath.tsx index 10aef57ee..031d13a07 100644 --- a/src/components/Learningpath/Learningpath.tsx +++ b/src/components/Learningpath/Learningpath.tsx @@ -29,15 +29,16 @@ import { contains } from "@ndla/util"; import LastLearningpathStepInfo from "./LastLearningpathStepInfo"; import LearningpathEmbed, { EmbedPageContent } from "./LearningpathEmbed"; import LearningpathMenu from "./LearningpathMenu"; +import type { LearningpathContext } from "./learningpathUtils"; import { GQLLearningpath_LearningpathFragment, GQLLearningpath_LearningpathStepFragment, GQLLearningpathPage_NodeFragment, } from "../../graphqlTypes"; import { Breadcrumb as BreadcrumbType } from "../../interfaces"; -import { toLearningPath } from "../../routeHelpers"; +import { routes, toLearningPath } from "../../routeHelpers"; import FavoriteButton from "../Article/FavoritesButton"; -import { PageContainer, PageLayout } from "../Layout/PageContainer"; +import { PageContainer } from "../Layout/PageContainer"; import AddResourceToFolderModal from "../MyNdla/AddResourceToFolderModal"; interface Props { @@ -47,6 +48,7 @@ interface Props { skipToContentId?: string; breadcrumbItems: BreadcrumbType[]; resourcePath?: string; + context?: LearningpathContext; } const StyledPageContainer = styled(PageContainer, { @@ -55,23 +57,40 @@ const StyledPageContainer = styled(PageContainer, { background: "background.subtle", gap: "large", }, + variants: { + rounded: { + true: { + borderRadius: "xsmall", + }, + }, + }, }); const ContentWrapper = styled("div", { base: { display: "grid", - gridTemplateRows: "auto auto 1fr", - gridAutoFlow: "column dense", - gridTemplateColumns: "minmax(200px, 1fr) minmax(300px, 3fr)", - gridTemplateAreas: ` + gap: "medium", + }, + variants: { + context: { + preview: { + display: "flex", + flexDirection: "column", + }, + default: { + gridTemplateRows: "auto auto 1fr", + gridAutoFlow: "column dense", + gridTemplateColumns: "minmax(200px, 1fr) minmax(300px, 3fr)", + gridTemplateAreas: ` "meta content" "steps content" ". content" `, - gap: "medium", - desktopDown: { - display: "flex", - flexDirection: "column", + desktopDown: { + display: "flex", + flexDirection: "column", + }, + }, }, }, }); @@ -115,8 +134,15 @@ const StyledAccordionRoot = styled(AccordionRoot, { zIndex: "sticky", top: "var(--masthead-height)", marginInline: "small", - desktop: { - display: "none", + }, + variants: { + context: { + default: { + desktop: { + display: "none", + }, + }, + preview: {}, }, }, }); @@ -161,6 +187,7 @@ const Learningpath = ({ resource, skipToContentId, breadcrumbItems, + context = "default", }: Props) => { const { t, i18n } = useTranslation(); const [accordionValue, setAccordionValue] = useState(); @@ -170,139 +197,151 @@ const Learningpath = ({ const nextStep = learningpath.learningsteps[learningpathStep.seqNo + 1]; const menu = useMemo( - () => , - [learningpath, learningpathStep, resourcePath], + () => ( + + ), + [context, learningpath, learningpathStep, resourcePath], ); const parents = resource?.context?.parents || []; const root = parents[0]; return ( - -
- - {!!breadcrumbItems.length && ( - - - - )} - - - - - {!!resourcePath && ( - - - + + {!!breadcrumbItems.length && ( + + + + )} + + {context === "default" && ( + + + + {!!resourcePath && ( + + + + )} + + + {`${t("learningPath.youAreInALearningPath")}:`} +
+ {learningpath.title} +
+
+ )} + setAccordionValue(details.value)} + variant="bordered" + context={context} + multiple + onBlur={(e) => { + // automatically close the accordion when focus leaves the accordion on mobile. + if (!contains(accordionRef.current, e.relatedTarget ?? e.target)) { + setAccordionValue([]); + } + }} + > + + +

+ + {t("learningpathPage.accordionTitle")} + + + + +

+
+ {menu} +
+
+ {context === "default" && {menu}} + + {(!!learningpathStep.description || !!learningpathStep.showTitle) && ( + + + {!!learningpathStep.showTitle && ( + + + {learningpathStep.title} + + + )} -
- - {`${t("learningPath.youAreInALearningPath")}:`} -
- {learningpath.title} -
-
- setAccordionValue(details.value)} - variant="bordered" - multiple - onBlur={(e) => { - // automatically close the accordion when focus leaves the accordion on mobile. - if (!contains(accordionRef.current, e.relatedTarget ?? e.target)) { - setAccordionValue([]); + + {!!learningpathStep.description &&
{transform(learningpathStep.description, {})}
} +
+ + + )} + + + + + {previousStep ? ( + - - -

- - {t("learningpathPage.accordionTitle")} - - - - -

-
- {menu} -
-
- {menu} - - {(!!learningpathStep.description || !!learningpathStep.showTitle) && ( - - - {!!learningpathStep.showTitle && ( - - - {learningpathStep.title} - - - - )} - - {!!learningpathStep.description && ( -
{transform(learningpathStep.description, {})}
- )} -
-
-
- )} - - - - - {previousStep ? ( - - - {previousStep.title} - - ) : ( -
- )} - {nextStep ? ( - - {nextStep.title} - - - ) : ( -
- )} - - - - -
-
+ + {previousStep.title} + + ) : ( +
+ )} + {nextStep ? ( + + {nextStep.title} + + + ) : ( +
+ )} + + + + ); }; diff --git a/src/components/Learningpath/LearningpathMenu.tsx b/src/components/Learningpath/LearningpathMenu.tsx index 0df07dc6f..7ba04b607 100644 --- a/src/components/Learningpath/LearningpathMenu.tsx +++ b/src/components/Learningpath/LearningpathMenu.tsx @@ -13,16 +13,18 @@ import { CheckLine } from "@ndla/icons"; import { SafeLink } from "@ndla/safelink"; import { styled } from "@ndla/styled-system/jsx"; import { ArticleByline } from "@ndla/ui"; +import { LearningpathContext } from "./learningpathUtils"; import { GQLLearningpathMenu_LearningpathFragment, GQLLearningpathMenu_LearningpathStepFragment, } from "../../graphqlTypes"; -import { toLearningPath } from "../../routeHelpers"; +import { routes, toLearningPath } from "../../routeHelpers"; interface Props { resourcePath: string | undefined; learningpath: GQLLearningpathMenu_LearningpathFragment; currentStep: GQLLearningpathMenu_LearningpathStepFragment; + context?: LearningpathContext; } const StepperList = styled("ol", { @@ -89,8 +91,15 @@ const StepIndicatorWrapper = styled("div", { display: "flex", alignItems: "center", background: "background.default", - desktop: { - background: "background.subtle", + }, + variants: { + context: { + default: { + desktop: { + background: "background.subtle", + }, + }, + preview: {}, }, }, }); @@ -110,13 +119,20 @@ const StepIndicator = styled("div", { transitionProperty: "background", transitionDuration: "fast", transitionTimingFunction: "default", - desktop: { - background: "background.subtle", - }, "&[data-completed='true']": { background: "surface.brand.3.moderate", }, }, + variants: { + context: { + default: { + desktop: { + background: "background.subtle", + }, + }, + preview: {}, + }, + }, }); const ListItem = styled("li", { @@ -130,17 +146,24 @@ const ListItem = styled("li", { bottom: "0", width: "100%", height: "50%", + }, + }, + }, + variants: { + context: { + default: { desktop: { background: "background.subtle", }, }, + preview: {}, }, }, }); const LEARNING_PATHS_STORAGE_KEY = "LEARNING_PATHS_COOKIES_KEY"; -const LearningpathMenu = ({ resourcePath, learningpath, currentStep }: Props) => { +const LearningpathMenu = ({ resourcePath, learningpath, currentStep, context }: Props) => { const [viewedSteps, setViewedSteps] = useState>({}); const { t } = useTranslation(); @@ -172,15 +195,24 @@ const LearningpathMenu = ({ resourcePath, learningpath, currentStep }: Props) => {learningpath.learningsteps.map((step, index) => ( - + - - + + {viewedSteps[step.id] && index !== currentStep.seqNo ? : index + 1} diff --git a/src/components/Learningpath/learningpathUtils.ts b/src/components/Learningpath/learningpathUtils.ts new file mode 100644 index 000000000..e39700b71 --- /dev/null +++ b/src/components/Learningpath/learningpathUtils.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2025-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export type LearningpathContext = "default" | "preview"; diff --git a/src/containers/LearningpathPage/LearningpathPage.tsx b/src/containers/LearningpathPage/LearningpathPage.tsx index ea8b75a43..b492fe831 100644 --- a/src/containers/LearningpathPage/LearningpathPage.tsx +++ b/src/containers/LearningpathPage/LearningpathPage.tsx @@ -12,6 +12,7 @@ import { gql } from "@apollo/client"; import { useTracker } from "@ndla/tracker"; import { AuthContext } from "../../components/AuthenticationContext"; import { DefaultErrorMessagePage } from "../../components/DefaultErrorMessage"; +import { PageLayout } from "../../components/Layout/PageContainer"; import Learningpath from "../../components/Learningpath"; import SocialMediaMetadata from "../../components/SocialMediaMetadata"; import { @@ -94,14 +95,18 @@ const LearningpathPage = ({ data, skipToContentId, stepId, loading }: Props) => description={learningpath.description} imageUrl={learningpath.coverphoto?.url} /> - + +
+ +
+
); }; diff --git a/src/containers/MyNdla/Learningpath/PreviewLearningpathPage.tsx b/src/containers/MyNdla/Learningpath/PreviewLearningpathPage.tsx new file mode 100644 index 000000000..dd220ae81 --- /dev/null +++ b/src/containers/MyNdla/Learningpath/PreviewLearningpathPage.tsx @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2025-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useContext, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import { gql, useQuery } from "@apollo/client"; +import { Heading, Text } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; +import { useTracker } from "@ndla/tracker"; +import { AuthContext } from "../../../components/AuthenticationContext"; +import { DefaultErrorMessagePage } from "../../../components/DefaultErrorMessage"; +import Learningpath from "../../../components/Learningpath"; +import { PageSpinner } from "../../../components/PageSpinner"; +import { SKIP_TO_CONTENT_ID } from "../../../constants"; +import { NotFoundPage } from "../../NotFoundPage/NotFoundPage"; +import MyNdlaBreadcrumb from "../components/MyNdlaBreadcrumb"; +import MyNdlaPageWrapper from "../components/MyNdlaPageWrapper"; +import { LearningpathStepper } from "./components/LearningpathStepper"; +import { GQLPreviewLearningpathQuery, GQLPreviewLearningpathQueryVariables } from "../../../graphqlTypes"; +import { getAllDimensions } from "../../../util/trackingUtil"; + +const TextWrapper = styled("div", { + base: { + display: "flex", + flexDirection: "column", + gap: "xsmall", + }, +}); + +const previewLearningpathQuery = gql` + query previewLearningpath($pathId: String!, $transformArgs: TransformedArticleContentInput) { + learningpath(pathId: $pathId) { + id + ...Learningpath_Learningpath + learningsteps { + ...Learningpath_LearningpathStep + } + } + } + ${Learningpath.fragments.learningpath} + ${Learningpath.fragments.learningpathStep} +`; + +export const PreviewLearningpathPage = () => { + const { t } = useTranslation(); + const { learningpathId, stepId } = useParams(); + const { user } = useContext(AuthContext); + const { trackPageView } = useTracker(); + + const learningpathQuery = useQuery( + previewLearningpathQuery, + { + variables: { pathId: learningpathId ?? "" }, + skip: !learningpathId, + }, + ); + + useEffect(() => { + if (!learningpathQuery.data?.learningpath?.title) return; + trackPageView({ + title: t("htmlTitles.learningpathSavePage", { name: learningpathQuery.data.learningpath.title }), + dimensions: getAllDimensions({ user }), + }); + }, [learningpathQuery.data?.learningpath?.title, t, trackPageView, user]); + + if (learningpathQuery.loading) { + return ; + } + + if (!learningpathQuery.data?.learningpath || (stepId && isNaN(Number(stepId)))) { + return ; + } + + const learningpath = learningpathQuery.data.learningpath; + const numericStepId = stepId ? Number(stepId) : undefined; + + // If stepId is provided, find it. If not, fall back to the first step. + const learningpathStep = numericStepId + ? learningpath.learningsteps.find((step) => step.id === numericStepId) + : learningpath.learningsteps[0]; + + // stepId is defined, but not found within the learningpath + if (numericStepId && numericStepId > 0 && !learningpathStep) { + return ; + } + + return ( + + {t("htmlTitles.learningpathPreviewPage", { name: learningpath.title })} + + + {learningpath.title} + + + + +

{t("myNdla.learningpath.previewLearningpath.pageHeading")}

+
+ {t("myNdla.learningpath.previewLearningpath.pageDescription")} +
+ {learningpathStep ? ( + + ) : ( + {t("myNdla.learningpath.previewLearningpath.noSteps")} + )} +
+ ); +}; diff --git a/src/containers/PlainLearningpathPage/PlainLearningpathContainer.tsx b/src/containers/PlainLearningpathPage/PlainLearningpathContainer.tsx index a2dffa1c3..b1343fc04 100644 --- a/src/containers/PlainLearningpathPage/PlainLearningpathContainer.tsx +++ b/src/containers/PlainLearningpathPage/PlainLearningpathContainer.tsx @@ -13,6 +13,7 @@ import { gql } from "@apollo/client"; import { useTracker } from "@ndla/tracker"; import { AuthContext } from "../../components/AuthenticationContext"; import { DefaultErrorMessagePage } from "../../components/DefaultErrorMessage"; +import { PageLayout } from "../../components/Layout/PageContainer"; import Learningpath from "../../components/Learningpath"; import SocialMediaMetadata from "../../components/SocialMediaMetadata"; import { GQLPlainLearningpathContainer_LearningpathFragment } from "../../graphqlTypes"; @@ -69,12 +70,16 @@ const PlainLearningpathContainer = ({ learningpath, skipToContentId, stepId }: P description={learningpath.description} imageUrl={learningpath.coverphoto?.url} /> - + +
+ +
+
); }; diff --git a/src/graphqlTypes.ts b/src/graphqlTypes.ts index a93448a3e..20e04bb18 100644 --- a/src/graphqlTypes.ts +++ b/src/graphqlTypes.ts @@ -3251,6 +3251,20 @@ export type GQLMovedResourcePage_NodeFragment = { resourceTypes?: Array<{ __typename?: "ResourceType"; id: string; name: string }>; }; +export type GQLPreviewLearningpathQueryVariables = Exact<{ + pathId: Scalars["String"]["input"]; + transformArgs?: InputMaybe; +}>; + +export type GQLPreviewLearningpathQuery = { + __typename?: "Query"; + learningpath?: { + __typename?: "Learningpath"; + id: number; + learningsteps: Array<{ __typename?: "LearningpathStep" } & GQLLearningpath_LearningpathStepFragment>; + } & GQLLearningpath_LearningpathFragment; +}; + export type GQLLearningpathStepEmbedUrlFragment = { __typename?: "LearningpathStepEmbedUrl"; url: string; diff --git a/src/messages/messagesEN.ts b/src/messages/messagesEN.ts index c296649c2..bea723752 100644 --- a/src/messages/messagesEN.ts +++ b/src/messages/messagesEN.ts @@ -236,6 +236,11 @@ const messages = { pageDescription: "Save and share your learning path. When you share the learning path, you create a link that can be shared with students or teachers.", }, + previewLearningpath: { + pageHeading: "Preview", + pageDescription: "Preview the learning path you have created.", + noSteps: "You haven't added any steps to the learning path yet.", + }, }, }, ndlaFilm: { diff --git a/src/messages/messagesNB.ts b/src/messages/messagesNB.ts index a18fa4a62..630b4483e 100644 --- a/src/messages/messagesNB.ts +++ b/src/messages/messagesNB.ts @@ -237,6 +237,11 @@ const messages = { pageDescription: "Lagre og del læringsstien din. Når du deler oppretter du en delbar lenke som du kan sende til elever eller lærere.", }, + previewLearningpath: { + pageHeading: "Se igjennom", + pageDescription: "Se igjennom læringsstien du har laget.", + noSteps: "Du har ikke lagt til noen steg i læringsstien ennå.", + }, }, }, ndlaFilm: { diff --git a/src/messages/messagesNN.ts b/src/messages/messagesNN.ts index 0885b2496..162eda839 100644 --- a/src/messages/messagesNN.ts +++ b/src/messages/messagesNN.ts @@ -237,6 +237,11 @@ const messages = { pageDescription: "Lagre og del læringstien din. Når du deler opprettar du ein delbar lenke som du kan sende til elevar eller lærarar.", }, + previewLearningpath: { + pageHeading: "Sjå igjennom", + pageDescription: "Sjå igjennom læringsstien du har laga.", + noSteps: "Du har ikkje lagt til nokon steg i læringsstien enno.", + }, }, }, ndlaFilm: { diff --git a/src/messages/messagesSE.ts b/src/messages/messagesSE.ts index ddc727db6..c9bbe4ad5 100644 --- a/src/messages/messagesSE.ts +++ b/src/messages/messagesSE.ts @@ -234,6 +234,11 @@ const messages = { pageDescription: "Lagre og del læringsstien din. Når du deler oppretter du en delbar lenke som du kan sende til elever eller lærere.", }, + previewLearningpath: { + pageHeading: "Preview", + pageDescription: "Preview the learning path you have created.", + noSteps: "You haven't added any steps to the learning path yet.", + }, }, }, ndlaFilm: { diff --git a/src/routeHelpers.tsx b/src/routeHelpers.tsx index 795d4d48c..83b2e3e1b 100644 --- a/src/routeHelpers.tsx +++ b/src/routeHelpers.tsx @@ -160,7 +160,10 @@ export const routes = { learningpathNew: "/minndla/learningpaths/new", learningpathEditTitle: (learningpathId: number) => `/minndla/learningpaths/${learningpathId}/edit/title`, learningpathEditSteps: (learningpathId: number) => `/minndla/learningpaths/${learningpathId}/edit/steps`, - learningpathPreview: (learningpathId: number) => `/minndla/learningpaths/${learningpathId}/preview`, + learningpathPreview: (learningpathId: number, stepId?: number) => { + const path = `/minndla/learningpaths/${learningpathId}/preview`; + return stepId ? `${path}/${stepId}` : path; + }, learningpathSave: (learningpathId: number) => `/minndla/learningpaths/${learningpathId}/save`, }, };