diff --git a/shell/header/Header.tsx b/shell/header/Header.tsx
index 3ec8622f..f0d0613e 100644
--- a/shell/header/Header.tsx
+++ b/shell/header/Header.tsx
@@ -6,12 +6,15 @@ export default function Header() {
const intl = useIntl();
return (
-
+ <>
+
+
+ >
);
}
diff --git a/shell/header/app.tsx b/shell/header/app.tsx
index a0d4dbe2..d9c52138 100644
--- a/shell/header/app.tsx
+++ b/shell/header/app.tsx
@@ -12,11 +12,12 @@ import MobileLayout from './mobile/MobileLayout';
import MobileNavLinks from './mobile/MobileNavLinks';
import messages from '../Shell.messages';
+import CourseTabsNavigation from './course-navigation-bar/CourseTabsNavigation';
+import { activeRolesForCourseNavigationBar } from './course-navigation-bar/constants';
const config: App = {
appId: 'org.openedx.frontend.app.header',
slots: [
-
// Layouts
{
slotId: 'org.openedx.frontend.slot.header.desktop.v1',
@@ -136,6 +137,15 @@ const config: App = {
authenticated: false,
}
},
+ {
+ slotId: 'org.openedx.frontend.slot.header.courseNavigationBar.v1',
+ id: 'org.openedx.frontend.widget.header.courseTabsNavigation.v1',
+ op: WidgetOperationTypes.APPEND,
+ component: CourseTabsNavigation,
+ condition: {
+ active: activeRolesForCourseNavigationBar,
+ }
+ }
]
};
diff --git a/shell/header/course-navigation-bar/CourseTabsNavigation.tsx b/shell/header/course-navigation-bar/CourseTabsNavigation.tsx
new file mode 100644
index 00000000..e54ae8c5
--- /dev/null
+++ b/shell/header/course-navigation-bar/CourseTabsNavigation.tsx
@@ -0,0 +1,81 @@
+import { useQuery } from '@tanstack/react-query';
+import { Slot, useIntl } from '../../../runtime';
+import classNames from 'classnames';
+import { getCourseHomeCourseMetadata } from './data/service';
+import { Tab, Tabs } from '@openedx/paragon';
+import messages from './messages';
+import { useNavigate, useLocation } from 'react-router-dom';
+import './course-tabs-navigation.scss';
+
+interface CourseMetaData {
+ tabs: {
+ title: string,
+ slug: string,
+ url: string,
+ }[],
+ isMasquerading: boolean,
+}
+
+const CourseTabsNavigation = () => {
+ const location = useLocation();
+ const intl = useIntl();
+ const navigate = useNavigate();
+
+ const extractCourseId = (pathname: string): string => {
+ const courseRegex = /\/courses?\/([^/]+)/;
+ const courseMatch = courseRegex.exec(pathname);
+ return courseMatch ? courseMatch[1] : '';
+ };
+
+ const courseId = extractCourseId(location.pathname);
+
+ const { data } = useQuery({
+ queryKey: ['org.openedx.frontend.app.header.course-meta', courseId],
+ queryFn: () => getCourseHomeCourseMetadata(courseId),
+ retry: 2,
+ });
+
+ if (!courseId) {
+ return null;
+ }
+
+ const { tabs = [] }: CourseMetaData = data ?? {};
+
+ const handleSelectedTab = (eventKey) => {
+ const selectedUrl = tabs.find(tab => tab.slug === eventKey)?.url ?? '/';
+
+ try {
+ if (selectedUrl.startsWith('http://') || selectedUrl.startsWith('https://')) {
+ const url = new URL(selectedUrl);
+ if (url.origin === window.location.origin) {
+ navigate(url.pathname + url.search + url.hash);
+ } else {
+ window.location.href = selectedUrl;
+ }
+ } else {
+ navigate(selectedUrl);
+ }
+ } catch (error) {
+ navigate(selectedUrl);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {tabs.map(({ title, slug }) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+export default CourseTabsNavigation;
diff --git a/shell/header/course-navigation-bar/constants.ts b/shell/header/course-navigation-bar/constants.ts
new file mode 100644
index 00000000..1c242a9a
--- /dev/null
+++ b/shell/header/course-navigation-bar/constants.ts
@@ -0,0 +1,5 @@
+export const activeRolesForCourseNavigationBar = [
+ 'org.openedx.frontend.role.learning',
+ 'org.openedx.frontend.role.discussions',
+ 'org.openedx.frontend.role.instructor',
+];
diff --git a/shell/header/course-navigation-bar/course-tabs-navigation.scss b/shell/header/course-navigation-bar/course-tabs-navigation.scss
new file mode 100644
index 00000000..10226bcd
--- /dev/null
+++ b/shell/header/course-navigation-bar/course-tabs-navigation.scss
@@ -0,0 +1,7 @@
+.course-tabs-navigation {
+ border-bottom: 2px solid rgb(232.5, 229.5, 228); // var(--pgn-color-nav-tabs-base-border-base)
+
+ .nav-tabs {
+ border-bottom: none;
+ }
+}
diff --git a/shell/header/course-navigation-bar/data/service.ts b/shell/header/course-navigation-bar/data/service.ts
new file mode 100644
index 00000000..bc0d1a51
--- /dev/null
+++ b/shell/header/course-navigation-bar/data/service.ts
@@ -0,0 +1,23 @@
+import { getSiteConfig, getAuthenticatedHttpClient, camelCaseObject } from '../../../../runtime';
+
+export const getCourseMetadataApiUrl = (courseId) => `${getSiteConfig().lmsBaseUrl}/api/course_home/course_metadata/${courseId}`;
+
+function normalizeCourseHomeCourseMetadata(metadata) {
+ const data = camelCaseObject(metadata);
+ return {
+ ...data,
+ tabs: data.tabs.map(tab => ({
+ slug: tab.tabId === 'courseware' ? 'outline' : tab.tabId,
+ title: tab.title,
+ url: tab.url,
+ })),
+ isMasquerading: data.originalUserIsStaff && !data.isStaff,
+ };
+}
+
+export async function getCourseHomeCourseMetadata(courseId) {
+ const url = getCourseMetadataApiUrl(courseId);
+ const { data } = await getAuthenticatedHttpClient().get(url);
+
+ return normalizeCourseHomeCourseMetadata(data);
+}
diff --git a/shell/header/course-navigation-bar/messages.ts b/shell/header/course-navigation-bar/messages.ts
new file mode 100644
index 00000000..06dc1c47
--- /dev/null
+++ b/shell/header/course-navigation-bar/messages.ts
@@ -0,0 +1,11 @@
+import { defineMessages } from '../../../runtime';
+
+const messages = defineMessages({
+ courseMaterial: {
+ id: 'org.openedx.frontend.slot.header.courseNavigationBar.tabs.label',
+ defaultMessage: 'Course Material',
+ description: 'The accessible label for course tabs navigation',
+ },
+});
+
+export default messages;