diff --git a/.eslintrc.js b/.eslintrc.js index 63967edf..d3e8b677 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,15 +5,15 @@ module.exports = { "react/jsx-filename-extension": [ 1, { - extensions: [".js", ".jsx"] - } + extensions: [".js", ".jsx"], + }, ], "no-console": 0, "react/forbid-prop-types": [ - true, + 2, { - forbid: ["any"] - } + forbid: ["any"], + }, ], "react/destructuring-assignment": 0, "react/no-array-index-key": 0, @@ -24,7 +24,7 @@ module.exports = { { blankLine: "never", prev: ["singleline-const", "singleline-let", "singleline-var"], - next: ["singleline-const", "singleline-let", "singleline-var"] + next: ["singleline-const", "singleline-let", "singleline-var"], }, { blankLine: "always", @@ -35,7 +35,7 @@ module.exports = { "multiline-let", "multiline-var", "multiline-expression", - "multiline-block-like" + "multiline-block-like", ], next: [ "class", @@ -47,8 +47,8 @@ module.exports = { "multiline-block-like", "singleline-const", "singleline-let", - "singleline-var" - ] + "singleline-var", + ], }, { blankLine: "always", @@ -62,7 +62,7 @@ module.exports = { "multiline-block-like", "singleline-const", "singleline-let", - "singleline-var" + "singleline-var", ], next: [ "class", @@ -71,42 +71,42 @@ module.exports = { "multiline-let", "multiline-var", "multiline-expression", - "multiline-block-like" - ] + "multiline-block-like", + ], }, { blankLine: "always", prev: "*", - next: "cjs-export" + next: "cjs-export", }, { blankLine: "always", prev: "cjs-import", - next: "*" + next: "*", }, { blankLine: "never", prev: "cjs-import", - next: "cjs-import" + next: "cjs-import", }, { blankLine: "always", prev: "*", - next: "return" - } - ] + next: "return", + }, + ], }, env: { jest: true, browser: true, node: true, - es6: true + es6: true, }, parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { - jsx: true - } - } + jsx: true, + }, + }, }; diff --git a/.gitignore b/.gitignore index 235ac9a7..13d893f0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ package-lock.json # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/package.json b/package.json index b0371ff4..953ee908 100644 --- a/package.json +++ b/package.json @@ -9,60 +9,57 @@ "predeploy": "npm run build", "deploy": "gh-pages -d build", "eject": "react-scripts eject", - "lint": "eslint \"src/**/*.{js,jsx}\"" + "lint": "eslint \"src/**/*.{js,jsx}\"", + "analyze": "source-map-explorer 'build/static/js/*.js'" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.15", - "@fortawesome/free-brands-svg-icons": "^5.7.2", - "@fortawesome/free-solid-svg-icons": "^5.7.2", - "@fortawesome/react-fontawesome": "^0.1.4", - "bootstrap": "^4.1.1", - "classnames": "^2.2.6", - "is-url": "^1.2.4", - "jquery": "^3.4.0", - "lodash": "^4.17.15", - "mapbox-gl": "^0.45.0-beta.1", - "node-sass": "^4.12.0", - "popper.js": "^1.14.7", - "prop-types": "^15.7.2", - "qs-lite": "^0.0.1", - "query-string": "^6.2.0", - "react": "16.8.6", - "react-beautiful-dnd": "^9.0.2", - "react-dom": "16.8.6", - "react-fontawesome": "^1.6.1", - "react-google-maps": "^9.4.5", - "react-loader-spinner": "^2.3.0", - "react-mapbox-gl": "^3.8.0", - "react-read-more-less": "^0.1.6", + "@fortawesome/fontawesome-svg-core": "1.2.21", + "@fortawesome/free-brands-svg-icons": "5.10.1", + "@fortawesome/free-solid-svg-icons": "5.10.1", + "@fortawesome/react-fontawesome": "0.1.4", + "bootstrap": "4.3.1", + "classnames": "2.2.6", + "is-url": "1.2.4", + "jquery": "3.4.1", + "lodash": "4.17.15", + "node-sass": "4.12.0", + "popper.js": "1.15.0", + "prop-types": "15.7.2", + "query-string": "6.8.2", + "react": "16.9.0", + "react-beautiful-dnd": "11.0.5", + "react-dom": "16.9.0", + "react-fontawesome": "1.6.1", + "react-google-maps": "9.4.5", + "react-read-more-less": "0.1.6", "react-redux": "7.1.0", "react-router": "5.0.1", "react-router-dom": "5.0.1", - "react-testing-library": "^6.1.2", - "reactstrap": "^5.0.0", + "react-testing-library": "8.0.1", + "reactstrap": "8.0.1", "recompose": "0.30.0", "redux": "4.0.4", - "redux-immutable-state-invariant": "^2.1.0", + "redux-immutable-state-invariant": "2.1.0", "redux-thunk": "2.3.0", - "sass-loader": "^7.1.0", - "tabletop": "^1.5.2" + "sass-loader": "7.2.0", + "source-map-explorer": "2.0.1", + "tabletop": "1.5.2" }, "devDependencies": { - "@fortawesome/fontawesome-free": "^5.7.2", - "babel-eslint": "10.0.1", - "enzyme": "^3.9.0", - "enzyme-adapter-react-16": "^1.12.1", - "eslint-config-airbnb": "17.1.1", + "@fortawesome/fontawesome-free": "5.10.1", + "enzyme": "3.10.0", + "enzyme-adapter-react-16": "1.14.0", + "eslint-config-airbnb": "18.0.0", "eslint-config-prettier": "6.0.0", - "eslint-plugin-import": "^2.18.0", - "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-import": "2.18.2", + "eslint-plugin-jsx-a11y": "6.2.3", "eslint-plugin-prettier": "3.1.0", - "eslint-plugin-react": "^7.14.2", + "eslint-plugin-react": "7.14.3", "husky": "3.0.3", - "jest-emotion": "^10.0.10", + "jest-emotion": "10.0.14", "lint-staged": "9.2.1", "prettier": "1.18.2", - "react-scripts": "3.0.1" + "react-scripts": "3.1.0" }, "husky": { "hooks": { diff --git a/src/App/AppContainer.js b/src/App/AppContainer.js index 39629a77..b55b5ddc 100644 --- a/src/App/AppContainer.js +++ b/src/App/AppContainer.js @@ -29,27 +29,20 @@ function sheetIdFromPath(directory, path) { } class AppContainer extends Component { - static propTypes = { - dispatch: PropTypes.func.isRequired, - match: PropTypes.object.isRequired, - isFetchingResource: PropTypes.bool.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - - position: {}, - displayFeedbackLink: true, - isValidPage: true, - }; - } - - static propTypes = { - dispatch: PropTypes.func + state = { + position: {}, + displayFeedbackLink: false, + isValidPage: true, }; componentDidMount() { + const hideFeedbackTs = localStorage.getItem("hideFeedback"); + + if (hideFeedbackTs === null || Date.now() > parseInt(hideFeedbackTs, 10)) { + localStorage.removeItem("hideFeedback"); + this.setState({ displayFeedbackLink: true }); + } + console.log(hideFeedbackTs, typeof hideFeedbackTs); const resourcePath = this.props.match.params.resource; let resourceSheetId = null; @@ -69,6 +62,8 @@ class AppContainer extends Component { } hideFeedbackLink = () => { + const weekMillis = 7 * 24 * 60 * 60 * 1000; + localStorage.setItem("hideFeedback", Date.now() + weekMillis); this.setState({ displayFeedbackLink: false }); }; @@ -166,6 +161,12 @@ class AppContainer extends Component { } } +AppContainer.propTypes = { + dispatch: PropTypes.func.isRequired, + match: PropTypes.object.isRequired, + isFetchingResource: PropTypes.bool.isRequired, +}; + function mapStateToProps(state) { const { isFetchingResource } = state; diff --git a/src/action/actionType.js b/src/action/actionType.js index 238d67bf..758972e7 100644 --- a/src/action/actionType.js +++ b/src/action/actionType.js @@ -2,8 +2,8 @@ export const LOAD_RESOURCE_DATA_START = "LOAD_RESOURCE_DATA_START"; export const LOAD_RESOURCE_DATA_SUCCESS = "LOAD_RESOURCE_DATA_SUCCESS"; export const LOAD_RESOURCE_DATA_FAILURE = "LOAD_RESOURCE_DATA_FAILURE"; export const LOAD_CATEGORIES = "LOAD_CATEGORIES"; -export const FILTER_RESOURCE_BY_CATEGORIES = "FILTER_RESOURCE_BY_CATEGORIES"; -export const FILTER_RESOURCE_BY_SEARCH = "FILTER_RESOURCE_BY_SEARCH"; +export const FILTER_RESOURCES_BY_CATEGORIES = "FILTER_RESOURCES_BY_CATEGORIES"; +export const FILTER_RESOURCES_BY_SEARCH = "FILTER_RESOURCES_BY_SEARCH"; export const ADD_SAVED_RESOURCE = "ADD_SAVED_RESOURCE"; export const REMOVE_SAVED_RESOURCE = "REMOVE_SAVED_RESOURCE"; -export const CLEAR_SAVED_RESOURCE = "CLEAR_SAVED_RESOURCE"; +export const CLEAR_SAVED_RESOURCES = "CLEAR_SAVED_RESOURCES"; diff --git a/src/action/resourceDataAction.js b/src/action/resourceDataAction.js index a1aad988..979bf7b9 100644 --- a/src/action/resourceDataAction.js +++ b/src/action/resourceDataAction.js @@ -6,9 +6,9 @@ const loadResourceDataStart = () => ({ isFetchingResource: true, }); -const loadResourceDataSuccess = resource => ({ +const loadResourceDataSuccess = resources => ({ type: types.LOAD_RESOURCE_DATA_SUCCESS, - resource, + resources, isFetchingResource: false, }); @@ -37,11 +37,11 @@ export function loadResources(resourcePath) { } export function filterByCategories(filteredResource) { - return { type: types.FILTER_RESOURCE_BY_CATEGORIES, filteredResource }; + return { type: types.FILTER_RESOURCES_BY_CATEGORIES, filteredResource }; } export function filterBySearch(searchedResource) { - return { type: types.FILTER_RESOURCE_BY_SEARCH, searchedResource }; + return { type: types.FILTER_RESOURCES_BY_SEARCH, searchedResource }; } export function addSavedResource(savedResource) { @@ -52,6 +52,6 @@ export function removeSavedResource(savedResourceIndex) { return { type: types.REMOVE_SAVED_RESOURCE, savedResourceIndex }; } -export function clearSavedResource() { - return { type: types.CLEAR_SAVED_RESOURCE }; +export function clearSavedResources() { + return { type: types.CLEAR_SAVED_RESOURCES }; } diff --git a/src/components/AdminPage/CardGrid.js b/src/components/AdminPage/CardGrid.js index 03e7af1b..5e4d3cf9 100644 --- a/src/components/AdminPage/CardGrid.js +++ b/src/components/AdminPage/CardGrid.js @@ -7,26 +7,10 @@ import SearchBar from "../Header/SearchBar"; import getDistance from "../../utils/distance"; class CardGrid extends Component { - static propTypes = { - currentPos: PropTypes.object.isRequired, - resource: PropTypes.array.isRequired, - handleFilter: PropTypes.func, - saveItem: PropTypes.func, + state = { + sortFunction: this.getCloserName, }; - static defaultProps = { - handleFilter: null, - saveItem: null, - }; - - constructor(props) { - super(props); - - this.state = { - dataSort: this.sortByAlphabet, - }; - } - getCloserResource = (a, b) => { if ( getDistance(a, this.props.currentPos) > @@ -45,26 +29,22 @@ class CardGrid extends Component { return 0; }; - sortByAlphabet = () => this.props.resource.slice().sort(this.getCloserName); - - sortByDistance = () => - this.props.resource.slice().sort(this.getCloserResource); + sortData = () => this.props.resources.slice().sort(this.state.sortFunction); handleSortChange = newSort => { - if (this.state.dataSort !== newSort) { + if (this.state.sortFunction !== newSort) { this.setState({ - // Set the dataSort variable to whichever sort function is chosen - dataSort: newSort, + sortFunction: newSort, }); } }; render() { const sortOptions = [ - { key: "A-Z", sort: this.sortByAlphabet, disabled: false }, + { key: "A-Z", sort: this.getCloserName, disabled: false }, { key: "Distance", - sort: this.sortByDistance, + sort: this.getCloserResource, disabled: !this.props.currentPos, }, ]; @@ -73,7 +53,7 @@ class CardGrid extends Component { // updates the this.state.dataSort variable. // this.state.dataSort() sorts data to feed into the OrganizationCards without modifying the // source of data - const sortedData = this.state.dataSort(); + const sortedData = this.sortData(); return (
@@ -101,14 +81,26 @@ class CardGrid extends Component { } } +CardGrid.propTypes = { + currentPos: PropTypes.object.isRequired, + resources: PropTypes.array.isRequired, + handleFilter: PropTypes.func, + saveItem: PropTypes.func, +}; + +CardGrid.defaultProps = { + handleFilter: null, + saveItem: null, +}; + function mapStateToProps(state) { - const filteredResourceSet = new Set(state.filteredResource.map(x => x.id)); + const filteredResourcesSet = new Set(state.filteredResources.map(x => x.id)); - const resource = state.searchedResource.filter(x => - filteredResourceSet.has(x.id) + const resources = state.searchedResources.filter(x => + filteredResourcesSet.has(x.id) ); - return { resource }; + return { resources }; } export default connect(mapStateToProps)(CardGrid); diff --git a/src/components/AdminPage/CategoryList.js b/src/components/AdminPage/CategoryList.js index 3a4d0048..e7e93d79 100644 --- a/src/components/AdminPage/CategoryList.js +++ b/src/components/AdminPage/CategoryList.js @@ -7,30 +7,23 @@ import _ from "lodash"; import * as resourceAction from "../../action/resourceDataAction"; class CategoryList extends Component { - constructor(props) { - super(props); - this.state = { - selectedCategory: [], - }; - } + state = { + selectedCategory: [], + }; componentDidUpdate() { const { selectedCategory } = this.state; - const { resource } = this.props; - const filteredResource = []; + const { resources } = this.props; if (selectedCategory.length === 0) { - this.props.actions.filterByCategories(resource); - } else { - resource.forEach(res => { - const isMatch = selectedCategory.some(cat => res.categories === cat); - - if (isMatch) { - filteredResource.push(res); - } - }); - this.props.actions.filterByCategories(filteredResource); + return this.props.actions.filterByCategories(resources); } + + const filteredResources = resources.filter(resource => + selectedCategory.some(cat => resource.categories === cat) + ); + + return this.props.actions.filterByCategories(filteredResources); } handleClick = async event => { @@ -73,8 +66,8 @@ class CategoryList extends Component { render() { const { selectedCategory } = this.state; const { categories } = this.props; - categories.sort(); - const categoryMenuItems = categories.map((curr, index) => ( + + const categoryMenuItems = [...categories].sort().map((curr, index) => ( r.id === props.organization.id)) { + if (!props.savedResources.some(r => r.id === props.organization.id)) { return { saveExist: false }; } @@ -46,13 +29,7 @@ class OrganizationCard extends Component { saveItem = () => { this.props.actions.addSavedResource(this.props.organization); - const query = qs.parse(window.location.search.replace("?", "")); - let resources = []; - - if (query.resources) { - resources = query.resources.split(","); - } - + const resources = getQueryResources(); const indexOfResource = resources.indexOf(this.props.organization.id); if (indexOfResource < 0) { @@ -61,24 +38,16 @@ class OrganizationCard extends Component { this.props.history.push({ pathname: window.location.pathname, - search: `?resources=${resources.join(",")}`, + search: encodeResources(resources), }); }; removeItem = () => { - // code copied verbatim from SavedResource.removalConfirmed() - // should probably refactor for cleanliness - const query = qs.parse(window.location.search.replace("?", "")); - let resources = []; - - if (query.resources) { - resources = query.resources.split(","); - } - + const resources = getQueryResources(); const indexOfResource = resources.indexOf(this.props.organization.id); if ( - this.props.savedResource.some( + this.props.savedResources.some( resource => resource.id === this.props.organization.id ) ) { @@ -88,7 +57,7 @@ class OrganizationCard extends Component { this.props.history.push({ pathname: window.location.pathname, - search: `?resources=${resources.join(",")}`, + search: encodeResources(resources), }); }; @@ -170,9 +139,23 @@ class OrganizationCard extends Component { } } +OrganizationCard.propTypes = { + organization: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + savedResources: PropTypes.array.isRequired, + currentPos: PropTypes.object.isRequired, + saveable: PropTypes.bool, + index: PropTypes.string.isRequired, +}; + +OrganizationCard.defaultProps = { + saveable: null, +}; + function mapStateToProps(state) { return { - savedResource: state.savedResource, + savedResources: state.savedResources, }; } diff --git a/src/components/Common/SortBar.js b/src/components/Common/SortBar.js index c50e0a37..6cdfb08c 100644 --- a/src/components/Common/SortBar.js +++ b/src/components/Common/SortBar.js @@ -2,11 +2,6 @@ import React from "react"; import PropTypes from "prop-types"; class SortBar extends React.Component { - static propTypes = { - sortOptions: PropTypes.array.isRequired, - onSortChange: PropTypes.func.isRequired, - }; - handleClick = e => { // Get new sort based on index of sortOption array if (this.props.sortOptions[e.target.value]) { @@ -34,4 +29,10 @@ class SortBar extends React.Component { ); } } + +SortBar.propTypes = { + sortOptions: PropTypes.array.isRequired, + onSortChange: PropTypes.func.isRequired, +}; + export default SortBar; diff --git a/src/components/Header/DropdownCategory.js b/src/components/Header/DropdownCategory.js index 2dc2388c..db35226d 100644 --- a/src/components/Header/DropdownCategory.js +++ b/src/components/Header/DropdownCategory.js @@ -9,19 +9,11 @@ import { // import { DropdownCategory } from './HeaderLayout'; class DropdownCategory extends Component { - static propTypes = { - categories: PropTypes.array.isRequired, - handleEvent: PropTypes.func.isRequired, + state = { + dropdownOpen: false, + activeItem: [], }; - constructor(props) { - super(props); - this.state = { - dropdownOpen: false, - activeItem: [], - }; - } - toggle = () => { this.setState(prevState => ({ dropdownOpen: !prevState.dropdownOpen })); }; @@ -86,4 +78,9 @@ class DropdownCategory extends Component { } } +DropdownCategory.propTypes = { + categories: PropTypes.array.isRequired, + handleEvent: PropTypes.func.isRequired, +}; + export default DropdownCategory; diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 78ce8fb4..33adf59e 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -15,31 +15,21 @@ import { ModalBody, ModalFooter, } from "reactstrap"; +import { getQueryResources, encodeResources } from "../../utils/resourcesQuery"; import * as resourceAction from "../../action/resourceDataAction"; class Header extends Component { - static propTypes = { - savedResource: PropTypes.array.isRequired, - actions: PropTypes.object.isRequired, - toggleSavedResourcesPane: PropTypes.func.isRequired, - match: PropTypes.object.isRequired, + state = { + collapsed: true, + modal: false, }; - constructor(props) { - super(props); - - this.state = { - collapsed: true, - modal: false, - }; - } - toggleNavbar = () => { this.setState(prevState => ({ collapsed: !prevState.collapsed })); }; modalOpen = () => { - if (this.props.savedResource.length > 0) { + if (this.props.savedResources.length > 0) { this.modalToggle(); } else { window.location.reload(); @@ -51,15 +41,24 @@ class Header extends Component { }; confirmationModalToggle = () => { - this.props.actions.clearSavedResource(); + this.props.actions.clearSavedResources(); this.modalToggle(); }; + toAdmin = () => { + const resources = getQueryResources(); + + this.props.history.push({ + pathname: `/${this.props.match.params.resource}/admin`, + search: encodeResources(resources), + }); + }; + render() { - const { savedResource, toggleSavedResourcesPane } = this.props; + const { savedResources, toggleSavedResourcesPane } = this.props; const savedResourceButtonClassNames = cx("saved-resource-button", { - "has-selections": savedResource.length, + "has-selections": savedResources.length, }); return ( @@ -68,18 +67,53 @@ class Header extends Component { Community Connect - ( - - )} - /> + +
+ ( + + )} + /> + ( + + )} + /> + ( + + )} + /> +
Alert @@ -106,9 +140,18 @@ class Header extends Component { } } +Header.propTypes = { + savedResources: PropTypes.array.isRequired, + actions: PropTypes.object.isRequired, + toggleSavedResourcesPane: PropTypes.func.isRequired, + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + location: PropTypes.object.isRequired, +}; + function mapStateToProps(state) { return { - savedResource: state.savedResource, + savedResources: state.savedResources, }; } diff --git a/src/components/Header/SearchBar.js b/src/components/Header/SearchBar.js index 35c02601..c8604b2a 100644 --- a/src/components/Header/SearchBar.js +++ b/src/components/Header/SearchBar.js @@ -6,26 +6,18 @@ import { bindActionCreators } from "redux"; import * as resourceAction from "../../action/resourceDataAction"; class SearchBar extends Component { - static propTypes = { - resource: PropTypes.array.isRequired, - actions: PropTypes.object.isRequired, + state = { + searchString: "", }; - constructor(props) { - super(props); - this.state = { - searchString: "", - }; - } - handleFilter = e => { this.setState({ searchString: e.target.value }); - const searchedResource = this.props.resource.filter(i => + const searchedResource = this.props.resources.filter(i => i.name.toLowerCase().match(e.target.value.toLowerCase()) ); this.props.actions.filterBySearch( - e.target.value.length > 0 ? searchedResource : this.props.resource + e.target.value.length > 0 ? searchedResource : this.props.resources ); }; @@ -42,12 +34,17 @@ class SearchBar extends Component { } } +SearchBar.propTypes = { + resources: PropTypes.array.isRequired, + actions: PropTypes.object.isRequired, +}; + function mapStateToProps(state) { return { - resource: - state.filteredResource.length > 0 - ? state.filteredResource - : state.resource, + resources: + state.filteredResources.length > 0 + ? state.filteredResources + : state.resources, }; } diff --git a/src/components/MapPage/Map.js b/src/components/MapPage/Map.js index 2b944b3e..b8c9307e 100644 --- a/src/components/MapPage/Map.js +++ b/src/components/MapPage/Map.js @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ import React from "react"; import { withScriptjs, withGoogleMap, GoogleMap } from "react-google-maps"; import { MarkerClusterer } from "react-google-maps/lib/components/addons/MarkerClusterer"; @@ -9,20 +10,22 @@ import Styles from "./mapStyles"; const Map = compose( lifecycle({ - componentWillMount() { + componentDidMount() { this.setState({ zoomToMarkers: map => { - const bounds = new window.google.maps.LatLngBounds(); + if (map) { + const bounds = new window.google.maps.LatLngBounds(); - map.props.children.props.children.forEach(child => { - bounds.extend( - new window.google.maps.LatLng( - child.props.resource.coordinates.lat, - child.props.resource.coordinates.lng - ) - ); - }); - map.fitBounds(bounds); + map.props.children.props.children.forEach(child => { + bounds.extend( + new window.google.maps.LatLng( + child.props.resource.coordinates.lat, + child.props.resource.coordinates.lng + ) + ); + }); + map.fitBounds(bounds); + } }, }); }, @@ -46,7 +49,7 @@ const Map = compose( ref={props.onMarkerClick} defaultMaxZoom={16} > - {props.resource + {props.resources .filter(resource => resource.coordinates) .map((resource, index) => ( ( +const OrganizationMap = ({ mapResources }) => ( } mapElement={
} loadingElement={
} - resource={mapResource} + resources={mapResources} /> ); OrganizationMap.propTypes = { - mapResource: PropTypes.array.isRequired, + mapResources: PropTypes.array.isRequired, }; function mapStateToProps(state) { - const currentResource = - state.savedResource.length > 0 ? state.savedResource : state.resource; + const currentResources = + state.savedResources.length > 0 ? state.savedResources : state.resources; - const locationArray = []; + const locations = {}; - currentResource.forEach(resource => { - if (!locationArray[resource.hashCoordinates]) { - locationArray[resource.hashCoordinates] = { + currentResources.forEach(resource => { + if (!locations[resource.hashCoordinates]) { + locations[resource.hashCoordinates] = { coordinates: resource.coordinates, - groupedResource: [], + groupedResources: [], showInfo: false, }; } - locationArray[resource.hashCoordinates].groupedResource.push(resource); + locations[resource.hashCoordinates].groupedResources.push(resource); }); - const resource = Object.values(locationArray); + const resources = Object.values(locations); return { - mapResource: resource, + mapResources: resources, }; } diff --git a/src/components/MapPage/OrganizationMarker.js b/src/components/MapPage/OrganizationMarker.js index b2d894b5..1ae384b3 100644 --- a/src/components/MapPage/OrganizationMarker.js +++ b/src/components/MapPage/OrganizationMarker.js @@ -13,19 +13,10 @@ import youth from '../../images/icons/youth.png' import couple from '../../images/icons/couple.png' class OrganizationMarker extends Component { - static propTypes = { - open: PropTypes.bool.isRequired, - resource: PropTypes.object.isRequired, + state = { + open: this.props.open, }; - constructor(props) { - super(props); - this.state = { - open: this.props.open, - }; - } - - componentDidUpdate(prevProps) { if (prevProps.open !== this.props.open) { this.updateOpen(); @@ -105,26 +96,24 @@ class OrganizationMarker extends Component { {this.state.open && (
- {resource.groupedResource.map(resourceData => ( + {this.props.resource.groupedResources.map(resource => (
-

{resourceData.name}

-
{resourceData.combinedaddress}
-
{resourceData.tags}
+

{resource.name}

+
{resource.combinedaddress}
+
{resource.tags}
))} @@ -136,4 +125,9 @@ class OrganizationMarker extends Component { } } +OrganizationMarker.propTypes = { + open: PropTypes.bool.isRequired, + resource: PropTypes.object.isRequired, +}; + export default OrganizationMarker; diff --git a/src/components/MapPage/ResultList.js b/src/components/MapPage/ResultList.js index 33a53659..7f5268fc 100644 --- a/src/components/MapPage/ResultList.js +++ b/src/components/MapPage/ResultList.js @@ -8,25 +8,10 @@ import getDistance from "../../utils/distance"; import * as resourceAction from "../../action/resourceDataAction"; class ResultList extends Component { - static propTypes = { - currentPos: PropTypes.object.isRequired, - savedResource: PropTypes.array.isRequired, - actions: PropTypes.object.isRequired, - saveItem: PropTypes.func, + state = { + sortFunction: this.getCloserName, }; - static defaultProps = { - saveItem: null, - }; - - constructor(props) { - super(props); - - this.state = { - dataSort: this.sortByAlphabet, - }; - } - getCloserResource = (a, b) => { if ( getDistance(a, this.props.currentPos) > @@ -45,37 +30,33 @@ class ResultList extends Component { return 0; }; - sortByAlphabet = () => - this.props.savedResource.slice().sort(this.getCloserName); - - sortByDistance = () => - this.props.savedResource.slice().sort(this.getCloserResource); + sortData = () => + this.props.savedResources.slice().sort(this.state.sortFunction); handleSortChange = newSort => { - if (this.state.dataSort !== newSort) { + if (this.state.sortFunction !== newSort) { this.setState({ - // Set the dataSort variable to whichever sort function is chosen - dataSort: newSort, + sortFunction: newSort, }); } }; cardClick = id => { - this.props.savedResource.findIndex(resource => resource.id === id); + this.props.savedResources.findIndex(resource => resource.id === id); }; saveResource = resource => { - if (!this.props.savedResource.some(r => r.id === resource.id)) { - this.props.actions.addSavedResource(this.props.savedResource.slice()); + if (!this.props.savedResources.some(r => r.id === resource.id)) { + this.props.actions.addSavedResource(this.props.savedResources.slice()); } }; render() { const sortOptions = [ - { key: "A-Z", sort: this.sortByAlphabet, disabled: false }, + { key: "A-Z", sort: this.getCloserName, disabled: false }, { key: "Distance", - sort: this.sortByDistance, + sort: this.getCloserResource, disabled: !this.props.currentPos, }, ]; @@ -84,7 +65,7 @@ class ResultList extends Component { // updates the this.state.dataSort variable. // this.state.dataSort() sorts data to feed into the OrganizationCards without modifying the // source of data - const sortedData = this.state.dataSort(); + const sortedData = this.sortData(); return (
@@ -109,10 +90,21 @@ class ResultList extends Component { } } +ResultList.propTypes = { + currentPos: PropTypes.object.isRequired, + savedResources: PropTypes.array.isRequired, + actions: PropTypes.object.isRequired, + saveItem: PropTypes.func, +}; + +ResultList.defaultProps = { + saveItem: null, +}; + function mapStateToProps(state) { return { - savedResource: - state.savedResource.length > 0 ? state.savedResource : state.resource, + savedResources: + state.savedResources.length > 0 ? state.savedResources : state.resources, }; } diff --git a/src/components/MapPage/SplitScreenSlidingPane.js b/src/components/MapPage/SplitScreenSlidingPane.js index 5ed6db52..7f6e57d8 100644 --- a/src/components/MapPage/SplitScreenSlidingPane.js +++ b/src/components/MapPage/SplitScreenSlidingPane.js @@ -5,10 +5,6 @@ import PropTypes from "prop-types"; import cx from "classnames"; class SplitScreenSlidingPane extends Component { - static propTypes = { - children: PropTypes.element.isRequired, - }; - state = { isOpen: true, }; @@ -35,4 +31,8 @@ class SplitScreenSlidingPane extends Component { } } +SplitScreenSlidingPane.propTypes = { + children: PropTypes.element.isRequired, +}; + export default SplitScreenSlidingPane; diff --git a/src/components/SavedResources/SavedResource.js b/src/components/SavedResources/SavedResource.js index daa0aa31..72ff092c 100644 --- a/src/components/SavedResources/SavedResource.js +++ b/src/components/SavedResources/SavedResource.js @@ -4,7 +4,6 @@ import { connect } from "react-redux"; import { bindActionCreators, compose } from "redux"; import { withRouter } from "react-router"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import qs from "qs-lite"; import { Alert, Card, @@ -13,31 +12,17 @@ import { ModalHeader, ModalBody, } from "reactstrap"; +import { getQueryResources, encodeResources } from "../../utils/resourcesQuery"; import getDistance from "../../utils/distance"; import * as resourceAction from "../../action/resourceDataAction"; import SavedResourceButton from "./SavedResourceButton"; class SavedResource extends Component { - static propTypes = { - organization: PropTypes.object.isRequired, - savedResource: PropTypes.array.isRequired, - actions: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, - currentPos: PropTypes.object, + state = { + visible: false, }; - static defaultProps = { - currentPos: null, - }; - - constructor(props) { - super(props); - this.state = { - visible: false, - }; - } - confirmationAlertToggle = () => { this.setState(prevState => ({ visible: !prevState.visible })); }; @@ -47,17 +32,11 @@ class SavedResource extends Component { }; removalConfirmed = () => { - const query = qs.parse(window.location.search.replace("?", "")); - let resources = []; - - if (query.resources) { - resources = query.resources.split(","); - } - + const resources = getQueryResources(); const indexOfResource = resources.indexOf(this.props.organization.id); if ( - this.props.savedResource.some( + this.props.savedResources.some( resource => resource.id === this.props.organization.id ) ) { @@ -67,7 +46,7 @@ class SavedResource extends Component { this.props.history.push({ pathname: window.location.pathname, - search: `?resources=${resources.join(",")}`, + search: encodeResources(resources), }); this.removeItem(); }; @@ -198,8 +177,20 @@ class SavedResource extends Component { } } +SavedResource.propTypes = { + organization: PropTypes.object.isRequired, + savedResources: PropTypes.array.isRequired, + actions: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + currentPos: PropTypes.object, +}; + +SavedResource.defaultProps = { + currentPos: null, +}; + function mapStateToProps(state) { - return { savedResource: state.savedResource }; + return { savedResources: state.savedResources }; } function mapDispatchToProps(dispatch) { diff --git a/src/components/SavedResources/SavedResourcePanel.js b/src/components/SavedResources/SavedResourcePanel.js index 4f2f2167..037884c2 100644 --- a/src/components/SavedResources/SavedResourcePanel.js +++ b/src/components/SavedResources/SavedResourcePanel.js @@ -1,28 +1,23 @@ import React from "react"; -import qs from "qs-lite"; import { Link, Route } from "react-router-dom"; import { Button } from "reactstrap"; import PropTypes from "prop-types"; import { faShare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { getQueryResources, encodeResources } from "../../utils/resourcesQuery"; import SavedResources from "./SavedResourcesContainer"; const ToShareButton = ({ resourcePath }) => { - const query = qs.parse(window.location.search.replace("?", "")); - let resources = []; - let tempUrl = ""; - - if (query.resources) { - resources = query.resources.split(","); - tempUrl = `/${resourcePath}/?resources=${resources.join(",")}`; - } + const resources = getQueryResources(); + const query = encodeResources(resources); + const url = query && `/${resourcePath}/?${query}`; return (