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;