From 5bab2902ecf092f92ac360a9a6b1dc5d389ce49c Mon Sep 17 00:00:00 2001 From: RobertoOchoaAldana Date: Mon, 11 Nov 2019 23:52:28 -0600 Subject: [PATCH] Adding functionality to filter buttons (#33) * adding functionality to filter buttons * adding EOF lines * Fixing issue with color filters * resolve issue #34, now you can click on an active filter button and remove the filter * fixing issue where comboEnergy was active and clicking on it wouldn't remove the filter * resolve issue #32, now generic energy cost is displayed correctly * Deleting semicolons, refactoring the filterbox file, adding hooks * refactoring the method to create buttons * added changes so when a filter by same field already exist it will add it as an or into the same filter, also added changes requested by irving * deleting semicolons --- web-page/src/App.js | 4 +- web-page/src/components/EnergyColor.js | 16 +- web-page/src/components/FilterBox.js | 304 ------------------ web-page/src/components/FilterButton.css | 3 + web-page/src/components/FilterButton.js | 14 + .../src/components/filter-box/filter.utils.js | 140 ++++++++ .../{FilterBox.css => filter-box/index.css} | 0 web-page/src/components/filter-box/index.js | 212 ++++++++++++ web-page/src/redux/modules/search.js | 31 +- 9 files changed, 406 insertions(+), 318 deletions(-) delete mode 100644 web-page/src/components/FilterBox.js create mode 100644 web-page/src/components/FilterButton.css create mode 100644 web-page/src/components/FilterButton.js create mode 100644 web-page/src/components/filter-box/filter.utils.js rename web-page/src/components/{FilterBox.css => filter-box/index.css} (100%) create mode 100644 web-page/src/components/filter-box/index.js diff --git a/web-page/src/App.js b/web-page/src/App.js index 380fa61..9d48321 100644 --- a/web-page/src/App.js +++ b/web-page/src/App.js @@ -1,8 +1,8 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react' import 'materialize-css/dist/css/materialize.min.css' import M from "materialize-css" import './App.css' -import FilterBox from './components/FilterBox' +import FilterBox from './components/filter-box' import CardsContainer from './components/CardsContainer' class App extends Component { diff --git a/web-page/src/components/EnergyColor.js b/web-page/src/components/EnergyColor.js index 4963357..b46c642 100644 --- a/web-page/src/components/EnergyColor.js +++ b/web-page/src/components/EnergyColor.js @@ -1,12 +1,16 @@ -import React, { Component } from 'react'; -import './EnergyColor.css'; +import React, { Component } from 'react' +import './EnergyColor.css' const reg = /[\dbugyr]+/g, filterColors = e => e.match(reg), iconPrinter = e => filterColors(e).map(i => { if (!isNaN(Number(i))) { - return {i} + let extraColors = filterColors(e).length - 1 + if (i-extraColors > 0) + return {i-extraColors} + else + return null } switch (i) { @@ -26,8 +30,8 @@ const class EnergyColor extends Component { render() { - const { energy } = this.props; - if (!energy) return {energy}; + const { energy } = this.props + if (!energy) return {energy} return ( @@ -37,4 +41,4 @@ class EnergyColor extends Component { } } -export default EnergyColor; +export default EnergyColor diff --git a/web-page/src/components/FilterBox.js b/web-page/src/components/FilterBox.js deleted file mode 100644 index b30a907..0000000 --- a/web-page/src/components/FilterBox.js +++ /dev/null @@ -1,304 +0,0 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import { searchCards, addFilter, removeFilter } from '../redux/modules/search' -import './FilterBox.css' -import Filter from './Filter' - -const NUMERIC_REGEXP = '^(?[<>]=?)\\s*?(?\\d+)' - -const findArrayItemsInArrayOrString = (filterConditions, valuesToSearchOn) => { - - for (let index = 0; index < filterConditions.length; index++) { - const { conditionType, condition, criteria } = filterConditions[index] - const found = valuesToSearchOn.find( - value => { - if (conditionType === 'numeric') { - switch(condition) { - case '<': return Number(value) < Number(criteria) - case '<=': return Number(value) <= Number(criteria) - case '>': return Number(value) > Number(criteria) - case '>=': return Number(value) >= Number(criteria) - default: break; - } - } - - return value.toString().toLocaleLowerCase().includes( - criteria.toString().toLocaleLowerCase() - ) - } - ) - - if (found) { - return true - } - } - return false -} - -const getFieldsToSearch = (card) => { - const struct = Object.keys(card).reduce( - (result, fieldName) => { - let type = typeof card[fieldName] - if (Array.isArray( card[fieldName] )) { - type = 'array' - } - - result.push({ fieldName, type, label: fieldName }) - return result - }, - [] - ).sort( - (a, b) => { - if (a.label > b.label) return 1 - else if (a.label < b.label) return -1 - return 0 - } - ) - - return struct -} - -class FilterBox extends Component { - - constructor(props) { - super(props) - - this.state = { - filterText: '', - fieldToSearch: props.fieldOptions[0], - isFilterNegation: false - } - } - - parseFilterText = text => { - const criteriaArray = text.trim().toLocaleLowerCase().split(/\s*\|\|\s*/gm) - - return criteriaArray.map( - txt => { - const foundNumericCondition = (new RegExp(NUMERIC_REGEXP, 'g')).exec(txt) - if (foundNumericCondition) { - const { groups: { condition, criteria }} = foundNumericCondition - return { conditionType: 'numeric', criteria, condition } - } - - return { conditionType: 'contains', criteria: txt } - } - ) - } - - onAddFilterClickHandler = _ => { - const { onFilterAdd, appliedFilters } = this.props - if (typeof onFilterAdd !== 'function') { - return// No handler, so do nothing - } - - const { filterText:origText, fieldToSearch: { type, fieldName }, isFilterNegation } = this.state - let filterText = origText.trim() - - if (typeof filterText !== 'string') { - return// Empty string - } - - if (appliedFilters.find(a => a.id === `${fieldName}-${filterText}`)) { - return// Filter already added - } - - const filterConditions = this.parseFilterText(filterText) - let filter = card => { - let criteriaToSearchOn - if (type === 'object') {// no implementation yet, so we search in the whole object - criteriaToSearchOn = [JSON.stringify( card[fieldName] )] - } - else { - let val = card[fieldName] - if (val === null || val === undefined) { - return false - } - criteriaToSearchOn = Array.isArray(val) ? val.slice() : [val] - - // We look for the field in the back of the card and add it to our search - if (card['cardBack'] && card['cardBack'][fieldName]) { - val = card['cardBack'][fieldName] - if (Array.isArray(val)) { - criteriaToSearchOn = criteriaToSearchOn.concat(val) - } - else { - criteriaToSearchOn.push( val ) - } - } - } - - return findArrayItemsInArrayOrString(filterConditions, criteriaToSearchOn) - } - - if (isFilterNegation) { - const oldFilter = filter - filter = card => !oldFilter(card) - } - - onFilterAdd(`${fieldName}: ${isFilterNegation ? 'NOT ' : ''} ${filterText}`, filter) - } - - onFieldSelectionChangeHandler = event => { - const { fieldOptions } = this.props - this.setState({ fieldToSearch: fieldOptions[event.target.value] }) - } - - onFilterTextChangeHandler = (event) => { - this.setState({ filterText: event.target.value }) - } - - onNegationFilterChangeHandler = (event) => { - this.setState({ isFilterNegation: event.target.checked }) - } - - onInputTextKeyDownHandler = (event) => { - const isEnter = event.keyCode === 13 - if (isEnter) { - this.onAddFilterClickHandler() - } - } - - render() { - const { filterText, isFilterNegation } = this.state - const { fieldOptions, appliedFilters, onFilterRemove, totalCards } = this.props - - const removedOptions = ['availableDate', 'cardImageUrl', 'cardBack', 'era', /*'type', 'color', 'energy', 'comboEnergy', 'rarity', 'character', 'skillKeywords', 'cardNumber', */] - const optionsToSelect = fieldOptions.map( - (option, index) => - !removedOptions.includes(option.fieldName) - ? - : null - ) - - const filtersApplied = appliedFilters.map( - ({ id }) => - ) - - return ( -
-
-
-
Card Type
-
- {typeOptions} -
-
-
-
Color
-
- {colorOptions} -
-
-
-
Energy
-
- {energyOptions} -
-
-
-
Combo Energy
-
- {cboEnergyOptions} -
-
-
- -
-
- -
-
- -
-
-
- - -
- -
- - -
- -
- -
- -
- -
-
-
    {filtersApplied}
-
-
- Total of cards: {totalCards} -
-
- ) - } -} - -const COLORS = ['B', 'U', 'G', 'Y', 'R'] -const colorOptions = COLORS.map((currentColor) => ) - -const TYPES = ['BATTLE', 'EXTRA', 'LEADER'] -const typeOptions = TYPES.map((currentType) => ) - -const energyOptions = [] -let i=0 -for(i=0; i<=5; i++) { - energyOptions[i] = -} -energyOptions[i] = - -const cboEnergyOptions = [] -for(let i=0; i<=2; i++) { - cboEnergyOptions[i] = -} - -const - mapStateToProps = ({ search }) => ({ - totalCards: search.result.length, - appliedFilters: search.filters, - fieldOptions: getFieldsToSearch( search.cardsDictionary[ Object.keys(search.cardsDictionary)[4] ] ) - }), - mapDispatchToProps = { searchCards, onFilterAdd: addFilter, onFilterRemove: removeFilter } -export default connect(mapStateToProps, mapDispatchToProps)(FilterBox) diff --git a/web-page/src/components/FilterButton.css b/web-page/src/components/FilterButton.css new file mode 100644 index 0000000..fc442dc --- /dev/null +++ b/web-page/src/components/FilterButton.css @@ -0,0 +1,3 @@ +.filter-on { + background-color: #ad2323; +} diff --git a/web-page/src/components/FilterButton.js b/web-page/src/components/FilterButton.js new file mode 100644 index 0000000..c5f9a5d --- /dev/null +++ b/web-page/src/components/FilterButton.js @@ -0,0 +1,14 @@ +import React from 'react' + +import './FilterButton.css' + +const FilterButton = (props) => { + const { id, fieldname, customClass, onClick } = props + return +} + +export default FilterButton diff --git a/web-page/src/components/filter-box/filter.utils.js b/web-page/src/components/filter-box/filter.utils.js new file mode 100644 index 0000000..625863a --- /dev/null +++ b/web-page/src/components/filter-box/filter.utils.js @@ -0,0 +1,140 @@ +import React from 'react' +import FilterButton from '../FilterButton' + +const NUMERIC_REGEXP = '^(?[<>]=?)\\s*?(?\\d+)' +const NUMERIC_REGEXP_WITH_OPERATORS = '^(?\\d+)\\s*?(?[+-])' +const TYPES = ['BATTLE', 'EXTRA', 'LEADER'] +const COLORS = ['B', 'U', 'G', 'Y', 'R'] +const ENERGYS = ['0', '1', '2', '3', '4', '5', '6+'] +const COMBOENERGYS = ['0', '1', '2'] +const FILTER_FIELDNAME = { + TYPE: 'type', + COLOR: 'color', + ENERGY: 'energy', + COMBO_ENERGY: 'comboEnergy' +} + +const findArrayItemsInArrayOrString = (filterConditions, valuesToSearchOn) => { + return !!filterConditions.find(values => { + const { conditionType, condition, criteria, negate } = values + return valuesToSearchOn.find( + value => { + if (conditionType === 'numeric') { + const nValue = Number(value.replace(/\D+/g, "")) + switch (condition) { + case '<': return nValue < Number(criteria) + case '>': return nValue > Number(criteria) + case '<=': + case '-': return nValue <= Number(criteria) + case '>=': + case '+': return nValue >= Number(criteria) + default: break + } + } + + const val = value.toString().toLocaleLowerCase().includes(criteria.toString().toLocaleLowerCase()) + return negate ? !val : val + } + ) + }) +} + +export const getFieldsToSearch = (card) => { + const struct = Object.keys(card).reduce( + (result, fieldName) => { + let type = typeof card[fieldName] + if (Array.isArray( card[fieldName] )) { + type = 'array' + } + + result.push({ fieldName, type, label: fieldName }) + return result + }, + [] + ).sort( + (a, b) => { + if (a.label > b.label) return 1 + else if (a.label < b.label) return -1 + return 0 + } + ) + + return struct +} + +export const isInFilters = (ar, field, current) => ( + ar.find(({ id })=> id.toLocaleLowerCase().startsWith(field.toLocaleLowerCase()) && id.toLocaleLowerCase().includes(current.toLocaleLowerCase())) +) + +export const mapIdToColor = { + B: "Black", + U: "Blue", + G: "Green", + Y: "Yellow", + R: "Red" +} + +export const parseFilterText = text => { + const criteriaArray = text.trim().toLocaleLowerCase().split(/\s*\|\|\s*/gm) + return criteriaArray.map( + txt => { + const foundNumericCondition = (new RegExp(NUMERIC_REGEXP, 'g')).exec(txt) + const foundNumericConditionFinal = foundNumericCondition ? foundNumericCondition : (new RegExp(NUMERIC_REGEXP_WITH_OPERATORS, 'g')).exec(txt) + if (foundNumericConditionFinal) { + const { groups: { condition, criteria } } = foundNumericConditionFinal + return { conditionType: 'numeric', criteria: criteria.includes('not') ? criteria.split(/\s/gm)[1] : criteria, condition, negate: criteria.includes('not') } + } + return { conditionType: 'contains', criteria: txt.includes('not') ? txt.split(/\s/gm)[1].trim() : txt, negate: txt.includes('not') } + } + ) +} + +const createButtons = (appliedFilters, onAddFilterButtonHandler, buttons, field) => ( + buttons.map((current, ind) => + + ) +) + +export const createAllButtons = (appliedFilters, onAddFilterButtonHandler) => { + return { + type: createButtons(appliedFilters, onAddFilterButtonHandler, TYPES, FILTER_FIELDNAME.TYPE), + color: createButtons(appliedFilters, onAddFilterButtonHandler, COLORS, FILTER_FIELDNAME.COLOR), + energy: createButtons(appliedFilters, onAddFilterButtonHandler, ENERGYS, FILTER_FIELDNAME.ENERGY), + comboEnergy: createButtons(appliedFilters, onAddFilterButtonHandler, COMBOENERGYS, FILTER_FIELDNAME.COMBO_ENERGY) + } +} + +export const createFilter = (fieldName, filterConditions, type) => { + let filter = card => { + let criteriaToSearchOn + if (type === 'object') {// no implementation yet, so we search in the whole object + criteriaToSearchOn = [JSON.stringify( card[fieldName] )] + } + else { + let val = card[fieldName] + if (val === null || val === undefined) { + return false + } + criteriaToSearchOn = Array.isArray(val) ? val.slice() : [val] + // We look for the field in the back of the card and add it to our search + if (card['cardBack'] && card['cardBack'][fieldName]) { + val = card['cardBack'][fieldName] + if (Array.isArray(val)) { + criteriaToSearchOn = criteriaToSearchOn.concat(val) + } + else { + criteriaToSearchOn.push( val ) + } + } + } + + return findArrayItemsInArrayOrString(filterConditions, criteriaToSearchOn) + } + return filter +} diff --git a/web-page/src/components/FilterBox.css b/web-page/src/components/filter-box/index.css similarity index 100% rename from web-page/src/components/FilterBox.css rename to web-page/src/components/filter-box/index.css diff --git a/web-page/src/components/filter-box/index.js b/web-page/src/components/filter-box/index.js new file mode 100644 index 0000000..296e699 --- /dev/null +++ b/web-page/src/components/filter-box/index.js @@ -0,0 +1,212 @@ +import React, { useState } from 'react' +import { connect } from 'react-redux' +import { + searchCards, + addFilter, + updateFilter, + removeFilter +} from '../../redux/modules/search' +import './index.css' +import Filter from '../Filter' +import { + parseFilterText, + getFieldsToSearch, + mapIdToColor, + createAllButtons, + createFilter +} from './filter.utils' + +const FilterBox = ({ totalCards, appliedFilters, fieldOptions, searchCards, onFilterAdd, onUpdateFilter, onFilterRemove }) => { + const [ filterValues, setFilter ] = useState({ filterText: '', fieldToSearch: '', isFilterNegation: false }) + const { filterText, isFilterNegation } = filterValues + + const onAddFilterButtonHandler = (id, fieldName) => { + const filterText = fieldName === "color" ? mapIdToColor[id] : id.toString(), type = 'string' + let isFilterAdd = appliedFilters.find(a=> a.id.toLocaleLowerCase().replace('not ', '') === `${fieldName}: ${filterText}`.toLocaleLowerCase()) + if (isFilterAdd) { + onFilterRemove(isFilterAdd.id) + return// Filter already added + } + onAddFilter(type, fieldName, filterText, isFilterNegation) + } + + const onAddFilterClickHandler = _ => { + const { filterText: origText, fieldToSearch: { type, fieldName }, isFilterNegation } = filterValues + let filterText = origText.trim() + if (appliedFilters.find(a => a.id === `${fieldName}-${filterText}`)) { + return// Filter already added + } + onAddFilter(type, fieldName, filterText, isFilterNegation) + } + + const onAddFilter = (type, fieldName, filterText, isFilterNegation) => { + if (typeof onFilterAdd !== 'function') { + return// No handler, so do nothing + } + + if (typeof filterText !== 'string') { + return// Empty string + } + + let filterExists = appliedFilters.find(a=> a.id.toLocaleLowerCase().startsWith(fieldName.toLocaleLowerCase())) + if(filterExists){ + let newId, newFilterText, removeFilterRegexp = new RegExp(`\\s\\|\\|\\s(not\\s)?${filterText}|(not\\s)?${filterText}\\s\\|\\|\\s`, 'i') + if(filterExists.id.toLocaleLowerCase().includes(filterText.toLocaleLowerCase())) + newId = filterExists.id.replace(removeFilterRegexp, '') + else + newId = filterExists.id + ' || ' + (isFilterNegation ? 'NOT ' : '') + filterText + newFilterText = newId.split(':')[1] + const filterConditions = parseFilterText(newFilterText) + const filter = createFilter(fieldName, filterConditions, type) + onUpdateFilter(filterExists.id, newId, filter) + return + } + + if(isFilterNegation) + filterText = 'not ' + filterText + const filterConditions = parseFilterText(filterText) + const filter = createFilter(fieldName, filterConditions, type) + onFilterAdd(`${fieldName}: ${filterText}`, filter) + } + + const onFieldSelectionChangeHandler = event => { + setFilter({ ...filterValues, fieldToSearch: fieldOptions[event.target.value] }) + } + + const onFilterTextChangeHandler = (event) => { + setFilter({ ...filterValues, filterText: event.target.value }) + } + + const onNegationFilterChangeHandler = (event) => { + setFilter({ ...filterValues, isFilterNegation: event.target.checked }) + } + + const onInputTextKeyDownHandler = (event) => { + const isEnter = event.keyCode === 13 + if (isEnter) { + onAddFilterClickHandler() + } + } + + const removedOptions = ['availableDate', 'cardImageUrl', 'cardBack', 'era', /*'type', 'color', 'energy', 'comboEnergy', 'rarity', 'character', 'skillKeywords', 'cardNumber', */] + const optionsToSelect = fieldOptions.map( + (option, index) => + !removedOptions.includes(option.fieldName) + ? + : null + ) + + const filtersApplied = appliedFilters.map( + ({ id }) => + ) + + const allButtons = createAllButtons(appliedFilters, onAddFilterButtonHandler) + + return ( +
+
+
+
Card Type
+
+ {allButtons.type} +
+
+
+
Color
+
+ {allButtons.color} +
+
+
+
Energy
+
+ {allButtons.energy} +
+
+
+
Combo Energy
+
+ {allButtons.comboEnergy} +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
    {filtersApplied}
+
+
+ Total of cards: {totalCards} +
+
+ ) +} + +const + mapStateToProps = ({ search }) => ({ + totalCards: search.result.length, + appliedFilters: search.filters, + fieldOptions: getFieldsToSearch( search.cardsDictionary[ Object.keys(search.cardsDictionary)[4] ] ) + }), + mapDispatchToProps = { searchCards, onFilterAdd: addFilter, onUpdateFilter: updateFilter, onFilterRemove: removeFilter } +export default connect(mapStateToProps, mapDispatchToProps)(FilterBox) diff --git a/web-page/src/redux/modules/search.js b/web-page/src/redux/modules/search.js index 9f622d3..11b988f 100644 --- a/web-page/src/redux/modules/search.js +++ b/web-page/src/redux/modules/search.js @@ -1,8 +1,8 @@ 'use stric' -import { pipe, from } from "rxjs"; -import { ofType } from 'redux-observable'; -import { tap, map, mergeMap, ignoreElements } from 'rxjs/operators'; +import { pipe, from } from "rxjs" +import { ofType } from 'redux-observable' +import { tap, map, mergeMap, ignoreElements } from 'rxjs/operators' import AllCards from '../../cards.json' @@ -21,7 +21,7 @@ const _searchCards = async (filters = []) => { return AllCards } - var t0 = performance.now(); + var t0 = performance.now() const cardsFound = AllCards.filter( card => { for (let index = 0; index < filters.length; index++) { @@ -32,7 +32,7 @@ const _searchCards = async (filters = []) => { return true } ) - var t1 = performance.now(); + var t1 = performance.now() console.log("Search cards took " + (t1 - t0) + " miliseconds.") return cardsFound @@ -62,6 +62,7 @@ const SEARCH_CARDS = 'dbs-scraper/search/SEARCH_CARDS', SEARCH_CARDS_SUCCESS = 'dbs-scraper/search/SEARCH_CARDS_SUCCESS', ADD_FILTER = 'dbs-scraper/search/ADD_FILTER', + UPDATE_FILTER = 'dbs-scraper/search/UPDATE_FILTER', REMOVE_FILTER = 'dbs-scraper/search/REMOVE_FILTER', CLEAR_FILTERS = 'dbs-scraper/search/CLEAR_FILTERS' @@ -82,6 +83,19 @@ export default function reducer(state = initState, action = {}) { return { ...state, filters: newFilterArray } } + case UPDATE_FILTER: { + return { + ...state, + filters: state.filters.map(f => { + if(f.id === action.id){ + f.id = action.newId + f.filterFn = action.filter + } + return f + } ) + } + } + case REMOVE_FILTER: { const newFilterArray = state.filters.slice() const index = newFilterArray.findIndex( f => f.id === action.filterId) @@ -112,6 +126,11 @@ export const addFilter = (id, filter) => ({ id, filter }) +export const updateFilter = (id, newId, filter) => ({ + type: UPDATE_FILTER, + id, newId, filter +}) + export const removeFilter = filterId => ({ type: REMOVE_FILTER, filterId @@ -124,7 +143,7 @@ export const clearFilters = () => ({ // Side effects export const updateSearchEpic = action$ => action$.pipe( - ofType(ADD_FILTER, REMOVE_FILTER), + ofType(ADD_FILTER, UPDATE_FILTER, REMOVE_FILTER), map( searchCards ) )