diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 3b4db2d..cc01531 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -219,3 +219,4 @@ task copyDownloadableDepsToLibs(type: Copy) { } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) +apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" diff --git a/example/package.json b/example/package.json index 8e2ea5b..6d47d13 100644 --- a/example/package.json +++ b/example/package.json @@ -22,6 +22,7 @@ "react-native-reanimated": "^1.13.1", "react-native-safe-area-context": "^3.1.8", "react-native-screens": "^2.13.0", + "react-native-vector-icons": "^8.1.0", "react-redux": "^7.2.2", "rxjs": "^6.6.3" }, diff --git a/example/src/Helper/config.js b/example/src/Helper/config.js index 0a7b000..b1decd9 100644 --- a/example/src/Helper/config.js +++ b/example/src/Helper/config.js @@ -1,7 +1,7 @@ import React from 'react'; class Config { - static merchant_url = 'https://magento24.pwa-commerce.com/Store/graphql'; + static merchant_url = 'https://magento.pwa-commerce.com/graphql'; } export default Config; diff --git a/example/yarn.lock b/example/yarn.lock index 170ca25..a042435 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1511,6 +1511,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -1893,6 +1902,11 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -2206,7 +2220,7 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -2835,6 +2849,51 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.frompairs@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz#bc4e5207fa2757c136e573614e9664506b2b1bd2" + integrity sha1-vE5SB/onV8E25XNhTpZkUGsrG9I= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.omit@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" + integrity sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA= + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.throttle@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" @@ -3710,6 +3769,11 @@ react-native-gesture-handler@^1.8.0: invariant "^2.2.4" prop-types "^15.7.2" +react-native-ionicons@^4.x: + version "4.6.5" + resolved "https://registry.yarnpkg.com/react-native-ionicons/-/react-native-ionicons-4.6.5.tgz#b792ec8896381e67ff237eba955e38afe7ceb7a4" + integrity sha512-s2Ia7M5t609LE9LWygMj3ALVPUlKhK7R9XcMb67fP4EYJv0oLcwg5pc+8ftv9XXaUuTW/WgL3zJlBYxAvtvMJg== + react-native-iphone-x-helper@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.2.1.tgz#645e2ffbbb49e80844bb4cbbe34a126fda1e6772" @@ -3737,6 +3801,20 @@ react-native-screens@^2.13.0: resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.13.0.tgz#6360dfd65d51f73479bea14373a8e289cd58a327" integrity sha512-B0kPYq5ZK/2G7pPhcAFLflDfm+HXktdVEeLFHnP/+oPnao+b28hXsOBCi0IgK72gxbdPtnpZME8KONRIKOBDug== +react-native-vector-icons@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-8.1.0.tgz#e8ee2b17bc4d9f636da1c6f67feee8e2a850c3d8" + integrity sha512-sHIdBB6Y0dHaot2fMXgy5J/hhCn5YuyN7SKDNFgPzL8KA1oF2/v7mgYMavnK7LIIs2dJoGnDANKf61dsU+TZlg== + dependencies: + lodash.frompairs "^4.0.1" + lodash.isequal "^4.5.0" + lodash.isstring "^4.0.1" + lodash.omit "^4.5.0" + lodash.pick "^4.4.0" + lodash.template "^4.5.0" + prop-types "^15.7.2" + yargs "^16.1.1" + react-native@0.62.2: version "0.62.2" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.62.2.tgz#d831e11a3178705449142df19a70ac2ca16bad10" @@ -4682,6 +4760,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -4749,6 +4836,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" + integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -4770,6 +4862,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.7" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" + integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== + yargs@^14.2.0: version "14.2.3" resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" @@ -4804,6 +4901,19 @@ yargs@^15.1.0: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + zen-observable@^0.8.14: version "0.8.15" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" diff --git a/src/lib/components/Product/ProductList/ApplyingLabel/ProductFilterLabel.js b/src/lib/components/Product/ProductList/ApplyingLabel/ProductFilterLabel.js deleted file mode 100644 index fee81b0..0000000 --- a/src/lib/components/Product/ProductList/ApplyingLabel/ProductFilterLabel.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import type {filterArray} from '../layers.flow.js'; - -const parseOperatorToSymbol = (operatorString: string, isMultipleValue?: boolean): string => { - console.log('pars'); - console.log(operatorString); - switch (operatorString) { - case 'greater_equal': - return '≥'; - case 'greater': - return '>'; - case 'equal': - if (isMultipleValue) { - return 'one of'; - } else { - return '='; - } - case 'less': - return '<'; - case 'less_equal': - return '≤'; - } -}; - -// Currently have View wrapper for future change (may add icon instead of asciiz -const generateFilterLabelName = (filterLayers: filterArray): Array => { - - return filterLayers.map(filterLayer => { - const operator = parseOperatorToSymbol(filterLayer.type, - (filterLayer.filterValue instanceof Array) && filterLayer.filterValue.length > 1); - - return `${filterLayer.name} ${operator} ${filterLayer.filterValue.toString()}`; - }); -}; - -export {generateFilterLabelName}; diff --git a/src/lib/components/Product/ProductList/ApplyingLabel/__test__/productFilterLabel.test.js b/src/lib/components/Product/ProductList/ApplyingLabel/__test__/productFilterLabel.test.js deleted file mode 100644 index 5421fca..0000000 --- a/src/lib/components/Product/ProductList/ApplyingLabel/__test__/productFilterLabel.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import {generateFilterLabelName} from "../ProductFilterLabel.js"; -import {shallow} from "enzyme"; -import type {filterArray} from "../../layers.flow"; - -describe('render correct filter labels', () => { - test('empty layers', () => { - const labels = generateFilterLabelName([]) - expect(labels).toHaveLength(0) - }) - - test('have something', () => { - const layers: filterArray = [{ - name: '1', - filterValue: '2-1', - type: 'equal', - }] - const labels = generateFilterLabelName(layers) - expect(labels).toHaveLength(1) - }) -}) \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/ApplyingLabel/productSortLabel.js b/src/lib/components/Product/ProductList/ApplyingLabel/productSortLabel.js deleted file mode 100644 index b95ba96..0000000 --- a/src/lib/components/Product/ProductList/ApplyingLabel/productSortLabel.js +++ /dev/null @@ -1,19 +0,0 @@ -// Currently have View wrapper for future change (may add icon instead of asciiz -import React from 'react'; -import type {sortArray} from '../layers.flow.js'; - -const generateSortLabelName = (sortLayers: sortArray): Array => { - - return sortLayers.map(sortLayer => { - let tailSymbol = '?'; - if (sortLayer.type === 'ascending') { - tailSymbol = '↑'; - } - else if (sortLayer.type === 'descending') { - tailSymbol = '↓'; - } - return `${sortLayer.name} ${tailSymbol}`; - }); -}; - -export {generateSortLabelName}; diff --git a/src/lib/components/Product/ProductList/ProductList.env.js b/src/lib/components/Product/ProductList/ProductList.env.js deleted file mode 100644 index c7f2b03..0000000 --- a/src/lib/components/Product/ProductList/ProductList.env.js +++ /dev/null @@ -1 +0,0 @@ -export const TRIM_TEXT_LIMIT = 10; diff --git a/src/lib/components/Product/ProductList/ProductList.js b/src/lib/components/Product/ProductList/ProductList.js deleted file mode 100644 index 995afc2..0000000 --- a/src/lib/components/Product/ProductList/ProductList.js +++ /dev/null @@ -1,155 +0,0 @@ -import React, {useContext, useState} from 'react'; -import { - FlatList, - Text, - TouchableOpacity, - View, -} from 'react-native'; -import {ThemeContext} from 'react-native-elements'; -import {SpaceBlock} from '../../others/spaceBlock.js'; -import {Product_Placeholder} from './placeholder/product_Placeholder.js'; -import {generateFilterLabelName} from './ApplyingLabel/ProductFilterLabel.js'; -import {generateSortLabelName} from './ApplyingLabel/productSortLabel.js'; -import {getFilteredData} from './logic/filter.js'; -import {getSortedData} from './logic/sort.js'; -import {AutoTrimFlatRemovableGreyBadge} from '../RoundSmallGreyBadge/autoTrimFlatRemovableGreyBadge.js'; -import {filterArray, sortArray} from './layers.flow.js'; - -const md5 = require('md5'); - -const fakeData = [...Array(30).keys()].map((x, index) => { - return { - name: md5(x.toString() + index), - size: x % 4 + 1, - binary: (x % 2 === 0) ? 'up' : 'down', - }; -}); - -function ProductList(props) { - const {productData = fakeData} = props; - const [sortLayers, setSortLayers] = useState([]); - const [filterLayers, setFilterLayers] = useState([]); - const { theme } = useContext(ThemeContext); - - - const handleSort = (sortLayers: sortArray) => { - setSortLayers(sortLayers); - }; - const handleFilter = (filterLayers: filterArray) => { - setFilterLayers(filterLayers); - }; - const cleanup = () => { - setSortLayers([]); - setFilterLayers([]); - }; - - const renderLabels = (sortLayers: sortArray, filterLayers: filterArray) => { - const labelList = generateSortLabelName(sortLayers) - .concat(generateFilterLabelName(filterLayers)); - - return ( - - {labelList.map(label => { - return ( - - ); - })} - - ); - }; - - const ProductListHeader = () => { - return ( - - - - {renderLabels(sortLayers, filterLayers)} - - - {(sortLayers.length + filterLayers.length) > 0 && - Clean up - } - - - - - - ); - }; - - const ProductListFooter = () => { - return ( - - {/*TODO: add nav to this*/} - { - }}> - - - - { - console.log('Filtering'); - handleFilter([ - { - name: 'name', - filterValue: 'd', - type: 'greater_equal', - }, - ]); - }}> - z - - - { - handleSort([ - { - name: 'name', - type: 'ascending', - }, - ]); - }}> - - - - - ); - }; - - // TODO: memo labels, remove inline style - return ( - - - { - return ( - - - ); - }} - keyExtractor={(item) => md5(JSON.stringify(item))} - numColumns={2} - initialNumToRender={15} - /> - - - ); -} - - -export {ProductList}; diff --git a/src/lib/components/Product/ProductList/filter.js b/src/lib/components/Product/ProductList/filter.js new file mode 100644 index 0000000..f48fd74 --- /dev/null +++ b/src/lib/components/Product/ProductList/filter.js @@ -0,0 +1,129 @@ +import React, { useEffect } from 'react' +import { ScrollView, View, TouchableOpacity, Text } from 'react-native' +import Ionicons from 'react-native-vector-icons/Ionicons' + +const FilterOptions = props => { + const { + categoryId, + filterFields, + showFilterOptions, + activeFilterCategories, + setActiveFilterCategories, + filterValues, + setFilterValues, + attributeCodes, + sortAndFilterProducts, + variablesObject, + setVariablesObject + } = props + + let varObj = new Object + attributeCodes.map((att, i) => varObj[att] = filterValues[i]) + varObj['category_id'] = categoryId + delete varObj['price'] + + useEffect(() => { + setVariablesObject({...variablesObject, ...varObj}) + }, [filterValues]) + + const filterOptions = ( + + {filterFields.map(({label, options}, i) => { + const optionsBlock = ( + + {options.map(({label, value, count}, index) => { + return ( + { + let allFields = [...filterValues] + let array = [...filterValues[i]] + const index = array.indexOf(value) + if(index < 0) array.push(value) + else array.splice(index, 1) + allFields[i] = array; + setFilterValues(allFields) + }} + > + {filterValues[i].indexOf(value) >= 0 ? + + : + } + {label} + {`(${count})`} + + ) + })} + + ) + return ( + + { + let triggeredArray = [...activeFilterCategories] + triggeredArray.splice(i, 1, !activeFilterCategories[i]) + setActiveFilterCategories(triggeredArray) + }} + > + {label} + {activeFilterCategories[i] ? + + : + } + + {activeFilterCategories[i] && optionsBlock} + + ) + })} + + ) + + return ( + + + SELECT A FILTER + {filterOptions} + + + { + showFilterOptions() + sortAndFilterProducts({variables: variablesObject}) + }} + > + + APPLY + + + + + ) +} + +export default FilterOptions \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/filterAndSortTags.js b/src/lib/components/Product/ProductList/filterAndSortTags.js new file mode 100644 index 0000000..1fe7f70 --- /dev/null +++ b/src/lib/components/Product/ProductList/filterAndSortTags.js @@ -0,0 +1,115 @@ +import React, { Fragment } from 'react' +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native' +import Ionicons from 'react-native-vector-icons/Ionicons' + +const FilterAndSortTags = (props) => { + const { + categoryId, + activeSortLabel, + setActiveSortLabel, + setActiveSortOption, + sortAndFilterProducts, + filterValues, + setFilterValues, + filterFields, + variablesObject, + setVariablesObject + } = props + + const values = [] + filterFields.map(({options}) => { + const arr = [] + options.map(({value}) => { + arr.push(value) + }) + values.push(arr) + }) + + const Tags = + + {filterValues.map((arr, i) => { + return ( + + {arr.map(val => { + const index = values[i].indexOf(val) + if(index > -1) { + const tag = filterFields[i].options[index].label + return ( + + {tag} + { + let _filterValues = [...filterValues] + _filterValues[i].splice(filterValues[i].indexOf(val), 1) + setFilterValues(_filterValues) + let varObj = {} + varObj[filterFields[i].attribute_code] = _filterValues[i] + sortAndFilterProducts({variables: {...variablesObject, ...varObj}}) + setVariablesObject({...variablesObject, ...varObj}) + }} + > + + + + ) + } + })} + + ) + })} + {activeSortLabel.length > 0 && + + {activeSortLabel[0]} + { + let varObj = {...variablesObject} + delete varObj[activeSortLabel[1]] + setActiveSortLabel([]) + setActiveSortOption(99) + sortAndFilterProducts({variables: varObj}) + setVariablesObject(varObj) + }} + > + + + + } + + + return ( + + {Tags} + {(filterValues.flat(1).length > 0 || activeSortLabel.length > 0) && + { + setFilterValues(Array(filterFields.length).fill([])) + setActiveSortLabel([]) + setActiveSortOption(99) + sortAndFilterProducts({variables: {category_id: categoryId}}) + }} + > + Clean all + + } + + ) +} + +const styles = StyleSheet.create({ + tag: { + flexDirection: 'row', + height: 30, + backgroundColor: '#D8D8D8', + borderRadius: 50, + alignItems: 'center', + justifyContent: 'center', + paddingLeft: 10, + marginRight: 15, + marginBottom: 10 + } +}) + +export default FilterAndSortTags \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/index.js b/src/lib/components/Product/ProductList/index.js index 88def50..60247c9 100644 --- a/src/lib/components/Product/ProductList/index.js +++ b/src/lib/components/Product/ProductList/index.js @@ -1,2 +1 @@ -export {ProductList} from './ProductList.js' -export {sortArray, filterArray} from './layers.flow.js' \ No newline at end of file +export {ProductList} from './productList.js' diff --git a/src/lib/components/Product/ProductList/item.js b/src/lib/components/Product/ProductList/item.js new file mode 100644 index 0000000..053883e --- /dev/null +++ b/src/lib/components/Product/ProductList/item.js @@ -0,0 +1,65 @@ +import React from 'react' +import { Image, View, Text, TouchableOpacity, StyleSheet } from 'react-native' +import { Dimensions } from 'react-native'; + +const {width} = Dimensions.get('window'); + +function currencyFormat(num) { + return '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') +} + +const Item = props => { + const { id, name, thumbnail, price_range, isGrid, navigation, index, length } = props + const { currency } = price_range.maximum_price.final_price + const { value: maxPrice } = price_range.maximum_price.final_price + const { value: minPrice } = price_range.minimum_price.final_price + const isRange = maxPrice != minPrice + + const price = isRange ? + + {`From: ${currencyFormat(minPrice)}`} + {`To: ${currencyFormat(maxPrice)}`} + + : {currencyFormat(minPrice)} + + return ( + navigation.navigate('Product Details')} + > + + + + {name} + {price} + + ) +} + +export default Item \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/layers.flow.js b/src/lib/components/Product/ProductList/layers.flow.js deleted file mode 100644 index 0d64af4..0000000 --- a/src/lib/components/Product/ProductList/layers.flow.js +++ /dev/null @@ -1,19 +0,0 @@ -//sort object by order from left to right - -type sortObject = { - name: string, - type: 'ascending' | 'descending' -} - -export type sortArray = Array - - -// if type = 'equal', and filterValue is an Array, -// all values in array passes -type filterObject = { - name: string, - filterValue: string | Array, - type: 'greater_equal' | 'greater' | 'equal' | 'less' | 'less_equal' -} - -export type filterArray = Array \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/logic/__test__/filter.test.js b/src/lib/components/Product/ProductList/logic/__test__/filter.test.js deleted file mode 100644 index d9ad1cc..0000000 --- a/src/lib/components/Product/ProductList/logic/__test__/filter.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import {getFilteredData} from "../filter.js"; -import type {filterArray} from "../../layers.flow"; - -const data = [...Array(10).keys()].map((x) => { - return { - name: x.toString(), - degree: (x % 2 === 1), // odd is True, even is False - } -}) -describe('Check filter logic in ProductList', () => { - - test(('filter 0 value'), () => { - const filterArray: filterArray = [] - expect(getFilteredData(data, filterArray)).toHaveLength(10) - }) - - test(('filter 1 value'), () => { - const filterArray: filterArray = [{ - name: 'name', - filterValue: '4', - type: 'greater_equal' - }] - expect(getFilteredData(data, filterArray)).toHaveLength(6) - }) - - test(('filter multiple value for \'equal\''), () => { - const filterArray: filterArray = [{ - name: 'name', - filterValue: ['4', '5', '7', 'oh no'], - type: 'equal' - }] - expect(getFilteredData(data, filterArray)).toHaveLength(3) - }) - - test(('multiple filter'), () => { - const filterArray: filterArray = [{ - name: 'name', - filterValue: '9', - type: 'less' - }, { - name: 'degree', - filterValue: 'true', - type: 'equal' - }] - expect(getFilteredData(data, filterArray)).toHaveLength(4) - }) - - test('invalid filter', () => { - const filterArray: filterArray = [{ - name: 'name', - filterValue: '9', - type: 'definitely_not_an_operator' - }] - expect(getFilteredData(data, filterArray)).toHaveLength(0) - }) -}) diff --git a/src/lib/components/Product/ProductList/logic/__test__/sort.test.js b/src/lib/components/Product/ProductList/logic/__test__/sort.test.js deleted file mode 100644 index 6c8d694..0000000 --- a/src/lib/components/Product/ProductList/logic/__test__/sort.test.js +++ /dev/null @@ -1,62 +0,0 @@ -import {getSortedData} from "../sort.js"; -import type {sortArray} from "../../layers.flow"; - -const convertDataToObjectArray = (x: Array<>): Array<> => x.map(x => { - return { - name: x.toString() - } -}) - -const data = convertDataToObjectArray([4, 153, 134, 432, 422]) - - -describe('test sort Logic in Product list', () => { - - test('empty sortLayer', () => { - const sortLayers = []; - const result = getSortedData(data, sortLayers) - const expected = convertDataToObjectArray([4, 153, 134, 432, 422]) - expect(result).toEqual(expected) - }) - - test('sort ascending', () => { - const sortLayers = [{ - name: 'name', - type: 'ascending', - }]; - const result = getSortedData(data, sortLayers) - const expected = convertDataToObjectArray([134, 153, 4, 422, 432]) - expect(result).toEqual(expected) - }) - - test('sort descending', () => { - const sortLayers = [{ - name: 'name', - type: 'descending', - }]; - const result = getSortedData(data, sortLayers) - const expected = convertDataToObjectArray( - [134, 153, 4, 422, 432].reverse() - ) - expect(result).toEqual(expected) - }) - - test('multiple sort', () => { - const sortLayers = [ - { - name: 'name', - type: 'ascending', - }, - { - name: 'name', - type: 'ascending', - }, { - name: 'name', - type: 'descending', - } - ]; - const result = getSortedData(data, sortLayers) - const expected = convertDataToObjectArray([134, 153, 4, 422, 432].reverse()) - expect(result).toEqual(expected) - }) -}) \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/logic/filter.js b/src/lib/components/Product/ProductList/logic/filter.js deleted file mode 100644 index 464268c..0000000 --- a/src/lib/components/Product/ProductList/logic/filter.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Compare 2 strings with an operator, and return Logical value of comparison. - *

