diff --git a/.changeset/eleven-dingos-smell.md b/.changeset/eleven-dingos-smell.md
new file mode 100644
index 0000000000..150ef75e93
--- /dev/null
+++ b/.changeset/eleven-dingos-smell.md
@@ -0,0 +1,12 @@
+---
+'@commercetools-website/self-learning-smoke-test': minor
+'@commercetools-docs/gatsby-theme-api-docs': minor
+'@commercetools-website/api-docs-smoke-test': minor
+'@commercetools-docs/gatsby-theme-docs': minor
+'@commercetools-website/docs-smoke-test': minor
+'@commercetools-website/documentation': minor
+'@commercetools-website/site-template': minor
+'@commercetools-docs/ui-kit': minor
+---
+
+Add the ability of tracking course content from video and text pages
diff --git a/packages/gatsby-theme-docs/gatsby-config.mjs b/packages/gatsby-theme-docs/gatsby-config.mjs
index 794338356a..a13aa5d977 100644
--- a/packages/gatsby-theme-docs/gatsby-config.mjs
+++ b/packages/gatsby-theme-docs/gatsby-config.mjs
@@ -31,8 +31,12 @@ const validateThemeOptions = (options) => {
};
const productionHostname = 'docs.commercetools.com';
+const getTagListOption = (themeOptions) =>
+ !themeOptions.transformerMdx ? [] : themeOptions.transformerMdx.tagList;
+
const config = (themeOptions = {}) => {
const pluginOptions = { ...defaultOptions, ...themeOptions };
+ const additionalTags = getTagListOption(themeOptions);
// backwards compat to single value GA configuration
if (
pluginOptions.gaTrackingId &&
@@ -318,6 +322,13 @@ const config = (themeOptions = {}) => {
],
},
},
+ {
+ resolve: '@commercetools-docs/gatsby-transformer-mdx-introspection',
+ options: {
+ ...themeOptions.transformerMdx,
+ tagList: [...additionalTags],
+ },
+ },
].filter(Boolean),
};
};
diff --git a/packages/gatsby-theme-docs/global.d.ts b/packages/gatsby-theme-docs/global.d.ts
new file mode 100644
index 0000000000..dda430ac9f
--- /dev/null
+++ b/packages/gatsby-theme-docs/global.d.ts
@@ -0,0 +1,11 @@
+import {
+ VideoProgressReachedEvent,
+ ContentPageViewedEvent,
+} from './src/modules/self-learning/hooks/use-learning-tracking';
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'selflearning:video:progressReached': VideoProgressReachedEvent;
+ 'selflearning:pageContent:viewed': ContentPageViewedEvent;
+ }
+}
diff --git a/packages/gatsby-theme-docs/package.json b/packages/gatsby-theme-docs/package.json
index f1a9990ab7..8e0aa93489 100644
--- a/packages/gatsby-theme-docs/package.json
+++ b/packages/gatsby-theme-docs/package.json
@@ -18,6 +18,7 @@
},
"dependencies": {
"@auth0/auth0-react": "2.1.0",
+ "@commercetools-docs/gatsby-transformer-mdx-introspection": "16.0.0",
"@commercetools-docs/ui-kit": "22.2.0",
"@commercetools-uikit/card": "16.3.0",
"@commercetools-uikit/checkbox-input": "16.3.0",
diff --git a/packages/gatsby-theme-docs/src/components/video-player-client-side.js b/packages/gatsby-theme-docs/src/components/video-player-client-side.js
index f0a360047a..ea9c48851a 100644
--- a/packages/gatsby-theme-docs/src/components/video-player-client-side.js
+++ b/packages/gatsby-theme-docs/src/components/video-player-client-side.js
@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import { useLazyLoad } from '@commercetools-docs/ui-kit';
import LoadingSpinner from '@commercetools-uikit/loading-spinner';
import VideoPlaceholder from './video-placeholder';
+import { EVENT_VIDEO_PROGRESS } from '../modules/self-learning/hooks/use-learning-tracking';
-const videoJsVersion = '8.2.1';
+const videoJsVersion = '8.3.0';
/**
* Preset value. Evaluate overtime if any of these needs to be a prop
@@ -52,6 +53,27 @@ const VideoPlayer = (props) => {
);
}
const player = playerRef.current;
+ let eventTriggered = false; // Flag to track event triggering
+
+ // for tracking purposes, the video player emits a selflearning:video:progressReached
+ // event when the play time reached a certain threshold (defaults to 80%)
+ player.on('timeupdate', () => {
+ const completeThreshold = props.completeAtPercent
+ ? parseInt(props.completeAtPercent) / 100
+ : 0.8; // defaults to 80% (0.8)
+ const currentTime = player.currentTime();
+ const duration = player.duration();
+ const progress = currentTime / duration;
+ if (!eventTriggered && progress >= completeThreshold) {
+ // Trigger custom event
+ const customEvent = new CustomEvent(EVENT_VIDEO_PROGRESS, {
+ detail: { progress, url: props.url },
+ });
+ const el = document.getElementById('application');
+ el.dispatchEvent(customEvent);
+ eventTriggered = true; // Set the flag to prevent further triggering
+ }
+ });
return () => {
if (player) {
player.dispose();
@@ -78,6 +100,7 @@ const VideoPlayer = (props) => {
VideoPlayer.propTypes = {
url: PropTypes.string.isRequired,
poster: PropTypes.string,
+ completeAtPercent: PropTypes.string,
};
export default VideoPlayer;
diff --git a/packages/gatsby-theme-docs/src/hooks/use-page-visibility.js b/packages/gatsby-theme-docs/src/hooks/use-page-visibility.js
new file mode 100644
index 0000000000..7ce37f4641
--- /dev/null
+++ b/packages/gatsby-theme-docs/src/hooks/use-page-visibility.js
@@ -0,0 +1,26 @@
+import { useState, useEffect } from 'react';
+
+const usePageVisibility = (isEnabled) => {
+ const [isVisible, setIsVisible] = useState(false);
+
+ useEffect(() => {
+ function handleVisibilityChange() {
+ setIsVisible(document.visibilityState === 'visible');
+ }
+
+ if (isEnabled) {
+ setIsVisible(document.visibilityState === 'visible');
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+ return () => {
+ document.removeEventListener(
+ 'visibilitychange',
+ handleVisibilityChange
+ );
+ };
+ }
+ }, [isEnabled]);
+
+ return isVisible;
+};
+
+export default usePageVisibility;
diff --git a/packages/gatsby-theme-docs/src/layouts/content.js b/packages/gatsby-theme-docs/src/layouts/content.js
index 8b3beaf28f..4d84512b45 100644
--- a/packages/gatsby-theme-docs/src/layouts/content.js
+++ b/packages/gatsby-theme-docs/src/layouts/content.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { Markdown } from '@commercetools-docs/ui-kit';
import SpacingsStack from '@commercetools-uikit/spacings-stack';
@@ -26,11 +26,36 @@ import {
CourseCompleteModal,
useCourseInfoByPageSlugs,
} from '../modules/self-learning';
+import {
+ useFetchCourseDetails,
+ getMatchingTopic,
+} from '../modules/self-learning/hooks/use-course-details';
+import {
+ useLearningTrackingHandler,
+ useContentPageTrackingDispatcher,
+} from '../modules/self-learning/hooks/use-learning-tracking';
const LayoutContent = (props) => {
const courseInfo = useCourseInfoByPageSlugs([props.pageContext.slug]);
const courseId = courseInfo[props.pageContext.slug]?.courseId;
+ const { data } = useFetchCourseDetails(courseId);
+ const isSelfLearningPage = !!courseId;
+ let selfLearningTopic;
+ if (isSelfLearningPage) {
+ selfLearningTopic = getMatchingTopic(
+ data?.result?.topics,
+ courseInfo[props.pageContext.slug]?.topicName
+ );
+ }
+ useLearningTrackingHandler(
+ courseId,
+ selfLearningTopic,
+ props.pageContext.slug
+ );
+ useContentPageTrackingDispatcher(selfLearningTopic);
+
const { ref, inView, entry } = useInView();
+ const contentRef = useRef();
const isSearchBoxInView = !Boolean(entry) || inView;
const layoutState = useLayoutState();
const siteData = useSiteData();
@@ -95,7 +120,7 @@ const LayoutContent = (props) => {
-
+
{props.children}
diff --git a/packages/gatsby-theme-docs/src/modules/self-learning/hooks/hooks.utils.ts b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/hooks.utils.ts
index a6bdb20d03..ad4950e230 100644
--- a/packages/gatsby-theme-docs/src/modules/self-learning/hooks/hooks.utils.ts
+++ b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/hooks.utils.ts
@@ -31,7 +31,8 @@ export const fetcherWithToken = async (
url: string,
getAuthToken: () => Promise,
learnApiBaseUrl: string,
- method: 'GET' | 'POST'
+ method: 'GET' | 'POST',
+ body?: object
): Promise> => {
const responseHandler = async (response: Response) => {
if (!response.ok) {
@@ -60,17 +61,23 @@ export const fetcherWithToken = async (
const accessToken = await getAuthToken();
// ...then performs fetch
+ let bodyHeaders = {};
+ if (body) {
+ bodyHeaders = { 'Content-Type': 'application/json' };
+ }
const response = await fetch(`${learnApiBaseUrl}${url}`, {
method,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
+ ...bodyHeaders,
},
// Allowing to include authorization and cookie headers because the API is hosted on a different
// subdomain even in production and as opposed to cookie domains, CORS same-origin
// policy does not consider a subdomain of the document domain same-origin.
// ("learning-api.docs.commercetools.com" is not same-origin with "docs.commercetools.com")
credentials: 'include',
+ body: body ? JSON.stringify(body) : undefined,
});
return responseHandler(response);
} catch (e) {
diff --git a/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-course-details.ts b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-course-details.ts
index b775e30bf7..a3352c6ae4 100644
--- a/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-course-details.ts
+++ b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-course-details.ts
@@ -49,17 +49,24 @@ export const useFetchCourseDetails = (
};
};
-export const getTopicStatusByPageTitle = (
+export const getMatchingTopic = (
topics: CourseTopic[] | undefined,
pageTitle: string
) => {
if (!topics || !pageTitle) {
- return 'notAvailable';
+ return;
}
- const matchingTopic = topics.find(
+ return topics.find(
(topic) =>
topic.name.trim().toLowerCase() === pageTitle.trim().toLowerCase()
);
+};
+
+export const getTopicStatusByPageTitle = (
+ topics: CourseTopic[] | undefined,
+ pageTitle: string
+) => {
+ const matchingTopic = getMatchingTopic(topics, pageTitle);
if (matchingTopic) {
return matchingTopic.completed ? 'completed' : 'inProgress';
}
diff --git a/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-learning-tracking.ts b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-learning-tracking.ts
new file mode 100644
index 0000000000..3fb5539188
--- /dev/null
+++ b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-learning-tracking.ts
@@ -0,0 +1,226 @@
+import { useEffect, useState } from 'react';
+import {
+ CourseActivities,
+ CourseTopic,
+ SupportedActivitiesType,
+} from '../external-types';
+import usePageVisibility from '../../../hooks/use-page-visibility';
+import useIsClientSide from './use-is-client-side';
+import { useTrackActivity } from './use-track-activity';
+import {
+ MapContentItem,
+ useTrackableContentByPageSlug,
+} from './use-trackable-content';
+
+export const EVENT_VIDEO_PROGRESS = 'selflearning:video:progressReached';
+export const EVENT_PAGECONTENT_VIEWED = 'selflearning:pageContent:viewed';
+
+const pageviewRegexp = /^pageview/;
+const videoRegexp = /^video/;
+
+const isPageviewActivity = (activity?: CourseActivities) => {
+ return activity?.name && pageviewRegexp.test(activity.name);
+};
+const isVideoActivity = (activity?: CourseActivities) => {
+ return activity?.name && videoRegexp.test(activity.name);
+};
+
+export interface VideoProgressReachedEvent extends Event {
+ detail: {
+ progress: number;
+ url: string;
+ };
+}
+
+export interface ContentPageViewedEvent extends Event {
+ detail: {
+ viewed: boolean;
+ };
+}
+
+type PageActivity = {
+ id: number;
+ type: string;
+ name: string;
+ status: string;
+};
+
+const matchTrackingComponent = (
+ activity: PageActivity,
+ trackableItems?: MapContentItem[]
+) => {
+ if (!trackableItems) {
+ return undefined;
+ }
+ return trackableItems.find(
+ (item) => item.component.type.toLowerCase() === activity.type.toLowerCase()
+ );
+};
+
+const getMdxAttributeValue = (
+ matchedActivity: MapContentItem,
+ attributeName: string
+) =>
+ matchedActivity.component.attributes.find(
+ (item) => item.name === attributeName
+ )?.value;
+
+export const useLearningTrackingHandler = (
+ courseId: number | undefined,
+ topic: CourseTopic | undefined,
+ pageSlug: string
+) => {
+ const [pageActivities, setPageActivities] = useState<
+ PageActivity[] | undefined
+ >(undefined);
+ const { trackActivity } = useTrackActivity(courseId);
+ const trackableItems = useTrackableContentByPageSlug(pageSlug);
+
+ useEffect(() => {
+ const ancestorElement = document.getElementById(
+ 'application'
+ ) as HTMLElement;
+
+ if (pageActivities === undefined) {
+ return;
+ }
+
+ console.log('[DBG] activities found in API', pageActivities.length);
+ pageActivities.forEach((pageActivity, index) => {
+ console.log('[DBG] processing item', index, pageActivity);
+ // 0. The activity is already completed, we don't even bother going further
+ if (pageActivity.status === 'completed') {
+ console.log('[DBG] activity complete - END ');
+ return;
+ }
+ const matchedActivity = matchTrackingComponent(
+ pageActivity,
+ trackableItems
+ );
+ console.log('[DBG] matched Activity ', matchedActivity);
+ // 1. activity exists in the API response (moodle) but is not matched in the mdx
+ // in this case we mark the activity completed straight away unless it's pageview, which is never
+ // matched by any actual mdx components but it will create a pageview listener
+ if (pageActivity.type !== 'pageview' && !matchedActivity) {
+ console.log('[DBG] unmatched - END ');
+ trackActivity(pageActivity.id, true);
+ return;
+ }
+
+ // 2. activity is pageview or has a matching element in the code. In this case we put in place
+ // the code needed to track the activity. For the time being, Quiz is totally standalone so the tracking
+ // will happen upon quiz submission. Pageview and video needs some further code (see below)
+ if (pageActivity.type === 'pageview') {
+ console.log('[DBG] pageview');
+
+ const handleContentPageViewed = (event: ContentPageViewedEvent) => {
+ const videoProgressEvent = event as ContentPageViewedEvent;
+ const viewed = videoProgressEvent.detail.viewed;
+ if (viewed) {
+ console.log('[DBG] pageview tracked - END');
+ trackActivity(pageActivity.id, true);
+ }
+ ancestorElement.removeEventListener(
+ EVENT_PAGECONTENT_VIEWED,
+ handleContentPageViewed
+ );
+ };
+
+ if (ancestorElement) {
+ ancestorElement.addEventListener(
+ EVENT_PAGECONTENT_VIEWED,
+ handleContentPageViewed
+ );
+
+ return () => {
+ ancestorElement.removeEventListener(
+ EVENT_PAGECONTENT_VIEWED,
+ handleContentPageViewed
+ );
+ };
+ }
+ }
+
+ if (pageActivity.type === 'video') {
+ console.log('[DBG] video', matchedActivity);
+
+ const handleVideoProgressReached = (
+ event: VideoProgressReachedEvent
+ ) => {
+ // let's make sure we're tracking the correct video
+ if (
+ matchedActivity &&
+ event.detail.url === getMdxAttributeValue(matchedActivity, 'url')
+ ) {
+ console.log('[DBG] video tracked - END', event.detail.url);
+ trackActivity(pageActivity.id, true);
+ }
+ };
+
+ const ancestorElement = document.getElementById(
+ 'application'
+ ) as HTMLElement;
+ if (ancestorElement) {
+ ancestorElement.addEventListener(
+ EVENT_VIDEO_PROGRESS,
+ handleVideoProgressReached
+ );
+
+ return () => {
+ ancestorElement.removeEventListener(
+ EVENT_VIDEO_PROGRESS,
+ handleVideoProgressReached
+ );
+ };
+ }
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [pageActivities, trackableItems]);
+
+ useEffect(() => {
+ console.log('[DBG]', topic?.activities);
+ const pageActs: PageActivity[] =
+ topic?.activities
+ .filter((act) => act.type === 'label' || 'quiz')
+ .map((act) => {
+ let learningType: string = act.type;
+ if (isPageviewActivity(act)) {
+ learningType = 'pageview';
+ }
+ if (isVideoActivity(act)) {
+ learningType = 'video';
+ }
+ return {
+ id: act.courseModuleId,
+ name: act.name,
+ type: learningType,
+ status: act.completionStatus,
+ };
+ }) || [];
+
+ setPageActivities(pageActs);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [topic]);
+};
+
+export const useContentPageTrackingDispatcher = (
+ topic: CourseTopic | undefined
+) => {
+ const isContentVisible = usePageVisibility(topic); // enabled only on self-learning pages
+ const { isClientSide } = useIsClientSide();
+
+ useEffect(() => {
+ if (!topic) {
+ return;
+ }
+ const pageviewActivity = topic.activities.find(isPageviewActivity);
+ if (isClientSide && pageviewActivity && isContentVisible) {
+ const customEvent = new CustomEvent(EVENT_PAGECONTENT_VIEWED, {
+ detail: { viewed: isContentVisible },
+ });
+ const el = document.getElementById('application');
+ el?.dispatchEvent(customEvent);
+ }
+ }, [topic, isContentVisible, isClientSide]);
+};
diff --git a/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-track-activity.ts b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-track-activity.ts
new file mode 100644
index 0000000000..9d32995911
--- /dev/null
+++ b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-track-activity.ts
@@ -0,0 +1,59 @@
+import useSWR, { useSWRConfig } from 'swr';
+import { useContext, useEffect, useState } from 'react';
+import ConfigContext from '../../../components/config-context';
+import { fetcherWithToken } from './hooks.utils';
+import { useAuthToken } from './use-auth-token';
+import useAuthentication from '../../sso/hooks/use-authentication';
+
+export const useTrackActivity = (courseId?: number) => {
+ const { learnApiBaseUrl } = useContext(ConfigContext);
+ const { getAuthToken } = useAuthToken();
+ const { isAuthenticated } = useAuthentication();
+ const [canTrack, setCanTrack] = useState(false);
+ const [completed, setCompleted] = useState(false);
+ const [activityId, setActivityId] = useState();
+ const { mutate } = useSWRConfig();
+
+ const apiEndpoint = `/api/courses/${courseId}/activities/${activityId}`;
+
+ useEffect(() => {
+ setCanTrack(
+ isAuthenticated && courseId !== undefined && activityId !== undefined
+ );
+ }, [courseId, activityId, isAuthenticated]);
+
+ const { data, error, isLoading } = useSWR(
+ canTrack ? apiEndpoint : null,
+ (url) =>
+ fetcherWithToken(url, getAuthToken, learnApiBaseUrl, 'POST', {
+ completed,
+ }),
+ {
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: true,
+ revalidateOnFocus: false,
+ }
+ );
+
+ useEffect(() => {
+ if (!isLoading && !error && data) {
+ // in case of a successfull response, we invalidate courses' cache
+ mutate('/api/courses');
+ mutate(`/api/courses/${courseId}`);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data, isLoading, error, courseId]);
+
+ const trackActivity = (activityId: number, completed: boolean) => {
+ setActivityId(activityId);
+ setCompleted(completed);
+ };
+
+ return {
+ data,
+ error,
+ isLoading,
+ trackActivity,
+ };
+};
diff --git a/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-trackable-content.ts b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-trackable-content.ts
new file mode 100644
index 0000000000..c09c6caa46
--- /dev/null
+++ b/packages/gatsby-theme-docs/src/modules/self-learning/hooks/use-trackable-content.ts
@@ -0,0 +1,110 @@
+import { useStaticQuery, graphql } from 'gatsby';
+
+type TrackableContentAttribute = {
+ name: string;
+ value: string;
+};
+
+type TrackableContentFileInfo = {
+ file: {
+ relativePath: string;
+ };
+ frontmatter: {
+ courseId: number;
+ topicName: string;
+ };
+};
+type TrackableContentNode = {
+ component: 'Quiz' | 'Video';
+ attributes: TrackableContentAttribute[];
+ mdx: TrackableContentFileInfo;
+};
+
+type TrackableContentQueryResult = {
+ allComponentInMdx: { nodes: TrackableContentNode[] };
+};
+
+let trackableContentMap = new Map();
+var isIndexed = false;
+
+const createTrackableContentMap = (data: TrackableContentQueryResult) => {
+ if (!isIndexed) {
+ trackableContentMap = new Map(
+ Object.entries(
+ data.allComponentInMdx.nodes.reduce(
+ (
+ result: Record,
+ item: TrackableContentNode
+ ) => {
+ const relativePath = item.mdx.file.relativePath;
+ if (!result[relativePath]) {
+ result[relativePath] = [];
+ }
+ const courseId = item.mdx.frontmatter.courseId;
+ const topicName = item.mdx.frontmatter.topicName;
+ const component = item.component;
+ result[relativePath].push({
+ component: { type: component, attributes: item.attributes },
+ courseId,
+ topicName,
+ });
+ return result;
+ },
+ {}
+ )
+ )
+ );
+
+ isIndexed = true;
+ }
+};
+
+const useTrackableContent = (): Map => {
+ const data = useStaticQuery(
+ graphql`
+ {
+ allComponentInMdx(filter: { component: { in: ["Video", "Quiz"] } }) {
+ nodes {
+ component
+ attributes {
+ name
+ value
+ }
+ mdx {
+ file: parent {
+ ... on File {
+ relativePath
+ }
+ }
+ frontmatter {
+ courseId
+ topicName
+ }
+ }
+ }
+ }
+ }
+ `
+ );
+ createTrackableContentMap(data);
+ return trackableContentMap;
+};
+
+export type MapContentItem = {
+ component: {
+ type: 'Quiz' | 'Video';
+ attributes: TrackableContentAttribute[];
+ };
+ courseId: number;
+ topicName: string;
+};
+
+const slugToRelativeFilePath = (slug: string) => {
+ const path = slug.startsWith('/') ? slug.substring(1) : slug;
+ return `${path}.mdx`;
+};
+
+export const useTrackableContentByPageSlug = (pageSlug: string) => {
+ const contentMap = useTrackableContent();
+ return contentMap.get(slugToRelativeFilePath(pageSlug));
+};
diff --git a/packages/ui-kit/src/icons/generated/Ribbon.tsx b/packages/ui-kit/src/icons/generated/Ribbon.tsx
new file mode 100644
index 0000000000..30f2cc6e32
--- /dev/null
+++ b/packages/ui-kit/src/icons/generated/Ribbon.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+const SvgRibbon = (props: SVGProps) => (
+
+);
+export default SvgRibbon;
diff --git a/packages/ui-kit/src/icons/generated/index.ts b/packages/ui-kit/src/icons/generated/index.ts
index cef6859f13..00693feb2e 100644
--- a/packages/ui-kit/src/icons/generated/index.ts
+++ b/packages/ui-kit/src/icons/generated/index.ts
@@ -10,6 +10,7 @@ export { default as MerchantCenterSvgIcon } from './MerchantCenter';
export { default as OpenSourceSmallSvgIcon } from './OpenSourceSmall';
export { default as OpenSourceSvgIcon } from './OpenSource';
export { default as ReleaseNotesSvgIcon } from './ReleaseNotes';
+export { default as RibbonSvgIcon } from './Ribbon';
export { default as RssSvgIcon } from './Rss';
export { default as SearchSvgIcon } from './Search';
export { default as SlashSvgIcon } from './Slash';
diff --git a/websites/documentation/src/content/writing/images.mdx b/websites/documentation/src/content/writing/images.mdx
index c631301768..b38aa5fc78 100644
--- a/websites/documentation/src/content/writing/images.mdx
+++ b/websites/documentation/src/content/writing/images.mdx
@@ -70,17 +70,20 @@ The component accepts the following properties:
- `url` (mandatory): The source URL to a video source to embed.
- `poster`: an optional parameter specifying a URL to an image that displays before the video begins playing. This is often a frame of the video or a custom title screen. As soon as the user hits "play" the image will go away. If not specified, the initial frame of the video will be displayed instead.
+- `completeAtPercent`: an optional parameter specifying the percentage of video the user need to watch to consider the learning activity complete. It must be a number between 0 and 100, it defaults to 80;
```md title="you write:" secondaryTheme
```
diff --git a/websites/self-learning-smoke-test/gatsby-config.mjs b/websites/self-learning-smoke-test/gatsby-config.mjs
index 061ad88202..e44e4c3369 100644
--- a/websites/self-learning-smoke-test/gatsby-config.mjs
+++ b/websites/self-learning-smoke-test/gatsby-config.mjs
@@ -22,6 +22,9 @@ const config = {
selfLearningFeatures: ['status-indicator', 'complete-profile-modal'],
hideLogin: false,
addOns: [],
+ transformerMdx: {
+ tagList: ['Quiz', 'Video'],
+ },
}),
],
};
diff --git a/websites/self-learning-smoke-test/src/content/course-3/multi-content.mdx b/websites/self-learning-smoke-test/src/content/course-3/multi-content.mdx
new file mode 100644
index 0000000000..d38e48850f
--- /dev/null
+++ b/websites/self-learning-smoke-test/src/content/course-3/multi-content.mdx
@@ -0,0 +1,36 @@
+---
+title: Multi content page
+courseId: 70
+topicName: 'Multi content page'
+---
+
+The purpose of this activity is to track page view. It contains dummy content
+
+## Introduction
+
+In recent years, the landscape of education has witnessed a remarkable transformation with the advent of e-learning. As technological advancements continue to shape the way we live and learn, online education has emerged as a powerful tool that transcends boundaries and provides unparalleled opportunities for knowledge acquisition. In this article, we will delve into the world of e-learning and explore its evolution, benefits, and the potential it holds for the future.
+
+## The Evolution of E-Learning
+
+E-learning, or electronic learning, traces its roots back to when the concept of computer-based training emerged. Initially, it involved the use of mainframe computers and punch cards to deliver educational content. However, it was not until the widespread availability of the internet in that e-learning truly took off, paving the way for a revolution in education.
+
+## Benefits of E-Learning
+
+E-learning offers numerous advantages over traditional classroom-based learning. First and foremost, it provides flexibility and convenience. Learners can access educational materials and participate in courses from anywhere and at any time, eliminating the need for physical attendance. This flexibility allows individuals to balance their personal and professional commitments while pursuing their educational goals.
+
+Furthermore, e-learning promotes self-paced learning, allowing students to progress at their own speed. This personalized approach caters to diverse learning styles and preferences, enabling individuals to grasp concepts more effectively. Moreover, e-learning platforms often provide interactive elements, such as quizzes, videos, and simulations, which enhance engagement and knowledge retention.
+
+Another significant benefit of e-learning is its cost-effectiveness. Traditional education often involves expenses related to transportation, accommodation, and printed materials. E-learning eliminates many of these costs, making education more accessible and affordable for a wider audience. Additionally, e-learning enables institutions and organizations to reach a global audience without the need for physical infrastructure, expanding educational opportunities for individuals in remote areas or underserved communities.
+
+
+## Watch this video
+
+
+
+
+# Test your knowledge
+
+
diff --git a/websites/self-learning-smoke-test/src/content/course-3/overview.mdx b/websites/self-learning-smoke-test/src/content/course-3/overview.mdx
new file mode 100644
index 0000000000..e99d57a72c
--- /dev/null
+++ b/websites/self-learning-smoke-test/src/content/course-3/overview.mdx
@@ -0,0 +1,35 @@
+---
+title: Course overview
+courseId: 70
+topicName: 'Overview'
+---
+
+The purpose of this activity is to track page view. It contains dummy content
+
+## Introduction
+
+In recent years, the landscape of education has witnessed a remarkable transformation with the advent of e-learning. As technological advancements continue to shape the way we live and learn, online education has emerged as a powerful tool that transcends boundaries and provides unparalleled opportunities for knowledge acquisition. In this article, we will delve into the world of e-learning and explore its evolution, benefits, and the potential it holds for the future.
+
+## The Evolution of E-Learning
+
+E-learning, or electronic learning, traces its roots back to when the concept of computer-based training emerged. Initially, it involved the use of mainframe computers and punch cards to deliver educational content. However, it was not until the widespread availability of the internet in that e-learning truly took off, paving the way for a revolution in education.
+
+## Benefits of E-Learning
+
+E-learning offers numerous advantages over traditional classroom-based learning. First and foremost, it provides flexibility and convenience. Learners can access educational materials and participate in courses from anywhere and at any time, eliminating the need for physical attendance. This flexibility allows individuals to balance their personal and professional commitments while pursuing their educational goals.
+
+Furthermore, e-learning promotes self-paced learning, allowing students to progress at their own speed. This personalized approach caters to diverse learning styles and preferences, enabling individuals to grasp concepts more effectively. Moreover, e-learning platforms often provide interactive elements, such as quizzes, videos, and simulations, which enhance engagement and knowledge retention.
+
+Another significant benefit of e-learning is its cost-effectiveness. Traditional education often involves expenses related to transportation, accommodation, and printed materials. E-learning eliminates many of these costs, making education more accessible and affordable for a wider audience. Additionally, e-learning enables institutions and organizations to reach a global audience without the need for physical infrastructure, expanding educational opportunities for individuals in remote areas or underserved communities.
+
+## The Future of E-Learning
+
+Looking ahead, the future of e-learning appears promising. As technology continues to advance, emerging trends such as artificial intelligence (AI), virtual reality (VR), and augmented reality (AR) are being integrated into e-learning platforms, revolutionizing the learning experience. AI-powered algorithms can personalize learning pathways based on individual strengths and weaknesses, providing targeted recommendations and adaptive feedback to enhance learning outcomes.
+
+Virtual reality and augmented reality have the potential to transform e-learning by creating immersive and interactive learning environments. Students can explore historical sites, conduct virtual science experiments, or engage in simulated real-life scenarios, significantly enhancing their understanding and retention of complex concepts.
+
+Moreover, the rise of microlearning, which involves delivering educational content in bite-sized modules, is gaining momentum. Microlearning caters to the shorter attention spans of learners in the digital age and allows for quick and efficient knowledge acquisition. It is particularly valuable for on-the-go professionals who can access short lessons or modules during their spare time.
+
+## Conclusion
+
+E-learning has come a long way since its inception and has proven to be a game-changer in the field of education. With its flexibility, cost-effectiveness, and personalized learning experiences, it has opened doors to education for millions worldwide. As technology continues to evolve, e-learning stands poised to unlock even greater potential, leveraging AI, VR, and AR to provide immersive and engaging educational experiences. The future of education is undoubtedly digital, and e-learning is at the forefront, revolutionizing the way we learn, grow, and adapt in an ever-changing world.
diff --git a/websites/self-learning-smoke-test/src/content/course-3/quiz.mdx b/websites/self-learning-smoke-test/src/content/course-3/quiz.mdx
new file mode 100644
index 0000000000..2efc7570b5
--- /dev/null
+++ b/websites/self-learning-smoke-test/src/content/course-3/quiz.mdx
@@ -0,0 +1,9 @@
+---
+title: Quiz component
+courseId: 70
+topicName: 'Quiz 1'
+---
+
+# Test your knowledge
+
+
diff --git a/websites/self-learning-smoke-test/src/content/course-3/video.mdx b/websites/self-learning-smoke-test/src/content/course-3/video.mdx
new file mode 100644
index 0000000000..cdc2142925
--- /dev/null
+++ b/websites/self-learning-smoke-test/src/content/course-3/video.mdx
@@ -0,0 +1,12 @@
+---
+title: Video component
+courseId: 70
+topicName: 'Video 1'
+---
+
+## Video
+
+
diff --git a/websites/self-learning-smoke-test/src/data/navigation.yaml b/websites/self-learning-smoke-test/src/data/navigation.yaml
index 430fd5814c..6dd239d0b2 100644
--- a/websites/self-learning-smoke-test/src/data/navigation.yaml
+++ b/websites/self-learning-smoke-test/src/data/navigation.yaml
@@ -13,3 +13,14 @@
path: /course-2/quiz
- title: Test your knowledge 2
path: /course-2/2-quiz
+
+- chapter-title: Self-learning course 3
+ pages:
+ - title: Overview
+ path: /course-3/overview
+ - title: Video
+ path: /course-3/video
+ - title: Quiz
+ path: /course-3/quiz
+ - title: Multi content page
+ path: /course-3/multi-content
diff --git a/yarn.lock b/yarn.lock
index 5c23fbed9d..c266e41af6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3544,6 +3544,7 @@ __metadata:
resolution: "@commercetools-docs/gatsby-theme-docs@workspace:packages/gatsby-theme-docs"
dependencies:
"@auth0/auth0-react": 2.1.0
+ "@commercetools-docs/gatsby-transformer-mdx-introspection": 16.0.0
"@commercetools-docs/ui-kit": 22.2.0
"@commercetools-uikit/card": 16.3.0
"@commercetools-uikit/checkbox-input": 16.3.0