Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Fix reviewer feedbacks (WIP) #25

Open
wants to merge 2 commits into
base: challenge-listing-part-1
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/actions/auth.js

This file was deleted.

178 changes: 144 additions & 34 deletions src/actions/challenges.js
Original file line number Diff line number Diff line change
@@ -7,28 +7,71 @@ async function doGetChallenges(filter) {
return service.getChallenges(filter);
}

async function getActiveChallenges(filter) {
const activeFilter = {
async function getAllActiveChallenges(filter) {
const BUCKET_ALL_ACTIVE_CHALLENGES = constants.FILTER_BUCKETS[0];
let page;

if (util.isDisplayingBucket(filter, BUCKET_ALL_ACTIVE_CHALLENGES)) {
page = filter.page;
} else {
page = 1;
}

const allActiveFilter = {
...util.createChallengeCriteria(filter),
...util.createActiveChallengeCriteria(),
...util.createAllActiveChallengeCriteria(),
page,
};
return doGetChallenges(activeFilter);
return doGetChallenges(allActiveFilter);
}

async function getOpenForRegistrationChallenges(filter) {
const BUCKET_OPEN_FOR_REGISTRATION = constants.FILTER_BUCKETS[1];
let page;

if (util.isDisplayingBucket(filter, BUCKET_OPEN_FOR_REGISTRATION)) {
page = filter.page;
} else {
page = 1;
}

const openForRegistrationFilter = {
...util.createChallengeCriteria(filter),
...util.createOpenForRegistrationChallengeCriteria(),
page,
};
return doGetChallenges(openForRegistrationFilter);
}

async function getPastChallenges(filter) {
const pastFilter = {
async function getClosedChallenges(filter) {
const BUCKET_CLOSED_CHALLENGES = constants.FILTER_BUCKETS[1];
let page;

if (util.isDisplayingBucket(filter, BUCKET_CLOSED_CHALLENGES)) {
page = filter.page;
} else {
page = 1;
}

const closedFilter = {
...util.createChallengeCriteria(filter),
...util.createPastChallengeCriteria(),
...util.createClosedChallengeCriteria(),
page,
};
return doGetChallenges(pastFilter);
return doGetChallenges(closedFilter);
}

async function getRecommendedChallenges(filter) {
let result = [];
result.meta = { total: 0 };

if (result.length === 0) {
const failbackFilter = { ...filter };
result = await getOpenForRegistrationChallenges(failbackFilter);
result.loadingRecommendedChallengesError = true;
}

return result;
}

function doFilterBySubSommunities(challenges) {
@@ -43,46 +86,113 @@ function doFilterByPrizeTo(challenges) {

async function getChallenges(filter, change) {
const FILTER_BUCKETS = constants.FILTER_BUCKETS;
let challenges;
let challengesFiltered;
let total;
let filterChange = change;

const getChallengesByBucket = async (f) => {
switch (f.bucket) {
case FILTER_BUCKETS[0]:
return getActiveChallenges(f);
case FILTER_BUCKETS[1]:
return getOpenForRegistrationChallenges(f);
case FILTER_BUCKETS[2]:
return getPastChallenges(f);
default:
return [];
}
const BUCKET_ALL_ACTIVE_CHALLENGES = FILTER_BUCKETS[0];
const BUCKET_OPEN_FOR_REGISTRATION = FILTER_BUCKETS[1];
const BUCKET_CLOSED_CHALLENGES = FILTER_BUCKETS[2];
const filterChange = change;
const bucket = filter.bucket;

const getChallengesByBuckets = async (f) => {
return FILTER_BUCKETS.includes(f.bucket)
? Promise.all([
getAllActiveChallenges(f),
f.recommended
? getRecommendedChallenges(f)
: getOpenForRegistrationChallenges(f),
getClosedChallenges(f),
])
: [[], [], []];
};

if (!filterChange) {
let [
allActiveChallenges,
openForRegistrationChallenges,
closedChallenges,
] = await getChallengesByBuckets(filter);
let challenges;
let openForRegistrationCount;
let total;
let loadingRecommendedChallengesError;

switch (bucket) {
case BUCKET_ALL_ACTIVE_CHALLENGES:
challenges = allActiveChallenges;
break;
case BUCKET_OPEN_FOR_REGISTRATION:
challenges = openForRegistrationChallenges;
break;
case BUCKET_CLOSED_CHALLENGES:
challenges = closedChallenges;
break;
}
openForRegistrationCount = openForRegistrationChallenges.meta.total;
total = challenges.meta.total;
loadingRecommendedChallengesError =
challenges.loadingRecommendedChallengesError;

return {
challenges,
total,
openForRegistrationCount,
loadingRecommendedChallengesError,
allActiveChallenges,
openForRegistrationChallenges,
closedChallenges,
};
}

if (!util.checkRequiredFilterAttributes(filter)) {
return { challenges: [], challengesFiltered: [], total: 0 };
}

if (!filterChange) {
const chs = await getChallengesByBucket(filter);
return { challenges: chs, challengesFiltered: chs, total: chs.meta.total };
}
let allActiveChallenges;
let openForRegistrationChallenges;
let closedChallenges;
let challenges;
let openForRegistrationCount;
let total;
let loadingRecommendedChallengesError;

if (util.shouldFetchChallenges(filterChange)) {
challenges = await getChallengesByBucket(filter);
[
allActiveChallenges,
openForRegistrationChallenges,
closedChallenges,
] = await getChallengesByBuckets(filter);
switch (bucket) {
case BUCKET_ALL_ACTIVE_CHALLENGES:
challenges = allActiveChallenges;
break;
case BUCKET_OPEN_FOR_REGISTRATION:
challenges = openForRegistrationChallenges;
break;
case BUCKET_CLOSED_CHALLENGES:
challenges = closedChallenges;
break;
}
}

challengesFiltered = challenges;
openForRegistrationCount = openForRegistrationChallenges.meta.total;
total = challenges.meta.total;
loadingRecommendedChallengesError =
challenges.loadingRecommendedChallengesError;

if (util.shouldFilterChallenges(filterChange)) {
challengesFiltered = doFilterBySubSommunities(challengesFiltered);
challengesFiltered = doFilterByPrizeFrom(challengesFiltered);
challengesFiltered = doFilterByPrizeTo(challengesFiltered);
challenges = doFilterBySubSommunities(challenges);
challenges = doFilterByPrizeFrom(challenges);
challenges = doFilterByPrizeTo(challenges);
}

return { challenges, challengesFiltered, total };
return {
challenges,
total,
openForRegistrationCount,
loadingRecommendedChallengesError,
allActiveChallenges,
openForRegistrationChallenges,
closedChallenges,
};
}

export default createActions({
5 changes: 5 additions & 0 deletions src/actions/filter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { createActions } from "redux-actions";

function restoreFilter(filter) {
return filter;
}

function updateFilter(partialUpdate) {
return partialUpdate;
}

export default createActions({
RESTORE_FILTER: restoreFilter,
UPDATE_FILTER: updateFilter,
});
10 changes: 10 additions & 0 deletions src/assets/icons/card-view.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions src/assets/icons/list-view.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/not-found-recommended.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 14 additions & 1 deletion src/components/Button/index.jsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import PT from "prop-types";
import "./styles.scss";

const Button = ({ children, onClick }) => (
<button styleName="button" onClick={onClick}>
<button styleName="button" onClick={onClick} type="button">
{children}
</button>
);
@@ -14,4 +14,17 @@ Button.propTypes = {
onClick: PT.func,
};

const ButtonIcon = ({ children, onClick }) => (
<button styleName="button-icon" onClick={onClick} type="button">
{children}
</button>
);

ButtonIcon.propTypes = {
children: PT.node,
onClick: PT.func,
};

export { Button, ButtonIcon };

export default Button;
14 changes: 12 additions & 2 deletions src/components/Button/styles.scss
Original file line number Diff line number Diff line change
@@ -22,5 +22,15 @@
background-color: $green;
}

.button-lg {}
.button-sm {}
.button-icon {
width: 32px;
height: 32px;
padding: 0;
line-height: 0;
text-align: center;
vertical-align: middle;
appearance: none;
background: none;
border: 0;
border-radius: 50%;
}
6 changes: 5 additions & 1 deletion src/components/Checkbox/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Checkbox component.
*/
import React, { useRef, useState } from "react";
import React, { useRef, useState, useEffect } from "react";
import PT from "prop-types";
import _ from "lodash";
import "./styles.scss";
@@ -22,6 +22,10 @@ function Checkbox({ checked, onChange, size, errorMsg }) {
_.debounce((q, cb) => cb(q), process.env.GUIKIT.DEBOUNCE_ON_CHANGE_TIME) // eslint-disable-line no-undef
).current;

useEffect(() => {
setCheckedInternal(checked);
}, [checked]);

return (
<label styleName={`container ${sizeStyle}`}>
<input
152 changes: 145 additions & 7 deletions src/components/DateRangePicker/DateInput/index.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,153 @@
import React from "react";
import React, { useRef, useEffect, useState } from "react";
import PT from "prop-types";
import TextInput from "../../TextInput";
import CalendarIcon from "assets/icons/icon-calendar.svg";

import "./styles.scss";

const DateInput = ({ value, onClick }) => (
<div onClick={onClick} styleName="date-range-input">
<TextInput label="From" size="xs" value={value} readonly />
</div>
);
const DateInput = ({
id,
isStartDateActive,
startDateString,
onStartDateChange,
onStartDateFocus,
isEndDateActive,
endDateString,
onEndDateChange,
onEndDateFocus,
error,
onClickCalendarIcon,
onStartEndDateChange,
}) => {
const ref = useRef(null);
const [focused, setFocused] = useState(false);

DateInput.propTypes = {};
let rangeText;
if (startDateString && endDateString) {
rangeText = `${startDateString} - ${endDateString}`;
} else {
rangeText = `${startDateString}${endDateString}`;
}

useEffect(() => {
const inputElement = ref.current.querySelector("input");
const onFocus = () => setFocused(true);
const onBlur = () => setFocused(false);

inputElement.addEventListener("focus", onFocus);
inputElement.addEventListener("blur", onBlur);

return () => {
inputElement.removeEventListener("focus", onFocus);
inputElement.removeEventListener("blur", onBlur);
};
}, []);

useEffect(() => {
const inputElement = ref.current.querySelector("input");

let caretPosition;
if (inputElement.selectionDirection === "forward") {
caretPosition = inputElement.selectionEnd;
} else {
caretPosition = inputElement.selectionStart;
}

if (caretPosition < 14) {
onStartDateFocus();
} else {
onEndDateFocus();
}
}, [focused]);

const onChangeRangeText = (value) => {
let [newStartDateString = "", newEndDateString = ""] = value
.trim()
.split("-");
newStartDateString = newStartDateString.trim();
newEndDateString = newEndDateString.trim();

if (
newStartDateString !== startDateString &&
newEndDateString !== endDateString
) {
const event = {
startDateString: newStartDateString,
endDateString: newEndDateString,
};
onStartEndDateChange(event);
onStartDateFocus();
} else if (newStartDateString !== startDateString) {
onStartDateFocus();
onStartDateChange(newStartDateString);
} else if (newEndDateString !== endDateString) {
onEndDateFocus();
onEndDateChange(newEndDateString);
if (newEndDateString === "") {
onStartDateFocus();
}
}
};

const onChangeRangeTextDebounced = useRef(_.debounce((f) => f(), 150));

const onClickIcon = () => {
const inputElement = ref.current.querySelector("input");

let caretPosition;
if (inputElement.selectionDirection === "forward") {
caretPosition = inputElement.selectionEnd;
} else {
caretPosition = inputElement.selectionStart;
}

if (caretPosition < 14) {
onClickCalendarIcon("start");
} else {
onClickCalendarIcon("end");
}
};

const label = startDateString ? "From" : endDateString ? "To" : "From";

return (
<div styleName={`container ${error ? "isError" : ""}`}>
<div styleName="date-range-input input-group" ref={ref}>
<TextInput
label={label}
size="xs"
value={rangeText}
onChange={(value) => {
onChangeRangeTextDebounced.current(() => onChangeRangeText(value));
}}
/>
<div
id={id}
styleName="icon"
role="button"
onClick={onClickIcon}
>
<CalendarIcon />
</div>
</div>
<div styleName="errorHint">{error}</div>
</div>
);
};

DateInput.propTypes = {
id: PT.string,
isStartDateActive: PT.bool,
startDateString: PT.string,
onStartDateChange: PT.func,
onStartDateFocus: PT.func,
isEndDateActive: PT.bool,
endDateString: PT.string,
onEndDateChange: PT.func,
onEndDateFocus: PT.func,
error: PT.string,
onClickCalendarIcon: PT.func,
onStartEndDateChange: PT.func,
};

export default DateInput;
37 changes: 35 additions & 2 deletions src/components/DateRangePicker/DateInput/styles.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
@import "styles/variables";

.container {
&.isError {
input {
border: 1px solid $tc-level-5;
}

.errorHint {
display: block;
color: $tc-level-5;
font-size: 12px;
padding: 4px 0;
height: 20px;
}
}
}

.date-range-input {
width: 230px;
margin-top: -12px;
font-size: $font-size-sm;
}

.input-group {
position: relative;

.icon {
position: absolute;
top: 22px;
right: 14px;
z-index: 1;
display: block;
cursor: pointer;
}

input {
color: $body-color !important;
border-color: $tc-gray-30 !important;
padding-right: 46px !important;
}
}

.errorHint {
display: none;
}
10 changes: 5 additions & 5 deletions src/components/DateRangePicker/helpers.js
Original file line number Diff line number Diff line change
@@ -50,11 +50,11 @@ const staticRangeHandler = {
* @return {object[]} list of defined ranges
*/
export function createStaticRanges() {
const now = moment();
const pastWeek = moment().subtract(1, "week");
const pastMonth = moment().subtract(1, "month");
const past6Months = moment().subtract(6, "month");
const pastYear = moment().subtract(1, "year");
const now = moment().utcOffset(0);
const pastWeek = now.clone().subtract(1, "week");
const pastMonth = now.clone().subtract(1, "month");
const past6Months = now.clone().subtract(6, "month");
const pastYear = now.clone().subtract(1, "year");

const ranges = [
{
228 changes: 171 additions & 57 deletions src/components/DateRangePicker/index.jsx
Original file line number Diff line number Diff line change
@@ -16,13 +16,7 @@ import {
} from "./helpers";

function DateRangePicker(props) {
const {
readOnly,
startDatePlaceholder,
endDatePlaceholder,
range,
onChange,
} = props;
const { id, range, onChange } = props;

const [rangeString, setRangeString] = useState({
startDateString: "",
@@ -45,15 +39,33 @@ function DateRangePicker(props) {
const isStartDateFocused = focusedRange[1] === 0;
const isEndDateFocused = focusedRange[1] === 1;

useEffect(() => {
setRangeString({
startDateString: range.startDate
? moment(range.startDate).format("MMM D, YYYY")
: "",
endDateString: range.endDate
? moment(range.endDate).format("MMM D, YYYY")
: "",
});
}, [range]);

/**
* Handle end date change on user input
* After user input the end date via keyboard, validate it then update the range state
* @param {Object} e Input Event.
*/
const onEndDateChange = (e) => {
const endDateString = e.target.value;
const endDate = moment(endDateString, "MM/DD/YYYY", true);
if (endDate.isValid()) {
const onEndDateChange = (value) => {
const endDateString = value;
const endDate = moment(endDateString, "MMM D, YYYY", true);
const startDate = moment(rangeString.startDateString, "MMM D, YYYY", true);

if (endDate.isValid() && isBeforeDay(endDate, startDate)) {
setErrors({
...errors,
endDate: "Range Error",
});
} else if (endDate.isValid()) {
onChange({
endDate: endDate.toDate(),
startDate: range.startDate,
@@ -66,15 +78,23 @@ function DateRangePicker(props) {

setRangeString({
...rangeString,
endDateString: endDate.format("MM/DD/YYYY"),
endDateString: endDate.format("MMM D, YYYY"),
});
} else if (endDateString === "") {
onChange({
endDate: null,
startDate: range.startDate,
});

setErrors({
...errors,
endDate: "",
});
} else {
if (endDateString && endDateString !== "mm/dd/yyyy") {
setErrors({
...errors,
endDate: "Invalid Format",
});
}
setErrors({
...errors,
endDate: "Invalid End Date Format",
});

setRangeString({
...rangeString,
@@ -88,10 +108,21 @@ function DateRangePicker(props) {
* After user input the start date via keyboard, validate it then update the range state
* @param {Object} e Input Event.
*/
const onStartDateChange = (e) => {
const startDateString = e.target.value;
const startDate = moment(startDateString, "MM/DD/YYYY", true);
if (startDate.isValid()) {
const onStartDateChange = (value) => {
const startDateString = value;
const startDate = moment(startDateString, "MMM D, YYYY", true);
const endDate = moment(rangeString.endDateString, "MMM D, YYYY", true);

if (
startDate.isValid() &&
endDate.isValid() &&
isAfterDay(startDate, endDate)
) {
setErrors({
...errors,
startDate: "Range Error",
});
} else if (startDate.isValid()) {
onChange({
endDate: range.endDate,
startDate: startDate.toDate(),
@@ -104,15 +135,23 @@ function DateRangePicker(props) {

setRangeString({
...rangeString,
startDateString: startDate.format("MM/DD/YYYY"),
startDateString: startDate.format("MMM D, YYYY"),
});
} else if (startDateString === "") {
onChange({
endDate: range.endDate,
startDate: null,
});

setErrors({
...errors,
startDate: "",
});
} else {
if (startDateString && startDateString !== "mm/dd/yyyy") {
setErrors({
...errors,
startDate: "Invalid Format",
});
}
setErrors({
...errors,
startDate: "Invalid Start Date Format",
});

setRangeString({
...rangeString,
@@ -121,13 +160,92 @@ function DateRangePicker(props) {
}
};

const onStartEndDateChange = ({ startDateString, endDateString }) => {
const startDate = moment(startDateString, "MMM D, YYYY", true);
const endDate = moment(endDateString, "MMM D, YYYY", true);

if (
startDate.isValid() &&
endDate.isValid() &&
isBeforeDay(endDate, startDate)
) {
setErrors({
...errors,
endDate: "Range Error",
});
} else if (startDate.isValid() && endDate.isValid()) {
onChange({
endDate: endDate.toDate(),
startDate: startDate.toDate(),
});
setErrors({
startDate: "",
endDate: "",
});
} else if (startDate.isValid()) {
onChange({
endDate: null,
startDate: startDate.toDate(),
});
setErrors({
...errors,
endDate: "Invalid End Date Format",
});
} else if (endDate.isValid()) {
onChange({
endDate: endDate.toDate(),
startDate: null,
});
setErrors({
...errors,
startDate: "Invalid Start Date Format",
});
} else if (startDateString === "" && endDateString === "") {
onChange({
endDate: null,
startDate: null,
});
setErrors({
startDate: "",
endDate: "",
});
} else if (startDateString === "") {
onChange({
endDate: endDate.toDate(),
startDate: null,
});

setErrors({
...errors,
startDate: "",
});
} else if (endDateString === "") {
onChange({
endDate: null,
startDate: startDate.toDate(),
});

setErrors({
...errors,
endDate: "",
});
} else {
onChange({
endDate: null,
startDate: null,
});
setErrors({
startDate: "Invalid Start Date Format",
endDate: "Invalid End Date Format",
});
}
};

/**
* Trigger to open calendar modal on calendar icon in start date input
*/
const onIconClickStartDate = () => {
const calendarIcon = document.querySelector(
"#input-start-date-range-calendar-icon"
);
const calendarIcon = document.querySelector(id);
if (calendarIcon) {
calendarIcon.blur();
}
@@ -141,9 +259,7 @@ function DateRangePicker(props) {
* Trigger to open calendar modal on calendar icon in end date input
*/
const onIconClickEndDate = () => {
const calendarIcon = document.querySelector(
"#input-end-date-range-calendar-icon"
);
const calendarIcon = document.querySelector(id);
if (calendarIcon) {
calendarIcon.blur();
}
@@ -205,9 +321,9 @@ function DateRangePicker(props) {

setRangeString({
startDateString: newStartDate
? moment(newStartDate).format("MM/DD/YYYY")
? moment(newStartDate).format("MMM D, YYYY")
: "",
endDateString: newEndDate ? moment(newEndDate).format("MM/DD/YYYY") : "",
endDateString: newEndDate ? moment(newEndDate).format("MMM D, YYYY") : "",
});

onChange({
@@ -405,22 +521,24 @@ function DateRangePicker(props) {
${(errors.startDate || errors.endDate) && styles.isErrorInput}
`;

let rangeText;
if (rangeString.startDateString && rangeString.endDateString) {
rangeText = `${rangeString.startDateString} - ${rangeString.endDateString}`;
} else {
rangeText = `${rangeString.startDateString}${rangeString.endDateString}`;
}

return (
<div styleName="dateRangePicker" className={className}>
<div styleName="dateInputWrapper">
<DateInput
onClick={() => {
onIconClickStartDate();
setFocusedRange([0, 0]);
id={id}
isStartDateActive={focusedRange[1] === 0 && isComponentVisible}
startDateString={rangeString.startDateString}
onStartDateChange={onStartDateChange}
onStartDateFocus={() => setFocusedRange([0, 0])}
isEndDateActive={focusedRange[1] === 1 && isComponentVisible}
endDateString={rangeString.endDateString}
onEndDateChange={onEndDateChange}
onEndDateFocus={() => setFocusedRange([0, 1])}
error={errors.startDate || errors.endDate}
onClickCalendarIcon={(event) => {
event === "start" ? onIconClickStartDate() : onIconClickEndDate();
}}
value={rangeText}
onStartEndDateChange={onStartEndDateChange}
/>
</div>
<div ref={calendarRef}>
@@ -453,18 +571,14 @@ function DateRangePicker(props) {
// It use https://www.npmjs.com/package/react-date-range internally
// Check the docs for further options

DateRangePicker.defaultProps = {
id: "input-date-range-calendar-icon"
}

DateRangePicker.propTypes = {
readOnly: PropTypes.bool,
startDatePlaceholder: PropTypes.string,
endDatePlaceholder: PropTypes.string,
id: PropTypes.string,
range: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
};

DateRangePicker.defaultProps = {
readOnly: false,
startDatePlaceholder: "Start Date",
endDatePlaceholder: "End Date",
};

export default DateRangePicker;
21 changes: 20 additions & 1 deletion src/components/DateRangePicker/style.scss
Original file line number Diff line number Diff line change
@@ -166,7 +166,21 @@
}

.rdrMonthAndYearPickers select {
background: url("data:image/svg+xml;utf8,<svg width='9px' height='6px' viewBox='0 0 9 6' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><g id='Artboard' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' transform='translate(-636.000000, -171.000000)'><g id='input' transform='translate(172.000000, 37.000000)' fill='%230B71E6' fill-rule='nonzero'><g id='Group-9' transform='translate(323.000000, 127.000000)'><path d='M142.280245,7.23952813 C141.987305,6.92353472 141.512432,6.92361662 141.219585,7.23971106 C140.926739,7.5558055 140.926815,8.06821394 141.219755,8.38420735 L145.498801,13 L149.780245,8.38162071 C150.073185,8.0656273 150.073261,7.55321886 149.780415,7.23712442 C149.487568,6.92102998 149.012695,6.92094808 148.719755,7.23694149 L145.498801,10.7113732 L142.280245,7.23952813 Z' id='arrow'></path></g></g></g></svg>") no-repeat right 8px center;
background: url("data:image/svg+xml;utf8,<svg width='9px' height='6px' viewBox='0 0 9 6' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><g id='Artboard' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' transform='translate(-636.000000, -171.000000)'><g id='input' transform='translate(172.000000, 37.000000)' fill='%23137D60' fill-rule='nonzero'><g id='Group-9' transform='translate(323.000000, 127.000000)'><path d='M142.280245,7.23952813 C141.987305,6.92353472 141.512432,6.92361662 141.219585,7.23971106 C140.926739,7.5558055 140.926815,8.06821394 141.219755,8.38420735 L145.498801,13 L149.780245,8.38162071 C150.073185,8.0656273 150.073261,7.55321886 149.780415,7.23712442 C149.487568,6.92102998 149.012695,6.92094808 148.719755,7.23694149 L145.498801,10.7113732 L142.280245,7.23952813 Z' id='arrow'></path></g></g></g></svg>") no-repeat right 8px center;

option {
background: $white;

&:checked {
font-weight: bold;
color: $white;
background-color: $green;
}
}
}

rdrMonthAndYearPickers option:hover {
background-color: yellow !important;
}

.rdrMonths {
@@ -336,6 +350,11 @@
z-index: 0;
}
}

.rdrDayNumber {
top: 0;
bottom: 0;
}
}

.rdrDayStartOfWeek,
7 changes: 6 additions & 1 deletion src/components/DropdownTerms/index.jsx
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ function DropdownTerms({
onChange,
errorMsg,
addNewOptionPlaceholder,
size,
}) {
const [internalTerms, setInternalTerms] = useState(terms);
const selectedOption = _.filter(internalTerms, { selected: true }).map(
@@ -87,7 +88,9 @@ function DropdownTerms({
selectedOption && !!selectedOption.length ? "haveValue" : ""
} ${errorMsg ? "haveError" : ""} ${
_.every(internalTerms, { selected: true }) ? "isEmptySelectList" : ""
} ${focused ? "isFocused" : ""}`}
} ${focused ? "isFocused" : ""} ${
size === "lg" ? "term-lgSize" : "term-xsSize"
}`}
>
<div styleName="relative">
<Creatable
@@ -179,6 +182,7 @@ DropdownTerms.defaultProps = {
onChange: () => {},
errorMsg: "",
addNewOptionPlaceholder: "",
size: "lg",
};

DropdownTerms.propTypes = {
@@ -194,6 +198,7 @@ DropdownTerms.propTypes = {
onChange: PT.func,
errorMsg: PT.string,
addNewOptionPlaceholder: PT.string,
size: PT.oneOf(["xs", "lg"]),
};

export default DropdownTerms;
35 changes: 34 additions & 1 deletion src/components/DropdownTerms/styles.scss
Original file line number Diff line number Diff line change
@@ -85,7 +85,7 @@
left: 0;
right: 0;
background: transparent;
height: 100% !important;
min-height: 100% !important;
z-index: 5;
margin: 0 !important;
cursor: pointer;
@@ -153,6 +153,39 @@
}
}
}

&.term-lgSize {
}

&.term-xsSize {
:global {
.Select-control {
min-height: 40px;
}

.Select-placeholder {
font-size: 14px;
}

.Select--multi .Select-multi-value-wrapper {
max-height: 90px;
}

.Select.is-open .Select-multi-value-wrapper {
overflow: hidden;
}

.Select--multi .Select-value {
line-height: 38px;
margin-top: 10px;
font-size: 14px;
}
}

.errorMessage {
@include errorMessageXs;
}
}
}

.addAnotherSkill {
2 changes: 2 additions & 0 deletions src/components/Menu/index.jsx
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ const Menu = ({ menu, icons, selected, onSelect }) => {
if (!submenu[key]) {
return null;
}

const subSubmenu = submenu[key];
return (
<ul styleName="sub-submenu">
@@ -52,6 +53,7 @@ const Menu = ({ menu, icons, selected, onSelect }) => {
if (!menu[key]) {
return null;
}

const subMenu = menu[key];
return (
<ul styleName="sub-menu">
19 changes: 15 additions & 4 deletions src/components/Menu/styles.scss
Original file line number Diff line number Diff line change
@@ -5,16 +5,16 @@ $menu-padding-y: 20px;

.menu {
padding: $menu-padding-y $menu-padding-x (3 * $base-unit);
line-height: 21px;
}

.menu-item {
padding: 7px 0;
padding: 4px 0;
cursor: pointer;

a {
display: flex;
align-items: center;
line-height: 26px;
}

.icon {
@@ -45,10 +45,21 @@ $menu-padding-y: 20px;
}

&.selected > a {
color: $green;
color: $lightGreen;
}
}

.menu-item-main > a {
margin-left: -20px;
margin-right: -20px;
padding-left: 20px;
padding-right: 20px;
}

.menu-item-main.active > a {
box-shadow: inset 4px 0 #06D6A0;
}

.menu-item-main > a + ul,
.menu-item-secondary > a + ul {
display: none;
@@ -60,7 +71,7 @@ $menu-padding-y: 20px;
}

.menu-item-secondary.active.collapsed {
color: $green;
color: $lightGreen;
}

.sub-menu {
8 changes: 6 additions & 2 deletions src/components/Pagination/index.jsx
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ const Pagination = ({ length, pageIndex, pageSize, onChange }) => {
const onChangePageSize = (options) => {
const selectedOption = utils.getSelectedDropdownOption(options);
const newPageSize = +selectedOption.label;
onChange({ pageIndex, pageSize: newPageSize });
onChange({ pageIndex: 0, pageSize: newPageSize });
};

const onChangePageIndex = (newPageIndex) => {
@@ -106,7 +106,11 @@ const Pagination = ({ length, pageIndex, pageSize, onChange }) => {
</button>
</li>
))}
<li styleName={`page next ${pageIndex === total - 1 ? "hidden" : ""}`}>
<li
styleName={`page next ${
pageIndex === total - 1 || length === 0 ? "hidden" : ""
}`}
>
<button onClick={next}>NEXT</button>
</li>
</ul>
3 changes: 1 addition & 2 deletions src/components/Pagination/styles.scss
Original file line number Diff line number Diff line change
@@ -12,8 +12,7 @@

:global {
.Select-value-label::after {
content: 'per page';
margin-left: 0.15em;
content: ' per page';
}
}
}
6 changes: 5 additions & 1 deletion src/components/Toggle/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Toggles component.
*/
import React, { useRef, useState } from "react";
import React, { useRef, useState, useEffect } from "react";
import PT from "prop-types";
import _ from "lodash";
import "./style.scss";
@@ -17,6 +17,10 @@ function Toggles({ checked, onChange, size }) {
sizeStyle = size === "xs" ? "xsSize" : "smSize";
}

useEffect(() => {
setInternalChecked(checked);
}, [checked]);

return (
<label styleName={`container ${sizeStyle}`}>
<input
15 changes: 12 additions & 3 deletions src/constants/index.js
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ export const NAV_MENU_ICONS = {
export const FILTER_BUCKETS = [
"All Active Challenges",
"Open for Registration",
"Past Challenges",
"Closed Challenges",
];

export const FILTER_CHALLENGE_TYPES = ["Challenge", "First2Finish", "Task"];
@@ -40,14 +40,14 @@ export const FILTER_CHALLENGE_TRACKS = [
"Design",
"Development",
"Data Science",
"Quality Assurance",
"QA",
];

export const FILTER_CHALLENGE_TRACK_ABBREVIATIONS = {
Design: "DES",
Development: "DEV",
"Data Science": "DS",
"Quality Assurance": "QA",
QA: "QA",
};

export const CHALLENGE_SORT_BY = {
@@ -57,6 +57,15 @@ export const CHALLENGE_SORT_BY = {
Title: "name",
};

export const CHALLENGE_SORT_BY_RECOMMENDED = "bestMatch";
export const CHALLENGE_SORT_BY_RECOMMENDED_LABEL = "Best Match";
export const CHALLENGE_SORT_BY_DEFAULT = "updated";

export const SORT_ORDER = {
DESC: "desc",
ASC: "asc",
};

export const TRACK_COLOR = {
Design: "#2984BD",
Development: "#35AC35",
3 changes: 1 addition & 2 deletions src/containers/Challenges/Listing/ChallengeError/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from "react";
import IconNotFound from "assets/icons/not-found.png";

import "./styles.scss";

const ChallengeError = () => (
@@ -9,7 +8,7 @@ const ChallengeError = () => (
<img src={IconNotFound} alt="not found" />
</h1>
<p>
No challenges were found. You can try changing your search perimeters.
No challenges were found. You can try changing your search parameters.
</p>
</div>
);
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
display: inline-block;
width: 14px;
height: 16px;
margin-right: 7px;
margin-right: 14px;
vertical-align: middle;
}
}
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
display: inline-block;
width: 14px;
height: 16px;
margin-right: 7px;
margin-right: 14px;
vertical-align: middle;
}
}
Original file line number Diff line number Diff line change
@@ -15,7 +15,13 @@ const PhaseEndDate = ({ challenge }) => {
);
const timeLeftColor = timeLeft.time < 12 * 60 * 60 * 1000 ? "#EF476F" : "";
const timeLeftMessage = timeLeft.late ? (
<span>{`Late by ${timeLeft.text}`}</span>
<span>
Late by
<span
style={{ color: "#EF476F" }}
styleName="uppercase"
>{` ${timeLeft.text}`}</span>
</span>
) : (
<span style={{ color: timeLeftColor }}>
<span
@@ -29,7 +35,7 @@ const PhaseEndDate = ({ challenge }) => {
<span>
<span styleName="phase-message">
{`${phaseMessage}`} {`${status}`}:
</span>
</span>{" "}
<span styleName="time-left">{timeLeftMessage}</span>
</span>
);
Original file line number Diff line number Diff line change
@@ -17,4 +17,8 @@

font-size: 24px;
line-height: 26px;

&::first-letter {
margin-right: -0.125em;
}
}
Original file line number Diff line number Diff line change
@@ -23,6 +23,10 @@ const Tags = ({ tags, onClickTag }) => {
);
};

Tags.defaultProps = {
tags: [],
};

Tags.propTypes = {
tags: PT.arrayOf(PT.string),
onClickTag: PT.func,
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@
.track-icon {
position: relative;
display: inline-block;
width: 42px;
height: 46px;
vertical-align: middle;
line-height: 1;

2 changes: 1 addition & 1 deletion src/containers/Challenges/Listing/ChallengeItem/index.jsx
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ const ChallengeItem = ({ challenge, onClickTag, onClickTrack }) => {
let purse = challenge.prizeSets
? utils.challenge.getChallengePurse(challenge.prizeSets)
: "";
purse = purse && utils.formatMoneyValue(purse);
purse = typeof purse === "number" && utils.formatMoneyValue(purse);

return (
<div styleName="challenge-item">
19 changes: 17 additions & 2 deletions src/containers/Challenges/Listing/ChallengeItem/styles.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "styles/variables";
@import "styles/mixins";

.challenge-item {
display: flex;
@@ -35,16 +36,30 @@
}

.tags {
max-width: calc(50% - 32px);
max-width: calc(50% - 84px);
min-width: calc(50% - 84px);
flex: 1 1 auto;

@media (min-width: $screen-xxl + 1px) {
min-width: 294px;
max-width: 25%;
}
}

.nums {
flex: 0 0 104px;
white-space: nowrap;

> * {
margin: 0 16px;
margin: 0 20px;

&:first-child {
margin-left: 0;
}

&:last-child {
margin-right: 0;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import IconNotFound from "assets/icons/not-found-recommended.png";
import "./styles.scss";

const ChallengeRecommendedError = () => (
<div styleName="challenge-recommended-error">
<h1>
<img src={IconNotFound} alt="not found" />
</h1>
<p>
Looks like there are no <strong>Recommended Challenges</strong> that best
match your skills at this point. But you can try to join other challenges
that work for you.
</p>
</div>
);

export default ChallengeRecommendedError;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@import "styles/variables";

.challenge-recommended-error {
padding: 16px 24px;
min-height: 136px;
margin-bottom: 35px;
font-size: $font-size-sm;
line-height: 22px;
text-align: center;
background: $white;
border-radius: $border-radius-lg;

h1 {
padding: 15px 0 10px;
}

p {
margin-bottom: 20px;
}

strong {
font-weight: bold;
}
}
11 changes: 8 additions & 3 deletions src/containers/Challenges/Listing/index.jsx
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import "./styles.scss";

const Listing = ({
challenges,
search,
page,
perPage,
sortBy,
@@ -25,9 +26,10 @@ const Listing = ({
updateFilter,
bucket,
getChallenges,
challengeSortBys,
}) => {
const sortByOptions = utils.createDropdownOptions(
Object.keys(constants.CHALLENGE_SORT_BY),
challengeSortBys,
utils.getSortByLabel(constants.CHALLENGE_SORT_BY, sortBy)
);

@@ -42,6 +44,7 @@ const Listing = ({
<img src={IconSearch} alt="search" />
</span>
<TextInput
value={search}
placeholder="Search for challenges"
size="xs"
onChange={(value) => {
@@ -64,11 +67,11 @@ const Listing = ({
options={sortByOptions}
size="xs"
onChange={(newSortByOptions) => {
const selectOption = utils.getSelectedDropdownOption(
const selectedOption = utils.getSelectedDropdownOption(
newSortByOptions
);
const filterChange = {
sortBy: constants.CHALLENGE_SORT_BY[selectOption.label],
sortBy: constants.CHALLENGE_SORT_BY[selectedOption.label],
};
updateFilter(filterChange);
getChallenges(filterChange);
@@ -140,6 +143,7 @@ const Listing = ({

Listing.propTypes = {
challenges: PT.arrayOf(PT.shape()),
search: PT.string,
page: PT.number,
perPage: PT.number,
sortBy: PT.string,
@@ -149,6 +153,7 @@ Listing.propTypes = {
getChallenges: PT.func,
updateFilter: PT.func,
bucket: PT.string,
challengeSortBys: PT.arrayOf(PT.string),
};

export default Listing;
61 changes: 55 additions & 6 deletions src/containers/Challenges/index.jsx
Original file line number Diff line number Diff line change
@@ -4,10 +4,17 @@ import { connect } from "react-redux";
import Listing from "./Listing";
import actions from "../../actions";
import ChallengeError from "./Listing/ChallengeError";
import ChallengeRecommendedError from "./Listing/ChallengeRecommendedError";
import IconListView from "../../assets/icons/list-view.svg";
import IconCardView from "../../assets/icons/card-view.svg";
import { ButtonIcon } from "../../components/Button";
import * as constants from "../../constants";

import "./styles.scss";

const Challenges = ({
challenges,
search,
page,
perPage,
sortBy,
@@ -17,30 +24,65 @@ const Challenges = ({
getChallenges,
updateFilter,
bucket,
recommended,
loadingRecommendedChallengesError,
}) => {
const [initialized, setInitialized] = useState(false);

useEffect(() => {
getChallenges().finally(() => setInitialized(true));
}, []);

const BUCKET_OPEN_FOR_REGISTRATION = constants.FILTER_BUCKETS[1];
const isRecommended = recommended && bucket === BUCKET_OPEN_FOR_REGISTRATION;
const sortByValue = isRecommended
? sortBy
: sortBy === constants.CHALLENGE_SORT_BY_RECOMMENDED
? constants.CHALLENGE_SORT_BY_DEFAULT
: sortBy;
const sortByLabels = isRecommended
? Object.keys(constants.CHALLENGE_SORT_BY)
: Object.keys(constants.CHALLENGE_SORT_BY).filter(
(label) => label !== constants.CHALLENGE_SORT_BY_RECOMMENDED_LABEL
);

const isNoRecommendedChallenges =
bucket === BUCKET_OPEN_FOR_REGISTRATION &&
recommended &&
loadingRecommendedChallengesError;

return (
<div styleName="page">
<h1 styleName="title">CHALLENGES</h1>
{challenges.length === 0 ? (
initialized && <ChallengeError />
) : (
<h1 styleName="title">
<span>CHALLENGES</span>
<span styleName="view-mode">
<ButtonIcon>
<IconListView />
</ButtonIcon>
<ButtonIcon>
<IconCardView />
</ButtonIcon>
</span>
</h1>

{isNoRecommendedChallenges && initialized && (
<ChallengeRecommendedError />
)}
{challenges.length === 0 && initialized && <ChallengeError />}
{challenges.length > 0 && (
<Listing
challenges={challenges}
search={search}
page={page}
perPage={perPage}
sortBy={sortBy}
sortBy={sortByValue}
total={total}
endDateStart={endDateStart}
startDateEnd={startDateEnd}
updateFilter={updateFilter}
bucket={bucket}
getChallenges={getChallenges}
challengeSortBys={sortByLabels}
/>
)}
</div>
@@ -49,6 +91,7 @@ const Challenges = ({

Challenges.propTypes = {
challenges: PT.arrayOf(PT.shape()),
search: PT.string,
page: PT.number,
perPage: PT.number,
sortBy: PT.string,
@@ -58,18 +101,24 @@ Challenges.propTypes = {
getChallenges: PT.func,
updateFilter: PT.func,
bucket: PT.string,
recommended: PT.bool,
loadingRecommendedChallengesError: PT.bool,
};

const mapStateToProps = (state) => ({
state: state,
search: state.filter.challenge.search,
page: state.filter.challenge.page,
perPage: state.filter.challenge.perPage,
sortBy: state.filter.challenge.sortBy,
total: state.challenges.total,
endDateStart: state.filter.challenge.endDateStart,
startDateEnd: state.filter.challenge.startDateEnd,
challenges: state.challenges.challengesFiltered,
challenges: state.challenges.challenges,
bucket: state.filter.challenge.bucket,
recommended: state.filter.challenge.recommended,
loadingRecommendedChallengesError:
state.challenges.loadingRecommendedChallengesError,
});

const mapDispatchToProps = {
8 changes: 8 additions & 0 deletions src/containers/Challenges/styles.scss
Original file line number Diff line number Diff line change
@@ -9,3 +9,11 @@
@include barlow-condensed-medium;
margin-bottom: 22px;
}

.view-mode {
float: right;

> * {
margin: 0 2px;
}
}
124 changes: 107 additions & 17 deletions src/containers/Filter/ChallengeFilter/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect, useRef } from "react";
import PT from "prop-types";
import _ from "lodash";
import RadioButton from "../../../components/RadioButton";
@@ -8,6 +8,7 @@ import Toggle from "../../../components/Toggle";
import Button from "../../../components/Button";
import TextInput from "../../../components/TextInput";
import * as utils from "../../../utils";
import * as constants from "../../../constants";

import "./styles.scss";

@@ -18,6 +19,7 @@ const ChallengeFilter = ({
tags,
prizeFrom,
prizeTo,
recommended,
subCommunities,
challengeBuckets,
challengeTypes,
@@ -26,7 +28,10 @@ const ChallengeFilter = ({
challengeSubCommunities,
saveFilter,
clearFilter,
switchBucket,
openForRegistrationCount,
}) => {
const BUCKET_OPEN_FOR_REGISTRATION = constants.FILTER_BUCKETS[1];
const tagOptions = utils.createDropdownTermOptions(challengeTags);
const bucketOptions = utils.createRadioOptions(challengeBuckets, bucket);

@@ -39,26 +44,66 @@ const ChallengeFilter = ({
prizeFrom,
prizeTo,
subCommunities,
recommended,
})
);

utils.setSelectedDropdownTermOptions(tagOptions, filter.tags);

useEffect(() => {
const newFilter = _.cloneDeep({
bucket,
types,
tracks,
tags,
prizeFrom,
prizeTo,
subCommunities,
recommended,
});
setFilter(newFilter);
}, [
bucket,
types,
tracks,
tags,
prizeFrom,
prizeTo,
subCommunities,
recommended,
]);

const ref = useRef(null);

useEffect(() => {
if (!ref.current) {
return;
}

const openForRegistrationElement = ref.current.children[0].children[1];
const badgeElement = utils.icon.createBadgeElement(
openForRegistrationElement,
`${openForRegistrationCount}`
);

return () => {
badgeElement.parentElement.removeChild(badgeElement);
};
}, [ref.current, openForRegistrationCount]);

return (
<div styleName="filter">
<div styleName="buckets vertical-list">
<div styleName="buckets vertical-list" ref={ref}>
<RadioButton
options={bucketOptions}
onChange={(newBucketOptions) => {
const filterChange = {
...filter,
bucket: utils.getSelectedRadioOption(newBucketOptions).label,
};
setFilter(filterChange);
saveFilter(filterChange);
const selectedBucket = utils.getSelectedRadioOption(
newBucketOptions
).label;
setFilter({ ...filter, bucket: selectedBucket });
switchBucket(selectedBucket);
}}
/>
<span></span>
</div>

<div styleName="challenge-types">
@@ -115,14 +160,43 @@ const ChallengeFilter = ({
tags: selectedTagOptions.map((tagOption) => tagOption.label),
});
}}
size="xs"
/>
</div>

<div styleName="prize">
<h3>Prize Amount</h3>
<TextInput size="xs" label="From" value={`${prizeFrom}`} />
<div styleName="input-group">
<TextInput
value={filter.prizeFrom}
size="xs"
label="From"
value={`${utils.formatPrizeAmount(prizeFrom)}`}
onChange={(value) => {
setFilter({
...filter,
prizeFrom: utils.parsePrizeAmountText(value),
});
}}
/>
<span styleName="suffix">USD</span>
</div>
<span styleName="separator" />
<TextInput size="xs" label="To" value={`${prizeTo}`} />
<div styleName="input-group">
<TextInput
value={filter.prizeTo}
size="xs"
label="To"
value={`${utils.formatPrizeAmount(prizeTo)}`}
onChange={(value) => {
setFilter({
...filter,
prizeTo: utils.parsePrizeAmountText(value),
});
}}
/>
<span styleName="suffix">USD</span>
</div>
</div>

{challengeSubCommunities.length > 0 && (
@@ -145,12 +219,26 @@ const ChallengeFilter = ({
</div>
</div>
)}
<div styleName="recommended-challenges">
<span styleName="toggle">
<Toggle />
</span>
<span>Recommended Challenges</span>
</div>

{bucket === BUCKET_OPEN_FOR_REGISTRATION && (
<div styleName="recommended-challenges">
<span styleName="toggle">
<Toggle
checked={filter.recommended}
onChange={(checked) => {
setFilter({
...filter,
recommended: checked,
sortBy: checked
? constants.CHALLENGE_SORT_BY_RECOMMENDED
: constants.CHALLENGE_SORT_BY_DEFAULT,
});
}}
/>
</span>
<span>Recommended Challenges</span>
</div>
)}

<div styleName="footer">
<Button onClick={clearFilter}>CLEAR FILTER</Button>
@@ -175,6 +263,8 @@ ChallengeFilter.propTypes = {
challengeSubCommunities: PT.arrayOf(PT.string),
saveFilter: PT.func,
clearFilter: PT.func,
switchBucket: PT.func,
openForRegistrationCount: PT.number,
};

export default ChallengeFilter;
37 changes: 36 additions & 1 deletion src/containers/Filter/ChallengeFilter/styles.scss
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ $filter-padding-y: 3 * $base-unit;
margin-bottom: 18px;

> h3 {
margin-bottom: 12px;
margin-bottom: 15px;
font-size: inherit;
line-height: 19px;
}
@@ -29,6 +29,18 @@ $filter-padding-y: 3 * $base-unit;
margin: $base-unit 0;
}
}

:global(.badge) {
display: inline-block;
margin-left: $base-unit;
padding: 0 5px;
font-weight: bold;
font-size: 11px;
line-height: 16px;
color: white;
background: red;
border-radius: 13px;
}
}

.challenge-types,
@@ -51,6 +63,7 @@ $filter-padding-y: 3 * $base-unit;

.skills {
margin-bottom: 34px;
max-width: 230px;
}

.prize {
@@ -74,6 +87,27 @@ $filter-padding-y: 3 * $base-unit;
margin-top: -12px;
}

.input-group {
position: relative;
margin-top: -12px;

input {
padding-right: 36px !important;
}

.suffix {
@include roboto-medium;

position: absolute;
top: 22px;
right: 10px;
font-size: 13px;
line-height: 22px;
color: $tc-gray-30;
pointer-events: none;
}
}

.recommended-challenges {
margin: 20px 0 25px;
font-size: $font-size-sm;
@@ -98,3 +132,4 @@ $filter-padding-y: 3 * $base-unit;
margin-left: 9px;
}
}

13 changes: 13 additions & 0 deletions src/containers/Filter/index.jsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ const Filter = ({
tags,
prizeFrom,
prizeTo,
recommended,
subCommunities,
challengeBuckets,
challengeTypes,
@@ -21,6 +22,7 @@ const Filter = ({
getChallenges,
getTags,
getSubCommunities,
openForRegistrationCount,
}) => {
useEffect(() => {
getTags();
@@ -32,6 +34,10 @@ const Filter = ({
getChallenges(filter);
};

const onSwitchBucket = (bucket) => {
updateFilter({ bucket });
};

return (
<ChallengeFilter
bucket={bucket}
@@ -40,6 +46,7 @@ const Filter = ({
tags={tags}
prizeFrom={prizeFrom}
prizeTo={prizeTo}
recommended={recommended}
subCommunities={subCommunities}
challengeBuckets={challengeBuckets}
challengeTypes={challengeTypes}
@@ -48,6 +55,8 @@ const Filter = ({
challengeSubCommunities={challengeSubCommunities}
saveFilter={onSaveFilter}
clearFilter={() => {}}
switchBucket={onSwitchBucket}
openForRegistrationCount={openForRegistrationCount}
/>
);
};
@@ -59,6 +68,7 @@ Filter.propTypes = {
tags: PT.arrayOf(PT.string),
prizeFrom: PT.number,
prizeTo: PT.number,
recommended: PT.bool,
subCommunities: PT.arrayOf(PT.string),
challengeBuckets: PT.arrayOf(PT.string),
challengeTypes: PT.arrayOf(PT.string),
@@ -69,6 +79,7 @@ Filter.propTypes = {
getChallenges: PT.func,
getTags: PT.func,
getSubCommunities: PT.func,
openForRegistrationCount: PT.number,
};

const mapStateToProps = (state) => ({
@@ -79,12 +90,14 @@ const mapStateToProps = (state) => ({
tags: state.filter.challenge.tags,
prizeFrom: state.filter.challenge.prizeFrom,
prizeTo: state.filter.challenge.prizeTo,
recommended: state.filter.challenge.recommended,
subCommunities: state.filter.challenge.subCommunities,
challengeBuckets: state.lookup.buckets,
challengeTypes: state.lookup.types,
challengeTracks: state.lookup.tracks,
challengeTags: state.lookup.tags,
challengeSubCommunities: state.lookup.subCommunities,
openForRegistrationCount: state.challenges.openForRegistrationCount,
});

const mapDispatchToProps = {
63 changes: 59 additions & 4 deletions src/reducers/challenges.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
import { handleActions } from "redux-actions";
import * as util from "../utils/challenge";
import * as constants from "../constants";

const defaultState = {
loadingChallenges: false,
loadingChallengesError: null,
loadingRecommendedChallengesError: false,
challenges: [],
challengesFiltered: [],
allActiveChallenges: [],
openForRegistrationChallenges: [],
closedChallenges: [],
openForRegistrationCount: 0,
total: 0,
};

function onGetChallengesInit(state) {
return { ...state, loadingChallenges: true, loadingChallengesError: null };
return {
...state,
loadingChallenges: true,
loadingChallengesError: null,
loadingRecommendedChallengesError: false,
};
}

function onGetChallengesDone(state, { payload }) {
return {
...state,
loadingChallenges: false,
loadingChallengesError: null,
loadingRecommendedChallengesError:
payload.loadingRecommendedChallengesError,
challenges: payload.challenges,
challengesFiltered: payload.challengesFiltered,
allActiveChallenges: payload.allActiveChallenges,
openForRegistrationChallenges: payload.openForRegistrationChallenges,
closedChallenges: payload.closedChallenges,
openForRegistrationCount: payload.openForRegistrationCount,
total: payload.total,
};
}
@@ -29,16 +45,55 @@ function onGetChallengesFailure(state, { payload }) {
loadingChallenges: false,
loadingChallengesError: payload,
challenges: [],
challengesFiltered: [],
allActiveChallenges: [],
openForRegistrationChallenges: [],
closedChallenges: [],
openForRegistrationCount: 0,
total: 0,
};
}

function onUpdateFilter(state, { payload }) {
const FILTER_BUCKETS = constants.FILTER_BUCKETS;
const BUCKET_ALL_ACTIVE_CHALLENGES = FILTER_BUCKETS[0];
const BUCKET_OPEN_FOR_REGISTRATION = FILTER_BUCKETS[1];
const BUCKET_CLOSED_CHALLENGES = FILTER_BUCKETS[2];
const filterChange = payload;
const {
allActiveChallenges,
openForRegistrationChallenges,
closedChallenges,
} = state;

let challenges;
let total;

if (util.isSwitchingBucket(filterChange)) {
switch (filterChange.bucket) {
case BUCKET_ALL_ACTIVE_CHALLENGES:
challenges = allActiveChallenges;
break;
case BUCKET_OPEN_FOR_REGISTRATION:
challenges = openForRegistrationChallenges;
break;
case BUCKET_CLOSED_CHALLENGES:
challenges = closedChallenges;
break;
}
total = challenges.meta.total;

return { ...state, challenges, total };
}

return { ...state };
}

export default handleActions(
{
GET_CHALLENGE_INIT: onGetChallengesInit,
GET_CHALLENGES_DONE: onGetChallengesDone,
GET_CHALLENGES_FAILURE: onGetChallengesFailure,
UPDATE_FILTER: onUpdateFilter,
},
defaultState
);
15 changes: 11 additions & 4 deletions src/reducers/filter.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import { handleActions } from "redux-actions";
import * as constants from "../constants";
import moment from "moment";

const defaultState = {
challenge: {
types: constants.FILTER_CHALLENGE_TYPES,
tracks: constants.FILTER_CHALLENGE_TRACKS,
tracks: constants.FILTER_CHALLENGE_TRACKS.filter((track) => track !== "QA"),
search: "",
tags: [],
groups: [],
startDateEnd: null,
endDateStart: null,
endDateStart: moment().subtract(99, "year").toDate().toISOString(),
startDateEnd: moment().add(1, "year").toDate().toISOString(),
page: 1,
perPage: constants.PAGINATION_PER_PAGE[0],
sortBy: constants.CHALLENGE_SORT_BY["Most recent"],
sortOrder: null,
sortOrder: constants.SORT_ORDER.ASC,

// ---

bucket: constants.FILTER_BUCKETS[1],
prizeFrom: 0,
prizeTo: 10000,
subCommunities: [],
recommended: false,
},
};

function onRestoreFilter(state, { payload }) {
return { ...state, ...payload };
}

function onUpdateFilter(state, { payload }) {
return { ...state, challenge: { ...state.challenge, ...payload } };
}

export default handleActions(
{
RESTORE_FILTER: onRestoreFilter,
UPDATE_FILTER: onUpdateFilter,
},
defaultState
9 changes: 9 additions & 0 deletions src/root.component.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ import { createHistory, LocationProvider } from "@reach/router";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
import * as util from "./utils/session";
import actions from "./actions";

// History for location provider
const history = createHistory(window);
@@ -13,6 +15,13 @@ export default function Root() {
useEffect(() => {
// when app starts it should set its side menu structure
setAppMenu("/earn", appMenu);

const unsubscribe = store.subscribe(() =>
util.persistFilter(util.selectFilter(store.getState()))
);
return () => {
unsubscribe();
};
}, []);

return (
6 changes: 6 additions & 0 deletions src/services/challenges.js
Original file line number Diff line number Diff line change
@@ -11,6 +11,12 @@ async function getChallenges(filter) {
return api.get(`/challenges/${challengeQuery}`);
}

async function getRecommendedChallenges(filter, handle) {
const challengeQuery = util.buildQueryString(filter);
return api.get(`/recommender-api/${handle}/${challengeQuery}`);
}

export default {
getChallenges,
getRecommendedChallenges,
};
8 changes: 7 additions & 1 deletion src/store.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@
import { createStore, compose, applyMiddleware } from "redux";
import { createPromise } from "redux-promise-middleware";
import root from "./reducers";
import actions from "./actions";
import * as util from "./utils/session";

const middlewares = [
createPromise({ promiseTypeSuffixes: ["INIT", "DONE", "FAILURE"] }),
@@ -15,4 +17,8 @@ if (process.env.APPMODE === "development") {
middlewares.push(logger);
}

export default createStore(root, compose(applyMiddleware(...middlewares)));
const store = createStore(root, compose(applyMiddleware(...middlewares)));

store.dispatch(actions.filter.restoreFilter(util.restoreFilter()));

export default store;
3 changes: 3 additions & 0 deletions src/styles/_variables.scss
Original file line number Diff line number Diff line change
@@ -127,6 +127,9 @@ $font-size-sm: 14px;
$font-size-xs: 12px;

/// APP

$screen-xxl: 1366px;

$base-unit: 5px;

$body-color: $tc-gray-90;
25 changes: 22 additions & 3 deletions src/utils/challenge.js
Original file line number Diff line number Diff line change
@@ -34,7 +34,9 @@ export function createChallengeCriteria(filter) {
startDateEnd: filter.startDateEnd,
endDateStart: filter.endDateStart,
endDateEnd: filter.endDateEnd,
sortBy: filter.sortBy,
sortBy: isValidCriteriaSortBy(filter.sortBy)
? filter.sortBy
: constants.CHALLENGE_SORT_BY_DEFAULT,
sortOrder: filter.sortOrder,
groups: filter.groups,
};
@@ -44,18 +46,22 @@ export function createOpenForRegistrationChallengeCriteria() {
return {
status: "Active",
currentPhaseName: "Registration",
endDateStart: null,
startDateEnd: null,
};
}

export function createActiveChallengeCriteria() {
export function createAllActiveChallengeCriteria() {
return {
status: "Active",
currentPhaseName: "Submission",
registrationEndDateEnd: new Date().toISOString(),
endDateStart: null,
startDateEnd: null,
};
}

export function createPastChallengeCriteria() {
export function createClosedChallengeCriteria() {
return {
status: "Completed",
};
@@ -102,6 +108,19 @@ export function checkRequiredFilterAttributes(filter) {
return valid;
}

export function isSwitchingBucket(filterChange) {
const keys = Object.keys(filterChange);
return keys.length === 1 && keys[0] === "bucket";
}

export function isDisplayingBucket(filter, bucket) {
return filter.bucket === bucket;
}

export function isValidCriteriaSortBy(sortBy) {
return ["updated", "overview.totalPrizes", "name"].includes(sortBy);
}

/**
* Returns phase's end date.
* @param {Object} phase
12 changes: 11 additions & 1 deletion src/utils/icon.js
Original file line number Diff line number Diff line change
@@ -119,7 +119,7 @@ function createTCOEventIcon (color) {
<g id="01_3_Find-Work-Challenges-Non-Logged-In-Hover" transform="translate(-346.000000, -264.000000)">
<g id="Group-17" transform="translate(329.000000, 245.000000)">
<g id="icon-/-challenge-/-track-copy" transform="translate(17.000000, 19.531250)">
<text id="TCO" fontFamily="Helvetica" fontSize="11" fontWeight="normal" lineSpacing="12" fill={color}>
<text id="TCO" fontFamily="Helvetica" fontSize="11" fontWeight="normal" linespacing="12" fill={color}>
<tspan x="0" y="44.8854167">TCO</tspan>
</text>
<g id="icon-/-track-/-design" transform="translate(22.000000, 23.177083)">
@@ -136,3 +136,13 @@ function createTCOEventIcon (color) {
</svg>
);
}

export function createBadgeElement(htmlElement, content) {
const badgeElement = document.createElement('span');

badgeElement.classList.add('badge');
badgeElement.textContent = content;
htmlElement.appendChild(badgeElement);

return badgeElement;
}
41 changes: 39 additions & 2 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -122,14 +122,51 @@ export function formatMoneyValue(value) {
}

if (val.startsWith("-")) {
val = `-$${val.slice(1)}`;
val = `-\uFF04${val.slice(1)}`;
} else {
val = `$${val}`;
val = `\uFF04${val}`;
}

return val;
}

/**
* Format a number value into the integer text of amount.
* Ex: 0 -> 0, greater than 10000 -> 10,000+
*/
export function formatPrizeAmount(value) {
let val = value || 0;
let greaterThan10000 = val >= 10000;

val = val.toLocaleString("en-US");

const i = val.indexOf(".");
if (i !== -1) {
val = val.slice(0, i);
}

val = greaterThan10000 ? "10,000+" : val;

return val;
}

export function parsePrizeAmountText(s) {
let val = s;
if (val.endsWith("+")) {
val = val.slice(0, val.length - 1);
}

const i = val.indexOf(".");
if (i !== -1) {
val = val.slice(0, i);
}

val = val.replace("/,/g", "");
val = parseInt(val);

return isNaN(val) ? 0 : val;
}

export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
100 changes: 48 additions & 52 deletions src/utils/menu.js
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ export class MenuSelection {
}

travel(root) {
Object.keys(root).forEach((key) => {
this.getMenuItems(root).forEach((key) => {
if (_.isObject(root[key])) {
root[key].expanded = false;
root[key].branch = true;
@@ -24,35 +24,37 @@ export class MenuSelection {
});
}

getMenuItems(menu) {
return Object.keys(_.omit(menu, "expanded", "active", "branch", "leaf"));
}

select(name) {
let found = false;

const selectInternal = (root) => {
Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
(key) => {
if (found) {
return;
}
this.getMenuItems(root).forEach((key) => {
if (found) {
return;
}

if (key !== name) {
if (root[key].branch) {
selectInternal(root[key]);
} else {
root[key].active = false;
}
if (key !== name) {
if (root[key].branch) {
selectInternal(root[key]);
} else {
if (root[key].leaf) {
root[key].active = true;
this.selectedMenuItem = name;
} else {
root[key].expanded = !root[key].expanded;
}

found = true;
this.emitSelectionEvent();
root[key].active = false;
}
} else {
if (root[key].leaf) {
root[key].active = true;
this.selectedMenuItem = name;
} else {
root[key].expanded = !root[key].expanded;
}

found = true;
this.emitSelectionEvent();
}
);
});
};

selectInternal(this.menu);
@@ -62,17 +64,15 @@ export class MenuSelection {
let leaf = false;

const isLeafInternal = (root) => {
Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
(key) => {
if (key !== name) {
if (root[key].branch) {
isLeafInternal(root[key]);
}
} else if (root[key].leaf) {
leaf = true;
this.getMenuItems(root).forEach((key) => {
if (key !== name) {
if (root[key].branch) {
isLeafInternal(root[key]);
}
} else if (root[key].leaf) {
leaf = true;
}
);
});
};

isLeafInternal(this.menu);
@@ -85,20 +85,18 @@ export class MenuSelection {
}

isExpanded(name) {
let expanded = false;
let expanded;

const isExpandedInternal = (root) => {
Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
(key) => {
if (key !== name) {
if (root[key].branch) {
isExpandedInternal(root[key]);
}
} else if (root[key].branch) {
expanded = root[key].expanded;
this.getMenuItems(root).forEach((key) => {
if (key !== name) {
if (root[key].branch) {
isExpandedInternal(root[key]);
}
} else if (root[key].branch) {
expanded = root[key].expanded;
}
);
});
};

isExpandedInternal(this.menu);
@@ -123,20 +121,18 @@ export class MenuSelection {
}

const isActiveInternal = (root) => {
Object.keys(_.omit(root, "expanded", "active", "branch", "leaf")).forEach(
(key) => {
if (key !== this.selectedMenuItem) {
if (root[key].branch) {
stack.push(key);
isActiveInternal(root[key]);
stack.pop(key);
}
} else {
this.getMenuItems(root).forEach((key) => {
if (key !== this.selectedMenuItem) {
if (root[key].branch) {
stack.push(key);
path = [...stack.arr];
isActiveInternal(root[key]);
stack.pop(key);
}
} else {
stack.push(key);
path = [...stack.arr];
}
);
});
};

isActiveInternal(this.menu);
10 changes: 5 additions & 5 deletions src/utils/pagination.js
Original file line number Diff line number Diff line change
@@ -10,11 +10,11 @@ export function pageIndexToPage(pageIndex) {
* @param {any} response Web APIs Response
* @return {Object} pagination data
*/
export function getResponseHeaders(reponse) {
export function getResponseHeaders(response) {
return {
page: +(reponse.headers.get("X-Page") || 0),
perPage: +(reponse.headers.get("X-Per-Page") || 0),
total: +(reponse.headers.get("X-Total") || 0),
totalPages: +(reponse.headers.get("X-Total-Pages") || 0),
page: +(response.headers.get("X-Page") || 0),
perPage: +(response.headers.get("X-Per-Page") || 0),
total: +(response.headers.get("X-Total") || 0),
totalPages: +(response.headers.get("X-Total-Pages") || 0),
};
}
29 changes: 29 additions & 0 deletions src/utils/session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function selectFilter(state) {
return state.filter;
}

let currentFilterValue;
function persistFilter(filter) {
let previousFilterValue = currentFilterValue;
currentFilterValue = filter;

if (previousFilterValue !== currentFilterValue) {
try {
sessionStorage.setItem("filter", JSON.stringify(filter));
} catch (e) {
console.error(e);
}
}
}

function restoreFilter() {
let filter;
try {
filter = JSON.parse(sessionStorage.getItem("filter"));
} catch (e) {
filter = {};
}
return filter;
}

export { selectFilter, persistFilter, restoreFilter };
2 changes: 1 addition & 1 deletion src/utils/tag.js
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ export function calculateNumberOfVisibleTags(tags) {
let n = tags.length;
if (tagsString.length > MAX_LEN) {
let ss = "";
for (n = 0; n < tags.length && ss.length < 20; n += 1) {
for (n = 0; n < tags.length && ss.length < MAX_LEN; n += 1) {
ss = ss.concat(tags[n]);
}
}
2 changes: 1 addition & 1 deletion src/utils/url.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import qs from "qs";
* `{ p: undefined }` => ""
* `{ p: value }` => "p=value"
* `{ p: [] }` => ""
* `{ p: ['Challenge', 'First2Finish', 'Task'] } => "p[]=Challenge&p[]=First2Finish&p[]=Taks`
* `{ p: ['Challenge', 'First2Finish', 'Task'] } => "p[]=Challenge&p[]=First2Finish&p[]=Task`
* `{ p: ['Design', 'Development', 'Data Science', 'Quality Assurance'] }` => "p[]=Design&p[]=Development&p=Data%20Science&p[]=Quality%20Assurance"
* `{ p: { Des: true, Dev: true, DS: false, QA: false } }` => "p[Des]=true&p[Dev]=true&p[DS]=false&p[QA]=false"
*