- * Should an object with no attribute that is filtered against pass? - * Currently no. - *

- * - * @constructor - * @param{string}x1 - * @param{string| Array}x2 - * @param{string}operator - * @return {boolean} - * - *
- *     compareIt('a', 'a', 'equal') -> true
- *     compareIt('a', 'b', 'equal') -> false
- *     compareIt('a', 'a', 'something_not_recognized') -> false
- * 
- */ - - -const compare = (x1, x2, operator) => { - switch (operator) { - case 'greater_equal': - return x1 >= x2; - case 'greater': - return x1 > x2; - case 'equal': - if (typeof x2 === 'string') { - return x1 === x2; - } else { - return x2.indexOf(x1) !== -1; - } - case 'less': - return x1 < x2; - case 'less_equal': - return x1 <= x2; - default: - console.debug(`Handling undefined operator ${operator}`) - return false - } -} - -/** - * Filter data by properties of filterObject and return what is left. - * @constructor - * @param{Array<{}>}data - * @param{filterArray}filterLayers - * @return {Array<{}>} - */ - -const getFilteredData = (data, filterLayers) => { - let filteredData = data; - for (const layer of filterLayers) { - console.log(JSON.stringify(layer, null, 2)) - filteredData = filteredData.filter(dataPiece => { - return compare(dataPiece[layer.name].toString(), layer.filterValue, layer.type); - }) - } - return filteredData; -} - -export {getFilteredData}; \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/logic/sort.js b/src/lib/components/Product/ProductList/logic/sort.js deleted file mode 100644 index ce3f9ef..0000000 --- a/src/lib/components/Product/ProductList/logic/sort.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Sort data from left to right. - * The first layer sort all data. - * TODO: The rest of layers only sort data in undecided_group(group that have equal values). - * Currently only sort by last sort layer - * - * @constructor - * @param{Array<{}>}data - * @param{sortArray}sortLayers - * @return{Array<{}>} - * - *
- *
- * 
- */ -const getSortedData = (data, sortLayers) => { - if (sortLayers.length === 0) { - return data - } - const layer = sortLayers[sortLayers.length - 1] || [] - - // Immutability, in case of remove sortLayer, original data is not affected - const returnData = Object.assign([], data) - - return returnData.sort((x, y) => { - if (layer.type === 'ascending') { - return x[layer.name].localeCompare(y[layer.name]); - } else if (layer.type === 'descending') { - return (-1) * x[layer.name].localeCompare(y[layer.name]); - } else { - return 1; - } - }) -} - -export {getSortedData} \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/placeholder/product_Placeholder.js b/src/lib/components/Product/ProductList/placeholder/product_Placeholder.js deleted file mode 100644 index 495da6e..0000000 --- a/src/lib/components/Product/ProductList/placeholder/product_Placeholder.js +++ /dev/null @@ -1,70 +0,0 @@ -import React, {useContext} from 'react'; -import {Text, View} from 'react-native'; -import {Dimensions} from 'react-native'; -import {ThemeContext} from 'react-native-elements'; -import {makeId} from '../../../../util/makeRandomString.js'; - -const windowWidth = Dimensions.get('window').width; - -function Product_Placeholder(props) { - const {data, width_percent = 0.5} = props; - const { theme } = useContext(ThemeContext); - - return ( - - - {makeId(5)} - {makeId(5)} - - {makeId(5)} - - - ); -} - -export {Product_Placeholder}; diff --git a/src/lib/components/Product/ProductList/productList.js b/src/lib/components/Product/ProductList/productList.js new file mode 100644 index 0000000..7aae7a6 --- /dev/null +++ b/src/lib/components/Product/ProductList/productList.js @@ -0,0 +1,198 @@ +import React, { useState, useEffect } from 'react' +import { + ScrollView, + View, + Text, + Image, + FlatList, + ListView, + TouchableOpacity, + SafeAreaView, + Checkbox, + StyleSheet, + Dimensions +} from 'react-native' +import Ionicons from 'react-native-vector-icons/Ionicons' +import MaterialIcons from 'react-native-vector-icons/MaterialIcons' +import Item from './item' +import FilterAndSortTags from './filterAndSortTags' +import FilterOptions from './filter' +import SortOptions from './sort' +import { useProductList } from '../../../../talon/Product/useProductList' + +const { height } = Dimensions.get('window'); + +const ProductList = props => { + const { id } = props.route.params + const [isGrid, setIsGrid] = useState(true) + const [isFilterActive, setIsFilterActive] = useState(false) + const [isSortActive, setIsSortActive] = useState(false) + const [activeFilterCategories, setActiveFilterCategories] = useState([]) + const [activeSortOption, setActiveSortOption] = useState(99) + const [activeSortLabel, setActiveSortLabel] = useState([]) + const [filterValues, setFilterValues] = useState([]) + const [attributeCodes, setAttributeCodes] = useState([]) //attribute codes of filter fields + const [variablesObject, setVariablesObject] = useState({}) //arguments pass to sortAndFilterProducts function + + const { + sortAndFilterProducts, + productList, + sortFields, + filterFields, + canLoadMore, + pageSizeStep, + loading, + derivedErrorMessage + } = useProductList(id) + + const [pageSize, setPageSize] = useState(pageSizeStep) + + useEffect(() => { + setActiveFilterCategories(Array(filterFields.length).fill(false)) + setFilterValues(Array(filterFields.length).fill([])) + const array = [] + if(filterFields) filterFields.map(({attribute_code}) => array.push(attribute_code)) + setAttributeCodes(array) + setActiveSortLabel([]) + setVariablesObject({pageSize: pageSizeStep, category_id: id}) + setPageSize(pageSizeStep) + },[filterFields, id]) + + if(!productList) return Loading + + const _renderItem = ({item, index}) => { + const { uid, thumbnail, name, price_range } = item + return ( + + ) + } + + const _renderFooter = () => { + if(canLoadMore) { + return ( + + { + setVariablesObject({...variablesObject, pageSize: pageSize + pageSizeStep}) + setPageSize(pageSize + pageSizeStep) + sortAndFilterProducts({variables: {...variablesObject, pageSize: pageSize + pageSizeStep}}) + }} + > + Load more + + + ) + } + else return + } + + const toggleDisplayModeChange = () => setIsGrid(!isGrid) + const showFilterOptions = () => setIsFilterActive(!isFilterActive) + const showSortOptions = () => setIsSortActive(!isSortActive) + + if(!isFilterActive && !isSortActive) { + return ( + + + + item.uid} + key={isGrid} + numColumns={isGrid ? 2 : 1} + style={{ flex: 1 }} + contentContainerStyle={{marginTop: 10, paddingBottom: 45}} + ListFooterComponent={_renderFooter} + /> + + + + + + + + + + + + + ) + } else if (isFilterActive) { + return ( + + ) + } + else if(isSortActive) { + return ( + + ) + } +} + +const styles = StyleSheet.create({ + footer: { + position: 'absolute', + bottom: 0, + width: '100%', + height: 45, + backgroundColor: '#BDBDBD', + opacity: 0.8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between' + } +}) + +export {ProductList} \ No newline at end of file diff --git a/src/lib/components/Product/ProductList/sort.js b/src/lib/components/Product/ProductList/sort.js new file mode 100644 index 0000000..5f4b154 --- /dev/null +++ b/src/lib/components/Product/ProductList/sort.js @@ -0,0 +1,74 @@ +import React from 'react' +import { View, TouchableOpacity, Text } from 'react-native' +import Ionicons from 'react-native-vector-icons/Ionicons' + +const SortOptions = props => { + const { + categoryId, + sortFields, + setActiveSortLabel, + activeSortOption, + setActiveSortOption, + setIsSortActive, + sortAndFilterProducts, + variablesObject, + setVariablesObject + } = props + + const SortField = (i, label, value) => { + const tagLabel = `${label} (${i%2 ? 'DESC' : 'ASC'})` + const varObj = new Object + varObj['category_id'] = categoryId + varObj[`${value}Dir`] = i%2 ? 'DESC' : 'ASC' + + return ( + { + sortAndFilterProducts({variables: {...variablesObject, ...varObj}}) + setVariablesObject({...variablesObject, ...varObj}) + setActiveSortLabel([tagLabel, `${value}Dir`, i%2 ? 'DESC' : 'ASC']) + setActiveSortOption(i) + setIsSortActive(false) + }} + > + + {label} + {i % 2 ? + + : + } + + {activeSortOption === i && } + + ) + } + + return ( + + {sortFields.map(({value, label}, i) => { + return ( + + {SortField(i*2,label,value)} + {SortField(i*2+1, label, value)} + + ) + })} + + ) +} + +export default SortOptions \ No newline at end of file diff --git a/src/lib/components/categoryList/index.js b/src/lib/components/categoryList/index.js index 8f09434..d6f0e76 100644 --- a/src/lib/components/categoryList/index.js +++ b/src/lib/components/categoryList/index.js @@ -56,7 +56,9 @@ function CategoryDumpComponent(props) { navigation.navigate('Categories', {id: id}); } else { console.log('now render product'); - navigation.navigate('ProductList'); + navigation.navigate('ProductList', { + id: id + }); } } diff --git a/src/talon/Product/useProductList.js b/src/talon/Product/useProductList.js new file mode 100644 index 0000000..30b8b22 --- /dev/null +++ b/src/talon/Product/useProductList.js @@ -0,0 +1,167 @@ +import { gql, useQuery, useLazyQuery } from '@apollo/client'; +import { useState, useEffect } from 'react' + +export const GET_PRODUCT_LIST = gql` + query( + $category_id: String!, + $pageSize: Int, + $priceDir: SortEnum, + $nameDir: SortEnum, + $positionDir: SortEnum, + $color: [String], + $climate: [String], + $eco_collection: [String], + $erin_recommends: [String], + $sale: [String], + $size: [String], + $style_general: [String] + ){ + products( + filter: { + category_id: {eq: $category_id}, + climate: {in: $climate} + color: {in: $color}, + eco_collection: {in: $eco_collection}, + erin_recommends: {in: $erin_recommends}, + sale: {in: $sale}, + size: {in: $size}, + style_general: {in: $style_general} + }, + pageSize: $pageSize, + currentPage: 1, + sort: { + price: $priceDir, + name: $nameDir, + position: $positionDir + } + ){ + aggregations { + label + attribute_code + options { + count + label + value + } + } + items { + uid + name + thumbnail { + disabled + position + url + } + price_range { + maximum_price { + final_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + } + } + } + sort_fields { + default + options { + label + value + } + } + page_info { + current_page + total_pages + } + total_count + } + } +` + +const pageSizeStep = 12 + +export const useProductList = id => { + const [productList, setProductList] = useState([]) + const [sortFields, setSortFields] = useState([]) + const [filterFields, setFilterFields] = useState([]) + const [canLoadMore, setCanLoadMore] = useState(true) + + const { + data: initialData, + loading: initialLoading, + error: initialError, + } = useQuery(GET_PRODUCT_LIST, { + variables: { category_id: `${id}`, pageSize: pageSizeStep } + }) + + const [ + sortAndFilterProducts, + { + data: filteredData, + loading: filteredLoading, + error: filteredError + } + ] = useLazyQuery(GET_PRODUCT_LIST) + + const loading = initialLoading || filteredLoading + + useEffect(() => { + let _productList = [] + let _sortFields = [] + let _filterFields = [] + let _canLoadMore = true + if(initialData && initialData.products) { + _productList = initialData.products.items + _sortFields = initialData.products.sort_fields.options + _filterFields = initialData.products.aggregations + if(initialData.products.page_info.total_pages === 1) _canLoadMore = false + } + setProductList(_productList) + setSortFields(_sortFields) + setFilterFields(_filterFields) + setCanLoadMore(_canLoadMore) + }, [initialData, id]) + + useEffect(() => { + let _productList = [] + let _canLoadMore = true + if(filteredData && filteredData.products) { + _productList = filteredData.products.items + if(filteredData.products.page_info.total_pages === 1) _canLoadMore = false + } + setProductList(_productList) + setCanLoadMore(_canLoadMore) + }, [filteredData]) + + let derivedErrorMessage; + if (initialError || filteredError) { + const errorTarget = initialError || filteredError; + if (errorTarget.graphQLErrors) { + // Apollo prepends "GraphQL Error:" onto the message, + // which we don't want to show to an end user. + // Build up the error message manually without the prepended text. + derivedErrorMessage = errorTarget.graphQLErrors + .map(({ message }) => message) + .join(', '); + } else { + // A non-GraphQL error occurred. + derivedErrorMessage = errorTarget.message; + } + } + + return { + sortAndFilterProducts, + productList, + sortFields, + filterFields, + canLoadMore, + pageSizeStep, + loading, + derivedErrorMessage + } +} \ No newline at end of file