Skip to content

Commit a79f6cf

Browse files
feat: introduce courseNavigationBar
1 parent 0c48661 commit a79f6cf

File tree

6 files changed

+149
-7
lines changed

6 files changed

+149
-7
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ 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';
1516

1617
const config: App = {
1718
appId: 'org.openedx.frontend.app.header',
@@ -136,6 +137,12 @@ 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+
}
139146
]
140147
};
141148

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
22+
const extractCourseId = (pathname: string): string => {
23+
const courseRegex = /\/courses?\/([^/]+)/;
24+
const courseMatch = courseRegex.exec(pathname);
25+
return courseMatch ? courseMatch[1] : '';
26+
};
27+
28+
const courseId = extractCourseId(location.pathname);
29+
30+
const { data } = useQuery({
31+
queryKey: ['org.openedx.frontend.app.header.course-meta', courseId],
32+
queryFn: () => getCourseHomeCourseMetadata(courseId),
33+
retry: 2,
34+
});
35+
36+
const { tabs = [] }: CourseMetaData = data ?? {};
37+
38+
const intl = useIntl();
39+
const navigate = useNavigate();
40+
41+
const handleSelectedTab = (eventKey) => {
42+
const selectedUrl = tabs.find(tab => tab.slug === eventKey)?.url ?? '/';
43+
44+
try {
45+
if (selectedUrl.startsWith('http://') || selectedUrl.startsWith('https://')) {
46+
const url = new URL(selectedUrl);
47+
if (url.origin === window.location.origin) {
48+
navigate(url.pathname + url.search + url.hash);
49+
} else {
50+
window.location.href = selectedUrl;
51+
}
52+
} else {
53+
navigate(selectedUrl);
54+
}
55+
} catch (error) {
56+
navigate(selectedUrl);
57+
}
58+
};
59+
60+
return (
61+
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation')}>
62+
<div className="container-xl">
63+
<div className="nav-bar">
64+
<div className="nav-menu">
65+
<Tabs className="nav-underline-tabs" aria-label={intl.formatMessage(messages.courseMaterial)} onSelect={handleSelectedTab}>
66+
{tabs.map(({ title, slug }) => (
67+
<Tab eventKey={slug} title={title} key={slug} />
68+
))}
69+
</Tabs>
70+
</div>
71+
{/* <div className="search-toggle">
72+
<CoursewareSearchToggle />
73+
</div>
74+
</div>
75+
</div>
76+
{show && <CoursewareSearch />} */}
77+
</div>
78+
</div>
79+
</div>
80+
);
81+
};
82+
83+
export default CourseTabsNavigation;
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)