diff --git a/cypress/integration/test_filters.spec.js b/cypress/integration/test_filters.spec.js index aeb9fe92..76492212 100644 --- a/cypress/integration/test_filters.spec.js +++ b/cypress/integration/test_filters.spec.js @@ -19,6 +19,13 @@ describe('Test the filters and search for stories in Home page', () => { cy.contains(value).click({ force: true }) } + const toggleCheckbox = (filter, value) => { + cy.get('[data-cy=search-filters]') + .get(`[data-cy=filter-section-${filter}]`) + .contains(value) + .click({ force: true }) + } + const searchByTitle = (value) => { cy.get('[data-cy=search-input]').type(value) cy.get('[data-cy=btn-search]').click() @@ -49,11 +56,13 @@ describe('Test the filters and search for stories in Home page', () => { }) it('Filters stories based on category', () => { - setDropdown('category', 'Bug') + cy.get('[data-cy=toggle-filters]').click() + + toggleCheckbox('category', 'Bug') cy.contains('No stories') - setDropdown('category', testStory.category) + toggleCheckbox('category', testStory.category) cy.contains(testStory.title) }) diff --git a/src/assets/scss/components/index.scss b/src/assets/scss/components/index.scss index 55222949..a505c646 100644 --- a/src/assets/scss/components/index.scss +++ b/src/assets/scss/components/index.scss @@ -6,6 +6,7 @@ @import 'dragdrop'; @import 'timeline'; @import 'storiesList'; +@import 'inputButtons'; @import 'commentForm'; @import 'comments'; @import 'pagination'; @@ -15,11 +16,11 @@ @import 'dropdownOptions'; @import 'vote'; @import 'notifications'; -@import 'roadmap'; +@import 'searchBar'; @import 'userProfile'; @import 'editableLabel'; @import 'footer'; @import 'alerts'; -@import 'search'; +@import 'searchInput'; @import 'storyPageTimeline'; @import 'productList.scss'; diff --git a/src/assets/scss/components/inputButtons.scss b/src/assets/scss/components/inputButtons.scss new file mode 100644 index 00000000..7043dfb8 --- /dev/null +++ b/src/assets/scss/components/inputButtons.scss @@ -0,0 +1,91 @@ +.checkbox-container { + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + cursor: pointer; + margin-right: 25px; + position: relative; + user-select: none; + + input { + cursor: pointer; + height: 0; + opacity: 0; + position: absolute; + width: 0; + } + + input:checked { + ~ .checkmark { + background-color: $primary; + } + + ~ .checkmark:after { + display: block; + } + } + + .checkmark { + border: 1px solid $light-gray; + height: 14px; + left: 0; + position: absolute; + top: 0; + width: 14px; + } + + .checkmark:after { + -ms-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + border: solid white; + border-width: 0 2px 2px 0; + content: ''; + display: none; + height: 10px; + left: 5px; + position: absolute; + top: 0px; + transform: rotate(45deg); + width: 3px; + } +} + +.radio-button-container { + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + cursor: pointer; + margin-right: 30px; + position: relative; + user-select: none; + + input { + cursor: pointer; + opacity: 0; + position: absolute; + } + + .radio-button-mark { + border: 1px solid $light-gray; + border-radius: 50%; + height: 14px; + left: 0; + position: absolute; + top: 0; + width: 14px; + + &:after { + background: $primary; + border-radius: 50%; + content: ''; + display: none; + height: 8px; + position: absolute; + width: 8px; + } + } + + input:checked ~ .radio-button-mark { + background-color: $primary; + } +} diff --git a/src/assets/scss/components/productList.scss b/src/assets/scss/components/productList.scss index 5fed2753..dd855e31 100644 --- a/src/assets/scss/components/productList.scss +++ b/src/assets/scss/components/productList.scss @@ -9,6 +9,7 @@ } .product-card { + background-color: $eos-white; border-radius: 5px; box-shadow: 0px 2px 15px $shadow-color; margin: 8px 15px; diff --git a/src/assets/scss/components/roadmap.scss b/src/assets/scss/components/roadmap.scss deleted file mode 100644 index aa8d94f7..00000000 --- a/src/assets/scss/components/roadmap.scss +++ /dev/null @@ -1,118 +0,0 @@ -.roadmap { - @extend .flex; - @extend .flex-row; - @extend .flex-space-between; - margin-bottom: 32px; - position: relative; - - &::before { - border-top: 1px solid gray; - content: ''; - position: absolute; - top: 22px; - width: 100%; - z-index: 1; - } - - .btn { - background: $eos-cerulean-50; - z-index: 10; - } - - .btn-tabs { - border-bottom: 2px solid transparent; - border-radius: 0; - - .eos-icons { - margin-right: 12px; - vertical-align: sub; - } - - &-selected, - &:hover, - &:focus { - border-bottom-color: $status-tabs-bg; - border-radius: 0px; - } - - &:focus { - outline: transparent 0px; - } - } -} - -.roadmap-one { - .btn-tabs { - - .eos-icons { - margin-right: 12px; - vertical-align: sub; - } - - &-selected, - &:hover, - &:focus { - border: 2px solid $light-gray; - } - - &:focus { - outline: transparent 0px; - } - } -} - -.roadmap-dropdown { - background-color: white; - display: none; - padding: 20px; -} - -@media (max-width: 900px) { - .roadmap-container { - display: none; - } - - .roadmap-dropdown { - display: block; - } - - .roadmap { - align-items: flex-start; - display: flex; - flex-direction: column; - position: relative; - - &::before { - border-left: 1px solid gray; - border-top: 0; - content: ''; - height: 80%; - left: 23px; - position: absolute; - width: 100%; - z-index: 1; - } - - .btn-tabs { - margin: 0.8rem 0; - } - } - - .btn-tabs { - margin: 0.5rem; - width: 200px; - } - -} - -@media (max-width: 600px) { - .roadmap-one { - flex-direction: column; - } - - .btn-tabs { - margin: 0.3rem 0; - text-align: left; - width: 100%; - } -} diff --git a/src/assets/scss/components/searchBar.scss b/src/assets/scss/components/searchBar.scss new file mode 100644 index 00000000..d21580a2 --- /dev/null +++ b/src/assets/scss/components/searchBar.scss @@ -0,0 +1,63 @@ +.search-filters { + @extend .flex; + @extend .flex-row; + @extend .flex-center; + width: 100%; +} + +.filter-container { + line-height: 40px; + padding: 15px; +} + +.filter-container-status { + border-right: 0.5px solid $gray-bg; + width: 42%; + + .checkbox-row { + min-width: 180px; + } +} + +.filter-container-category { + border-right: 0.5px solid $gray-bg; + min-width: 130px; + width: 30%; + + .checkbox-row { + min-width: 130px; + } +} + +.filter-container-sort { + min-width: 130px; + width: 14%; +} + +.filter-section { + @extend .flex; + @extend .flex-row; + @extend .flex-space-between; +} + +@media (max-width: 900px) { + .filter-container-status { + border: unset; + width: unset; + } + + .filter-container-category { + width: 45%; + } +} + +@media (max-width: 600px) { + .search-filters { + justify-content: flex-start; + } + + .filter-container-category { + border: unset; + width: unset; + } +} diff --git a/src/assets/scss/components/search.scss b/src/assets/scss/components/searchInput.scss similarity index 100% rename from src/assets/scss/components/search.scss rename to src/assets/scss/components/searchInput.scss diff --git a/src/assets/scss/pages/index.scss b/src/assets/scss/pages/index.scss index d23db0bf..7a3913a7 100644 --- a/src/assets/scss/pages/index.scss +++ b/src/assets/scss/pages/index.scss @@ -5,3 +5,4 @@ @import 'profileRelated'; @import 'newstory'; @import 'notifications'; +@import 'myStories'; diff --git a/src/assets/scss/pages/myStories.scss b/src/assets/scss/pages/myStories.scss new file mode 100644 index 00000000..eb6105ec --- /dev/null +++ b/src/assets/scss/pages/myStories.scss @@ -0,0 +1,32 @@ +.roadmap-one { + .btn-tabs { + &-selected, + &:hover, + &:focus { + border: 2px solid $light-gray; + } + + &:focus { + outline: transparent 0px; + } + } +} + +@media (max-width: 900px) { + .btn-tabs { + margin: 0.5rem; + width: 200px; + } +} + +@media (max-width: 600px) { + .btn-tabs { + margin: 0.3rem 0; + text-align: left; + width: 100%; + } + + .roadmap-one { + flex-direction: column; + } +} diff --git a/src/components/InputButtons.js b/src/components/InputButtons.js new file mode 100644 index 00000000..136b4828 --- /dev/null +++ b/src/components/InputButtons.js @@ -0,0 +1,32 @@ +import React from 'react' + +export const CheckBox = ({ checked, id, textLabel, icon, onChange }) => { + if (!textLabel) { + textLabel = id + } + + return ( + <> + + + + ) +} + +export const RadioButton = ({ id, checked, onChange }) => { + return ( + <> + + + + ) +} diff --git a/src/components/Pagination.js b/src/components/Pagination.js index 31daf231..6a9a85e0 100644 --- a/src/components/Pagination.js +++ b/src/components/Pagination.js @@ -20,7 +20,7 @@ const Pagination = (props) => { }, [status, productQuery, getPage]) useEffect(() => { - if (storyCount) { + if (typeof storyCount === typeof 0) { if (storyCount >= 5) { const n = Math.ceil(storyCount / 5) setPages([...Array(n + 1).keys()].slice(1)) diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js new file mode 100644 index 00000000..bfbc32d6 --- /dev/null +++ b/src/components/SearchBar.js @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from 'react' +import { EOS_KEYBOARD_ARROW_UP, EOS_KEYBOARD_ARROW_DOWN } from 'eos-icons-react' +import { CheckBox, RadioButton } from './InputButtons' +import SearchInput from '../modules/SearchInput' +import Lists from '../utils/Lists' +import userStory from '../services/user_story' + +const SearchBar = (props) => { + const { + sort, + setSort, + setSearchQuery, + setAuthorQuery, + setPage, + selectedStatuses, + setSelectedStatuses, + selectedCategories, + setSelectedCategories + } = props + + const [categories, setCategories] = useState([]) + + const [filtersOpened, setFiltersOpened] = useState(false) + + const toggleFilters = (filters, setFilters, value) => () => { + if (filters.find((filter) => filter === value)) { + setFilters(filters.filter((filter) => filter !== value)) + } else { + setFilters(filters.concat(value)) + } + setPage(1) + } + + useEffect(() => { + const fetchCategories = async () => { + const response = await userStory.getCategories() + setCategories( + response.data.data.__type.enumValues.map((ele) => { + return ele.name + }) + ) + } + fetchCategories() + }, []) + + return ( +
+ + + + + {filtersOpened && ( +
+
+

Stages

+
+ {Lists.stateList.map((state, key) => { + return ( + + status === state.status + ) + } + /> + + ) + })} +
+ +
+ +
+

Categories

+
+ {categories.map((category, key) => ( +
+ c === category)} + onChange={toggleFilters( + selectedCategories, + setSelectedCategories, + category + )} + /> +
+ ))} +
+ +
+ +
+

Sort By

+
+ {Lists.sortByList.map((item, key) => ( +
+ { + setSort(item) + }} + /> +
+ ))} +
+
+
+ )} +
+ ) +} + +export default SearchBar diff --git a/src/components/Stories.js b/src/components/Stories.js index 98e3507b..8a78cb23 100644 --- a/src/components/Stories.js +++ b/src/components/Stories.js @@ -1,32 +1,20 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { trackPromise, usePromiseTracker } from 'react-promise-tracker' -import Button from './Button' import StoriesList from './StoriesList' import Pagination from './Pagination' -import Dropdown from './Dropdown' -import SearchInput from '../modules/SearchInput' - -import Lists from '../utils/Lists' -import userStory from '../services/user_story' +import SearchBar from './SearchBar' import ProductList from './ProductList' +import userStory from '../services/user_story' const Stories = ({ authorId, followerId }) => { - const [currentStateSelected, selectState] = useState('Under consideration') + const [selectedStatuses, setSelectedStatuses] = useState([]) const [page, setPage] = useState(1) - const statusOptions = [] - - const [status, setStatus] = useState('Under consideration') - const [sort, setSort] = useState('Most Voted') - const [category, setCategory] = useState('All') - - const [categories, setCategories] = useState([]) - - const [searchTerm, setSearchTerm] = useState('') + const [selectedCategories, setSelectedCategories] = useState([]) const { promiseInProgress } = usePromiseTracker({ area: 'stories-div' }) @@ -34,39 +22,23 @@ const Stories = ({ authorId, followerId }) => { const [stories, setStories] = useState([]) - const statusDropdownContainer = useRef() - - const sortDropdownContainer = useRef() - - const categoryDropdownContainer = useRef() - const [productQuery, setProductQuery] = useState(``) - const [categoryQuery, setCategoryQuery] = useState(``) - const [searchQuery, setSearchQuery] = useState('') - const [userTerm, setUserTerm] = useState('') - const [authorQuery, setAuthorQuery] = useState('') const getPage = useCallback((page) => { setPage(page) }, []) - useEffect(() => { - for (let i = 0; i < Lists.stateList.length; i++) { - statusOptions.push(Lists.stateList[i].status) - } - }, [statusOptions]) - useEffect(() => { const fetchStoryCount = async () => { const response = await userStory.getStoryCount( - currentStateSelected, + selectedStatuses, authorId, authorQuery, - categoryQuery, + selectedCategories, productQuery, searchQuery, followerId @@ -75,8 +47,8 @@ const Stories = ({ authorId, followerId }) => { } fetchStoryCount() }, [ - currentStateSelected, - categoryQuery, + selectedStatuses, + selectedCategories, productQuery, searchQuery, authorQuery, @@ -84,28 +56,14 @@ const Stories = ({ authorId, followerId }) => { followerId ]) - useEffect(() => { - if (category !== 'All') { - setCategoryQuery(`Category : "${category}"`) - } else { - setCategoryQuery(``) - } - if (searchTerm === '') { - setSearchQuery('') - } - if (userTerm === '') { - setAuthorQuery('') - } - }, [category, searchTerm, userTerm]) - useEffect(() => { const fetchStories = async () => { const response = await userStory.getStories( page, - currentStateSelected, + selectedStatuses, authorId, authorQuery, - categoryQuery, + selectedCategories, productQuery, searchQuery, followerId @@ -114,8 +72,8 @@ const Stories = ({ authorId, followerId }) => { } trackPromise(fetchStories(), 'stories-div') }, [ - categoryQuery, - currentStateSelected, + selectedCategories, + selectedStatuses, page, productQuery, searchQuery, @@ -124,19 +82,6 @@ const Stories = ({ authorId, followerId }) => { followerId ]) - useEffect(() => { - const fetchCategories = async () => { - const response = await userStory.getCategories() - setCategories([ - 'All', - ...response.data.data.__type.enumValues.map((ele) => { - return ele.name - }) - ]) - } - fetchCategories() - }, []) - useEffect(() => { const comparatorVotes = (a, b) => { return a.followers.length > b.followers.length ? -1 : 1 @@ -159,83 +104,31 @@ const Stories = ({ authorId, followerId }) => { }, [sort, stories, setStories]) return ( -
+ <> -
-
- {Lists.stateList && - Lists.stateList.map((state, key) => { - return ( - - ) - })} -
-
-
- -
+ -
- -
- - -
-
-
+ ) } diff --git a/src/modules/SearchInput.js b/src/modules/SearchInput.js index b3772aad..bbedc776 100644 --- a/src/modules/SearchInput.js +++ b/src/modules/SearchInput.js @@ -1,18 +1,15 @@ -import React, { useState, useRef } from 'react' +import React, { useState, useRef, useEffect } from 'react' import Button from '../components/Button' import Dropdown from '../components/Dropdown' import UsersSuggestionDropdown from '../components/UsersSuggestionDropdown' import { EOS_CLOSE, EOS_SEARCH } from 'eos-icons-react' function SearchInput(props) { - const { - searchTerm, - setSearchTerm, - userTerm, - setUserTerm, - setSearchQuery, - setAuthorQuery - } = props + const { setSearchQuery, setAuthorQuery } = props + + const [searchTerm, setSearchTerm] = useState('') + + const [userTerm, setUserTerm] = useState('') const fieldToSearchDropdownContainer = useRef() @@ -29,6 +26,15 @@ function SearchInput(props) { } } + useEffect(() => { + if (searchTerm === '') { + setSearchQuery('') + } + if (userTerm === '') { + setAuthorQuery('') + } + }, [searchTerm, userTerm, setSearchQuery, setAuthorQuery]) + return (
diff --git a/src/services/user_story.js b/src/services/user_story.js index 4eeb913d..ea1e92dc 100644 --- a/src/services/user_story.js +++ b/src/services/user_story.js @@ -1,4 +1,5 @@ import apiCall from './api' +import { parseArrayToQuery } from '../utils/filterText' import { BASIC_STORY_INFO_FRAGMENT, NOTIFICATION_DATA_FRAGMENT @@ -30,10 +31,10 @@ const userStory = { }, getStories: ( page, - currentStateSelected, + selectedStatuses, authorId, authorQuery, - categoryQuery, + selectedCategories, productQuery, searchQuery, followerId @@ -46,20 +47,18 @@ const userStory = { (page - 1) * 5 }, where: { user_story_status : { - Status: "${currentStateSelected}" + ${parseArrayToQuery('Status', selectedStatuses)} }, author: { ${authorId} username_contains: "${authorQuery}" } - ${categoryQuery} + ${parseArrayToQuery('Category', selectedCategories)} ${productQuery} ${searchQuery} ${followerId} }) { - id - Title - Description + ...BasicStoryInfo user_story_status { Status } @@ -81,13 +80,10 @@ const userStory = { url } } - followers { - id - username - } Category } } + ${BASIC_STORY_INFO_FRAGMENT} ` } return apiCall('/graphql', storiesQuery) @@ -119,10 +115,10 @@ const userStory = { return apiCall('/graphql', storyQuery) }, getStoryCount: ( - currentStateSelected, + selectedStatuses, authorId, authorQuery, - categoryQuery, + selectedCategories, productQuery, searchQuery, followerId @@ -133,13 +129,13 @@ const userStory = { query: `query { userStoriesConnection(where: { user_story_status: { - Status: "${currentStateSelected}" + ${parseArrayToQuery('Status', selectedStatuses)} }, author: { ${authorId} username_contains: "${authorQuery}" } - ${categoryQuery} + ${parseArrayToQuery('Category', selectedCategories)} ${productQuery} ${searchQuery} ${followerId} diff --git a/src/utils/filterText.js b/src/utils/filterText.js index 38b20c1c..dee1a57c 100644 --- a/src/utils/filterText.js +++ b/src/utils/filterText.js @@ -11,3 +11,8 @@ export const strip = (html, len) => { } return html } + +export const parseArrayToQuery = (queryParam, items) => { + if (items.length === 0) return '' + return `${queryParam}: ["${items.join(`", "`)}"]` +}