Skip to content

Commit 8fecef0

Browse files
authored
Merge pull request #52 from edx/eahmadjaved/ENT-10848
feat: added header bar
2 parents 0a457bc + 8c63858 commit 8fecef0

File tree

5 files changed

+254
-20
lines changed

5 files changed

+254
-20
lines changed

src/components/Header/Header.tsx

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
1+
import { AppContext } from '@edx/frontend-platform/react';
12
import { Container, Image, Navbar } from '@openedx/paragon';
3+
import { useContext, useMemo } from 'react';
24
import { Link } from 'react-router-dom';
35

4-
import edxLogo from '@/assets/images/edx-enterprise.svg';
6+
import edxEnterpriseLogo from './images/edx-enterprise.svg';
7+
import UserMenu from './UserMenu/UserMenu';
58

6-
const Header: React.FC = () => (
7-
<header>
8-
<Navbar bg="dark" variant="dark" expand="lg">
9-
<Container size="lg" className="py-3">
10-
<div className="navbar__brand">
11-
<Link to="/">
12-
<Image
13-
src={edxLogo}
14-
alt="edX for Business logo"
15-
style={{ maxWidth: 200 }}
16-
fluid
17-
/>
18-
</Link>
19-
</div>
20-
{/* TODO: When authenticated, user dropdown menu */}
21-
</Container>
22-
</Navbar>
23-
</header>
24-
);
9+
const Header: React.FC = () => {
10+
const { authenticatedUser } = useContext(AppContext) as AppContextValue;
11+
12+
const hasUser = Boolean(authenticatedUser && authenticatedUser.username);
13+
14+
const brand = useMemo(() => (
15+
<div className="navbar__brand" data-testid="header-brand">
16+
<Link to="/">
17+
<Image
18+
src={edxEnterpriseLogo}
19+
alt="edX for Business logo"
20+
style={{ maxWidth: 200 }}
21+
fluid
22+
/>
23+
</Link>
24+
</div>
25+
), []);
26+
27+
return (
28+
<header className="checkout-header" role="banner">
29+
<Navbar variant="dark" expand="lg" className="bg-dark-900">
30+
<Container size="lg" className="py-3 d-flex align-items-center justify-content-between">
31+
{brand}
32+
{hasUser && (
33+
<div data-testid="header-user-menu">
34+
<UserMenu user={authenticatedUser} />
35+
</div>
36+
)}
37+
</Container>
38+
</Navbar>
39+
</header>
40+
);
41+
};
2542

2643
export default Header;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getConfig } from '@edx/frontend-platform/config';
2+
import { AvatarButton, Dropdown } from '@openedx/paragon';
3+
import { useMemo } from 'react';
4+
5+
interface UserMenuProps { user: AuthenticatedUser }
6+
7+
const UserMenu = ({ user }: UserMenuProps) => {
8+
const config = getConfig();
9+
const {
10+
LMS_BASE_URL,
11+
LOGOUT_URL,
12+
ACCOUNT_PROFILE_URL = LMS_BASE_URL,
13+
ACCOUNT_SETTINGS_URL = `${LMS_BASE_URL}/account/settings`,
14+
} = config;
15+
const { username, name, email } = user;
16+
const profileImage = user.profile_image?.image_url_medium;
17+
const profileUrl = `${ACCOUNT_PROFILE_URL}/u/${username}`;
18+
const accountUrl = ACCOUNT_SETTINGS_URL;
19+
const logoutUrl = LOGOUT_URL;
20+
21+
const displayName = name || username;
22+
23+
const metaBlock = useMemo(() => (
24+
<div className="small text-left" style={{ maxWidth: 240 }}>
25+
<div className="font-weight-bold" data-testid="user-full-name">{displayName}</div>
26+
{email && <div className="text-muted" data-testid="user-email" style={{ wordBreak: 'break-all' }}>{email}</div>}
27+
</div>
28+
), [displayName, email]);
29+
30+
return (
31+
<Dropdown className="ml-2" data-testid="user-menu">
32+
<Dropdown.Toggle
33+
as={AvatarButton}
34+
id="checkout-header-avatar-dropdown"
35+
src={profileImage}
36+
className="text-white"
37+
variant="primary-900"
38+
showLabel
39+
>
40+
{username}
41+
</Dropdown.Toggle>
42+
<Dropdown.Menu alignRight style={{ maxWidth: 280 }}>
43+
<Dropdown.Header>
44+
{metaBlock}
45+
</Dropdown.Header>
46+
<Dropdown.Item href={profileUrl} data-testid="user-profile-link">Profile</Dropdown.Item>
47+
<Dropdown.Item href={accountUrl} data-testid="user-account-link">Account</Dropdown.Item>
48+
<Dropdown.Divider />
49+
<Dropdown.Item href={logoutUrl} data-testid="user-logout-link">Sign Out</Dropdown.Item>
50+
</Dropdown.Menu>
51+
</Dropdown>
52+
);
53+
};
54+
55+
export default UserMenu;
Lines changed: 20 additions & 0 deletions
Loading
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { IntlProvider } from '@edx/frontend-platform/i18n';
2+
import { AppContext } from '@edx/frontend-platform/react';
3+
import { QueryClientProvider } from '@tanstack/react-query';
4+
import { render, screen } from '@testing-library/react';
5+
import userEvent from '@testing-library/user-event';
6+
import '@testing-library/jest-dom';
7+
import { MemoryRouter } from 'react-router-dom';
8+
9+
import Header from '@/components/Header/Header';
10+
import { queryClient } from '@/utils/tests';
11+
12+
const renderHeader = (appContextValue: any) => (
13+
render(
14+
<IntlProvider locale="en">
15+
<QueryClientProvider client={queryClient()}>
16+
<AppContext.Provider value={appContextValue}>
17+
<MemoryRouter>
18+
<Header />
19+
</MemoryRouter>
20+
</AppContext.Provider>
21+
</QueryClientProvider>
22+
</IntlProvider>,
23+
)
24+
);
25+
26+
describe('Header', () => {
27+
it('renders the brand logo linking to home', () => {
28+
renderHeader({ config: {}, authenticatedUser: null });
29+
30+
const brand = screen.getByTestId('header-brand');
31+
expect(brand).toBeInTheDocument();
32+
33+
// Image alt text
34+
expect(screen.getByAltText('edX for Business logo')).toBeInTheDocument();
35+
36+
// Link href should be '/'
37+
const link = brand.querySelector('a');
38+
expect(link).toBeTruthy();
39+
expect(link!.getAttribute('href')).toBe('/');
40+
});
41+
42+
it('does not render the user menu when unauthenticated', () => {
43+
renderHeader({ config: {}, authenticatedUser: null });
44+
expect(screen.queryByTestId('header-user-menu')).not.toBeInTheDocument();
45+
});
46+
47+
it('renders the user menu when authenticated and shows username on the toggle', async () => {
48+
const user = {
49+
userId: 1,
50+
username: 'alice',
51+
name: 'Alice Example',
52+
53+
roles: [],
54+
administrator: false,
55+
};
56+
57+
renderHeader({ config: {}, authenticatedUser: user });
58+
59+
expect(screen.getByTestId('header-user-menu')).toBeInTheDocument();
60+
61+
// The avatar toggle should be present with the username label
62+
const toggle = screen.getByRole('button', { name: /alice/i });
63+
expect(toggle).toBeInTheDocument();
64+
65+
await userEvent.click(toggle);
66+
});
67+
});

0 commit comments

Comments
 (0)