diff --git a/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap b/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap index 8ec048cd49..4d015bc53b 100644 --- a/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap @@ -2,7 +2,7 @@ exports[`Default render 1`] = `
+ + 066D371D-4870-4B72-8053-753A70C9C648 + + + + + + \ No newline at end of file diff --git a/src/assets/images/tc-edu/icon-filter-arrow.png b/src/assets/images/tc-edu/icon-filter-arrow.png new file mode 100644 index 0000000000..e3c38f8489 Binary files /dev/null and b/src/assets/images/tc-edu/icon-filter-arrow.png differ diff --git a/src/shared/components/Contentful/Article/themes/default.scss b/src/shared/components/Contentful/Article/themes/default.scss index 5b9b4b32a6..9078820319 100644 --- a/src/shared/components/Contentful/Article/themes/default.scss +++ b/src/shared/components/Contentful/Article/themes/default.scss @@ -430,6 +430,8 @@ .site-header-background { width: 100%; + height: calc(100vw * (9 / 16)); + max-height: 565px; } } diff --git a/src/shared/components/Contentful/SearchPageFilter/FilterAuthor/index.jsx b/src/shared/components/Contentful/SearchPageFilter/FilterAuthor/index.jsx index 8d29acf250..e9a5dc0e91 100644 --- a/src/shared/components/Contentful/SearchPageFilter/FilterAuthor/index.jsx +++ b/src/shared/components/Contentful/SearchPageFilter/FilterAuthor/index.jsx @@ -58,6 +58,9 @@ export class FilterAuthorInner extends Component { isShowPopup, } = this.state; + // sort by author name + options.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + return (
Author diff --git a/src/shared/components/Contentful/TracksFilter/TracksAuthor/index.jsx b/src/shared/components/Contentful/TracksFilter/TracksAuthor/index.jsx index 1a2cd91352..75dc52511e 100644 --- a/src/shared/components/Contentful/TracksFilter/TracksAuthor/index.jsx +++ b/src/shared/components/Contentful/TracksFilter/TracksAuthor/index.jsx @@ -6,7 +6,7 @@ import PT from 'prop-types'; import React, { Component } from 'react'; import { themr } from 'react-css-super-themr'; -import IconArrow from 'assets/images/tc-edu/icon-arrow-up-big.svg'; +import iconDown from 'assets/images/dropdown-arrow.png'; import defaultTheme from './themes/default.scss'; @@ -58,9 +58,12 @@ export class TracksAuthorInner extends Component { isShowPopup, } = this.state; + // sort by author name + options.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + return (
- Author + Authors {isShowPopup && (
diff --git a/src/shared/components/Contentful/TracksFilter/TracksAuthor/themes/default.scss b/src/shared/components/Contentful/TracksFilter/TracksAuthor/themes/default.scss index 7d19d0c896..46477452e0 100644 --- a/src/shared/components/Contentful/TracksFilter/TracksAuthor/themes/default.scss +++ b/src/shared/components/Contentful/TracksFilter/TracksAuthor/themes/default.scss @@ -8,14 +8,13 @@ $text-black: #2a2a2a; display: flex; flex-direction: row; color: $text-black; - background-color: #f4f4f4; border-radius: 6px; width: 100%; border: 1px solid #aaa; position: relative; padding: 0 15px; max-width: 220px; - height: 38px; + height: 40px; align-items: center; } @@ -26,7 +25,7 @@ $text-black: #2a2a2a; padding: 0 7px; left: 8px; top: -7px; - background: #f4f4f4; + background: #fff; } .header { @@ -42,18 +41,15 @@ $text-black: #2a2a2a; @include min-ellipsis; font-size: 14px; + color: #2a2a2a !important; } } .icon-arrow { - -moz-transform: scale(-1, -1); - -o-transform: scale(-1, -1); - -webkit-transform: scale(-1, -1); - transform: scale(-1, -1); - - polygon { - fill: #aaa; - } + -moz-transform: scale(1, -1); + -o-transform: scale(1, -1); + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } .popup { diff --git a/src/shared/components/Contentful/TracksFilter/TracksDate/index.jsx b/src/shared/components/Contentful/TracksFilter/TracksDate/index.jsx index 3cf740d99c..bbf0eacb0c 100644 --- a/src/shared/components/Contentful/TracksFilter/TracksDate/index.jsx +++ b/src/shared/components/Contentful/TracksFilter/TracksDate/index.jsx @@ -5,14 +5,9 @@ import PT from 'prop-types'; import moment from 'moment'; import React from 'react'; import { themr } from 'react-css-super-themr'; -import DatePicker from 'components/challenge-listing/Filters/DatePicker'; -import CalendarWeek from 'react-dates/lib/components/CalendarWeek'; -import IconCalendar from 'assets/images/tc-edu/icon-calendar.svg'; +import Datepicker from 'components/GUIKit/Datepicker'; import defaultTheme from './themes/default.scss'; -// eslint-disable-next-line no-unused-expressions, react/forbid-foreign-prop-types -CalendarWeek && CalendarWeek.propTypes && delete CalendarWeek.propTypes.children; // fixing the bug in react-dates, more detail in here https://github.com/airbnb/react-dates/issues/1121 - export function TracksDateInner(props) { const { @@ -26,23 +21,20 @@ export function TracksDateInner(props) { return (
- Date - - { onSelectStartDate(date); }} + { onSelectStartDate(date); }} + size="xs" + isOutsideRange={day => moment().isSameOrBefore(day)} /> - - - - { onSelectEndDate(date); }} +
+ { onSelectEndDate(date); }} + size="xs" + isOutsideRange={day => moment().isSameOrBefore(day)} />
); @@ -52,14 +44,13 @@ TracksDateInner.defaultProps = { className: '', startDate: null, endDate: null, - onSelectStartDate: () => {}, - onSelectEndDate: () => {}, + onSelectStartDate: () => { }, + onSelectEndDate: () => { }, }; TracksDateInner.propTypes = { theme: PT.shape({ container: PT.string.isRequired, - title: PT.string.isRequired, separator: PT.string.isRequired, }).isRequired, className: PT.string, diff --git a/src/shared/components/Contentful/TracksFilter/TracksDate/themes/default.scss b/src/shared/components/Contentful/TracksFilter/TracksDate/themes/default.scss index 13107405f1..86da928101 100644 --- a/src/shared/components/Contentful/TracksFilter/TracksDate/themes/default.scss +++ b/src/shared/components/Contentful/TracksFilter/TracksDate/themes/default.scss @@ -13,55 +13,23 @@ $text-black: #2a2a2a; @include roboto-regular; display: flex; + flex: 0 0 270px; flex-direction: row; color: $text-black; - background-color: #f4f4f4; - border-radius: 6px; - border: 1px solid #aaa; position: relative; - padding: 0 15px; min-width: 245px; align-items: center; font-size: 14px; - height: 38px; - - :global { - .SingleDatePicker { - width: 105px; - margin-left: 12px; - } - - input { - height: 21px !important; - margin: 0 !important; - padding: 0 !important; - border: none !important; - box-shadow: none !important; - background-color: #f4f4f4 !important; - color: #2a2a2a !important; - font-size: 16px !important; - } - - @include xs-to-sm { - .SingleDatePicker { - width: 105px; - margin-left: 6px; - } - } - } -} - -.title { - position: absolute; - color: #aaa; - font-size: 12px; - padding: 0 7px; - left: 8px; - top: -7px; - background: #f4f4f4; } .separator { - margin-left: 0; - margin-right: 12px; + margin: 0 6px; + width: 6px; + height: 1px; + background-color: #aaa; + + @include xs-to-sm { + width: 30px; + background-color: #fff; + } } diff --git a/src/shared/components/Contentful/TracksFilter/TracksFilter.jsx b/src/shared/components/Contentful/TracksFilter/TracksFilter.jsx index 77cd842919..432a25d596 100644 --- a/src/shared/components/Contentful/TracksFilter/TracksFilter.jsx +++ b/src/shared/components/Contentful/TracksFilter/TracksFilter.jsx @@ -1,3 +1,5 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable react/destructuring-assignment */ /** * The core tracks filter */ @@ -7,10 +9,13 @@ import PT from 'prop-types'; import React, { Component } from 'react'; import { themr } from 'react-css-super-themr'; import { getService } from 'services/contentful'; +import Dropdown from 'components/GUIKit/Dropdown'; +import MediaQuery from 'react-responsive'; import IconCloseBig from 'assets/images/tc-edu/icon-close-big.svg'; +import IconClearFilter from 'assets/images/tc-edu/icon-clear-filter.svg'; import TracksTags from './TracksTags'; -import TracksAuthor from './TracksAuthor'; +// import TracksAuthor from './TracksAuthor'; import TracksDate from './TracksDate'; import defaultTheme from './themes/default.scss'; @@ -25,6 +30,7 @@ export class TracksFilterInner extends Component { tags: props.tags, startDate: props.startDate, endDate: props.endDate, + sortBy: props.sortBy, }; // create a service to work with Contentful @@ -43,7 +49,10 @@ export class TracksFilterInner extends Component { if (results.total) { const { authorList } = this.state; this.setState({ - authorList: _.concat(authorList, _.map(results.items, item => item.fields.name)), + authorList: _.concat(authorList, _.map( + _.sortBy(results.items, i => i.fields.name.toLowerCase()), + item => ({ label: item.fields.name, selected: false }), + )), }); } }); @@ -52,12 +61,23 @@ export class TracksFilterInner extends Component { /** * Reset filter form to init value */ - onReset() { + onReset(isMobile) { + const { + sortBy, + onApply, + } = this.props; this.setState({ selectedAuthor: DEF_SELECTED_AUTHOR, tags: [], - startDate: moment().subtract(1, 'months'), + startDate: moment('2001-01-02'), endDate: moment(), + sortBy: sortBy.map((o) => { + // eslint-disable-next-line no-param-reassign + o.selected = o.label === 'Content Publish Date'; + return o; + }), + }, () => { + if (!isMobile) onApply(this.state); }); } @@ -74,55 +94,114 @@ export class TracksFilterInner extends Component { startDate, endDate, tags, + sortBy, } = this.state; + // selected author + const updatedAuthorList = authorList.map((a) => { + a.selected = a.label === selectedAuthor; + return a; + }); return (
-
+
filter +
- { - this.setState(prevState => ({ - tags: [...prevState.tags, tag], - })); - }} - onRemoveTag={(index) => { - const newTags = _.cloneDeep(tags); - if (index !== -1) { - newTags.splice(index, 1); - this.setState({ tags: newTags }); - } - }} - /> + + {mediaMatches => ( + { + this.setState(prevState => ({ + tags: [...prevState.tags, tag], + }), () => (mediaMatches ? onApply(this.state) : null)); + }} + onRemoveTag={(index) => { + const newTags = _.cloneDeep(tags); + if (index !== -1) { + newTags.splice(index, 1); + this.setState( + { tags: newTags }, + () => (mediaMatches ? onApply(this.state) : null), + ); + } + }} + /> + )} +
- { this.setState({ selectedAuthor: item }); }} - /> - { this.setState({ startDate: date }); }} - onSelectEndDate={(date) => { this.setState({ endDate: date }); }} - /> +
+ + {mediaMatches => ( + { + this.setState( + { selectedAuthor: _.find(authors, { selected: true }).label }, + () => (mediaMatches ? onApply(this.state) : null), + ); + }} + /> + )} + +
+ + {mediaMatches => ( + { + this.setState({ startDate: date ? moment(date) : null }, + () => (mediaMatches ? onApply(this.state) : null)); + }} + onSelectEndDate={(date) => { + this.setState({ endDate: date ? moment(date) : null }, + () => (mediaMatches ? onApply(this.state) : null)); + }} + /> + )} + +
+ + {mediaMatches => ( + { + this.setState( + { sortBy: newSortBy }, + () => (mediaMatches ? onApply(this.state) : null), + ); + }} + /> + )} + +
-
+
@@ -152,10 +232,11 @@ TracksFilterInner.defaultProps = { onClose: () => {}, onApply: () => {}, selectedAuthor: DEF_SELECTED_AUTHOR, - authorList: [DEF_SELECTED_AUTHOR], + authorList: [{ label: DEF_SELECTED_AUTHOR, selected: true }], startDate: moment('2001-01-02'), endDate: moment(), tags: [], + sortBy: [], }; TracksFilterInner.propTypes = { @@ -166,9 +247,12 @@ TracksFilterInner.propTypes = { 'author-date-container': PT.string.isRequired, 'track-date-container': PT.string.isRequired, 'track-author-container': PT.string.isRequired, + 'track-sortBy': PT.string.isRequired, 'btn-reset': PT.string.isRequired, 'btn-apply': PT.string.isRequired, 'is-mobile': PT.string.isRequired, + 'is-mobile-hidden': PT.string.isRequired, + 'clear-filter': PT.string.isRequired, }).isRequired, onClose: PT.func, onApply: PT.func, @@ -177,6 +261,12 @@ TracksFilterInner.propTypes = { endDate: PT.instanceOf(moment), authorList: PT.arrayOf(PT.string), tags: PT.arrayOf(PT.string), + sortBy: PT.arrayOf( + PT.shape({ + label: PT.string.isRequired, + selected: PT.bool.isRequired, + }), + ), }; export default themr('Contentful-Blog', defaultTheme)(TracksFilterInner); diff --git a/src/shared/components/Contentful/TracksFilter/TracksTags/index.jsx b/src/shared/components/Contentful/TracksFilter/TracksTags/index.jsx index 2c8d2e6abb..7531e8cd4a 100644 --- a/src/shared/components/Contentful/TracksFilter/TracksTags/index.jsx +++ b/src/shared/components/Contentful/TracksFilter/TracksTags/index.jsx @@ -57,7 +57,6 @@ export class TracksTagsInner extends Component { }; return (
- Tags
{ _.map(tags, (option, index) => ( @@ -85,7 +84,7 @@ export class TracksTagsInner extends Component { }} type="text" className={theme['tags-field']} - placeholder="Add tags to filter..." + placeholder="Add tags to filter content..." />
diff --git a/src/shared/components/Contentful/TracksFilter/TracksTags/themes/default.scss b/src/shared/components/Contentful/TracksFilter/TracksTags/themes/default.scss index 92c0990a5b..cd4b294e29 100644 --- a/src/shared/components/Contentful/TracksFilter/TracksTags/themes/default.scss +++ b/src/shared/components/Contentful/TracksFilter/TracksTags/themes/default.scss @@ -8,7 +8,6 @@ $text-black: #2a2a2a; display: flex; flex-direction: row; color: $text-black; - background-color: #f4f4f4; border-radius: 6px; width: 100%; border: 1px solid #aaa; @@ -25,35 +24,24 @@ $text-black: #2a2a2a; flex-wrap: wrap; } -.title { - position: absolute; - color: #aaa; - font-size: 12px; - padding: 0 7px; - left: 8px; - top: -7px; - background: #f4f4f4; -} - .item { @include min-ellipsis; display: flex; padding-left: 13px; - height: 30px; + height: 26px; align-items: center; - background: #fff; - border-radius: 15px; - margin-top: 11px; + background: #e0e0e0; + border-radius: 5px; + margin-top: 6px; margin-right: 5px; max-width: 100%; padding-bottom: 2px; - border: 1px solid #2a2a2a; span { @include min-ellipsis; - font-size: 14px; + font-size: 12px; line-height: normal; } } @@ -63,6 +51,10 @@ $text-black: #2a2a2a; height: 100%; padding-left: 16px !important; padding-right: 13px !important; + + svg > path { + fill: #2a2a2a; + } } .tags-field { @@ -72,23 +64,15 @@ $text-black: #2a2a2a; height: 30px !important; width: auto !important; flex: 1; - margin: 0; + margin: 4px 0 !important; padding: 0 10px; - margin-top: 11px; min-width: 50px; - background: #f4f4f4 !important; color: #2a2a2a !important; font-size: 16px !important; &::placeholder { color: #aaa; text-transform: none; - font-size: 16px; - } -} - -@include xs-to-sm { - .container { - min-height: 192px; + font-size: 14px; } } diff --git a/src/shared/components/Contentful/TracksFilter/themes/default.scss b/src/shared/components/Contentful/TracksFilter/themes/default.scss index 31cd94de68..a4b1c48baa 100644 --- a/src/shared/components/Contentful/TracksFilter/themes/default.scss +++ b/src/shared/components/Contentful/TracksFilter/themes/default.scss @@ -9,11 +9,15 @@ $green-color: #137d60; display: flex; flex-direction: column; color: $text-black; - background-color: #f4f4f4; - border-radius: 10px; width: 100%; - padding: 31px 31px 28px 30px; - margin-top: 14px; + margin-top: 16px; + border-top: 1px solid #e9e9e9; + padding-top: 21px; + background-color: #fff; + + @include xs-to-sm { + border-top: none; + } button { background: transparent; @@ -28,16 +32,25 @@ $green-color: #137d60; display: none !important; } +.is-mobile-hidden { + display: flex; +} + .author-date-container { display: flex; margin-top: 31px; - margin-bottom: 31px; + margin-bottom: 57px; } .track-date-container { margin-left: 20px; } +.track-sortBy { + margin-left: 34px; + flex: 1; +} + .bottom { display: flex; margin-top: auto; @@ -69,41 +82,45 @@ $green-color: #137d60; margin-left: 15px !important; } +.header { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; + margin-bottom: 21px; + + span { + @include barlow-semi-bold; + + font-size: 20px; + line-height: 24px; + text-transform: uppercase; + } + + button { + background: $tc-white; + } +} + @include xs-to-sm { .is-mobile { display: flex !important; } + .is-mobile-hidden { + display: none !important; + } + .container { padding: 20px 20px 30px; box-shadow: none; position: fixed; - top: 0; + bottom: 0; left: 0; - height: 100%; + max-height: 100%; overflow-y: auto; z-index: 999999; - } - - .header { - display: flex; - width: 100%; - justify-content: space-between; - align-items: center; - margin-bottom: 39px; - - span { - @include barlow-semi-bold; - - font-size: 24px; - letter-spacing: 0.2px; - line-height: 26px; - text-transform: uppercase; - } - - button { - background: $tc-white; - } + border-radius: 10px 10px 0 0; } .author-date-container { @@ -111,12 +128,27 @@ $green-color: #137d60; } .track-author-container { - max-width: none; + min-width: 250px; } .track-date-container { + margin: 31px 0; + flex: auto; + } + + .track-sortBy { margin-left: 0; - margin-top: 31px; - max-width: 273px; } } + +.clear-filter { + color: #229174; + font-size: 15px; + font-weight: 500; + letter-spacing: 1px; + line-height: 17.58px; + text-transform: uppercase; + display: flex; + align-items: center; + font-family: Roboto, sans-serif; +} diff --git a/src/shared/components/GUIKit/Datepicker/index.jsx b/src/shared/components/GUIKit/Datepicker/index.jsx index 5cf376d7ef..3ac949e4c7 100644 --- a/src/shared/components/GUIKit/Datepicker/index.jsx +++ b/src/shared/components/GUIKit/Datepicker/index.jsx @@ -2,7 +2,7 @@ /** * Datepicker component. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PT from 'prop-types'; import moment from 'moment'; import './style.scss'; @@ -25,13 +25,19 @@ function Datepicker({ onChange, errorMsg, required, + size, + isOutsideRange, }) { + const sizeStyle = size === 'lg' ? 'lgSize' : 'xsSize'; const [date, setDate] = useState(value ? moment(value) : null); const [focused, setFocused] = useState(false); const { width } = useWindowSize(); + useEffect(() => { + setDate(value ? moment(value) : null); + }, [value]); return (
@@ -58,6 +64,7 @@ function Datepicker({ enableOutsideDays firstDayOfWeek={1} weekDayFormat="ddd" + isOutsideRange={isOutsideRange} /> {label ? ( @@ -77,6 +84,8 @@ Datepicker.defaultProps = { onChange: () => {}, errorMsg: '', required: false, + size: 'lg', + isOutsideRange: day => moment().isSameOrAfter(day), }; Datepicker.propTypes = { @@ -86,6 +95,8 @@ Datepicker.propTypes = { onChange: PT.func, errorMsg: PT.string, required: PT.bool, + size: PT.oneOf(['xs', 'lg']), + isOutsideRange: PT.func, }; export default Datepicker; diff --git a/src/shared/components/GUIKit/Datepicker/style.scss b/src/shared/components/GUIKit/Datepicker/style.scss index 320798b212..88a1f20494 100644 --- a/src/shared/components/GUIKit/Datepicker/style.scss +++ b/src/shared/components/GUIKit/Datepicker/style.scss @@ -1,5 +1,9 @@ @import '~components/GUIKit/Assets/Styles/default'; +.errorMessage { + @include errorMessage; +} + .container { position: relative; display: flex; @@ -344,8 +348,43 @@ } } } -} -.errorMessage { - @include errorMessage; + // lg size + &.lgSize { + display: flex; + } + + // xs size + &.xsSize { + &.haveValue .label, + &.isFocused .label { + margin-top: -12px; + } + + :global { + .SingleDatePickerInput { + .DateInput { + input { + margin-top: 0; + height: 40px; + font-size: 14px; + padding: 10px 15px 8px; + } + } + } + + .SingleDatePickerInput_calendarIcon { + top: 3px; + width: 38px !important; + } + + .SingleDatePicker_picker { + top: 40px !important; + } + } + + .errorMessage { + @include errorMessageXs; + } + } } diff --git a/src/shared/components/GUIKit/Dropdown/index.jsx b/src/shared/components/GUIKit/Dropdown/index.jsx index 082cba1d5c..67b36fb51c 100644 --- a/src/shared/components/GUIKit/Dropdown/index.jsx +++ b/src/shared/components/GUIKit/Dropdown/index.jsx @@ -2,7 +2,7 @@ /** * Dropdown component. */ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import PT from 'prop-types'; import _ from 'lodash'; import ReactSelect from 'react-select'; @@ -27,7 +27,9 @@ function Dropdown({ _.debounce((q, cb) => cb(q), config.GUIKIT.DEBOUNCE_ON_CHANGE_TIME), ).current; const sizeStyle = size === 'lg' ? 'lgSize' : 'xsSize'; - + useEffect(() => { + setInternalOptions(options); + }, [options]); return (
setFocused(true)} diff --git a/src/shared/components/GUIKit/Dropdown/style.scss b/src/shared/components/GUIKit/Dropdown/style.scss index 79a2fc4451..5af4148882 100644 --- a/src/shared/components/GUIKit/Dropdown/style.scss +++ b/src/shared/components/GUIKit/Dropdown/style.scss @@ -188,6 +188,13 @@ // xs size &.xsSize { + padding-top: 0; + + &.haveValue .label, + &.isFocused .label { + margin-top: -12px; + } + :global { .Select-control { height: 40px; diff --git a/src/shared/containers/EDU/Tracks.jsx b/src/shared/containers/EDU/Tracks.jsx index 8ede19d331..2d01ccd850 100644 --- a/src/shared/containers/EDU/Tracks.jsx +++ b/src/shared/containers/EDU/Tracks.jsx @@ -1,3 +1,4 @@ +/* eslint-disable no-plusplus */ /** * Container for EDU Portal tracks page. */ @@ -15,6 +16,7 @@ import qs from 'qs'; import TracksTree from 'components/Contentful/TracksTree/TracksTree'; import LoadingIndicator from 'components/LoadingIndicator'; import TracksFilter from 'components/Contentful/TracksFilter/TracksFilter'; +import MediaQuery from 'react-responsive'; // SVGs & Assets import Dev from 'assets/images/img-development.png'; import Design from 'assets/images/img_design.png'; @@ -23,6 +25,7 @@ import Algo from 'assets/images/img-algorithm.png'; import QA from 'assets/images/img-QA.png'; import Topcoder from 'assets/images/img-Topcoder.png'; import GigWork from 'assets/images/img-gig-work.png'; +import iconFilterArrow from 'assets/images/tc-edu/icon-filter-arrow.png'; // Partials import ResultTabs from './partials/ResultTabs'; // CSS @@ -46,6 +49,10 @@ const TRACK_IMAGES = { Topcoder, 'Gig Work': GigWork, }; +const SORT_BY_OPTIONS = [ + { label: 'Content Publish Date', selected: true }, + { label: 'Likes', selected: false }, +]; export default class EDUTracks extends React.Component { constructor(props) { @@ -56,9 +63,10 @@ export default class EDUTracks extends React.Component { this.state = { query: { tags: [], + sortBy: 'Content Publish Date', }, tree: [], - isShowFilter: false, + isShowFilter: true, }; // bindings this.onTreeClick = this.onTreeClick.bind(this); @@ -66,6 +74,7 @@ export default class EDUTracks extends React.Component { } componentDidMount() { + const { query } = this.state; // set current query let urlQuery = {}; if (isomorphy.isClientSide()) { @@ -73,16 +82,21 @@ export default class EDUTracks extends React.Component { // eslint-disable-next-line no-nested-ternary urlQuery.tags = _.isArray(urlQuery.tags) ? urlQuery.tags : (urlQuery.tags ? [urlQuery.tags] : []); + if (urlQuery.startDate) urlQuery.startDate = moment(urlQuery.startDate).format(); + if (urlQuery.endDate) urlQuery.endDate = moment(urlQuery.endDate).format(); this.setState({ - query: urlQuery, + query: { + ...query, + ...urlQuery, + }, + isShowFilter: window.screen.width >= 769, }); } // Fire API requests // Get the EDU taxonomy this.apiService.getEDUTaxonomy() .then((taxonomy) => { - const { query } = this.state; - const tree = tracksTreeBuilder(taxonomy, query); + const tree = tracksTreeBuilder(taxonomy, urlQuery); this.setState({ tree, taxonomy, @@ -131,18 +145,23 @@ export default class EDUTracks extends React.Component { }), }); // Update the url query + const urlQuery = qs.parse(window.location.search.slice(1)); updateQuery({ + ...urlQuery, track, tax, }); } onApplyFilter(filterState) { + const urlQuery = qs.parse(window.location.search.slice(1)); const queryUpdate = { + ...urlQuery, author: filterState.selectedAuthor, tags: filterState.tags, - startDate: filterState.startDate.format(), - endDate: filterState.endDate.format(), + startDate: filterState.startDate ? moment(filterState.startDate).format('YYYY-MM-DD') : null, + endDate: filterState.endDate ? moment(filterState.endDate).format('YYYY-MM-DD') : null, + sortBy: _.find(filterState.sortBy, { selected: true }).label, }; // Update the state this.setState(prevState => ({ @@ -152,6 +171,7 @@ export default class EDUTracks extends React.Component { tags: queryUpdate.tags, startDate: queryUpdate.startDate, endDate: queryUpdate.endDate, + sortBy: queryUpdate.sortBy, }, })); // Update the url query @@ -165,7 +185,6 @@ export default class EDUTracks extends React.Component { } = this.state; const title = 'Topcoder Thrive | Topcoder Community | Topcoder'; const description = 'Thrive is our vault of content that we have been gathering over the years. It is full of tutorials and workshops that matter. Grow with us!'; - const metaTags = ( ); } + // find selected sortBy + const sortByState = SORT_BY_OPTIONS.map((o) => { + // eslint-disable-next-line no-param-reassign + o.selected = o.label === query.sortBy; + return o; + }); + // filter cnt + let filterCnt = 0; + if (query.author && query.author !== 'All authors') filterCnt++; + if (query.tags.length) filterCnt++; + if (query.startDate) filterCnt++; + if (query.endDate) filterCnt++; + + return (
{ metaTags } @@ -232,13 +265,22 @@ export default class EDUTracks extends React.Component {

{query.tax || query.track}

- + +
diff --git a/src/shared/containers/EDU/partials/ResultTabs.jsx b/src/shared/containers/EDU/partials/ResultTabs.jsx index 5fe428abf3..d2bac322dd 100644 --- a/src/shared/containers/EDU/partials/ResultTabs.jsx +++ b/src/shared/containers/EDU/partials/ResultTabs.jsx @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ /* eslint-disable react/destructuring-assignment */ /** * Container for EDU Portal home page. @@ -32,6 +33,7 @@ export default class ResultTabs extends React.Component { this.state = { loading: true, + loadingMore: false, taxonomy: props.taxonomy, query: _.cloneDeep(props.query), articles: { total: 0 }, @@ -68,6 +70,13 @@ export default class ResultTabs extends React.Component { // When input query from props change // we need to update the data loaded to the tabs if (!_.isEqual(newQuery, oldQuery)) { + const { loading } = this.state; + if (!loading) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + loading: true, + }); + } this.apiService.getEDUContent({ ...newQuery, taxonomy: this.state.taxonomy }) .then((data) => { // Select Videos or Forum posts tab @@ -89,12 +98,13 @@ export default class ResultTabs extends React.Component { loadMore(type) { const tabData = this.state[type]; - const { query, taxonomy } = this.state; + const { query, taxonomy, loadingMore } = this.state; const contTypesMap = { articles: 'Article', videos: 'Video', posts: 'Forum post', }; + if (!loadingMore) this.setState({ loadingMore: true }); this.apiService.getEDUContent({ ...query, taxonomy, @@ -103,7 +113,7 @@ export default class ResultTabs extends React.Component { types: [contTypesMap[type]], }) .then((data) => { - const stateUpdate = {}; + const stateUpdate = { loadingMore: false }; const newData = data[contTypesMap[type]]; tabData.includes.Asset = _.concat(tabData.includes.Asset, newData.includes.Asset); tabData.includes.Entry = _.concat(tabData.includes.Entry, newData.includes.Entry); @@ -117,7 +127,7 @@ export default class ResultTabs extends React.Component { render() { const { - articles, videos, posts, loading, tabIndex, + articles, videos, posts, loading, tabIndex, loadingMore, } = this.state; if (loading) return ; return ( @@ -165,8 +175,8 @@ export default class ResultTabs extends React.Component { ) : (

No results found

) } { - articles.total > articles.items.length ? ( - + articles.total > articles.items.length ? (loadingMore ? + : ) : null } @@ -196,8 +206,8 @@ export default class ResultTabs extends React.Component { ) : (

No results found

) } { - videos.total > videos.items.length ? ( - + videos.total > videos.items.length ? (loadingMore ? + : ) : null } @@ -228,8 +238,8 @@ export default class ResultTabs extends React.Component { ) : (

No results found

) } { - posts.total > posts.items.length ? ( - + posts.total > posts.items.length ? (loadingMore ? + : ) : null } diff --git a/src/shared/containers/EDU/styles/tabs.scss b/src/shared/containers/EDU/styles/tabs.scss index eee81a60b5..533777ac41 100644 --- a/src/shared/containers/EDU/styles/tabs.scss +++ b/src/shared/containers/EDU/styles/tabs.scss @@ -1,7 +1,7 @@ @import "~styles/mixins"; .container { - margin-top: 44px; + margin-top: 20px; .tabListWrap { border-bottom: 1px solid #e9e9e9; diff --git a/src/shared/containers/EDU/styles/tracks.scss b/src/shared/containers/EDU/styles/tracks.scss index c7383e51d3..7de1df9462 100644 --- a/src/shared/containers/EDU/styles/tracks.scss +++ b/src/shared/containers/EDU/styles/tracks.scss @@ -167,25 +167,45 @@ } .filterToggle { - color: #0d61bf; + color: #137d60; font-family: Roboto, sans-serif; - font-size: 16px; - font-weight: 400; - line-height: 19px; + font-size: 15px; + font-weight: 500; + line-height: 18px; background: transparent; border: none; padding: 0; outline: none; margin: 0; + display: flex; + justify-content: center; + align-items: center; @include xs-to-sm { - margin-top: 38px; + margin-top: 29px; + margin-bottom: 46px; + } + + .filterArrow { + width: 16px; + height: 14px; + margin-left: 7px; } } } .filterVisible { display: flex; + + @include xs-to-sm { + position: fixed; + z-index: 9; + top: 0; + left: 0; + background: rgba(42, 42, 42, 0.73); + width: 100vw; + height: 100vh; + } } .filterHidden { diff --git a/src/shared/services/contentful.js b/src/shared/services/contentful.js index 60fc4a5a50..c8509fe9da 100644 --- a/src/shared/services/contentful.js +++ b/src/shared/services/contentful.js @@ -302,7 +302,7 @@ class Service { */ async getEDUContent({ track, types, limit = 5, skip = 0, tags, - tax, startDate, endDate, author, taxonomy, phrase, title, + tax, startDate, endDate, author, taxonomy, phrase, title, sortBy, }) { const query = { content_type: 'article', @@ -350,6 +350,12 @@ class Service { if (endDate) query['fields.creationDate[lte]'] = endDate; if (phrase) query.query = phrase; if (title) query['fields.title[match]'] = title; + if (sortBy) { + switch (sortBy) { + case 'Likes': query.order = '-fields.upvotes,-fields.creationDate'; break; + default: query.order = '-fields.creationDate'; break; + } + } const content = {}; await Promise.all( _.map(types || EDU_ARTICLE_TYPES,