Skip to content

Commit 3a35c46

Browse files
feat: introduce courseNavigationBar
1 parent 8a42bd5 commit 3a35c46

File tree

7 files changed

+161
-8
lines changed

7 files changed

+161
-8
lines changed

shell/header/Header.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ export default function Header() {
66
const intl = useIntl();
77

88
return (
9-
<header className="border-bottom py-2">
10-
<nav className="py-2">
11-
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
12-
<Slot id="org.openedx.frontend.slot.header.desktop.v1" />
13-
<Slot id="org.openedx.frontend.slot.header.mobile.v1" />
14-
</nav>
15-
</header>
9+
<>
10+
<header className="border-bottom py-2">
11+
<nav className="py-2">
12+
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
13+
<Slot id="org.openedx.frontend.slot.header.desktop.v1" />
14+
<Slot id="org.openedx.frontend.slot.header.mobile.v1" />
15+
</nav>
16+
</header>
17+
<Slot id="org.openedx.frontend.slot.header.courseNavigationBar.v1" />
18+
</>
1619
);
1720
}

shell/header/app.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import MobileLayout from './mobile/MobileLayout';
1212
import MobileNavLinks from './mobile/MobileNavLinks';
1313

1414
import messages from '../Shell.messages';
15+
import CourseTabsNavigation from './course-navigation-bar/CourseTabsNavigation';
16+
import { activeRolesForCourseNavigationBar } from './course-navigation-bar/constants';
1517

1618
const config: App = {
1719
appId: 'org.openedx.frontend.app.header',
1820
slots: [
19-
2021
// Layouts
2122
{
2223
slotId: 'org.openedx.frontend.slot.header.desktop.v1',
@@ -136,6 +137,15 @@ const config: App = {
136137
authenticated: false,
137138
}
138139
},
140+
{
141+
slotId: 'org.openedx.frontend.slot.header.courseNavigationBar.v1',
142+
id: 'org.openedx.frontend.widget.header.courseTabsNavigation.v1',
143+
op: WidgetOperationTypes.APPEND,
144+
component: CourseTabsNavigation,
145+
condition: {
146+
active: activeRolesForCourseNavigationBar,
147+
}
148+
}
139149
]
140150
};
141151

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { useIntl } from '../../../runtime';
3+
import classNames from 'classnames';
4+
import { getCourseHomeCourseMetadata } from './data/service';
5+
import { Tab, Tabs } from '@openedx/paragon';
6+
import messages from './messages';
7+
import { useNavigate, useLocation } from 'react-router-dom';
8+
import './course-tabs-navigation.scss';
9+
10+
interface CourseMetaData {
11+
tabs: {
12+
title: string,
13+
slug: string,
14+
url: string,
15+
}[],
16+
isMasquerading: boolean,
17+
}
18+
19+
const CourseTabsNavigation = () => {
20+
const location = useLocation();
21+
const intl = useIntl();
22+
const navigate = useNavigate();
23+
24+
const extractCourseId = (pathname: string): string => {
25+
const courseRegex = /\/courses?\/([^/]+)/;
26+
const courseMatch = courseRegex.exec(pathname);
27+
return courseMatch ? courseMatch[1] : '';
28+
};
29+
30+
const courseId = extractCourseId(location.pathname);
31+
32+
const { data } = useQuery({
33+
queryKey: ['org.openedx.frontend.app.header.course-meta', courseId],
34+
queryFn: () => getCourseHomeCourseMetadata(courseId),
35+
retry: 2,
36+
});
37+
38+
if (!courseId) {
39+
return null;
40+
}
41+
42+
const { tabs = [] }: CourseMetaData = data ?? {};
43+
44+
const handleSelectedTab = (eventKey) => {
45+
const selectedUrl = tabs.find(tab => tab.slug === eventKey)?.url ?? '/';
46+
47+
try {
48+
if (selectedUrl.startsWith('http://') || selectedUrl.startsWith('https://')) {
49+
const url = new URL(selectedUrl);
50+
if (url.origin === window.location.origin) {
51+
navigate(url.pathname + url.search + url.hash);
52+
} else {
53+
window.location.href = selectedUrl;
54+
}
55+
} else {
56+
navigate(selectedUrl);
57+
}
58+
} catch (error) {
59+
navigate(selectedUrl);
60+
}
61+
};
62+
63+
return (
64+
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation')}>
65+
<div className="container-xl">
66+
<div className="nav-bar">
67+
<div className="nav-menu">
68+
<Tabs className="nav-underline-tabs" aria-label={intl.formatMessage(messages.courseMaterial)} onSelect={handleSelectedTab}>
69+
{tabs.map(({ title, slug }) => (
70+
<Tab eventKey={slug} title={title} key={slug} />
71+
))}
72+
</Tabs>
73+
</div>
74+
{/* <div className="search-toggle">
75+
<CoursewareSearchToggle />
76+
</div>
77+
</div>
78+
</div>
79+
{show && <CoursewareSearch />} */}
80+
</div>
81+
</div>
82+
</div>
83+
);
84+
};
85+
86+
export default CourseTabsNavigation;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const activeRolesForCourseNavigationBar = [
2+
'org.openedx.frontend.role.learning',
3+
'org.openedx.frontend.role.discussions',
4+
'org.openedx.frontend.role.instructor',
5+
];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.course-tabs-navigation {
2+
position: relative;
3+
border-bottom: 2px solid rgb(232.5, 229.5, 228); // var(--pgn-color-nav-tabs-base-border-base)
4+
5+
.search-toggle {
6+
flex-grow: 0;
7+
text-align: right;
8+
white-space: nowrap;
9+
margin-bottom: 10px;
10+
}
11+
12+
.nav-tabs {
13+
border-bottom: none;
14+
}
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { getSiteConfig, getAuthenticatedHttpClient, camelCaseObject } from '../../../../runtime';
2+
3+
export const getCourseMetadataApiUrl = (courseId) => `${getSiteConfig().lmsBaseUrl}/api/course_home/course_metadata/${courseId}`;
4+
5+
function normalizeCourseHomeCourseMetadata(metadata) {
6+
const data = camelCaseObject(metadata);
7+
return {
8+
...data,
9+
tabs: data.tabs.map(tab => ({
10+
slug: tab.tabId === 'courseware' ? 'outline' : tab.tabId,
11+
title: tab.title,
12+
url: tab.url,
13+
})),
14+
isMasquerading: data.originalUserIsStaff && !data.isStaff,
15+
};
16+
}
17+
18+
export async function getCourseHomeCourseMetadata(courseId) {
19+
const url = getCourseMetadataApiUrl(courseId);
20+
const { data } = await getAuthenticatedHttpClient().get(url);
21+
22+
return normalizeCourseHomeCourseMetadata(data);
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineMessages } from '../../../runtime';
2+
3+
const messages = defineMessages({
4+
courseMaterial: {
5+
id: 'org.openedx.frontend.slot.header.courseNavigationBar.tabs.label',
6+
defaultMessage: 'Course Material',
7+
description: 'The accessible label for course tabs navigation',
8+
},
9+
});
10+
11+
export default messages;

0 commit comments

Comments
 (0)