diff --git a/Makefile b/Makefile
index b0ee4649..2a7a37e3 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,7 @@ clean:
build: clean
tsc --build ./tsconfig.build.json
cp ./shell/app.scss ./dist/shell/app.scss
+ cp ./shell/header/course-navigation-bar/course-tabs-navigation.scss ./dist/shell/header/course-navigation-bar/course-tabs-navigation.scss
docs-build:
${doc_command}
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..cd05e74a
--- /dev/null
+++ b/shell/header/course-navigation-bar/CourseTabsNavigation.tsx
@@ -0,0 +1,65 @@
+import { useState } from 'react';
+import { useLocation } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import { Slot, useIntl } from '../../../runtime';
+import { CourseTab, getCourseHomeCourseMetadata } from './data/service';
+import './course-tabs-navigation.scss';
+import { Nav, Navbar, Skeleton } from '@openedx/paragon';
+import messages from './messages';
+
+const extractCourseId = (pathname: string): string => {
+ const courseRegex = /\/courses?\/([^/]+)/;
+ const courseMatch = courseRegex.exec(pathname);
+ return courseMatch ? courseMatch[1] : '';
+};
+
+const CourseTabsNavigation = () => {
+ const location = useLocation();
+ const [currentTab, setCurrentTab] = useState(null);
+ const intl = useIntl();
+
+ const courseId = extractCourseId(location.pathname);
+
+ const { data = { tabs: [], isMasquerading: false }, isLoading } = useQuery({
+ queryKey: ['org.openedx.frontend.app.header.course-meta', courseId],
+ queryFn: () => getCourseHomeCourseMetadata(courseId),
+ retry: 2,
+ enabled: !!courseId,
+ });
+
+ const { tabs } = data;
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!courseId || !tabs || tabs.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+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..6e08ee84
--- /dev/null
+++ b/shell/header/course-navigation-bar/course-tabs-navigation.scss
@@ -0,0 +1,7 @@
+.course-tabs-navigation {
+ border-bottom: 2px solid 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..4d0af744
--- /dev/null
+++ b/shell/header/course-navigation-bar/data/service.ts
@@ -0,0 +1,33 @@
+import { getSiteConfig, getAuthenticatedHttpClient, camelCaseObject } from '../../../../runtime';
+
+export const getCourseMetadataApiUrl = (courseId: string) => `${getSiteConfig().lmsBaseUrl}/api/course_home/course_metadata/${courseId}`;
+
+export interface CourseTab {
+ tabId: string,
+ title: string,
+ url: string,
+}
+export interface CourseHomeCourseMetadata {
+ tabs: CourseTab[],
+ isMasquerading: boolean,
+}
+
+function normalizeCourseHomeCourseMetadata(metadata: CourseHomeCourseMetadata): CourseHomeCourseMetadata {
+ const data = camelCaseObject(metadata);
+ return {
+ ...data,
+ tabs: (data.tabs || []).map((tab: CourseTab) => ({
+ tabId: tab.tabId === 'courseware' ? 'outline' : tab.tabId,
+ title: tab.title,
+ url: tab.url,
+ })),
+ isMasquerading: data.originalUserIsStaff && !data.isStaff,
+ };
+}
+
+export async function getCourseHomeCourseMetadata(courseId: string): Promise {
+ 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..ac84b866
--- /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 Navigation Bar',
+ description: 'The accessible label for course tabs navigation',
+ },
+});
+
+export default messages;