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;