Skip to content
This repository has been archived by the owner on Sep 14, 2021. It is now read-only.

Commit

Permalink
feat(Header): Add Notifications Bell (#621)
Browse files Browse the repository at this point in the history
* feat: Added bell

* feat: Make bell pink

* feat: Update snapshots

* feat: Added NotificationsLink component

* feat: Check authenticated

* refactor: Change showBell to isInExperiment

* feat: Move link into NotificationsLink

* feat: Remove unnecessary propTypes on BellIcon

* feat: Update snapshots

* feat: Split to notifications container

* feat: Make NotificationsLink an SFC

* feat: Test NotificationsLink

* feat: Add test for getCookieFromString

* feat: Add catch to promise chain

* refactor: Move Bell to  UserAccount & adjust styles

* feat: Update snapshots
  • Loading branch information
chardos authored May 13, 2019
1 parent e5b376e commit dcf5778
Show file tree
Hide file tree
Showing 16 changed files with 577 additions and 291 deletions.
2 changes: 1 addition & 1 deletion docs/src/components/App/Header/Header.less
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
}

.link {
color: white;
color: @sk-white;
text-decoration: none;
display: inline-block;
&:hover {
Expand Down
10 changes: 10 additions & 0 deletions react/BellIcon/BellIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import markup from './BellIcon.svg';
import React from 'react';

import Icon from '../private/Icon/Icon';

export default function BellIcon(props) {
return <Icon markup={markup} {...props} />;
}

BellIcon.displayName = 'BellIcon';
1 change: 1 addition & 0 deletions react/BellIcon/BellIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions react/Header/Header.less
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@

.userWrapper {
display: flex;
align-items: center;
}

.user {
Expand Down Expand Up @@ -149,7 +150,12 @@
height: 14px;
border-right: @divider-width solid @sk-mid-gray-dark;
display: none;

@media @desktop {
display: inline-block;
}
}

.bellWrapper {
display: inline-block;
}
28 changes: 28 additions & 0 deletions react/Header/NotificationsLink/NotificationsLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import BellIcon from '../../BellIcon/BellIcon';
import styles from './NotificationsLink.less';

const NotificationsLink = ({
isAuthenticated,
linkRenderer,
isInExperiment
}) => {
return isInExperiment && isAuthenticated ? (
<Fragment>
{linkRenderer({
href: '/notifications',
children: <BellIcon className={styles.bell} />
})}
<span className={styles.bellDivider} />
</Fragment>
) : null;
};

NotificationsLink.propTypes = {
isAuthenticated: PropTypes.bool,
linkRenderer: PropTypes.func,
isInExperiment: PropTypes.bool
};

export default NotificationsLink;
28 changes: 28 additions & 0 deletions react/Header/NotificationsLink/NotificationsLink.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@import (reference) "~seek-style-guide/theme";
@import (reference) "../Header.less";

.bell {
@bell-touch-area: @interaction-type-row-span * @row-height;

height: @bell-touch-area;
width: @bell-touch-area;
display: flex;
align-items: center;
justify-content: center;
color: @sk-white;

@media @desktop {
color: @sk-black;
}
}

.bellDivider {
.divider();
margin-left: 1px; // To balance with space between user name
display: inline-block; // Override display:none from Header usage

@media @mobile {
border-color: @sk-white;
opacity: 0.2;
}
}
44 changes: 44 additions & 0 deletions react/Header/NotificationsLink/NotificationsLink.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { shallow } from 'enzyme';
import NotificationsLink from './NotificationsLink';

const renderNotificationsLink = props =>
shallow(<NotificationsLink linkRenderer={() => <a />} {...props} />);

describe('NotificationsLink:', () => {
describe('when the user is not authenticated', () => {
it('and they are inExperiment, they should not see the link', () => {
const component = renderNotificationsLink({
isAuthenticated: false,
isInExperiment: true
});
expect(component).toMatchSnapshot();
});

it('and they are not inExperiment, they should not see the link', () => {
const component = renderNotificationsLink({
isAuthenticated: false,
isInExperiment: false
});
expect(component).toMatchSnapshot();
});
});

describe('when the user is authenticated', () => {
it('and they are inExperiment, they should see the link', () => {
const component = renderNotificationsLink({
isAuthenticated: true,
isInExperiment: true
});
expect(component).toMatchSnapshot();
});

it('and they are not inExperiment, they should not see the link', () => {
const component = renderNotificationsLink({
isAuthenticated: true,
isInExperiment: false
});
expect(component).toMatchSnapshot();
});
});
});
65 changes: 65 additions & 0 deletions react/Header/NotificationsLink/NotificationsLinkContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getCookieFromString } from './utils';
import {
AUTHENTICATED,
UNAUTHENTICATED,
AUTH_PENDING
} from '../../private/authStatusTypes';
import { EXPERIMENT_ID, AB_CRUNCH_URL } from './constants';
import NotificationsLink from './NotificationsLink';

class NotificationsLinkContainer extends React.Component {
static propTypes = {
authenticationStatus: PropTypes.oneOf([
AUTHENTICATED,
UNAUTHENTICATED,
AUTH_PENDING
]),
linkRenderer: PropTypes.func
};

state = {
isInExperiment: false
};

componentDidMount() {
let visitorId = null;

try {
if (typeof document !== 'undefined') {
visitorId = getCookieFromString('JobseekerVisitorId', document.cookie);
}
if (visitorId) {
const url = `${AB_CRUNCH_URL}/${visitorId}`;

fetch(url)
.then(response => response.json())
.then(response => {
const isInExperiment = Boolean(response.experiments[EXPERIMENT_ID]);

if (isInExperiment) {
this.setState({ isInExperiment: true });
}
})
.catch(() => {});
}
// eslint-disable-next-line no-empty
} catch (e) {}
}

render() {
const { authenticationStatus, linkRenderer } = this.props;
const isAuthenticated = authenticationStatus === AUTHENTICATED;

return (
<NotificationsLink
isAuthenticated={isAuthenticated}
isInExperiment={this.state.isInExperiment}
linkRenderer={linkRenderer}
/>
);
}
}

export default NotificationsLinkContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NotificationsLink: when the user is authenticated and they are inExperiment, they should see the link 1`] = `
<Fragment>
<a />
<span
className="NotificationsLink__bellDivider"
/>
</Fragment>
`;

exports[`NotificationsLink: when the user is authenticated and they are not inExperiment, they should not see the link 1`] = `""`;

exports[`NotificationsLink: when the user is not authenticated and they are inExperiment, they should not see the link 1`] = `""`;

exports[`NotificationsLink: when the user is not authenticated and they are not inExperiment, they should not see the link 1`] = `""`;
3 changes: 3 additions & 0 deletions react/Header/NotificationsLink/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const EXPERIMENT_ID = '58CC4E12-A4E0-4C0B-BD2A-681FD98FDC2E';
export const AB_CRUNCH_URL =
'https://experiments.cloud.seek.com.au/participants';
11 changes: 11 additions & 0 deletions react/Header/NotificationsLink/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function getCookieFromString(key, cookieString) {
const value = `; ${cookieString}`;
const parts = value.split(`; ${key}=`);
if (parts.length === 2) {
return parts
.pop()
.split(';')
.shift();
}
return null;
}
23 changes: 23 additions & 0 deletions react/Header/NotificationsLink/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getCookieFromString } from './utils';

describe('getCookieFromString(key, cookieString)', () => {
it('should get the key from the cookie string', () => {
const cookieString =
'Foo=bar; JobseekerVisitorId=23fba469f32dfd44602bcda9c520bed7; Baz=qux;';
const visitorId = '23fba469f32dfd44602bcda9c520bed7';
const result = getCookieFromString('JobseekerVisitorId', cookieString);
expect(result).toEqual(visitorId);
});

it('when the key does not exist, it should return null', () => {
const cookieString = 'Foo=bar;';
const result = getCookieFromString('Baz', cookieString);
expect(result).toBeNull();
});

it('when the string is empty, it should return null', () => {
const cookieString = '';
const result = getCookieFromString('Baz', cookieString);
expect(result).toBeNull();
});
});
52 changes: 32 additions & 20 deletions react/Header/UserAccount/UserAccount.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ChevronIcon from '../../ChevronIcon/ChevronIcon';
import Hidden from '../../Hidden/Hidden';
import NotificationsLinkContainer from '../NotificationsLink/NotificationsLinkContainer';
import ScreenReaderOnly from '../../ScreenReaderOnly/ScreenReaderOnly';
import UserAccountMenu from '../UserAccountMenu/UserAccountMenu';
import {
Expand Down Expand Up @@ -104,27 +106,37 @@ export default class UserAccount extends Component {
</label>
</div>

<label
data-automation="user-account-menu-toggle"
className={styles.toggleLabel}
htmlFor="user-account-menu-toggle"
>
<ScreenReaderOnly>Show user menu</ScreenReaderOnly>
<span data-hj-masked={true}>
<span className={styles.mobileMenuLabel}>{mobileMenuLabel}</span>
<span
className={styles.desktopMenuLabel}
data-automation="user-account-name"
>
{desktopMenuLabel}
</span>
</span>
<ChevronIcon
direction="down"
className={styles.chevron}
svgClassName={styles.chevronSvg}
<div className={styles.accountContainer}>
<NotificationsLinkContainer
authenticationStatus={authenticationStatus}
linkRenderer={linkRenderer}
/>
</label>

<label
data-automation="user-account-menu-toggle"
className={styles.toggleLabel}
htmlFor="user-account-menu-toggle"
>
<ScreenReaderOnly>Show user menu</ScreenReaderOnly>
<span data-hj-masked={true}>
<Hidden desktop className={styles.menuLabel}>
{mobileMenuLabel}
</Hidden>
<Hidden
mobile
className={styles.menuLabel}
data-automation="user-account-name"
>
{desktopMenuLabel}
</Hidden>
</span>
<ChevronIcon
direction="down"
className={styles.chevron}
svgClassName={styles.chevronSvg}
/>
</label>
</div>

<div onClick={this.handleMenuClick} className={styles.toggleContainer}>
<UserAccountMenu
Expand Down
27 changes: 11 additions & 16 deletions react/Header/UserAccount/UserAccount.less
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
}
}

.accountContainer {
display: flex;
align-items: center;
}

.toggleLabel {
.touchableText();
display: flex;
Expand Down Expand Up @@ -96,28 +101,18 @@
bottom: 0;
}

.mobileMenuLabel {
user-select: none;
display: inline-block;
min-width: 60px;
text-align: right;
@media @desktop {
display: none;
}
}

.desktopMenuLabel {
.menuLabel {
pointer-events: none;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
max-width: 100px; // ~ 10 characters
@media only screen and (min-width: 400px) {
max-width: 200px; // ~ 20 characters
max-width: 90px; // ~ 10 characters
@media only screen and (min-width: 385px) {
max-width: 150px; // ~ 16 characters
}
@media @mobile {
display: none;
@media only screen and (min-width: 435px) {
max-width: 200px; // ~ 20 characters
}
}

Expand Down
Loading

0 comments on commit dcf5778

Please sign in to comment.