}
diff --git a/src/Components/ProfileDashboard/UserProfile/UserProfileGeneralInformation/UserProfileGeneralInformation.jsx b/src/Components/ProfileDashboard/UserProfile/UserProfileGeneralInformation/UserProfileGeneralInformation.jsx
index 6a69bec4dd..cf824f6090 100644
--- a/src/Components/ProfileDashboard/UserProfile/UserProfileGeneralInformation/UserProfileGeneralInformation.jsx
+++ b/src/Components/ProfileDashboard/UserProfile/UserProfileGeneralInformation/UserProfileGeneralInformation.jsx
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { USER_PROFILE } from '../../../../Constants/PropTypes';
import SectionTitle from '../../SectionTitle';
import InformationDataPoint from '../../InformationDataPoint';
-import Status from '../Status';
import EditProfile from '../EditProfile';
import Avatar from '../../../Avatar';
import StaticDevContent from '../../../StaticDevContent';
@@ -13,7 +12,6 @@ const UserProfileGeneralInformation = ({ userProfile, showEditLink, useGroup })
-
-
-
-
{
return (
{() => (
-
-
-
-
- {title}
- View position
+
+
+
+
+
+ {title}
+ View position
+
+
+ Post: {post}
+
-
- Post: {post}
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
+
+
+
+
+
+
+
+
+
+ {
+ get(stats, 'has_handshake_offered', false) &&
+ }
+
+
+
+
+ {
!!favorites &&
}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
)}
);
diff --git a/src/Components/ResultsCondensedCard/ResultsCondensedCard.jsx b/src/Components/ResultsCondensedCard/ResultsCondensedCard.jsx
index f6f344df6e..fa66486c02 100644
--- a/src/Components/ResultsCondensedCard/ResultsCondensedCard.jsx
+++ b/src/Components/ResultsCondensedCard/ResultsCondensedCard.jsx
@@ -3,26 +3,35 @@ import PropTypes from 'prop-types';
import ResultsCondensedCardTop from '../ResultsCondensedCardTop';
import ResultsCondensedCardBottom from '../ResultsCondensedCardBottom';
import ResultsCondensedCardFooter from '../ResultsCondensedCardFooter';
+import BoxShadow from '../BoxShadow';
import { POSITION_DETAILS, FAVORITE_POSITIONS_ARRAY, BID_RESULTS, HOME_PAGE_CARD_TYPE } from '../../Constants/PropTypes';
-const ResultsCondensedCard = ({ position, favorites, bidList, type, refreshFavorites }) => (
-
-
-
-
-
-
+const ResultsCondensedCard = (
+ {
+ position,
+ favorites,
+ bidList,
+ type,
+ refreshFavorites,
+ showBidListButton,
+ }) => (
+
+
+
+
+
);
ResultsCondensedCard.propTypes = {
@@ -31,11 +40,13 @@ ResultsCondensedCard.propTypes = {
bidList: BID_RESULTS.isRequired,
type: HOME_PAGE_CARD_TYPE.isRequired,
refreshFavorites: PropTypes.bool,
+ showBidListButton: PropTypes.bool,
};
ResultsCondensedCard.defaultProps = {
favorites: [],
refreshFavorites: false,
+ showBidListButton: false,
};
export default ResultsCondensedCard;
diff --git a/src/Components/ResultsCondensedCard/__snapshots__/ResultsCondensedCard.test.jsx.snap b/src/Components/ResultsCondensedCard/__snapshots__/ResultsCondensedCard.test.jsx.snap
index 6c54256bcd..85827a1f70 100644
--- a/src/Components/ResultsCondensedCard/__snapshots__/ResultsCondensedCard.test.jsx.snap
+++ b/src/Components/ResultsCondensedCard/__snapshots__/ResultsCondensedCard.test.jsx.snap
@@ -1,8 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResultsCondensedCardComponent matches snapshot 1`] = `
-
-
+
`;
diff --git a/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.jsx b/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.jsx
index 3e49970aa1..9d5dbf8985 100644
--- a/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.jsx
+++ b/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.jsx
@@ -1,38 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { get } from 'lodash';
import CondensedCardData from '../CondensedCardData';
import { POSITION_DETAILS, FAVORITE_POSITIONS_ARRAY } from '../../Constants/PropTypes';
import Favorite from '../../Containers/Favorite';
+import BidListButton from '../../Containers/BidListButton';
+import PermissionsWrapper from '../../Containers/PermissionsWrapper';
import ResultsCondensedCardStats from '../ResultsCondensedCardStats';
-const ResultsCondensedCardBottom = ({ position, favorites, refreshFavorites }) => (
-
-
-
-
-
-
+const ResultsCondensedCardBottom = (
+ { position,
+ favorites,
+ refreshFavorites,
+ showBidListButton,
+ }) => (
+
+
+
+
+
+
+ {
+ showBidListButton &&
+
+
+
+ }
+
-
);
ResultsCondensedCardBottom.propTypes = {
position: POSITION_DETAILS.isRequired,
favorites: FAVORITE_POSITIONS_ARRAY.isRequired,
refreshFavorites: PropTypes.bool,
+ showBidListButton: PropTypes.bool,
};
ResultsCondensedCardBottom.defaultProps = {
type: 'default',
refreshFavorites: false,
+ showBidListButton: false,
};
export default ResultsCondensedCardBottom;
diff --git a/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.test.jsx b/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.test.jsx
index 90cb14bd6c..889ba5d526 100644
--- a/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.test.jsx
+++ b/src/Components/ResultsCondensedCardBottom/ResultsCondensedCardBottom.test.jsx
@@ -41,4 +41,16 @@ describe('ResultsCondensedCardBottomComponent', () => {
);
expect(toJSON(wrapper)).toMatchSnapshot();
});
+
+ it('matches snapshot with bidlist button', () => {
+ const wrapper = shallow(
+
,
+ );
+ expect(toJSON(wrapper)).toMatchSnapshot();
+ });
});
diff --git a/src/Components/ResultsCondensedCardBottom/__snapshots__/ResultsCondensedCardBottom.test.jsx.snap b/src/Components/ResultsCondensedCardBottom/__snapshots__/ResultsCondensedCardBottom.test.jsx.snap
index ef32a1f0b3..ac8353d92d 100644
--- a/src/Components/ResultsCondensedCardBottom/__snapshots__/ResultsCondensedCardBottom.test.jsx.snap
+++ b/src/Components/ResultsCondensedCardBottom/__snapshots__/ResultsCondensedCardBottom.test.jsx.snap
@@ -119,12 +119,153 @@ exports[`ResultsCondensedCardBottomComponent matches snapshot 1`] = `
]
}
hasBorder={true}
+ hideText={false}
refKey={6}
refresh={false}
useButtonClass={true}
+ useButtonClassSecondary={false}
useLongText={true}
/>
`;
+
+exports[`ResultsCondensedCardBottomComponent matches snapshot with bidlist button 1`] = `
+
+`;
diff --git a/src/Components/ResultsCondensedCardStats/ResultsCondensedCardStats.jsx b/src/Components/ResultsCondensedCardStats/ResultsCondensedCardStats.jsx
index 057816c936..b3c69eb183 100644
--- a/src/Components/ResultsCondensedCardStats/ResultsCondensedCardStats.jsx
+++ b/src/Components/ResultsCondensedCardStats/ResultsCondensedCardStats.jsx
@@ -7,7 +7,7 @@ const ResultsCondensedCardStats = ({ bidStatisticsArray }) => {
const bidStatistics = getBidStatisticsObject(bidStatisticsArray);
return (
-
diff --git a/src/Components/ResultsCondensedCardStats/__snapshots__/ResultsCondensedCardStats.test.jsx.snap b/src/Components/ResultsCondensedCardStats/__snapshots__/ResultsCondensedCardStats.test.jsx.snap
index ff0a6b3116..6877659b51 100644
--- a/src/Components/ResultsCondensedCardStats/__snapshots__/ResultsCondensedCardStats.test.jsx.snap
+++ b/src/Components/ResultsCondensedCardStats/__snapshots__/ResultsCondensedCardStats.test.jsx.snap
@@ -5,7 +5,7 @@ exports[`ResultsCondensedCardStatsComponent matches snapshot 1`] = `
className="condensed-card-footer condensed-card-statistics"
>
{
let icon = '';
@@ -12,25 +16,30 @@ const ResultsCondensedCardTop = ({ position, type }) => {
cardTopClass = 'card-top-alternate';
useType = true;
}
+ const stats = getBidStatisticsObject(position.bid_statistics);
+ const hasHandshake = get(stats, 'has_handshake_offered', false);
return (
{useType && }
-
{position.title}
-
-
-
Grade: {position.grade}
+ {position.title} View position
-
-
View position
+
+
+ Post: {getPostName(position.post, NO_POST)}
+
+ {
+ hasHandshake &&
+
+
+
+ }
);
diff --git a/src/Components/ResultsCondensedCardTop/__snapshots__/ResultsCondensedCardTop.test.jsx.snap b/src/Components/ResultsCondensedCardTop/__snapshots__/ResultsCondensedCardTop.test.jsx.snap
index 14f67ae3b2..035b970a7d 100644
--- a/src/Components/ResultsCondensedCardTop/__snapshots__/ResultsCondensedCardTop.test.jsx.snap
+++ b/src/Components/ResultsCondensedCardTop/__snapshots__/ResultsCondensedCardTop.test.jsx.snap
@@ -8,33 +8,38 @@ exports[`ResultsCondensedCardTopComponent matches snapshot 1`] = `
className="usa-grid-full condensed-card-top-header-container"
>
OMS (DCM)
-
-
-
- Grade:
-
-
- 06
-
+
+ View position
+
-
- View position
-
+
+
+
+ Post:
+
+
+
+ Freetown, Sierra Leone
+
+
+
`;
@@ -47,7 +52,7 @@ exports[`ResultsCondensedCardTopComponent matches snapshot when type is serviceN
className="usa-grid-full condensed-card-top-header-container"
>
OMS (DCM)
-
-
-
- Grade:
-
-
- 06
-
+
+ View position
+
-
- View position
-
+
+
+
+ Post:
+
+
+
+ Freetown, Sierra Leone
+
+
+
`;
diff --git a/src/Components/Ribbon/Handshake/Handshake.jsx b/src/Components/Ribbon/Handshake/Handshake.jsx
new file mode 100644
index 0000000000..9f16b073d2
--- /dev/null
+++ b/src/Components/Ribbon/Handshake/Handshake.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import Ribbon from '../Ribbon';
+
+const Handshake = ({ ...props }) => (
+
+);
+
+export default Handshake;
diff --git a/src/Components/Ribbon/Handshake/Handshake.test.jsx b/src/Components/Ribbon/Handshake/Handshake.test.jsx
new file mode 100644
index 0000000000..ae198c7b85
--- /dev/null
+++ b/src/Components/Ribbon/Handshake/Handshake.test.jsx
@@ -0,0 +1,11 @@
+import { shallow } from 'enzyme';
+import React from 'react';
+import Handshake from './Handshake';
+
+describe('HandshakeComponent', () => {
+ it('is defined', () => {
+ const wrapper = shallow(
);
+
+ expect(wrapper).toBeDefined();
+ });
+});
diff --git a/src/Components/Ribbon/Handshake/index.js b/src/Components/Ribbon/Handshake/index.js
new file mode 100644
index 0000000000..f67d74d363
--- /dev/null
+++ b/src/Components/Ribbon/Handshake/index.js
@@ -0,0 +1 @@
+export { default } from './Handshake';
diff --git a/src/Components/Ribbon/Ribbon.jsx b/src/Components/Ribbon/Ribbon.jsx
new file mode 100644
index 0000000000..0782784ce1
--- /dev/null
+++ b/src/Components/Ribbon/Ribbon.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import FA from 'react-fontawesome';
+
+const Ribbon = ({ type, className, icon, text, cutSide, containerProps }) => (
+
+);
+
+
+Ribbon.propTypes = {
+ className: PropTypes.string,
+ icon: PropTypes.string,
+ text: PropTypes.string,
+ containerProps: PropTypes.shape({}),
+ type: PropTypes.oneOf(['primary', 'secondary']),
+ cutSide: PropTypes.oneOf(['left', 'right']),
+};
+
+Ribbon.defaultProps = {
+ className: '',
+ icon: 'handshake',
+ text: '',
+ containerProps: {},
+ type: 'primary',
+ cutSide: 'left',
+};
+
+export default Ribbon;
diff --git a/src/Components/Ribbon/Ribbon.test.jsx b/src/Components/Ribbon/Ribbon.test.jsx
new file mode 100644
index 0000000000..35d694d3c1
--- /dev/null
+++ b/src/Components/Ribbon/Ribbon.test.jsx
@@ -0,0 +1,80 @@
+import { shallow } from 'enzyme';
+import React from 'react';
+import toJSON from 'enzyme-to-json';
+import Ribbon from './Ribbon';
+
+describe('RibbonComponent', () => {
+ const props = {
+ type: 'primary',
+ className: '',
+ icon: '',
+ text: '',
+ cutSide: 'left',
+ containerProps: {},
+ };
+
+ it('is defined', () => {
+ const wrapper = shallow(
);
+
+ expect(wrapper).toBeDefined();
+ });
+
+ it('creates class names', () => {
+ const props$ = {
+ ...props,
+ className: 'custom-class',
+ cutSide: 'right',
+ type: 'primary',
+ };
+ const wrapper = shallow(
);
+
+ expect(wrapper.find('div').at(0).props().className)
+ .toBe(`ribbon-outer-container ribbon-outer-container-cut-${props$.cutSide} ${props$.className}`);
+ expect(wrapper.find('div').at(1).props().className)
+ .toBe(`ribbon ribbon-${props$.type} ribbon-cut-${props$.cutSide}`);
+ });
+
+ it('passes the correct icon name', () => {
+ const props$ = {
+ ...props,
+ icon: 'message',
+ };
+ const wrapper = shallow(
);
+
+ expect(wrapper.find('FontAwesome').at(0).props().name)
+ .toBe(props$.icon);
+ });
+
+ it('passes the correct text', () => {
+ const props$ = {
+ ...props,
+ text: 'Ribbon text',
+ };
+ const wrapper = shallow(
);
+
+ expect(wrapper.find('span').text())
+ .toBe(props$.text);
+ });
+
+ it('spreads the containerProps', () => {
+ const props$ = {
+ ...props,
+ containerProps: {
+ alt: 'alt text',
+ tabIndex: 0,
+ },
+ };
+ const wrapper = shallow(
);
+
+ Object.keys(props$.containerProps).map(m => (
+ expect(wrapper.find('div').at(0).props()[m])
+ .toBe(props$.containerProps[m])
+ ));
+ });
+
+ it('matches snapshot', () => {
+ const wrapper = shallow(
);
+
+ expect(toJSON(wrapper)).toMatchSnapshot();
+ });
+});
diff --git a/src/Components/Ribbon/__snapshots__/Ribbon.test.jsx.snap b/src/Components/Ribbon/__snapshots__/Ribbon.test.jsx.snap
new file mode 100644
index 0000000000..160eac8b84
--- /dev/null
+++ b/src/Components/Ribbon/__snapshots__/Ribbon.test.jsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RibbonComponent matches snapshot 1`] = `
+
+`;
diff --git a/src/Components/Ribbon/index.js b/src/Components/Ribbon/index.js
new file mode 100644
index 0000000000..96c43933bd
--- /dev/null
+++ b/src/Components/Ribbon/index.js
@@ -0,0 +1 @@
+export { default } from './Ribbon';
diff --git a/src/Components/SearchFilters/SearchFiltersContainer/SearchFiltersContainer.jsx b/src/Components/SearchFilters/SearchFiltersContainer/SearchFiltersContainer.jsx
index c6843e2826..341fc2f25d 100644
--- a/src/Components/SearchFilters/SearchFiltersContainer/SearchFiltersContainer.jsx
+++ b/src/Components/SearchFilters/SearchFiltersContainer/SearchFiltersContainer.jsx
@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
+import { includes, sortBy } from 'lodash';
import MultiSelectFilterContainer from '../MultiSelectFilterContainer/MultiSelectFilterContainer';
import MultiSelectFilter from '../MultiSelectFilter/MultiSelectFilter';
import BooleanFilterContainer from '../BooleanFilterContainer/BooleanFilterContainer';
@@ -9,7 +10,7 @@ import PostFilter from '../PostFilter';
import SkillFilter from '../SkillFilter';
import { FILTER_ITEMS_ARRAY, POST_DETAILS_ARRAY } from '../../../Constants/PropTypes';
import { propSort, sortGrades, getPostName, propOrDefault } from '../../../utilities';
-import { ENDPOINT_PARAMS } from '../../../Constants/EndpointParams';
+import { ENDPOINT_PARAMS, COMMON_PROPERTIES } from '../../../Constants/EndpointParams';
class SearchFiltersContainer extends Component {
@@ -62,7 +63,9 @@ class SearchFiltersContainer extends Component {
});
// get our normal multi-select filters
- const multiSelectFilterNames = ['bidCycle', 'skill', 'grade', 'region', 'post', 'tod', 'language', 'postDiff', 'dangerPay'];
+ const multiSelectFilterNames = ['bidCycle', 'skill', 'grade', 'region', 'post', 'tod', 'language',
+ 'postDiff', 'dangerPay'];
+ const blackList = []; // don't create accordions for these
// create map
const multiSelectFilterMap = new Map();
@@ -77,6 +80,10 @@ class SearchFiltersContainer extends Component {
f.data.sort(sortGrades);
} else if (f.item.description === 'language' && f.data) {
f.data.sort(propSort('custom_description'));
+ // Push the "NONE" code choice to the bottom. We're already sorting
+ // data, and this is readable, so the next line is eslint-disabled.
+ // eslint-disable-next-line
+ f.data = sortBy(f.data, item => item.code === COMMON_PROPERTIES.NULL_LANGUAGE ? 1 : 0);
}
// add to Map
multiSelectFilterMap.set(f.item.description, f);
@@ -158,6 +165,19 @@ class SearchFiltersContainer extends Component {
skillCones={skillCones}
/>
);
+ case 'language':
+ return (
+
+
+
+ );
+ case includes(blackList, type) ? type : null:
+ return null;
default:
return (
@@ -173,7 +193,7 @@ class SearchFiltersContainer extends Component {
}
};
- if (item) {
+ if (item && !includes(blackList, n)) {
sortedFilters.push(
{ content: getFilter(n),
title: item.item.title,
diff --git a/src/Components/SearchResultsExportLink/SearchResultsExportLink.jsx b/src/Components/SearchResultsExportLink/SearchResultsExportLink.jsx
index 2bd4bf7e0e..be6708936b 100644
--- a/src/Components/SearchResultsExportLink/SearchResultsExportLink.jsx
+++ b/src/Components/SearchResultsExportLink/SearchResultsExportLink.jsx
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { CSVDownload } from 'react-csv';
+import { CSVLink } from 'react-csv';
import queryString from 'query-string';
import { POSITION_SEARCH_SORTS } from '../../Constants/Sort';
import { fetchResultData } from '../../actions/results';
@@ -52,7 +52,11 @@ class SearchResultsExportLink extends Component {
};
fetchResultData(queryString.stringify(query)).then((results) => {
const data = processData(results.results);
- this.setState({ data });
+ this.setState({ data }, () => {
+ // click the CSVLink component to trigger the CSV download
+ // This is needed for the download to work in Edge.
+ this.csvLink.link.click();
+ });
});
}
@@ -60,10 +64,8 @@ class SearchResultsExportLink extends Component {
const { data } = this.state;
return (
- Download
- {
- data &&
- }
+ Export
+ { this.csvLink = x; }} target="_blank" filename={this.props.filename} data={data} headers={HEADERS} />
);
}
diff --git a/src/Components/SearchResultsExportLink/SearchResultsExportLink.test.jsx b/src/Components/SearchResultsExportLink/SearchResultsExportLink.test.jsx
index 7958269e88..2c0fb49a2a 100644
--- a/src/Components/SearchResultsExportLink/SearchResultsExportLink.test.jsx
+++ b/src/Components/SearchResultsExportLink/SearchResultsExportLink.test.jsx
@@ -24,11 +24,11 @@ describe('SearchResultsExportLink', () => {
expect(fetchResultDataStub.calledOnce).toBe(true);
});
- it('shows download component when state has data', () => {
+ it('shows link component when state has data', () => {
const wrapper = shallow(
);
wrapper.setState({ data: 'test' });
wrapper.update();
- expect(wrapper.find('CSVDownload').prop('data')).toBeTruthy();
+ expect(wrapper.find('CSVLink').prop('data')).toBeTruthy();
});
it('matches snapshot', () => {
diff --git a/src/Components/SearchResultsExportLink/__snapshots__/SearchResultsExportLink.test.jsx.snap b/src/Components/SearchResultsExportLink/__snapshots__/SearchResultsExportLink.test.jsx.snap
index fb3d0cc497..8e4de7dc4e 100644
--- a/src/Components/SearchResultsExportLink/__snapshots__/SearchResultsExportLink.test.jsx.snap
+++ b/src/Components/SearchResultsExportLink/__snapshots__/SearchResultsExportLink.test.jsx.snap
@@ -6,7 +6,71 @@ exports[`SearchResultsExportLink matches snapshot 1`] = `
className="usa-button-secondary"
onClick={[Function]}
>
- Download
+ Export
+
`;
diff --git a/src/Constants/EndpointParams.js b/src/Constants/EndpointParams.js
index d8f9905d31..95ea2aee08 100644
--- a/src/Constants/EndpointParams.js
+++ b/src/Constants/EndpointParams.js
@@ -2,7 +2,7 @@
export const ENDPOINT_PARAMS = {
skill: 'skill__code__in',
- language: 'languages__language__code__in',
+ language: 'language_codes',
grade: 'grade__code__in',
tod: 'post__tour_of_duty__code__in',
org: 'bureau__code__in',
@@ -21,6 +21,7 @@ export const ENDPOINT_PARAMS = {
// any properties that we want to abstract to a common name
export const COMMON_PROPERTIES = {
posted: 'posted_date',
+ NULL_LANGUAGE: 'NONE',
};
// Take our custom query param from the Bidder Portfolio navigation and convert them to queries
diff --git a/src/Constants/SetType.test.jsx b/src/Constants/SetType.test.jsx
new file mode 100644
index 0000000000..32ce8d9981
--- /dev/null
+++ b/src/Constants/SetType.test.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { omit, pick } from 'lodash';
+import SetType from './SetType';
+
+describe('PropTypes', () => {
+ const props = {
+ a: new Set(),
+ b: 1,
+ c: () => {},
+ d: {},
+ e: [],
+ f: null,
+ g: undefined,
+ h: 'h',
+ i:
,
+ j: 0,
+ k: { method: 1 /* some non-function */ },
+ };
+
+ const shouldReturnNull = ['a', 'f', 'g'];
+
+ Object.keys(pick(props, shouldReturnNull)).map(k => (
+ it(`should return null for Set, null, and undefined (key = ${k}: ${JSON.stringify(props[k])})`, () => {
+ const output = SetType(props, k, 'component');
+ expect(output).toBeNull();
+ })
+ ));
+
+ Object.keys(omit(props, shouldReturnNull)).map(k => (
+ it(`should return an error if type !== Set (key = ${k}: ${JSON.stringify(props[k])})`, () => {
+ const output = SetType(props, k, 'component');
+ expect(output.toString()).toEqual((expect.stringMatching(/^(Error)/)));
+ })
+ ));
+
+ it('return an error if the prop is required and a null value is provided', () => {
+ const output = SetType.isRequired(props, 'f', 'component');
+ expect(output.toString()).toEqual((expect.stringMatching(/^(Error)/)));
+ });
+
+ it('return a null if the prop is required and a valid value is provided', () => {
+ const output = SetType.isRequired(props, 'a', 'component');
+ expect(output).toBeNull();
+ });
+});
diff --git a/src/Constants/SystemMessages.js b/src/Constants/SystemMessages.js
index 483723e2e5..feffb73604 100644
--- a/src/Constants/SystemMessages.js
+++ b/src/Constants/SystemMessages.js
@@ -1,3 +1,5 @@
+import FavoriteSuccess from '../Components/FavoriteMessages/Success';
+
export const DEFAULT_TEXT = 'None listed';
export const NO_ASSIGNMENT_DATE = DEFAULT_TEXT;
@@ -37,6 +39,14 @@ export const DELETE_BID_ITEM_ERROR = 'Error trying to delete this bid.';
export const ADD_BID_ITEM_SUCCESS = 'Bid successfully added.';
export const ADD_BID_ITEM_ERROR = 'Error trying to add this bid.';
+export const ADD_FAVORITE_TITLE = 'Favorite Added';
+export const DELETE_FAVORITE_TITLE = 'Favorite Removed';
+export const ERROR_FAVORITE_TITLE = 'Favorite Error';
+export const DELETE_FAVORITE_SUCCESS = pos => `${pos.title} (${pos.position_number}) has been successfully removed from favorites.`;
+export const DELETE_FAVORITE_ERROR = () => "We're experiencing an error attemtping to remove this position to your Favorites. Please try again.";
+export const ADD_FAVORITE_SUCCESS = pos => FavoriteSuccess({ pos });
+export const ADD_FAVORITE_ERROR = () => "We're experiencing an error attemtping to add this position to your Favorites. Please try again.";
+
export const ACCEPT_BID_SUCCESS = 'Bid successfully accepted.';
export const ACCEPT_BID_ERROR = 'Error trying to accept this bid.';
export const DECLINE_BID_SUCCESS = 'Bid successfully declined.';
diff --git a/src/Containers/BidListButton/BidListButton.jsx b/src/Containers/BidListButton/BidListButton.jsx
index e2f076bd48..f3ea1f7f25 100644
--- a/src/Containers/BidListButton/BidListButton.jsx
+++ b/src/Containers/BidListButton/BidListButton.jsx
@@ -1,26 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+import { SetType, BID_LIST } from '../../Constants/PropTypes';
import { toggleBidPosition } from '../../actions/bidList';
import BidListButton from '../../Components/BidListButton';
-import { SetType } from '../../Constants/PropTypes';
-const BidListButtonContainer = ({ toggleBid, isLoading, id, ...rest }) => (
-
- );
+const BidListButtonContainer = ({ toggleBid, isLoading, id, compareArray, ...rest }) => (
+
+);
BidListButtonContainer.propTypes = {
toggleBid: PropTypes.func.isRequired,
isLoading: SetType,
id: PropTypes.number.isRequired,
+ compareArray: BID_LIST.isRequired,
};
BidListButtonContainer.defaultProps = {
isLoading: new Set(),
+ compareArray: { results: [] },
};
export const mapStateToProps = state => ({
isLoading: state.bidListToggleIsLoading,
+ compareArray: state.bidListFetchDataSuccess,
});
export const mapDispatchToProps = dispatch => ({
diff --git a/src/Containers/Compare/Compare.jsx b/src/Containers/Compare/Compare.jsx
index 6a46ceb08d..246796925f 100644
--- a/src/Containers/Compare/Compare.jsx
+++ b/src/Containers/Compare/Compare.jsx
@@ -12,7 +12,7 @@ import { COMPARE_LIST, POSITION_SEARCH_RESULTS, BID_LIST, SetType } from '../../
import { POSITION_RESULTS_OBJECT } from '../../Constants/DefaultProps';
import { LOGIN_REDIRECT } from '../../login/routes';
-class Compare extends Component {
+export class Compare extends Component {
constructor(props) {
super(props);
this.onToggle = this.onToggle.bind(this);
diff --git a/src/Containers/Compare/Compare.test.jsx b/src/Containers/Compare/Compare.test.jsx
index 4956bd6cd1..2e1cd20962 100644
--- a/src/Containers/Compare/Compare.test.jsx
+++ b/src/Containers/Compare/Compare.test.jsx
@@ -1,11 +1,13 @@
import React from 'react';
+import sinon from 'sinon';
+import { shallow } from 'enzyme';
import TestUtils from 'react-dom/test-utils';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { testDispatchFunctions } from '../../testUtilities/testUtilities';
-import Compare, { mapDispatchToProps } from './Compare';
+import Compare, { Compare as CompareComponent, mapDispatchToProps } from './Compare';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -21,6 +23,27 @@ describe('Main', () => {
expect(compare).toBeDefined();
});
+ it('passes the correct value to getComparisons when onToggle is called', () => {
+ const compare = shallow(
+
true}
+ onNavigateTo={() => {}}
+ match={{ params: { ids: '1,2,3' } }}
+ fetchData={() => {}}
+ hasErrored={false}
+ isLoading={false}
+ fetchFavorites={() => {}}
+ fetchBidList={() => {}}
+ />,
+ );
+ const instance = compare.instance();
+ const input = '1';
+ const getComparisonsSpy = sinon.spy(instance, 'getComparisons');
+ instance.onToggle(input);
+ const exp = '2,3';
+ expect(getComparisonsSpy.getCall(0).args[0]).toBe(exp);
+ });
+
it('can handle authentication redirects', () => {
const compare = TestUtils.renderIntoDocument(
(
-
-);
+
+ );
FavoriteContainer.propTypes = {
onToggle: PropTypes.func.isRequired,
- isLoading: PropTypes.bool.isRequired,
+ isLoading: SetType,
hasErrored: PropTypes.bool.isRequired,
+ refKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string.isRequired]).isRequired,
+};
+
+FavoriteContainer.defaultProps = {
+ isLoading: new Set(),
};
export const mapStateToProps = state => ({
- isLoading: state.userProfileFavoritePositionIsLoading || false,
+ isLoading: state.userProfileFavoritePositionIsLoading,
hasErrored: state.userProfileFavoritePositionHasErrored || false,
});
diff --git a/src/Containers/Favorite/Favorite.test.jsx b/src/Containers/Favorite/Favorite.test.jsx
index 68906af49c..02cadbdcdd 100644
--- a/src/Containers/Favorite/Favorite.test.jsx
+++ b/src/Containers/Favorite/Favorite.test.jsx
@@ -6,7 +6,7 @@ import Favorite, { mapDispatchToProps } from './Favorite';
describe('Favorite', () => {
const props = {
onToggle: () => {},
- isLoading: false,
+ isLoading: new Set(),
hasErrored: false,
refKey: 'key',
compareArray: [],
diff --git a/src/Containers/Toast/Toast.jsx b/src/Containers/Toast/Toast.jsx
index 264645cb62..838851cf77 100644
--- a/src/Containers/Toast/Toast.jsx
+++ b/src/Containers/Toast/Toast.jsx
@@ -17,12 +17,13 @@ export class Toast extends Component {
}
}
- notify({ type = 'success', message = 'Message' }) { // eslint-disable-line
- let title;
- if (type === 'success') { title = 'Success'; }
- if (type === 'error') { title = 'Error'; }
+ notify({ type = 'success', message = 'Message', title = '' }) { // eslint-disable-line
+ let title$;
+ if (type === 'success') { title$ = 'Success'; }
+ if (type === 'error') { title$ = 'Error'; }
+ if (title) { title$ = title; }
toast[type](
- ,
+ ,
);
}
@@ -36,7 +37,8 @@ export class Toast extends Component {
Toast.propTypes = {
toastData: PropTypes.shape({
type: PropTypes.string,
- message: PropTypes.string,
+ message: PropTypes.node,
+ title: PropTypes.string,
}),
};
diff --git a/src/actions/favoritePositions.js b/src/actions/favoritePositions.js
index 2ac22d8bc5..e3c716b9fc 100644
--- a/src/actions/favoritePositions.js
+++ b/src/actions/favoritePositions.js
@@ -31,9 +31,9 @@ export function favoritePositionsFetchData(sortType) {
api.get(url)
.then(response => response.data)
.then((results) => {
+ dispatch(favoritePositionsFetchDataSuccess(results));
dispatch(favoritePositionsHasErrored(false));
dispatch(favoritePositionsIsLoading(false));
- dispatch(favoritePositionsFetchDataSuccess(results));
})
.catch(() => {
dispatch(favoritePositionsHasErrored(true));
diff --git a/src/actions/filters/filters.js b/src/actions/filters/filters.js
index a77752f78d..3f7d37c4d1 100644
--- a/src/actions/filters/filters.js
+++ b/src/actions/filters/filters.js
@@ -1,3 +1,4 @@
+import { union } from 'lodash';
import api from '../../api';
import { ASYNC_PARAMS, ENDPOINT_PARAMS } from '../../Constants/EndpointParams';
import { removeDuplicates } from '../../utilities';
@@ -218,7 +219,8 @@ export function filtersFetchData(items = { filters: [] }, queryParams = {}, save
api.get(`/${item.item.endpoint}`)
.then((response) => {
const itemFilter = Object.assign({}, item);
- itemFilter.data = response.data.results;
+ // We have a mix of server-supplied and hard-coded data, so we combine them with union.
+ itemFilter.data = union(response.data.results, item.initialData);
return itemFilter;
})
),
diff --git a/src/actions/filters/helpers.js b/src/actions/filters/helpers.js
index 2565d6b5a3..9bf0e27391 100644
--- a/src/actions/filters/helpers.js
+++ b/src/actions/filters/helpers.js
@@ -1,4 +1,5 @@
import { getPostName } from '../../utilities';
+import { COMMON_PROPERTIES } from '../../Constants/EndpointParams';
// Attempt to map the non-numeric grade codes to a full description.
// If no match is found, return the unmodified code.
@@ -29,7 +30,11 @@ export function getFilterCustomDescription(filterItem, filterItemObject) {
case 'bidCycle':
return filterItemObject.name;
case 'language':
- return `${filterItemObject.formal_description} (${filterItemObject.code})`;
+ // language code NONE gets displayed differently
+ return filterItemObject.code === COMMON_PROPERTIES.NULL_LANGUAGE ?
+ filterItemObject.customDescription
+ :
+ `${filterItemObject.formal_description} (${filterItemObject.code})`;
case 'grade':
return getCustomGradeDescription(filterItemObject.code);
case 'postDiff':
diff --git a/src/actions/toast.js b/src/actions/toast.js
index 749325200e..8e234b8d54 100644
--- a/src/actions/toast.js
+++ b/src/actions/toast.js
@@ -1,13 +1,15 @@
-export function toastSuccess(toast) {
+export function toastSuccess(toast, title) {
return {
type: 'TOAST_NOTIFICATION_SUCCESS',
toast,
+ title,
};
}
-export function toastError(toast) {
+export function toastError(toast, title) {
return {
type: 'TOAST_NOTIFICATION_ERROR',
toast,
+ title,
};
}
diff --git a/src/actions/userProfile.js b/src/actions/userProfile.js
index 28aa5a7bf3..24e2eb0e43 100644
--- a/src/actions/userProfile.js
+++ b/src/actions/userProfile.js
@@ -3,6 +3,8 @@ import { indexOf } from 'lodash';
import api from '../api';
import { favoritePositionsFetchData } from './favoritePositions';
+import { toastSuccess, toastError } from './toast';
+import * as SystemMessages from '../Constants/SystemMessages';
export function userProfileHasErrored(bool) {
return {
@@ -26,10 +28,10 @@ export function userProfileFetchDataSuccess(userProfile) {
}
// when adding or removing a favorite
-export function userProfileFavoritePositionIsLoading(bool) {
+export function userProfileFavoritePositionIsLoading(bool, id) {
return {
type: 'USER_PROFILE_FAVORITE_POSITION_IS_LOADING',
- userProfileFavoritePositionIsLoading: bool,
+ userProfileFavoritePositionIsLoading: { bool, id },
};
}
@@ -48,10 +50,9 @@ export function unsetUserProfile() {
}
// include an optional bypass for when we want to silently update the profile
-export function userProfileFetchData(bypass) {
+export function userProfileFetchData(bypass, cb) {
return (dispatch) => {
if (!bypass) {
- dispatch(userProfileIsLoading(true));
dispatch(userProfileHasErrored(false));
}
@@ -77,16 +78,20 @@ export function userProfileFetchData(bypass) {
};
// then perform dispatches
+ if (cb) {
+ dispatch(cb());
+ }
dispatch(userProfileFetchDataSuccess(newProfileObject));
dispatch(userProfileIsLoading(false));
dispatch(userProfileHasErrored(false));
dispatch(userProfileFavoritePositionHasErrored(false));
- dispatch(userProfileFavoritePositionIsLoading(false));
}))
.catch(() => {
+ if (cb) {
+ dispatch(cb());
+ }
dispatch(userProfileHasErrored(true));
dispatch(userProfileIsLoading(false));
- dispatch(userProfileFavoritePositionIsLoading(false));
});
};
}
@@ -107,21 +112,40 @@ export function userProfileToggleFavoritePosition(id, remove, refreshFavorites =
url: `/position/${idString}/favorite/`,
};
- dispatch(userProfileFavoritePositionIsLoading(true));
+ /**
+ * create functions for creating the action and fetching position data to supply to message
+ */
+ // action
+ const getAction = () => api(config);
+
+ // position
+ const getPosition = () => api.get(`/position/${id}/`);
+
+ dispatch(userProfileFavoritePositionIsLoading(true, id));
dispatch(userProfileFavoritePositionHasErrored(false));
- api(config)
- .then(() => {
- dispatch(userProfileFetchData(true));
- dispatch(userProfileFavoritePositionIsLoading(false));
+ axios.all([getAction(), getPosition()])
+ .then(axios.spread((action, position) => {
+ const pos = position.data;
+ const message = remove ?
+ SystemMessages.DELETE_FAVORITE_SUCCESS(pos) : SystemMessages.ADD_FAVORITE_SUCCESS(pos);
+ const title = remove ? SystemMessages.DELETE_FAVORITE_TITLE
+ : SystemMessages.ADD_FAVORITE_TITLE;
+ const cb = () => userProfileFavoritePositionIsLoading(false, id);
+ dispatch(userProfileFetchData(true, cb));
dispatch(userProfileFavoritePositionHasErrored(false));
+ dispatch(toastSuccess(message, title));
if (refreshFavorites) {
dispatch(favoritePositionsFetchData());
}
- })
+ }))
.catch(() => {
+ const message = remove ?
+ SystemMessages.DELETE_FAVORITE_ERROR() : SystemMessages.ADD_FAVORITE_ERROR();
+ const title = SystemMessages.ERROR_FAVORITE_TITLE;
+ dispatch(userProfileFavoritePositionIsLoading(false, id));
dispatch(userProfileFavoritePositionHasErrored(true));
- dispatch(userProfileFavoritePositionIsLoading(false));
+ dispatch(toastError(message, title));
});
};
}
diff --git a/src/reducers/filters/filters.js b/src/reducers/filters/filters.js
index d71d11be5b..1f0a993479 100644
--- a/src/reducers/filters/filters.js
+++ b/src/reducers/filters/filters.js
@@ -1,4 +1,4 @@
-import { ENDPOINT_PARAMS } from '../../Constants/EndpointParams';
+import { COMMON_PROPERTIES, ENDPOINT_PARAMS } from '../../Constants/EndpointParams';
// Set what filters we want to fetch
const items =
@@ -50,6 +50,15 @@ const items =
},
data: [
],
+ // Allow users to include languages with no code. This option is not supplied from
+ // the endpoint, so we define it here.
+ initialData: [
+ {
+ code: COMMON_PROPERTIES.NULL_LANGUAGE,
+ short_description: 'No language requirement',
+ custom_description: 'No language requirement',
+ },
+ ],
},
{
item: {
diff --git a/src/reducers/toast/toast.js b/src/reducers/toast/toast.js
index 6ef75182c5..ae92449461 100644
--- a/src/reducers/toast/toast.js
+++ b/src/reducers/toast/toast.js
@@ -1,9 +1,9 @@
-export default function toast(state = { type: 'success', message: '' }, action) {
+export default function toast(state = { type: 'success', message: '', title: '' }, action) {
switch (action.type) {
case 'TOAST_NOTIFICATION_SUCCESS':
- return { type: 'success', message: action.toast };
+ return { type: 'success', message: action.toast, title: action.title };
case 'TOAST_NOTIFICATION_ERROR':
- return { type: 'error', message: action.toast };
+ return { type: 'error', message: action.toast, title: action.title };
default:
return state;
}
diff --git a/src/reducers/userProfile/userProfile.js b/src/reducers/userProfile/userProfile.js
index cfc9cc3703..01863f3da3 100644
--- a/src/reducers/userProfile/userProfile.js
+++ b/src/reducers/userProfile/userProfile.js
@@ -32,10 +32,16 @@ export function userProfileFavoritePositionHasErrored(state = false, action) {
return state;
}
}
-export function userProfileFavoritePositionIsLoading(state = false, action) {
+export function userProfileFavoritePositionIsLoading(state = new Set(), action) {
+ const newSet = new Set(state);
switch (action.type) {
case 'USER_PROFILE_FAVORITE_POSITION_IS_LOADING':
- return action.userProfileFavoritePositionIsLoading;
+ if (action.userProfileFavoritePositionIsLoading.bool) {
+ newSet.add(action.userProfileFavoritePositionIsLoading.id);
+ return newSet;
+ }
+ newSet.delete(action.userProfileFavoritePositionIsLoading.id);
+ return newSet;
default:
return state;
}
diff --git a/src/reducers/userProfile/userProfile.test.js b/src/reducers/userProfile/userProfile.test.js
index 20afea3552..828acf2c92 100644
--- a/src/reducers/userProfile/userProfile.test.js
+++ b/src/reducers/userProfile/userProfile.test.js
@@ -14,10 +14,14 @@ describe('reducers', () => {
});
it('can set reducer USER_PROFILE_FAVORITE_POSITION_HAS_ERRORED', () => {
- expect(reducers.userProfileFavoritePositionHasErrored(false, { type: 'USER_PROFILE_FAVORITE_POSITION_HAS_ERRORED', userProfileFavoritePositionHasErrored: true })).toBe(true);
+ expect(reducers.userProfileFavoritePositionHasErrored(new Set(), { type: 'USER_PROFILE_FAVORITE_POSITION_HAS_ERRORED', userProfileFavoritePositionHasErrored: true })).toBe(true);
});
- it('can set reducer USER_PROFILE_FAVORITE_POSITION_IS_LOADING', () => {
- expect(reducers.userProfileFavoritePositionIsLoading(false, { type: 'USER_PROFILE_FAVORITE_POSITION_IS_LOADING', userProfileFavoritePositionIsLoading: true })).toBe(true);
+ it('can set reducer USER_PROFILE_FAVORITE_POSITION_IS_LOADING to add an id', () => {
+ expect(reducers.userProfileFavoritePositionIsLoading(new Set(), { type: 'USER_PROFILE_FAVORITE_POSITION_IS_LOADING', userProfileFavoritePositionIsLoading: { bool: true, id: 1 } })).toEqual(new Set([1]));
+ });
+
+ it('can set reducer USER_PROFILE_FAVORITE_POSITION_IS_LOADING to remove an id', () => {
+ expect(reducers.userProfileFavoritePositionIsLoading(new Set([1]), { type: 'USER_PROFILE_FAVORITE_POSITION_IS_LOADING', userProfileFavoritePositionIsLoading: { bool: false, id: 1 } })).toEqual(new Set());
});
});
diff --git a/src/sass/_bidListButton.scss b/src/sass/_bidListButton.scss
index f599c23191..46b13495f6 100644
--- a/src/sass/_bidListButton.scss
+++ b/src/sass/_bidListButton.scss
@@ -1,5 +1,4 @@
.bid-list-button {
- padding: 1.5rem 4rem;
.button-icon {
color: $color-white;
diff --git a/src/sass/_bidTracker.scss b/src/sass/_bidTracker.scss
index 4c98d85f19..20a62bce02 100644
--- a/src/sass/_bidTracker.scss
+++ b/src/sass/_bidTracker.scss
@@ -593,7 +593,14 @@ $draft-icon-offset: 120px;
}
.bid-tracker-standby-container {
+ $link-color: #2B4559;
+
display: flex;
+ opacity: .75; // lowest opacity without violating 4.5:1 contrast ratio
+
+ a:link {
+ color: $link-color; // opacity lightens normal link color below the required 4.5:1 ratio
+ }
.bid-tracker-standby-inner-container {
display: flex;
diff --git a/src/sass/_buttons.scss b/src/sass/_buttons.scss
index 134dc00143..c39ccac233 100644
--- a/src/sass/_buttons.scss
+++ b/src/sass/_buttons.scss
@@ -144,8 +144,8 @@
&.button-text-hidden {
margin-top: 0;
padding-bottom: 11.5px;
- padding-left: 15px;
- padding-right: 15px;
+ padding-left: 14px;
+ padding-right: 14px;
padding-top: 11.5px;
.spinner-white,
@@ -155,6 +155,7 @@
.fa {
margin-right: 0;
+ padding-right: 0;
}
}
diff --git a/src/sass/_compare.scss b/src/sass/_compare.scss
index 8fbf3322a3..b3107840a7 100644
--- a/src/sass/_compare.scss
+++ b/src/sass/_compare.scss
@@ -10,10 +10,6 @@
.bid-list-button {
min-width: 100%;
padding: .75em 1em;
-
- .button-icon {
- display: none;
- }
}
.post-data-button {
diff --git a/src/sass/_condensedCard.scss b/src/sass/_condensedCard.scss
index dc22327777..b5d54c35ab 100644
--- a/src/sass/_condensedCard.scss
+++ b/src/sass/_condensedCard.scss
@@ -67,15 +67,27 @@ $condensed-card-data-padding: 15px 20px 9px;
background-color: $blue-primary;
border-bottom: 1px solid $color-gray-lightest;
color: $color-white;
- overflow: hidden;
padding: $condensed-card-padding;
position: relative;
+ width: 100%;
+
+ a,
+ .data {
+ font-weight: 300;
+ }
a {
color: $color-white;
}
}
+ .post-ribbon-container {
+ display: flex;
+
+ div:nth-of-type(1) { flex: 3; }
+ div:nth-of-type(2) { flex: 2; }
+ }
+
.card-top-alternate {
background-color: $tm-navy;
}
@@ -96,10 +108,14 @@ $condensed-card-data-padding: 15px 20px 9px;
.condensed-card-top-header-left {
float: left;
+ a {
+ display: inline-block;
+ }
+
h3 {
display: inline;
font-family: inherit;
- font-size: inherit;
+ font-size: 1.1em;
font-weight: inherit;
line-height: inherit;
margin-bottom: inherit;
@@ -132,26 +148,48 @@ $condensed-card-data-padding: 15px 20px 9px;
.condensed-card-bottom-container {
background-color: $color-white;
+ flex-grow: 1;
padding: $condensed-card-data-padding;
padding-bottom: 5px;
position: relative;
}
.condensed-card-inner {
+ display: flex;
+ flex-direction: column;
height: 100%;
position: relative;
}
.condensed-card-bottom {
+ display: flex;
+ flex-direction: column;
font-size: 16px;
+ height: 100%;
}
.condensed-card-buttons-section {
margin-bottom: 10px;
+ margin-left: 0;
+ margin-right: 0;
+
+ button {
+ height: 44px;
+ margin-top: 0;
+ }
+ }
+
+ .condensed-card-statistics-inner {
+ margin-left: 0;
+ margin-right: 0;
}
.condensed-card-statistics {
border-top: 0;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ justify-content: flex-end;
position: relative;
.bid-count-list-item {
@@ -182,8 +220,11 @@ $condensed-card-data-padding: 15px 20px 9px;
}
.condensed-card-data {
+ flex-grow: 1;
line-height: 1.4em;
margin-bottom: 20px;
+ margin-left: 0;
+ margin-right: 0;
a {
color: $blue-primary;
@@ -302,4 +343,3 @@ $condensed-card-data-padding: 15px 20px 9px;
display: flex;
flex-wrap: wrap;
}
-
diff --git a/src/sass/_details.scss b/src/sass/_details.scss
index 0ed78f9452..27296267f4 100644
--- a/src/sass/_details.scss
+++ b/src/sass/_details.scss
@@ -1,3 +1,13 @@
+.position-details-outer-container {
+ position: relative;
+
+ .handshake-offset-container {
+ left: -3px;
+ position: absolute;
+ top: -37px;
+ }
+}
+
.position-details-header-container {
color: $color-white;
position: relative;
diff --git a/src/sass/_ribbon.scss b/src/sass/_ribbon.scss
new file mode 100644
index 0000000000..ddaa3b4c14
--- /dev/null
+++ b/src/sass/_ribbon.scss
@@ -0,0 +1,60 @@
+$ribbon-shadow-color: rgba(112, 112, 112, .5);
+
+.ribbon {
+ color: $color-black;
+ display: flex;
+ font-family: 'Merriweather';
+ font-size: 1.3rem;
+ font-weight: bold;
+ padding-bottom: 5px;
+ padding-top: 6px;
+
+ .fa {
+ font-size: 1.8rem;
+ margin-right: .5em;
+ }
+
+ .text {
+ margin-top: .12em;
+ }
+}
+
+.ribbon-primary {
+ background-color: $tertiary-gold-lighter;
+}
+
+.ribbon-secondary {
+ background-color: $blue-primary-darkest;
+ color: $color-white;
+}
+
+.ribbon-outer-container-cut-left {
+ box-shadow: 3px 4px 4px -4px $ribbon-shadow-color;
+}
+
+.ribbon-outer-container-cut-right {
+ box-shadow: -3px 4px 4px -4px $ribbon-shadow-color;
+}
+
+.ribbon-cut-left {
+ clip-path: polygon(9% 0, 100% 0, 100% 100%, 0 100%, 0 100%);
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+.ribbon-cut-right {
+ clip-path: polygon(0 0, 100% 0, 91% 100%, 0 100%, 0 100%);
+ padding-left: .5em;
+ padding-right: 1em;
+}
+
+.ribbon-results-card {
+ position: absolute;
+ right: -1px;
+}
+
+.ribbon-condensed-card {
+ position: absolute;
+ right: 0;
+ z-index: $ribbon-z;
+}
diff --git a/src/sass/_variables.scss b/src/sass/_variables.scss
index 027a206732..0c708ad074 100644
--- a/src/sass/_variables.scss
+++ b/src/sass/_variables.scss
@@ -161,13 +161,14 @@ $tm-focus-outline-offset-default: 3px;
//* z-index
//*
// we'll use this section to keep track of and order them
-$autosuggest-suggestions-container-open-z: 2;
+$autosuggest-suggestions-container-open-z: 20;
$dropdown-content-z: 15000;
$feedback-z: 11000;
$feedback-button-z: 10000;
$scrollbar-z: 0;
$skill-code-dropdown-z: 10500;
$spinner-z: 100;
+$ribbon-z: 10;
//**
// other variables
diff --git a/src/sass/styles.scss b/src/sass/styles.scss
index f45bd109a2..c81858d9ff 100644
--- a/src/sass/styles.scss
+++ b/src/sass/styles.scss
@@ -82,5 +82,6 @@
@import 'compareDrawer';
@import 'inputs';
@import 'toast';
+@import 'ribbon';
@import 'overrides';
@import 'accessibility';
diff --git a/yarn.lock b/yarn.lock
index 5c61eef9e2..5dadd06ea7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2310,6 +2310,11 @@ crypto-random-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
+css-box-shadow@^1.0.0-3:
+ version "1.0.0-3"
+ resolved "https://registry.yarnpkg.com/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz#9eaeb7140947bf5d649fc49a19e4bbaa5f602713"
+ integrity sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==
+
css-color-names@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"