├── .eslintrc.js ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── .yarnrc ├── LICENSE ├── README.md ├── docs ├── CONTRIBUTING.md ├── issue_template.md └── pull_request_template.md ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html └── manifest.json ├── sass-lint.yml ├── share-symbol.svg ├── src ├── App │ ├── App.test.js │ └── AppContainer.js ├── action │ ├── actionType.js │ └── resourceDataAction.js ├── api │ ├── directoryGoogleSheets.js │ └── googlesheetApi.js ├── components │ ├── AdminPage │ │ ├── AdminPage.js │ │ ├── CardGrid.js │ │ ├── CategoryList.js │ │ └── SplitScreenTogglePane.js │ ├── Common │ │ ├── FeedbackContainer.js │ │ ├── Loading.js │ │ ├── OrganizationCard.js │ │ ├── SortBar.js │ │ └── subcomponents │ │ │ ├── OrganizationCardBody.js │ │ │ ├── OrganizationCardOverview.js │ │ │ ├── OrganizationCardSaveButton.js │ │ │ ├── OrganizationCardSocialMedia.js │ │ │ └── index.js │ ├── Header │ │ ├── DropdownCategory.js │ │ ├── Header.js │ │ └── SearchBar.js │ ├── MapPage │ │ ├── Map.js │ │ ├── MapPage.js │ │ ├── OrganizationMap.js │ │ ├── OrganizationMarker.js │ │ ├── ResultList.js │ │ └── SplitScreenSlidingPane.js │ ├── NotFoundPage │ │ └── NotFoundPage.js │ ├── PrintPage │ │ ├── PrintPageContainer.jsx │ │ └── PrintPanel.jsx │ └── SavedResources │ │ ├── SavedResource.js │ │ ├── SavedResourceButton.js │ │ ├── SavedResourcePanel.js │ │ └── SavedResourcesContainer.js ├── css │ ├── base-constants.scss │ ├── component-styles │ │ ├── Admin.scss │ │ ├── AppContainer.scss │ │ ├── CategoryList.scss │ │ ├── DropdownCategory.scss │ │ ├── Header.scss │ │ ├── MapPage.scss │ │ ├── NotFoundPage.scss │ │ ├── OrganizationCard.scss │ │ ├── OrganizationCardSaveButton.scss │ │ ├── PrintPage.scss │ │ ├── ResultList.scss │ │ ├── SavedResource.scss │ │ ├── SavedResourceContainer.scss │ │ ├── SavedResourcePanel.scss │ │ ├── SplitScreenSlidingPane.scss │ │ └── SplitScreenTogglePane.scss │ ├── index.scss │ ├── main.scss │ ├── mobile.scss │ └── print.scss ├── data.json ├── images │ ├── cc-logo-home.png │ └── cc-logo-icon.png ├── index.js ├── logo.svg ├── reducers │ ├── index.js │ ├── initialState.js │ └── resourceReducers.js ├── registerServiceWorker.js ├── share-symbol.svg ├── store │ └── configureStore.js └── utils │ ├── distance.js │ └── resourcesQuery.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "babel-eslint", 3 | extends: ["airbnb", "plugin:prettier/recommended", "prettier/react"], 4 | rules: { 5 | "react/jsx-filename-extension": [ 6 | 1, 7 | { 8 | extensions: [".js", ".jsx"], 9 | }, 10 | ], 11 | "no-console": 0, 12 | "react/forbid-prop-types": [ 13 | 2, 14 | { 15 | forbid: ["any"], 16 | }, 17 | ], 18 | "react/destructuring-assignment": 0, 19 | "react/no-array-index-key": 0, 20 | "jsx-a11y/no-static-element-interactions": 0, 21 | "jsx-a11y/click-events-have-key-events": 0, 22 | "padding-line-between-statements": [ 23 | "error", 24 | { 25 | blankLine: "never", 26 | prev: ["singleline-const", "singleline-let", "singleline-var"], 27 | next: ["singleline-const", "singleline-let", "singleline-var"], 28 | }, 29 | { 30 | blankLine: "always", 31 | prev: [ 32 | "class", 33 | "function", 34 | "multiline-const", 35 | "multiline-let", 36 | "multiline-var", 37 | "multiline-expression", 38 | "multiline-block-like", 39 | ], 40 | next: [ 41 | "class", 42 | "function", 43 | "multiline-const", 44 | "multiline-let", 45 | "multiline-var", 46 | "multiline-expression", 47 | "multiline-block-like", 48 | "singleline-const", 49 | "singleline-let", 50 | "singleline-var", 51 | ], 52 | }, 53 | { 54 | blankLine: "always", 55 | prev: [ 56 | "class", 57 | "function", 58 | "multiline-const", 59 | "multiline-let", 60 | "multiline-var", 61 | "multiline-expression", 62 | "multiline-block-like", 63 | "singleline-const", 64 | "singleline-let", 65 | "singleline-var", 66 | ], 67 | next: [ 68 | "class", 69 | "function", 70 | "multiline-const", 71 | "multiline-let", 72 | "multiline-var", 73 | "multiline-expression", 74 | "multiline-block-like", 75 | ], 76 | }, 77 | { 78 | blankLine: "always", 79 | prev: "*", 80 | next: "cjs-export", 81 | }, 82 | { 83 | blankLine: "always", 84 | prev: "cjs-import", 85 | next: "*", 86 | }, 87 | { 88 | blankLine: "never", 89 | prev: "cjs-import", 90 | next: "cjs-import", 91 | }, 92 | { 93 | blankLine: "always", 94 | prev: "*", 95 | next: "return", 96 | }, 97 | ], 98 | }, 99 | env: { 100 | jest: true, 101 | browser: true, 102 | node: true, 103 | es6: true, 104 | }, 105 | parserOptions: { 106 | ecmaVersion: 6, 107 | sourceType: "module", 108 | ecmaFeatures: { 109 | jsx: true, 110 | }, 111 | }, 112 | }; 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | package-lock.json 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # code editor (IntelliJ) 14 | .idea 15 | 16 | # misc 17 | .DS_Store 18 | .env 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // "editor.formatOnSave": true, 3 | // "eslint.validate": [ 4 | // "javascript", 5 | // "javascriptreact", 6 | // { 7 | // "language": "html", 8 | // "autoFix": true 9 | // } 10 | // ], 11 | // "eslint.enable": true, 12 | // "eslint.options": { 13 | // "extensions": [".html", ".js", ".vue", ".jsx"] 14 | // }, 15 | // "eslint.run": "onSave", 16 | // "eslint.autoFixOnSave": true 17 | } 18 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2018 Code for Boston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 5 |

Community Connect

6 |
7 | 8 | 9 | 10 | 11 |
12 | 13 |

14 | 15 |

A tool for connecting those in need to services or materials that improve their quality of life.

16 |
17 | Click to read more 18 |

19 | "Community Connect" is a health resource web application that aims to consolidate information about businesses and organization available in communities that promote healthy lifestyle choices. A health resource is defined as services or materials that improve the quality of life of others, ranging from affordable child care, substance abuse counseling, domestic violence support, and more. We are working in conjunction with Massachusetts General Hospital's Center for Community Health Improvement, MGH Revere HealthCare Center, and Revere CARES Coalition to create an extensive database in our pilot region of Revere, Chelsea, Charlestown, and eventually the Greater Boston Area. 20 |

21 |
22 | 23 | --- 24 | 25 | ## Table of Contents 26 | 27 | - [Table of Contents](#Table-of-Contents) 28 | - [Features](#Features) 29 | - [Getting Started](#Getting-Started) 30 | - [Live Demo](#Live-Demo) 31 | - [Running Locally](#Running-Locally) 32 | - [Customizing Google Sheet](#Customizing-Google-Sheet) 33 | - [Contributing](#Contributing) 34 | - [Support](#Support) 35 | - [License](#License) 36 | - [History](#History) 37 | 38 | --- 39 | 40 | ## Features 41 | 42 | - Dynamically updated resources from any Google Sheet 43 | - Save resources for viewing later 44 | - Share a link with your saved resources 45 | - Filter resources by category or search term 46 | 47 | --- 48 | 49 | ## Getting Started 50 | 51 | ### Live Demo 52 | 53 | Want to see what Community Connect is all about? Check out our live site for Revere at [ccfor.me/`revere`](http://ccfor.me/revere). To manage resources, enter the 'admin' view by adding `/admin` to the end of the URL, such as [ccfor.me/`revere/admin`](http://ccfor.me/revere/admin). 54 | 55 | ### Running Locally 56 | 57 | 1. Clone the repository 58 | 2. Install yarn 59 | 3. Install dependencies by running `yarn` 60 | 4. Start the development server by running `yarn start` 61 | 5. Visit `localhost:3000` in your browser to see it running! 🎉 62 | 63 | ### Customizing Google Sheet 64 | 65 | You can use a custom Google Sheet with your local installation of Community Connect. You might want to do this for testing or development purposes. 66 | _Prefer to see a gif of this process instead of reading steps? Click [here](https://imgur.com/a/N6kdSjC)_ 67 | 68 |
69 | Click to see instructions for creating your own sheet 70 |
    71 |
  1. Visit the current spreadsheet
  2. 72 |
  3. Click File and select Make a Copy
  4. 73 |
  5. Click OK
  6. 74 |
  7. When viewing your copy, click SHARE in the upper-right hand corner.
  8. 75 |
  9. Click "Get shareable link" in the upper-right hand corner of the modal.
  10. 76 |
  11. Ensure that "Anyone with the link can view" is selected.
  12. 77 |
  13. Copy link
  14. 78 |
  15. Click done
  16. 79 |
  17. Click File and select "Publish to the web"
  18. 80 |
  19. Click Publish
  20. 81 |
  21. Open "src/googlesheetApi.js" in the codebase
  22. 82 |
  23. Replace "revere_key" with a portion of the URL in your clipboard
  24. 83 | 84 | For Example, if the URL of your Google Spreadsheet is 85 | https://docs.google.com/spreadsheets/d/1FRd8Jw7y4CnnHCKIvkM-pjNjRVFHFHuobVU-ajXre6M/edit?usp=sharing 86 | 87 | Set the build-time environment variable REACT_APP_GOOGLE_SHEETS_ID to "1FRd8Jw7y4CnnHCKIvkM-pjNjRVFHFHuobVU-ajXre6M" 88 | 89 |
90 |
91 | 92 | --- 93 | 94 | ## Contributing 95 | 96 | Thank you for your willingness to help out! To get started on helping build Community Connect, take a look at [our contribution guide.](/docs/CONTRIBUTING.md) 97 | 98 | --- 99 | 100 | ## Support 101 | 102 | Join our [Code for Boston](https://www.codeforboston.org/) Slack channel: [#community-connect](https://communityinviter.com/apps/cfb-public/code-for-boston-slack-invite) or look for us at the [Code for Boston Tuesday meet-ups](https://meetup.com/Code-For-Boston). 103 | 104 | --- 105 | 106 | ## License 107 | 108 | [MIT License](/LICENSE) 109 | 110 | --- 111 | 112 | ## History 113 | 114 | The original architectural design for this app was designed proven out by [Bob Breznak](https://github.com/bobbrez) for an organization assisting with the refugee crisis in Greece in 2016, [Prosper](http://prosper.community/). They needed help consolidating, vetting and displaying resources on the web. In May 2018 he re-wrote the frontend in react.js to create an app that assists homeless people [Seeking Shelter](https://makao2.brez.io/) and resources. In August 2018 Code for Boston’s Community Connect project had similar aims and the repo was moved into their org. The data used for this project was initially collected from [Nevil Desai](https://www.linkedin.com/in/nevildesai/) during his internship with Revere CARES, a coalition group under the umbrella of MGH Center for Community Health Improvement. 115 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to _Community Connect_ 2 | 3 | Thank you for taking the time to contribute! 👍 Please read the following sections to learn how to make the most of your time and energy when helping out with Community Connect. 4 | 5 | > **⚠️ Note:** Community Connect is based out of Boston and is a team within [Code for Boston](https://www.codeforboston.org/), our local [Code for America](https://www.codeforamerica.org/) brigade. This project is maintained and developed through weekly in-person meetups as part of the organization. 6 | > 7 | > To chat with the team, join us at [the next Code for Boston Meetup](https://www.meetup.com/Code-for-Boston/) or connect with us at `#communityconnect` on [our Slack workspace.](https://communityinviter.com/apps/cfb-public/code-for-boston-slack-invite) 8 | 9 | ## I want to... 10 | 11 | # Contribute _Writing_ Code ⌨️ 12 | 13 | If you want to write code for the project, start by looking at issues to tackle on our backlog. Can't find anything worth working on? Consider [reporting a bug](#Report-a-Bug-%F0%9F%90%9E) or [suggesting a feature.](#Suggest-a-Feature-%F0%9F%9B%A0) 14 | Our open issues are listed on the project's [Issues](https://github.com/codeforboston/communityconnect/issues) tab. The best places to start are unassigned issues with the label "[good first issue](https://github.com/codeforboston/communityconnect/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22CfB+-+good+first+issue%22)". 15 | 16 | To contribute your code to the project: 17 | 18 | 1. Fork the project (https://github.com/codeforboston/communityconnect/fork) 19 | 2. Create a feature branch (`git checkout -b feature/{{your new feature}}`) 20 | 3. Commit your changes to the branch (`git commit -am 'Add {{your changes}}`) 21 | 4. Push to the branch (`git push origin feature/{{your new feature}}`) 22 | 5. Create a new Pull Request 23 | 24 | Steps not quite clear? First time? Check out [this 'first contributions' guide.](https://github.com/firstcontributions/first-contributions) 25 | 26 | ### A Note on Pull Requests 27 | 28 | A good pull request should include: 29 | 30 | - [ ] A description of the change being made 31 | - [ ] A reference to the issue being addressed 32 | - [ ] A brief description of how you verified your changes 33 | 34 | If you know someone specific will be interested in your change (such as the issue reporter), add them as a reviewer! Otherwise, all pull requests will be reviewed by contributors and maintainers. 35 | 36 | # Contribute _Reading_ Code 🖥 37 | 38 | You can help contribute to our code base by reviewing [open pull requests on the 'Pull requests' tab](https://github.com/codeforboston/communityconnect/pulls) or by reading our current source code in the `development` branch. 39 | 40 | Check out this [Guide from GitHub on reviewing pull requests](https://lab.github.com/githubtraining/reviewing-pull-requests) for further tips and suggestions. 41 | 42 | # Report a Bug 🐞 43 | 44 | Want to report an issue or bug you've found in Community Connect? Great! Before creating an issue and submitting a bug report, be sure to [search for related issues](https://github.com/search?utf8=%E2%9C%93&q=is%3Aissue+repo%3Acodeforboston%2Fcommunityconnect+state%3Aopen&type=Issues&ref=advsearch&l=&l=) to see if the problem has already been documented. If the issue has already been reported **and it is still open,** add a comment to the existing issue instead of opening a new one. 45 | 46 | Bugs are tracked as issues in our backlog. To report one, [create an issue](https://github.com/codeforboston/communityconnect/issues/new?labels=bug&title=New+bug+report) including: 47 | 48 | - [ ] The `[bug]` label 49 | - [ ] A clear and descriptive title of the problem 50 | - [ ] A description of the issue and expected outcome 51 | - [ ] Specific steps used to reproduce the problem 52 | (examples are welcome, especially images or code snippets!) 53 | - [ ] A capture of error messages or other unexpected output 54 | 55 | # Suggest a Feature 🛠 56 | 57 | Community Connect is always growing based on user feedback and suggestions - thanks for sharing yours! Just like [reporting a bug](#Report-a-Bug-%F0%9F%90%9E), be sure to check existing issues for the feature you're requesting - you might not be the only one! 58 | 59 | Feature suggestions (enhancements) are also tracked as issues in our backlog. To suggest a feature or enhnacement, [create an issue](https://github.com/codeforboston/communityconnect/issues/new?labels=enhancement&title=New+feature) including: 60 | 61 | - [ ] The `[enhancement]` label 62 | - [ ] A clear and descriptive title of the new behavior or functionality 63 | - [ ] A description of the feature 64 | - [ ] An example depicting this feature 'in the wild' or a simple use case for this feature 65 | - [ ] Why this feature could be useful for you and other users 66 | -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | ## Expected Behavior 11 | 12 | 13 | 14 | 15 | ## Current Behavior 16 | 17 | 18 | 19 | 20 | ## Possible Solution 21 | 22 | 23 | 24 | 25 | 26 | 27 | ## Context 28 | 29 | 30 | 31 | 32 | 33 | 34 | ## Steps to Reproduce 35 | 36 | 37 | 38 | 39 | 1. 2. 3. 4. 40 | 41 | ## Your Environment 42 | 43 | 44 | 45 | - Browser name and version: 46 | - Operating system and version (desktop or mobile): 47 | -------------------------------------------------------------------------------- /docs/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 15 | 16 | ## Description 17 | 18 | 19 | 20 | 21 | 22 | Fixes # 23 | 24 | 25 | 26 | 27 | ## Motivation and Context 28 | 29 | 30 | 31 | ## Type of change 32 | 33 | 34 | 35 | - [ ] Bug fix (non-breaking change which fixes an issue) 36 | - [ ] New feature (non-breaking change which adds functionality) 37 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 38 | - [ ] Documentation change (updates to README and related files) 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "community-connect", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test --env=jsdom", 9 | "predeploy": "npm run build", 10 | "deploy": "gh-pages -d build", 11 | "eject": "react-scripts eject", 12 | "lint": "eslint \"src/**/*.{js,jsx}\"", 13 | "analyze": "source-map-explorer 'build/static/js/*.js'" 14 | }, 15 | "dependencies": { 16 | "@fortawesome/fontawesome-svg-core": "1.2.21", 17 | "@fortawesome/free-brands-svg-icons": "5.10.1", 18 | "@fortawesome/free-solid-svg-icons": "5.10.1", 19 | "@fortawesome/react-fontawesome": "0.1.4", 20 | "bootstrap": "4.3.1", 21 | "classnames": "2.2.6", 22 | "is-url": "1.2.4", 23 | "jquery": "3.4.1", 24 | "lodash": "4.17.15", 25 | "node-sass": "4.12.0", 26 | "popper.js": "1.15.0", 27 | "prop-types": "15.7.2", 28 | "query-string": "6.8.2", 29 | "react": "16.9.0", 30 | "react-beautiful-dnd": "11.0.5", 31 | "react-collapsible": "2.6.0", 32 | "react-dom": "16.9.0", 33 | "react-fontawesome": "1.6.1", 34 | "react-google-maps": "9.4.5", 35 | "react-read-more-less": "0.1.6", 36 | "react-redux": "7.1.0", 37 | "react-router": "5.0.1", 38 | "react-router-dom": "5.0.1", 39 | "react-testing-library": "8.0.1", 40 | "reactstrap": "8.0.1", 41 | "recompose": "0.30.0", 42 | "redux": "4.0.4", 43 | "redux-immutable-state-invariant": "2.1.0", 44 | "redux-thunk": "2.3.0", 45 | "sass-loader": "7.2.0", 46 | "source-map-explorer": "2.0.1", 47 | "tabletop": "1.5.2" 48 | }, 49 | "devDependencies": { 50 | "@fortawesome/fontawesome-free": "5.10.1", 51 | "enzyme": "3.10.0", 52 | "enzyme-adapter-react-16": "1.14.0", 53 | "eslint-config-airbnb": "18.0.0", 54 | "eslint-config-prettier": "6.0.0", 55 | "eslint-plugin-import": "2.18.2", 56 | "eslint-plugin-jsx-a11y": "6.2.3", 57 | "eslint-plugin-prettier": "3.1.0", 58 | "eslint-plugin-react": "7.14.3", 59 | "husky": "3.0.3", 60 | "jest-emotion": "10.0.14", 61 | "lint-staged": "9.2.1", 62 | "prettier": "1.18.2", 63 | "react-scripts": "3.1.0" 64 | }, 65 | "husky": { 66 | "hooks": { 67 | "pre-commit": "lint-staged" 68 | } 69 | }, 70 | "lint-staged": { 71 | "*.{js,jsx}": [ 72 | "eslint --fix", 73 | "git add" 74 | ] 75 | }, 76 | "prettier": { 77 | "trailingComma": "es5" 78 | }, 79 | "browserslist": { 80 | "development": [ 81 | "last 2 chrome versions", 82 | "last 2 firefox versions", 83 | "last 2 edge versions" 84 | ], 85 | "production": [ 86 | ">1%", 87 | "last 4 versions", 88 | "Firefox ESR", 89 | "not ie < 11" 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforboston/communityconnect/896cbacd912b0acf331f56fbda6cc29a1ca09e64/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Community Connect 23 | 24 | 28 | 29 | 30 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | formatter: stylish 3 | files: 4 | include: '**/*.s+(a|c)ss' 5 | rules: 6 | # Extends 7 | extends-before-mixins: 1 8 | extends-before-declarations: 1 9 | placeholder-in-extend: 1 10 | 11 | # Mixins 12 | mixins-before-declarations: 1 13 | 14 | # Line Spacing 15 | one-declaration-per-line: 1 16 | empty-line-between-blocks: 1 17 | single-line-per-selector: 1 18 | 19 | # Disallows 20 | no-attribute-selectors: 0 21 | no-color-hex: 0 22 | no-color-keywords: 1 23 | no-color-literals: 1 24 | no-combinators: 0 25 | no-css-comments: 1 26 | no-debug: 1 27 | no-disallowed-properties: 0 28 | no-duplicate-properties: 1 29 | no-empty-rulesets: 1 30 | no-extends: 0 31 | no-ids: 1 32 | no-important: 1 33 | no-invalid-hex: 1 34 | no-mergeable-selectors: 1 35 | no-misspelled-properties: 1 36 | no-qualifying-elements: 1 37 | no-trailing-whitespace: 1 38 | no-trailing-zero: 1 39 | no-transition-all: 1 40 | no-universal-selectors: 0 41 | no-url-domains: 1 42 | no-url-protocols: 1 43 | no-vendor-prefixes: 1 44 | no-warn: 1 45 | property-units: 0 46 | 47 | # Nesting 48 | declarations-before-nesting: 1 49 | force-attribute-nesting: 1 50 | force-element-nesting: 1 51 | force-pseudo-nesting: 1 52 | 53 | # Name Formats 54 | class-name-format: 1 55 | function-name-format: 1 56 | id-name-format: 0 57 | mixin-name-format: 1 58 | placeholder-name-format: 1 59 | variable-name-format: 1 60 | 61 | # Style Guide 62 | attribute-quotes: 1 63 | bem-depth: 0 64 | border-zero: 1 65 | brace-style: 1 66 | clean-import-paths: 1 67 | empty-args: 1 68 | hex-length: 1 69 | hex-notation: 1 70 | indentation: 1 71 | leading-zero: 1 72 | max-line-length: 0 73 | max-file-line-count: 0 74 | nesting-depth: 1 75 | property-sort-order: 1 76 | pseudo-element: 1 77 | quotes: 1 78 | shorthand-values: 1 79 | url-quotes: 1 80 | variable-for-property: 1 81 | zero-unit: 1 82 | 83 | # Inner Spacing 84 | space-after-comma: 1 85 | space-before-colon: 1 86 | space-after-colon: 1 87 | space-before-brace: 1 88 | space-before-bang: 1 89 | space-after-bang: 1 90 | space-between-parens: 1 91 | space-around-operator: 1 92 | 93 | # Final Items 94 | trailing-semicolon: 1 95 | final-newline: 1 96 | -------------------------------------------------------------------------------- /share-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/App/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { createStore } from "redux"; 4 | import { MemoryRouter } from "react-router"; 5 | import Enzyme, { mount } from "enzyme"; 6 | import Adapter from "enzyme-adapter-react-16"; 7 | import AppContainer from "./AppContainer"; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | const store = createStore( 12 | () => ({ isFetchingResource: false, savedResource: [], resource: [] }), 13 | ["Use Redux"] 14 | ); 15 | 16 | test("renders without crashing", () => { 17 | mount( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /src/App/AppContainer.js: -------------------------------------------------------------------------------- 1 | // import React/Redux dependencies 2 | import React, { Component } from "react"; 3 | import { Route } from "react-router"; 4 | import { connect } from "react-redux"; 5 | import PropTypes from "prop-types"; 6 | import { loadResources } from "../action/resourceDataAction"; 7 | import getAllSites from "../api/directoryGoogleSheets"; 8 | 9 | // import components 10 | 11 | import Header from "../components/Header/Header"; 12 | import MapPage from "../components/MapPage/MapPage"; 13 | import AdminPage from "../components/AdminPage/AdminPage"; 14 | import SplitScreenTogglePane from "../components/AdminPage/SplitScreenTogglePane"; 15 | import SavedResourcePanel from "../components/SavedResources/SavedResourcePanel"; 16 | import NotFoundPage from "../components/NotFoundPage/NotFoundPage"; 17 | import Loading from "../components/Common/Loading"; 18 | import FeedbackContainer from "../components/Common/FeedbackContainer"; 19 | 20 | const envSheetId = process.env.REACT_APP_GOOGLE_SHEETS_ID; 21 | 22 | // unused code 23 | // const revereSheetId = '1QolGVE4wVWSKdiWeMaprQGVI6MsjuLZXM5XQ6mTtONA'; 24 | 25 | function sheetIdFromPath(directory, path) { 26 | return directory.find(x => x.path === path).sheetId; 27 | } 28 | 29 | class AppContainer extends Component { 30 | state = { 31 | position: {}, 32 | displayFeedbackLink: false, 33 | isValidPage: true, 34 | }; 35 | 36 | componentDidMount() { 37 | const hideFeedbackTs = localStorage.getItem("hideFeedback"); 38 | 39 | if (hideFeedbackTs === null || Date.now() > parseInt(hideFeedbackTs, 10)) { 40 | localStorage.removeItem("hideFeedback"); 41 | this.setState({ displayFeedbackLink: true }); 42 | } 43 | console.log(hideFeedbackTs, typeof hideFeedbackTs); 44 | const resourcePath = this.props.match.params.resource; 45 | let resourceSheetId = null; 46 | 47 | getAllSites.then(sites => { 48 | resourceSheetId = sheetIdFromPath(sites, resourcePath) || envSheetId; 49 | 50 | if (resourceSheetId == null) { 51 | this.setState({ isValidPage: false }); 52 | } else { 53 | const resourcesFromSheet = loadResources(resourceSheetId); 54 | 55 | this.props.dispatch(resourcesFromSheet); 56 | } 57 | }); 58 | 59 | this.getLocation(); 60 | } 61 | 62 | hideFeedbackLink = () => { 63 | const weekMillis = 7 * 24 * 60 * 60 * 1000; 64 | localStorage.setItem("hideFeedback", Date.now() + weekMillis); 65 | this.setState({ displayFeedbackLink: false }); 66 | }; 67 | 68 | getLocation = () => { 69 | if (window.navigator.geolocation) { 70 | window.navigator.geolocation.getCurrentPosition( 71 | position => { 72 | this.setState({ 73 | position: { 74 | coordinates: { 75 | lat: parseFloat(position.coords.latitude), 76 | lng: parseFloat(position.coords.longitude), 77 | }, 78 | }, 79 | }); 80 | }, 81 | error => { 82 | console.log(error); 83 | } 84 | ); 85 | } 86 | }; 87 | 88 | toggleSavedResourcesPane = () => { 89 | this.setState(prevState => ({ 90 | isSavedResourcePaneOpen: !prevState.isSavedResourcePaneOpen, 91 | })); 92 | }; 93 | 94 | render() { 95 | if (!this.state.isValidPage) { 96 | return ; 97 | } 98 | 99 | if (this.props.isFetchingResource) { 100 | return ( 101 | 102 | ); 103 | } 104 | 105 | return ( 106 |
107 |
108 |
109 |
110 | 111 |
112 | } 116 | /> 117 | ( 121 | 125 | )} 126 | /> 127 | 128 | 131 | 132 |
133 | 134 | {this.state.displayFeedbackLink && ( 135 | 136 | )} 137 |
138 | ); 139 | } 140 | } 141 | 142 | AppContainer.propTypes = { 143 | dispatch: PropTypes.func.isRequired, 144 | match: PropTypes.object.isRequired, 145 | isFetchingResource: PropTypes.bool.isRequired, 146 | }; 147 | 148 | function mapStateToProps(state) { 149 | const { isFetchingResource } = state; 150 | 151 | return { isFetchingResource }; 152 | } 153 | export default connect(mapStateToProps)(AppContainer); 154 | -------------------------------------------------------------------------------- /src/action/actionType.js: -------------------------------------------------------------------------------- 1 | export const LOAD_RESOURCE_DATA_START = "LOAD_RESOURCE_DATA_START"; 2 | export const LOAD_RESOURCE_DATA_SUCCESS = "LOAD_RESOURCE_DATA_SUCCESS"; 3 | export const LOAD_RESOURCE_DATA_FAILURE = "LOAD_RESOURCE_DATA_FAILURE"; 4 | export const LOAD_CATEGORIES = "LOAD_CATEGORIES"; 5 | export const FILTER_RESOURCES_BY_CATEGORIES = "FILTER_RESOURCES_BY_CATEGORIES"; 6 | export const FILTER_RESOURCES_BY_SEARCH = "FILTER_RESOURCES_BY_SEARCH"; 7 | export const ADD_SAVED_RESOURCE = "ADD_SAVED_RESOURCE"; 8 | export const REMOVE_SAVED_RESOURCE = "REMOVE_SAVED_RESOURCE"; 9 | export const CLEAR_SAVED_RESOURCES = "CLEAR_SAVED_RESOURCES"; 10 | -------------------------------------------------------------------------------- /src/action/resourceDataAction.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionType"; 2 | import getAllResources from "../api/googlesheetApi"; 3 | 4 | const loadResourceDataStart = () => ({ 5 | type: types.LOAD_RESOURCE_DATA_START, 6 | isFetchingResource: true, 7 | }); 8 | 9 | const loadResourceDataSuccess = resources => ({ 10 | type: types.LOAD_RESOURCE_DATA_SUCCESS, 11 | resources, 12 | isFetchingResource: false, 13 | }); 14 | 15 | const loadResourceDataFailure = error => ({ 16 | type: types.LOAD_RESOURCE_DATA_FAILURE, 17 | error, 18 | }); 19 | 20 | export function loadCategories() { 21 | return { type: types.LOAD_CATEGORIES }; 22 | } 23 | 24 | export function loadResources(resourcePath) { 25 | return dispatch => { 26 | dispatch(loadResourceDataStart()); 27 | 28 | return getAllResources(resourcePath) 29 | .then(resources => { 30 | // update with call using specific 31 | dispatch(loadResourceDataSuccess(resources)); 32 | }) 33 | .catch(error => { 34 | dispatch(loadResourceDataFailure(error)); 35 | }); 36 | }; 37 | } 38 | 39 | export function filterByCategories(filteredResource) { 40 | return { type: types.FILTER_RESOURCES_BY_CATEGORIES, filteredResource }; 41 | } 42 | 43 | export function filterBySearch(searchedResource) { 44 | return { type: types.FILTER_RESOURCES_BY_SEARCH, searchedResource }; 45 | } 46 | 47 | export function addSavedResource(savedResource) { 48 | return { type: types.ADD_SAVED_RESOURCE, savedResource }; 49 | } 50 | 51 | export function removeSavedResource(savedResourceIndex) { 52 | return { type: types.REMOVE_SAVED_RESOURCE, savedResourceIndex }; 53 | } 54 | 55 | export function clearSavedResources() { 56 | return { type: types.CLEAR_SAVED_RESOURCES }; 57 | } 58 | -------------------------------------------------------------------------------- /src/api/directoryGoogleSheets.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import Tabletop from "tabletop"; 3 | 4 | const directoryKey = "1X3FsZ_sOjyROQER3-ywqObQW8sjV5kHNhdRdTR8DTc4"; 5 | 6 | function normalizeHeaders(element) { 7 | element.path = element.whatdoyouwantyourpathtobe; 8 | element.title = element.whatisyourorganizationname; 9 | element.sheetId = element.whatisyourgooglesheetid; 10 | } 11 | 12 | const getAllSites = new Promise(resolve => { 13 | Tabletop.init({ 14 | key: directoryKey, 15 | simpleSheet: false, 16 | prettyColumnNames: false, 17 | postProcess: normalizeHeaders, 18 | callback: (data, tabletop) => { 19 | const directory = tabletop.sheets("data").elements; 20 | resolve(directory); 21 | }, 22 | }); 23 | }); 24 | 25 | export default getAllSites; 26 | -------------------------------------------------------------------------------- /src/api/googlesheetApi.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import Tabletop from "tabletop"; 3 | 4 | function normalizeHeaders(element) { 5 | if (element.serviceprovided) { 6 | element.tags = String(element.serviceprovided).split(", "); 7 | } 8 | element.twitterUrl = element.twitterurl; 9 | element.facebookUrl = element.facebookurl; 10 | element.instagramUrl = element.instagramurl; 11 | element.hashCoordinates = element.latitude + element.longitude; 12 | if (element.latitude && element.longitude) { 13 | element.coordinates = { 14 | lat: parseFloat(element.latitude), 15 | lng: parseFloat(element.longitude), 16 | }; 17 | } 18 | 19 | if (element.categoryautosortscript) { 20 | element.categories = element.categoryautosortscript; 21 | } 22 | if (element.city || element.address || element.state || element.zipcode) { 23 | element.location = element.combinedaddress; 24 | } 25 | } 26 | 27 | const getAllResources = resourceSheetId => 28 | new Promise(resolve => { 29 | Tabletop.init({ 30 | key: resourceSheetId, 31 | simpleSheet: false, 32 | prettyColumnNames: false, 33 | postProcess: normalizeHeaders, 34 | callback: (data, tabletop) => { 35 | const resource = tabletop.sheets("Data").elements; 36 | 37 | const filteredResource = resource.filter( 38 | resourceData => resourceData.truefalsevetting === "TRUE" 39 | ); 40 | resolve(filteredResource); 41 | }, 42 | }); 43 | }); 44 | export default getAllResources; 45 | -------------------------------------------------------------------------------- /src/components/AdminPage/AdminPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import CategoryList from "./CategoryList"; 4 | import CardGrid from "./CardGrid"; 5 | 6 | const AdminPage = ({ currentPosition }) => ( 7 |
8 | 9 | 10 |
11 | ); 12 | 13 | AdminPage.propTypes = { 14 | currentPosition: PropTypes.object.isRequired, 15 | }; 16 | 17 | export default AdminPage; 18 | -------------------------------------------------------------------------------- /src/components/AdminPage/CardGrid.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import OrganizationCard from "../Common/OrganizationCard"; 5 | import SortBar from "../Common/SortBar"; 6 | import SearchBar from "../Header/SearchBar"; 7 | import getDistance from "../../utils/distance"; 8 | 9 | class CardGrid extends Component { 10 | state = { 11 | sortFunction: this.getCloserName, 12 | }; 13 | 14 | getCloserResource = (a, b) => { 15 | if ( 16 | getDistance(a, this.props.currentPos) > 17 | getDistance(b, this.props.currentPos) 18 | ) { 19 | return 1; 20 | } 21 | 22 | return -1; 23 | }; 24 | 25 | getCloserName = (a, b) => { 26 | if (a.name > b.name) return 1; 27 | if (a.name < b.name) return -1; 28 | 29 | return 0; 30 | }; 31 | 32 | sortData = () => this.props.resources.slice().sort(this.state.sortFunction); 33 | 34 | handleSortChange = newSort => { 35 | if (this.state.sortFunction !== newSort) { 36 | this.setState({ 37 | sortFunction: newSort, 38 | }); 39 | } 40 | }; 41 | 42 | render() { 43 | const sortOptions = [ 44 | { key: "A-Z", sort: this.getCloserName, disabled: false }, 45 | { 46 | key: "Distance", 47 | sort: this.getCloserResource, 48 | disabled: !this.props.currentPos, 49 | }, 50 | ]; 51 | 52 | // Render will be called every time this.props.data is updated, and every time handleSortChange 53 | // updates the this.state.dataSort variable. 54 | // this.state.dataSort() sorts data to feed into the OrganizationCards without modifying the 55 | // source of data 56 | const sortedData = this.sortData(); 57 | 58 | return ( 59 |
60 |
61 | 62 | 66 |
67 |
68 | {sortedData.map(resource => ( 69 | this.props.saveItem(resource)} 75 | saveable 76 | /> 77 | ))} 78 |
79 |
80 | ); 81 | } 82 | } 83 | 84 | CardGrid.propTypes = { 85 | currentPos: PropTypes.object.isRequired, 86 | resources: PropTypes.array.isRequired, 87 | handleFilter: PropTypes.func, 88 | saveItem: PropTypes.func, 89 | }; 90 | 91 | CardGrid.defaultProps = { 92 | handleFilter: null, 93 | saveItem: null, 94 | }; 95 | 96 | function mapStateToProps(state) { 97 | const filteredResourcesSet = new Set(state.filteredResources.map(x => x.id)); 98 | 99 | const resources = state.searchedResources.filter(x => 100 | filteredResourcesSet.has(x.id) 101 | ); 102 | 103 | return { resources }; 104 | } 105 | 106 | export default connect(mapStateToProps)(CardGrid); 107 | -------------------------------------------------------------------------------- /src/components/AdminPage/CategoryList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { bindActionCreators } from "redux"; 5 | import { ListGroup, ListGroupItem, Button } from "reactstrap"; 6 | import Collapsible from "react-collapsible"; 7 | import _ from "lodash"; 8 | import * as resourceAction from "../../action/resourceDataAction"; 9 | 10 | class CategoryList extends Component { 11 | state = { 12 | selectedCategory: [], 13 | }; 14 | 15 | componentDidUpdate() { 16 | const { selectedCategory } = this.state; 17 | const { resources } = this.props; 18 | 19 | if (selectedCategory.length === 0) { 20 | return this.props.actions.filterByCategories(resources); 21 | } 22 | 23 | const filteredResources = resources.filter(resource => 24 | selectedCategory.some(cat => resource.categories === cat) 25 | ); 26 | 27 | return this.props.actions.filterByCategories(filteredResources); 28 | } 29 | 30 | handleClick = async event => { 31 | event.persist(); 32 | const isContains = event.target.classList.contains( 33 | "list-group-item-success" 34 | ); 35 | 36 | const selectedCategoryLength = this.state.selectedCategory.length; 37 | 38 | if (isContains && selectedCategoryLength === 1) { 39 | this.setState({ 40 | selectedCategory: [], 41 | }); 42 | } else if (isContains) { 43 | this.setState(prevState => { 44 | const selectedCategoryCopy = prevState.selectedCategory.slice(); 45 | _.remove(selectedCategoryCopy, cat => cat === event.target.innerHTML); 46 | 47 | return { 48 | selectedCategory: selectedCategoryCopy, 49 | }; 50 | }); 51 | } else { 52 | this.setState(prevState => ({ 53 | selectedCategory: [ 54 | ...prevState.selectedCategory, 55 | event.target.innerHTML, 56 | ], 57 | })); 58 | } 59 | }; 60 | 61 | clearChecks = () => { 62 | this.setState({ 63 | selectedCategory: [], 64 | }); 65 | }; 66 | 67 | render() { 68 | const { selectedCategory } = this.state; 69 | const { categories } = this.props; 70 | 71 | const categoryMenuItems = [...categories].sort().map((curr, index) => ( 72 | 78 | {curr} 79 | 80 | )); 81 | 82 | return ( 83 |
84 | 90 |
91 | 92 | {categoryMenuItems} 93 | 94 |
95 | 102 |
103 |
104 | ); 105 | } 106 | } 107 | 108 | CategoryList.propTypes = { 109 | resources: PropTypes.array.isRequired, 110 | actions: PropTypes.object.isRequired, 111 | filterByCategories: PropTypes.func, 112 | categories: PropTypes.array.isRequired, 113 | }; 114 | 115 | CategoryList.defaultProps = { 116 | filterByCategories: undefined, 117 | }; 118 | 119 | function mapStateToProps(state) { 120 | return { 121 | categories: state.categories, 122 | resources: state.resources, 123 | }; 124 | } 125 | 126 | function mapDispatchToProps(dispatch) { 127 | return { 128 | actions: bindActionCreators(resourceAction, dispatch), 129 | }; 130 | } 131 | 132 | export default connect( 133 | mapStateToProps, 134 | mapDispatchToProps 135 | )(CategoryList); 136 | -------------------------------------------------------------------------------- /src/components/AdminPage/SplitScreenTogglePane.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import cx from "classnames"; 4 | 5 | const SplitScreenTogglePane = ({ isOpen, children }) => { 6 | const splitScreenTogglePaneClassName = cx("split-screen", { 7 | hidden: !isOpen, 8 | }); 9 | 10 | return
{children}
; 11 | }; 12 | 13 | SplitScreenTogglePane.propTypes = { 14 | isOpen: PropTypes.bool, 15 | children: PropTypes.object.isRequired, 16 | }; 17 | 18 | SplitScreenTogglePane.defaultProps = { 19 | isOpen: false, 20 | }; 21 | export default SplitScreenTogglePane; 22 | -------------------------------------------------------------------------------- /src/components/Common/FeedbackContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Badge } from "reactstrap"; 4 | 5 | const FeedbackContainer = ({ hideFeedbackLink }) => ( 6 |
7 | Want to improve Community Connect? 8 |
9 | 15 | Submit feedback 16 | 17 | 18 | Do it later 19 | 20 |
21 |
22 | ); 23 | 24 | FeedbackContainer.propTypes = { 25 | hideFeedbackLink: PropTypes.func.isRequired, 26 | }; 27 | 28 | export default FeedbackContainer; 29 | -------------------------------------------------------------------------------- /src/components/Common/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Header from "../Header/Header"; 4 | 5 | // import image 6 | import Logo from "../../images/cc-logo-home.png"; 7 | 8 | const Loading = ({ toggleSavedResourcesPane }) => ( 9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | ); 18 | 19 | Loading.propTypes = { 20 | toggleSavedResourcesPane: PropTypes.func.isRequired, 21 | }; 22 | 23 | export default Loading; 24 | -------------------------------------------------------------------------------- /src/components/Common/OrganizationCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { bindActionCreators, compose } from "redux"; 5 | import { withRouter } from "react-router"; 6 | import isUrl from "is-url"; 7 | import { getQueryResources, encodeResources } from "../../utils/resourcesQuery"; 8 | import getDistance from "../../utils/distance"; 9 | import * as resourceAction from "../../action/resourceDataAction"; 10 | 11 | import { 12 | OrganizationCardBody, 13 | OrganizationCardOverview, 14 | OrganizationCardSocialMedia, 15 | OrganizationCardSaveButton, 16 | } from "./subcomponents"; 17 | 18 | class OrganizationCard extends Component { 19 | state = {}; 20 | 21 | static getDerivedStateFromProps(props) { 22 | if (!props.savedResources.some(r => r.id === props.organization.id)) { 23 | return { saveExist: false }; 24 | } 25 | return { saveExist: true }; 26 | } 27 | 28 | saveItem = () => { 29 | this.props.actions.addSavedResource(this.props.organization); 30 | 31 | return { saveExist: true }; 32 | } 33 | 34 | saveItem = () => { 35 | this.props.actions.addSavedResource(this.props.organization); 36 | 37 | const resources = getQueryResources(); 38 | const indexOfResource = resources.indexOf(this.props.organization.id); 39 | 40 | if (indexOfResource < 0) { 41 | resources.push(this.props.organization.id); 42 | } 43 | 44 | this.props.history.push({ 45 | pathname: window.location.pathname, 46 | search: encodeResources(resources), 47 | }); 48 | }; 49 | 50 | removeItem = () => { 51 | const resources = getQueryResources(); 52 | const indexOfResource = resources.indexOf(this.props.organization.id); 53 | 54 | if ( 55 | this.props.savedResources.some( 56 | resource => resource.id === this.props.organization.id 57 | ) 58 | ) { 59 | this.props.actions.removeSavedResource(this.props.organization.id); 60 | resources.splice(indexOfResource, 1); 61 | } 62 | 63 | this.props.history.push({ 64 | pathname: window.location.pathname, 65 | search: encodeResources(resources), 66 | }); 67 | }; 68 | 69 | toggleItem = () => { 70 | // if saved, remove. otherwise, save 71 | if (this.state.saveExist) { 72 | this.removeItem(); 73 | } else { 74 | this.saveItem(); 75 | } 76 | }; 77 | 78 | render() { 79 | const { 80 | name, 81 | categories, 82 | overview, 83 | location, 84 | website, 85 | facebookUrl, 86 | instagramUrl, 87 | twitterUrl, 88 | phone, 89 | latitude, 90 | longitude, 91 | } = this.props.organization; 92 | 93 | const websiteUrl = isUrl(website) ? website : ""; 94 | let distance; 95 | 96 | if (this.props.currentPos && this.props.organization.coordinates) { 97 | distance = getDistance( 98 | { coordinates: this.props.organization.coordinates }, 99 | this.props.currentPos 100 | ); 101 | } 102 | 103 | const encodedCoordinates = encodeURIComponent(`${latitude},${longitude}`); 104 | const directionUrl = `https://www.google.com/maps?saddr=My+Location&daddr=${encodedCoordinates}`; 105 | 106 | return ( 107 |
108 |
109 |
{name}
110 | {this.props.saveable ? ( 111 | 115 | ) : null} 116 |
117 | 126 | 127 | 132 | 137 | 142 |
143 | ); 144 | } 145 | } 146 | 147 | OrganizationCard.propTypes = { 148 | organization: PropTypes.object.isRequired, 149 | actions: PropTypes.object.isRequired, 150 | history: PropTypes.object.isRequired, 151 | savedResources: PropTypes.array.isRequired, 152 | currentPos: PropTypes.object.isRequired, 153 | saveable: PropTypes.bool, 154 | index: PropTypes.string.isRequired, 155 | }; 156 | 157 | OrganizationCard.defaultProps = { 158 | saveable: null, 159 | }; 160 | 161 | function mapStateToProps(state) { 162 | return { 163 | savedResources: state.savedResources, 164 | }; 165 | } 166 | 167 | function mapDispatchToProps(dispatch) { 168 | return { 169 | actions: bindActionCreators(resourceAction, dispatch), 170 | }; 171 | } 172 | 173 | export default compose( 174 | connect( 175 | mapStateToProps, 176 | mapDispatchToProps 177 | ), 178 | withRouter 179 | )(OrganizationCard); 180 | -------------------------------------------------------------------------------- /src/components/Common/SortBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class SortBar extends React.Component { 5 | handleClick = e => { 6 | // Get new sort based on index of sortOption array 7 | if (this.props.sortOptions[e.target.value]) { 8 | const newSort = this.props.sortOptions[e.target.value].sort; 9 | this.props.onSortChange(newSort); 10 | } 11 | }; 12 | 13 | render() { 14 | return ( 15 | 16 | Sort by: 17 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | SortBar.propTypes = { 34 | sortOptions: PropTypes.array.isRequired, 35 | onSortChange: PropTypes.func.isRequired, 36 | }; 37 | 38 | export default SortBar; 39 | -------------------------------------------------------------------------------- /src/components/Common/subcomponents/OrganizationCardBody.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | 5 | const OrganizationCardBody = ({ 6 | categories, 7 | distance, 8 | location, 9 | directionUrl, 10 | phone, 11 | url, 12 | }) => ( 13 |
14 |
{categories}
15 | {distance &&

Distance from your location:  {distance} miles

} 16 | {location && ( 17 | 18 | {" "} 19 | {location} 20 | 21 | )} 22 | {directionUrl && ( 23 | 24 | Get directions 25 | 26 | )} 27 | {url && ( 28 | 29 | Go to website 30 | 31 | )} 32 | {phone && ( 33 | 39 | {phone} 40 | 41 | )} 42 |
43 | ); 44 | 45 | OrganizationCardBody.propTypes = { 46 | categories: PropTypes.string, 47 | distance: PropTypes.number, 48 | location: PropTypes.string, 49 | directionUrl: PropTypes.string, 50 | url: PropTypes.string, 51 | phone: PropTypes.string, 52 | }; 53 | 54 | OrganizationCardBody.defaultProps = { 55 | categories: null, 56 | distance: null, 57 | location: null, 58 | directionUrl: null, 59 | url: null, 60 | phone: null, 61 | }; 62 | export default OrganizationCardBody; 63 | -------------------------------------------------------------------------------- /src/components/Common/subcomponents/OrganizationCardOverview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ReadMoreAndLess from "react-read-more-less"; 4 | 5 | const OrganizationCardOverview = ({ overview }) => 6 | overview ? ( 7 |
8 | 14 | {overview} 15 | 16 |
17 | ) : null; 18 | 19 | OrganizationCardOverview.propTypes = { 20 | overview: PropTypes.string, 21 | }; 22 | 23 | OrganizationCardOverview.defaultProps = { 24 | overview: null, 25 | }; 26 | export default OrganizationCardOverview; 27 | -------------------------------------------------------------------------------- /src/components/Common/subcomponents/OrganizationCardSaveButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Button } from "reactstrap"; 4 | import { faPlus, faMinus } from "@fortawesome/free-solid-svg-icons"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import cx from "classnames"; 7 | 8 | const OrganizationCardSaveButton = ({ saveExist, onClick }) => { 9 | const buttonIcon = saveExist ? faMinus : faPlus; 10 | 11 | const buttonClassName = cx("organization-card-button", { 12 | plus: !saveExist, 13 | minus: saveExist, 14 | }); 15 | 16 | return ( 17 | 18 | 27 | 28 | ); 29 | }; 30 | 31 | OrganizationCardSaveButton.propTypes = { 32 | saveExist: PropTypes.bool.isRequired, 33 | onClick: PropTypes.func.isRequired, 34 | }; 35 | export default OrganizationCardSaveButton; 36 | -------------------------------------------------------------------------------- /src/components/Common/subcomponents/OrganizationCardSocialMedia.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | 5 | const OrganizationCardSocialMedia = ({ url, icon, title }) => 6 | url ? ( 7 | 14 | 15 | 16 | ) : null; 17 | 18 | OrganizationCardSocialMedia.propTypes = { 19 | url: PropTypes.string, 20 | icon: PropTypes.string, 21 | title: PropTypes.string, 22 | }; 23 | 24 | OrganizationCardSocialMedia.defaultProps = { 25 | url: null, 26 | icon: null, 27 | title: null, 28 | }; 29 | export default OrganizationCardSocialMedia; 30 | -------------------------------------------------------------------------------- /src/components/Common/subcomponents/index.js: -------------------------------------------------------------------------------- 1 | export { default as OrganizationCardBody } from "./OrganizationCardBody"; 2 | export { 3 | default as OrganizationCardOverview, 4 | } from "./OrganizationCardOverview"; 5 | export { 6 | default as OrganizationCardSocialMedia, 7 | } from "./OrganizationCardSocialMedia"; 8 | export { 9 | default as OrganizationCardSaveButton, 10 | } from "./OrganizationCardSaveButton"; 11 | -------------------------------------------------------------------------------- /src/components/Header/DropdownCategory.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Dropdown, 5 | DropdownToggle, 6 | DropdownMenu, 7 | DropdownItem, 8 | } from "reactstrap"; 9 | // import { DropdownCategory } from './HeaderLayout'; 10 | 11 | class DropdownCategory extends Component { 12 | state = { 13 | dropdownOpen: false, 14 | activeItem: [], 15 | }; 16 | 17 | toggle = () => { 18 | this.setState(prevState => ({ dropdownOpen: !prevState.dropdownOpen })); 19 | }; 20 | 21 | handleClick = (cat, index) => { 22 | this.props.handleEvent(cat, "categories"); 23 | if (index === -1) 24 | this.setState({ 25 | activeItem: [], 26 | }); 27 | const includesIndex = this.state.activeItem.includes(index); 28 | 29 | if (includesIndex) { 30 | return this.setState(prevState => ({ 31 | activeItem: prevState.activeItem.filter(selected => selected !== index), 32 | })); 33 | } 34 | 35 | return this.state.activeItem.push(index); 36 | }; 37 | 38 | categoryMenuItems() { 39 | return this.props.categories.map((cat, index) => ( 40 | this.handleClick(cat, index)} 43 | key={cat} 44 | > 45 | {this.state.activeItem.includes(index) ? ( 46 | ✔ {cat} 47 | ) : ( 48 | cat 49 | )} 50 | 51 | )); 52 | } 53 | 54 | render () { 55 | return ( 56 |
57 | 62 | 63 | Category 64 | 65 | 66 | this.handleClick("Clear", -1)} 68 | key="Clear" 69 | > 70 | Clear 71 | 72 | 73 | {this.categoryMenuItems()} 74 | 75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | DropdownCategory.propTypes = { 82 | categories: PropTypes.array.isRequired, 83 | handleEvent: PropTypes.func.isRequired, 84 | }; 85 | 86 | export default DropdownCategory; 87 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Route, withRouter } from "react-router"; 4 | import { Link } from "react-router-dom"; 5 | import { connect } from "react-redux"; 6 | import { bindActionCreators, compose } from "redux"; 7 | import cx from "classnames"; 8 | 9 | import { faPrint } from "@fortawesome/free-solid-svg-icons"; 10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 11 | 12 | import { 13 | Navbar, 14 | NavbarBrand, 15 | Button, 16 | Modal, 17 | ModalHeader, 18 | ModalBody, 19 | ModalFooter, 20 | } from "reactstrap"; 21 | import { getQueryResources, encodeResources } from "../../utils/resourcesQuery"; 22 | import * as resourceAction from "../../action/resourceDataAction"; 23 | 24 | class Header extends Component { 25 | state = { 26 | collapsed: true, 27 | modal: false, 28 | }; 29 | 30 | toggleNavbar = () => { 31 | this.setState(prevState => ({ collapsed: !prevState.collapsed })); 32 | }; 33 | 34 | modalOpen = () => { 35 | if (this.props.savedResources.length > 0) { 36 | this.modalToggle(); 37 | } else { 38 | window.location.reload(); 39 | } 40 | }; 41 | 42 | modalToggle = () => { 43 | this.setState(prevState => ({ modal: !prevState.modal })); 44 | }; 45 | 46 | confirmationModalToggle = () => { 47 | this.props.actions.clearSavedResources(); 48 | this.modalToggle(); 49 | }; 50 | 51 | toAdmin = () => { 52 | const resources = getQueryResources(); 53 | 54 | this.props.history.push({ 55 | pathname: `/${this.props.match.params.resource}/admin`, 56 | search: encodeResources(resources), 57 | }); 58 | }; 59 | 60 | render() { 61 | const { savedResources, toggleSavedResourcesPane } = this.props; 62 | 63 | const savedResourceButtonClassNames = cx("saved-resource-button", { 64 | "has-selections": savedResources.length, 65 | }); 66 | 67 | const printButtonClassNames = cx("print-button"); 68 | 69 | return ( 70 | <> 71 | 72 | 73 | Community Connect 74 | 75 | 76 |
77 | ( 81 | 92 | )} 93 | /> 94 | ( 98 | 108 | )} 109 | /> 110 | ( 114 | 124 | )} 125 | /> 126 | ( 129 | 136 | )} 137 | /> 138 |
139 |
140 | 141 | Alert 142 | 143 | This action will clear all your saved resources. Do you want to 144 | proceed? 145 | 146 | 147 | {" "} 150 | 158 | 159 | 160 | 161 | ); 162 | } 163 | } 164 | 165 | Header.propTypes = { 166 | savedResources: PropTypes.array.isRequired, 167 | actions: PropTypes.object.isRequired, 168 | toggleSavedResourcesPane: PropTypes.func.isRequired, 169 | match: PropTypes.object.isRequired, 170 | history: PropTypes.object.isRequired, 171 | location: PropTypes.object.isRequired, 172 | }; 173 | 174 | function mapStateToProps(state) { 175 | return { 176 | savedResources: state.savedResources, 177 | }; 178 | } 179 | 180 | function mapDispatchToProps(dispatch) { 181 | return { 182 | actions: bindActionCreators(resourceAction, dispatch), 183 | }; 184 | } 185 | export default compose( 186 | connect( 187 | mapStateToProps, 188 | mapDispatchToProps 189 | ), 190 | withRouter 191 | )(Header); 192 | -------------------------------------------------------------------------------- /src/components/Header/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { bindActionCreators } from "redux"; 5 | 6 | import * as resourceAction from "../../action/resourceDataAction"; 7 | 8 | class SearchBar extends Component { 9 | state = { 10 | searchString: "", 11 | }; 12 | 13 | handleFilter = e => { 14 | this.setState({ searchString: e.target.value }); 15 | const searchedResource = this.props.resources.filter(i => 16 | i.name.toLowerCase().match(e.target.value.toLowerCase()) 17 | ); 18 | 19 | this.props.actions.filterBySearch( 20 | e.target.value.length > 0 ? searchedResource : this.props.resources 21 | ); 22 | }; 23 | 24 | render () { 25 | return ( 26 | 33 | ); 34 | } 35 | } 36 | 37 | SearchBar.propTypes = { 38 | resources: PropTypes.array.isRequired, 39 | actions: PropTypes.object.isRequired, 40 | }; 41 | 42 | function mapStateToProps(state) { 43 | return { 44 | resources: 45 | state.filteredResources.length > 0 46 | ? state.filteredResources 47 | : state.resources, 48 | }; 49 | } 50 | 51 | function mapDispatchToProps(dispatch) { 52 | return { 53 | actions: bindActionCreators(resourceAction, dispatch), 54 | }; 55 | } 56 | 57 | export default connect( 58 | mapStateToProps, 59 | mapDispatchToProps 60 | )(SearchBar); 61 | -------------------------------------------------------------------------------- /src/components/MapPage/Map.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React from "react"; 3 | import { withScriptjs, withGoogleMap, GoogleMap } from "react-google-maps"; 4 | import { MarkerClusterer } from "react-google-maps/lib/components/addons/MarkerClusterer"; 5 | import { compose, lifecycle } from "recompose"; 6 | import OrganizationMarker from "./OrganizationMarker"; 7 | 8 | const Map = compose( 9 | lifecycle({ 10 | componentDidMount() { 11 | this.setState({ 12 | zoomToMarkers: map => { 13 | if (map) { 14 | const bounds = new window.google.maps.LatLngBounds(); 15 | 16 | map.props.children.props.children.forEach(child => { 17 | bounds.extend( 18 | new window.google.maps.LatLng( 19 | child.props.resource.coordinates.lat, 20 | child.props.resource.coordinates.lng 21 | ) 22 | ); 23 | }); 24 | map.fitBounds(bounds); 25 | } 26 | }, 27 | }); 28 | }, 29 | }), 30 | withScriptjs, 31 | withGoogleMap 32 | )(props => ( 33 | 34 | 41 | {props.resources 42 | .filter(resource => resource.coordinates) 43 | .map((resource, index) => ( 44 | 49 | ))} 50 | 51 | 52 | )); 53 | export default Map; 54 | -------------------------------------------------------------------------------- /src/components/MapPage/MapPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import cx from "classnames"; 4 | import ResultList from "./ResultList"; 5 | import OrganizationMap from "./OrganizationMap"; 6 | import SplitScreenSlidingPane from "./SplitScreenSlidingPane"; 7 | 8 | class MapPage extends Component { 9 | render() { 10 | const mapClassName = cx("map-container"); 11 | 12 | return ( 13 |
14 | 15 | 20 | 21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | MapPage.propTypes = { 30 | currentPosition: PropTypes.object.isRequired, 31 | }; 32 | 33 | export default MapPage; 34 | -------------------------------------------------------------------------------- /src/components/MapPage/OrganizationMap.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import Map from "./Map"; 5 | 6 | const googleMapKey = "AIzaSyAwKdrqS2GfCt9b2K1wAopDc9Ga0N1BVUM"; 7 | const googleMapURL = `https://maps.googleapis.com/maps/api/js?key=${googleMapKey}&v=3.exp&libraries=geometry,drawing,places`; 8 | 9 | const OrganizationMap = ({ mapResources }) => ( 10 | } 13 | mapElement={
} 14 | loadingElement={
} 15 | resources={mapResources} 16 | /> 17 | ); 18 | 19 | OrganizationMap.propTypes = { 20 | mapResources: PropTypes.array.isRequired, 21 | }; 22 | 23 | function mapStateToProps(state) { 24 | const currentResources = 25 | state.savedResources.length > 0 ? state.savedResources : state.resources; 26 | 27 | const locations = {}; 28 | 29 | currentResources.forEach(resource => { 30 | if (!locations[resource.hashCoordinates]) { 31 | locations[resource.hashCoordinates] = { 32 | coordinates: resource.coordinates, 33 | groupedResources: [], 34 | showInfo: false, 35 | }; 36 | } 37 | 38 | locations[resource.hashCoordinates].groupedResources.push(resource); 39 | }); 40 | 41 | const resources = Object.values(locations); 42 | 43 | return { 44 | mapResources: resources, 45 | }; 46 | } 47 | 48 | export default connect(mapStateToProps)(OrganizationMap); 49 | -------------------------------------------------------------------------------- /src/components/MapPage/OrganizationMarker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Marker, InfoWindow } from "react-google-maps"; 4 | 5 | class OrganizationMarker extends Component { 6 | state = { 7 | open: this.props.open, 8 | }; 9 | 10 | componentDidUpdate(prevProps) { 11 | if (prevProps.open !== this.props.open) { 12 | this.updateOpen(); 13 | } 14 | } 15 | 16 | // scrollToElement and handleClickOfInfoWindow is currently non-functional 17 | updateOpen = () => { 18 | this.setState({ open: this.props.open }); 19 | }; 20 | 21 | scrollToElement = () => { 22 | this.setState({ open: true }); 23 | }; 24 | 25 | handleClickOfInfoWindow = e => { 26 | const element = document.getElementById(e.currentTarget.id); 27 | element.scrollIntoView(); 28 | }; 29 | 30 | handleClose = () => { 31 | this.setState({ open: false }); 32 | }; 33 | 34 | render() { 35 | return ( 36 | 41 | {this.state.open && ( 42 | 43 |
44 | {this.props.resource.groupedResources.map(resource => ( 45 |
50 |

{resource.name}

51 |
{resource.combinedaddress}
52 |
{resource.tags}
53 |
54 | {resource.phone} 55 |
56 |
57 | ))} 58 |
59 |
60 | )} 61 |
62 | ); 63 | } 64 | } 65 | 66 | OrganizationMarker.propTypes = { 67 | open: PropTypes.bool.isRequired, 68 | resource: PropTypes.object.isRequired, 69 | }; 70 | 71 | export default OrganizationMarker; 72 | -------------------------------------------------------------------------------- /src/components/MapPage/ResultList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { bindActionCreators } from "redux"; 5 | import OrganizationCard from "../Common/OrganizationCard"; 6 | import SortBar from "../Common/SortBar"; 7 | import getDistance from "../../utils/distance"; 8 | import * as resourceAction from "../../action/resourceDataAction"; 9 | 10 | class ResultList extends Component { 11 | state = { 12 | sortFunction: this.getCloserName, 13 | }; 14 | 15 | getCloserResource = (a, b) => { 16 | if ( 17 | getDistance(a, this.props.currentPos) > 18 | getDistance(b, this.props.currentPos) 19 | ) { 20 | return 1; 21 | } 22 | 23 | return -1; 24 | }; 25 | 26 | getCloserName = (a, b) => { 27 | if (a.name > b.name) return 1; 28 | if (a.name < b.name) return -1; 29 | 30 | return 0; 31 | }; 32 | 33 | sortData = () => 34 | this.props.savedResources.slice().sort(this.state.sortFunction); 35 | 36 | handleSortChange = newSort => { 37 | if (this.state.sortFunction !== newSort) { 38 | this.setState({ 39 | sortFunction: newSort, 40 | }); 41 | } 42 | }; 43 | 44 | cardClick = id => { 45 | this.props.savedResources.findIndex(resource => resource.id === id); 46 | }; 47 | 48 | saveResource = resource => { 49 | if (!this.props.savedResources.some(r => r.id === resource.id)) { 50 | this.props.actions.addSavedResource(this.props.savedResources.slice()); 51 | } 52 | }; 53 | 54 | render () { 55 | const sortOptions = [ 56 | { key: "A-Z", sort: this.getCloserName, disabled: false }, 57 | { 58 | key: "Distance", 59 | sort: this.getCloserResource, 60 | disabled: !this.props.currentPos, 61 | }, 62 | ]; 63 | 64 | // Render will be called every time this.props.data is updated, and every time handleSortChange 65 | // updates the this.state.dataSort variable. 66 | // this.state.dataSort() sorts data to feed into the OrganizationCards without modifying the 67 | // source of data 68 | const sortedData = this.sortData(); 69 | 70 | return ( 71 |
72 | 76 |
77 | {sortedData.map(resource => ( 78 | this.props.saveItem(resource)} 85 | /> 86 | ))} 87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | ResultList.propTypes = { 94 | currentPos: PropTypes.object.isRequired, 95 | savedResources: PropTypes.array.isRequired, 96 | actions: PropTypes.object.isRequired, 97 | saveItem: PropTypes.func, 98 | }; 99 | 100 | ResultList.defaultProps = { 101 | saveItem: null, 102 | }; 103 | 104 | function mapStateToProps(state) { 105 | return { 106 | savedResources: 107 | state.savedResources.length > 0 ? state.savedResources : state.resources, 108 | }; 109 | } 110 | 111 | function mapDispatchToProps (dispatch) { 112 | return { 113 | actions: bindActionCreators(resourceAction, dispatch), 114 | }; 115 | } 116 | 117 | export default connect( 118 | mapStateToProps, 119 | mapDispatchToProps 120 | )(ResultList); 121 | -------------------------------------------------------------------------------- /src/components/MapPage/SplitScreenSlidingPane.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 3 | import React, { Component } from "react"; 4 | import PropTypes from "prop-types"; 5 | import cx from "classnames"; 6 | 7 | class SplitScreenSlidingPane extends Component { 8 | state = { 9 | isOpen: true, 10 | }; 11 | 12 | toggle = e => { 13 | e.preventDefault(); 14 | 15 | this.setState(prevState => ({ isOpen: !prevState.isOpen })); 16 | }; 17 | 18 | render() { 19 | const slidingPaneClassNames = cx("sliding-pane", { 20 | open: this.state.isOpen, 21 | }); 22 | 23 | return ( 24 |
25 |
26 | ☰ 27 |
28 | {this.props.children} 29 |
30 | ); 31 | } 32 | } 33 | 34 | SplitScreenSlidingPane.propTypes = { 35 | children: PropTypes.element.isRequired, 36 | }; 37 | 38 | export default SplitScreenSlidingPane; 39 | -------------------------------------------------------------------------------- /src/components/NotFoundPage/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Button } from "reactstrap"; 4 | 5 | const NotFoundPage = () => ( 6 |
7 |

Error 404

8 |

Page Not Found

9 |

10 | The resource you are trying to access could not be found. Navigate back to 11 | the Home Page and try again. 12 |

13 | {" "} 16 |
17 | ); 18 | 19 | export default NotFoundPage; 20 | -------------------------------------------------------------------------------- /src/components/PrintPage/PrintPageContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import PrintPanel from "./PrintPanel"; 5 | 6 | export const date = new Date().toLocaleDateString("en-US"); 7 | 8 | const PrintPage = ({ savedResources }) => ( 9 |
10 |
11 |

Resource List

12 |
Date printed: {date}
13 |
14 |
15 | {savedResources.map((resource, index) => ( 16 | 17 | ))} 18 |
19 |
20 | ); 21 | 22 | PrintPage.propTypes = { 23 | savedResources: PropTypes.array.isRequired, 24 | }; 25 | 26 | function mapStateToProps(state) { 27 | return { 28 | savedResources: 29 | state.savedResources.length > 0 ? state.savedResources : state.resources, 30 | }; 31 | } 32 | export default connect(mapStateToProps)(PrintPage); 33 | -------------------------------------------------------------------------------- /src/components/PrintPage/PrintPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const PrintPanel = ({ resource }) => ( 5 |
6 |
7 | {resource.name} 8 |
9 | {resource.location && ( 10 |
11 | Address: 12 | {resource.location} 13 |
14 | )} 15 | {resource.website && ( 16 |
17 | Website: 18 | {resource.website} 19 |
20 | )} 21 | {resource.phone && ( 22 |
23 | Phone: 24 | 25 | {resource.phone 26 | .replace("(", "") 27 | .replace(")", "") 28 | .replace(" ", "-")} 29 | 30 |
31 | )} 32 |
33 |
34 | ); 35 | 36 | PrintPanel.propTypes = { 37 | resource: PropTypes.object.isRequired, 38 | }; 39 | 40 | export default PrintPanel; 41 | -------------------------------------------------------------------------------- /src/components/SavedResources/SavedResource.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { bindActionCreators, compose } from "redux"; 5 | import { withRouter } from "react-router"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { 8 | Alert, 9 | Card, 10 | CardBody, 11 | CardSubtitle, 12 | ModalHeader, 13 | ModalBody, 14 | } from "reactstrap"; 15 | import { getQueryResources, encodeResources } from "../../utils/resourcesQuery"; 16 | import getDistance from "../../utils/distance"; 17 | import * as resourceAction from "../../action/resourceDataAction"; 18 | 19 | import SavedResourceButton from "./SavedResourceButton"; 20 | 21 | class SavedResource extends Component { 22 | state = { 23 | visible: false, 24 | }; 25 | 26 | confirmationAlertToggle = () => { 27 | this.setState(prevState => ({ visible: !prevState.visible })); 28 | }; 29 | 30 | removeItem = () => { 31 | this.confirmationAlertToggle(); 32 | }; 33 | 34 | removalConfirmed = () => { 35 | const resources = getQueryResources(); 36 | const indexOfResource = resources.indexOf(this.props.organization.id); 37 | 38 | if ( 39 | this.props.savedResources.some( 40 | resource => resource.id === this.props.organization.id 41 | ) 42 | ) { 43 | this.props.actions.removeSavedResource(this.props.organization.id); 44 | resources.splice(indexOfResource, 1); 45 | } 46 | 47 | this.props.history.push({ 48 | pathname: window.location.pathname, 49 | search: encodeResources(resources), 50 | }); 51 | this.removeItem(); 52 | }; 53 | 54 | render() { 55 | const { 56 | id, 57 | name, 58 | categories, 59 | overview, 60 | location, 61 | website, 62 | facebookUrl, 63 | instagramUrl, 64 | twitterUrl, 65 | phone, 66 | } = this.props.organization; 67 | 68 | let distance; 69 | let distanceElement; 70 | 71 | if (this.props.currentPos && this.props.currentPos.coordinates) { 72 | distance = getDistance( 73 | { coordinates: this.props.organization.coordinates }, 74 | this.props.currentPos 75 | ); 76 | 77 | if (distance) { 78 | distanceElement = ( 79 |

80 | Distance from your Location: 81 | {distance.toPrecision(4)} 82 | miles 83 |

84 | ); 85 | } 86 | } 87 | 88 | return ( 89 |
90 | 91 | 92 |
93 | {website ? ( 94 | 95 |

{name}

96 |
97 | ) : ( 98 |

{name}

99 | )} 100 | 101 |
102 | 103 | {categories} 104 | 105 | {distance &&
{distanceElement}
} 106 | {location && ( 107 |

108 | 109 | {location} 110 |

111 | )} 112 | {overview &&

{overview}

} 113 | {phone && ( 114 |

115 | {" "} 116 | 117 | {" "} 118 | 📞 119 | {" "} 120 | {phone} 121 |

122 | )} 123 | {(facebookUrl || instagramUrl || twitterUrl) && ( 124 |
    125 | {facebookUrl && ( 126 |
  • 127 | 128 | 134 | 135 |
  • 136 | )} 137 | {instagramUrl && ( 138 |
  • 139 | 140 | 146 | 147 |
  • 148 | )} 149 | {twitterUrl && ( 150 |
  • 151 | 152 | 158 | 159 |
  • 160 | )} 161 |
162 | )} 163 |
164 |
165 | 166 | Are you sure? 167 | {name} 168 | closed 169 | 170 | Would you like to remove 171 | {name} 172 | from your saved resources? 173 | {" "} 174 | 175 |
176 | ); 177 | } 178 | } 179 | 180 | SavedResource.propTypes = { 181 | organization: PropTypes.object.isRequired, 182 | savedResources: PropTypes.array.isRequired, 183 | actions: PropTypes.object.isRequired, 184 | history: PropTypes.object.isRequired, 185 | currentPos: PropTypes.object, 186 | }; 187 | 188 | SavedResource.defaultProps = { 189 | currentPos: null, 190 | }; 191 | 192 | function mapStateToProps(state) { 193 | return { savedResources: state.savedResources }; 194 | } 195 | 196 | function mapDispatchToProps(dispatch) { 197 | return { 198 | actions: bindActionCreators(resourceAction, dispatch), 199 | }; 200 | } 201 | 202 | export default compose( 203 | connect( 204 | mapStateToProps, 205 | mapDispatchToProps 206 | ), 207 | withRouter 208 | )(SavedResource); 209 | -------------------------------------------------------------------------------- /src/components/SavedResources/SavedResourceButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Button } from "reactstrap"; 4 | import { faMinus } from "@fortawesome/free-solid-svg-icons"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import cx from "classnames"; 7 | 8 | const SavedResourceButton = ({ onClick }) => { 9 | const buttonIcon = faMinus; 10 | 11 | const buttonClassName = cx("saved-resource-card-button", { 12 | faMinus, 13 | }); 14 | 15 | return ( 16 | 17 | 26 | 27 | ); 28 | }; 29 | 30 | SavedResourceButton.propTypes = { 31 | onClick: PropTypes.func.isRequired, 32 | }; 33 | 34 | export default SavedResourceButton; 35 | -------------------------------------------------------------------------------- /src/components/SavedResources/SavedResourcePanel.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Link, Route } from "react-router-dom"; 3 | import { Button } from "reactstrap"; 4 | import PropTypes from "prop-types"; 5 | import { faShare, faCopy, faCheck } from "@fortawesome/free-solid-svg-icons"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { getQueryResources, encodeResources } from "../../utils/resourcesQuery"; 8 | import SavedResources from "./SavedResourcesContainer"; 9 | 10 | const ToShareButton = ({ resourcePath }) => { 11 | const resources = getQueryResources(); 12 | const query = encodeResources(resources); 13 | const url = query && `/${resourcePath}/?${query}`; 14 | 15 | return ( 16 | 26 | ); 27 | }; 28 | 29 | ToShareButton.propTypes = { 30 | resourcePath: PropTypes.string.isRequired, 31 | }; 32 | 33 | const CopyButton = () => { 34 | // TODO: button should revert to original version (Copy icon) when resources change 35 | const [btnTitle, setBtnTitle] = useState("Copy Resource URL to Clipboard"); 36 | const [btnIcon, setBtnIcon] = useState(faCopy); 37 | const tmpUrl = window.location.href.toString().replace("/admin", "/"); 38 | 39 | return ( 40 | 53 | ); 54 | }; 55 | 56 | const SavedResourcePanel = () => ( 57 |
58 |
59 | Saved Resources 60 | 61 | ( 65 |
66 | 67 | 68 |
69 | )} 70 | /> 71 |
72 |
73 | 74 |
75 | ); 76 | 77 | export default SavedResourcePanel; 78 | -------------------------------------------------------------------------------- /src/components/SavedResources/SavedResourcesContainer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React, { Component } from "react"; 3 | import { connect } from "react-redux"; 4 | 5 | import PropTypes from "prop-types"; 6 | import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; 7 | // import styles from './SavedResourcesContainer.module.css'; 8 | import SavedResource from "./SavedResource"; 9 | 10 | const getItemStyle = (_, draggableStyle) => ({ 11 | // some basic styles to make the items look a bit nicer 12 | userSelect: "none", 13 | 14 | // change background colour if dragging 15 | 16 | // styles we need to apply on draggables 17 | ...draggableStyle, 18 | }); 19 | 20 | class SavedResourcesContainer extends Component { 21 | state = { 22 | data: Object.assign([], this.props.data), 23 | }; 24 | 25 | // Using deprecated function necessary to update data with store's data 26 | static getDerivedStateFromProps(props) { 27 | return { 28 | data: [...props.data], 29 | }; 30 | } 31 | 32 | onDragEnd = result => { 33 | // dropped outside the list 34 | if (!result.destination) { 35 | return; 36 | } 37 | 38 | this.orderResources(result.source.index, result.destination.index); 39 | }; 40 | 41 | orderResources = (sourceIndex, destinationIndex) => { 42 | const newSavedResources = this.props.data.slice(); 43 | const movedResource = newSavedResources[sourceIndex]; 44 | newSavedResources.splice(sourceIndex, 1); 45 | newSavedResources.splice(destinationIndex, 0, movedResource); 46 | 47 | this.setState({ 48 | data: newSavedResources, 49 | }); 50 | }; 51 | render() { 52 | // Render will be called every time this.props.data is updated, and every time handleSortChange 53 | // updates the this.state.dataSort variable. 54 | // this.state.dataSort() sorts data to feed into the OrganizationCards without modifying the 55 | // source of data 56 | 57 | const { data } = this.state; 58 | return ( 59 |
60 |
61 | 62 | 63 | {provided => ( 64 |
65 | {data.length ? ( 66 | data.map((item, index) => ( 67 | 72 | {(subprovided, snapshot) => ( 73 |
82 | this.props.removeItem(item)} 87 | /> 88 |
89 | )} 90 |
91 | )) 92 | ) : ( 93 | 94 | There are no resources added to the cart 95 | 96 | )} 97 | {provided.placeholder} 98 |
99 | )} 100 |
101 |
102 |
103 |
104 | ); 105 | } 106 | } 107 | 108 | SavedResourcesContainer.propTypes = { 109 | data: PropTypes.array.isRequired, 110 | currentPos: PropTypes.object, 111 | removeItem: PropTypes.func, 112 | }; 113 | 114 | SavedResourcesContainer.defaultProps = { 115 | currentPos: null, 116 | removeItem: null, 117 | }; 118 | 119 | function mapStateToProps(state) { 120 | return { 121 | data: state.savedResources, 122 | }; 123 | } 124 | 125 | export default connect(mapStateToProps)(SavedResourcesContainer); 126 | -------------------------------------------------------------------------------- /src/css/base-constants.scss: -------------------------------------------------------------------------------- 1 | // screen sizes 2 | $screen-size-sm: 576px; 3 | $screen-size-md: 768px; 4 | $screen-size-lg: 1024px; 5 | $screen-size-xlg: 1440px; 6 | $screen-size-xxlg: 2560px; 7 | 8 | // colors 9 | $primary: blue; 10 | $success: green; 11 | $danger: red; 12 | $black: black; 13 | $white: white; 14 | $gray: gray; 15 | $light-black: rgb(33, 33, 33); 16 | $blue: rgb(0, 86, 179); 17 | $super-light-gray: rgb(248, 249, 250); 18 | $light-gray: rgb(230, 230, 230); 19 | $dark-gray: rgb(108, 117, 125); 20 | $turquoise: rgb(0, 126, 163); 21 | $shadow-black: rgba(0, 0, 0, 0.2); 22 | $transparent-white: rgba(255, 255, 255, 0.9); 23 | 24 | // font sizes 25 | $font-size-sm: 12px; 26 | $font-size-md: 16px; 27 | $font-size-lg: 24px; 28 | $font-size-xlg: 32px; 29 | $font-size-xxlg: 64px; 30 | 31 | // radius sizes 32 | $radius-sm: 4px; 33 | $radius-md: 10px; 34 | $radius-lg: 20px; 35 | 36 | // spacing sizes: 37 | $spacing-sm: 5px; 38 | $spacing-md: 15px; 39 | $spacing-lg: 30px; 40 | $spacing-xlg: 60px; 41 | $spacing-xxlg: 120px; 42 | 43 | // element variables 44 | $nav-height: 55px; 45 | -------------------------------------------------------------------------------- /src/css/component-styles/Admin.scss: -------------------------------------------------------------------------------- 1 | .admin-pane { 2 | display: grid; 3 | grid-template-columns: 200px auto; 4 | padding: $spacing-lg; 5 | gap: $spacing-lg; 6 | } 7 | 8 | .card-grid { 9 | display: grid; 10 | grid-template-rows: $spacing-lg auto; 11 | } 12 | 13 | .category-container { 14 | height: 50vh; 15 | overflow: auto; 16 | cursor: pointer; 17 | } 18 | 19 | .search-and-sort { 20 | display: grid; 21 | grid-template-columns: auto auto; 22 | justify-content: space-between; 23 | } 24 | 25 | .card-list { 26 | display: grid; 27 | grid-template-columns: 1fr 1fr 1fr; 28 | gap: $spacing-lg; 29 | padding: $spacing-lg 0; 30 | } 31 | -------------------------------------------------------------------------------- /src/css/component-styles/AppContainer.scss: -------------------------------------------------------------------------------- 1 | .viewport { 2 | width: 100vw; 3 | height: 100vh; 4 | } 5 | 6 | .viewport-header { 7 | position: fixed; 8 | display: flex; 9 | justify-content: space-between; 10 | z-index: 1000; 11 | width: 100vw; 12 | height: $nav-height; 13 | } 14 | 15 | .feedback-container { 16 | background-color: #e6e6e6; 17 | bottom: 0px; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | position: fixed; 22 | left: 0px; 23 | padding: 10px 30px 10px 10px; 24 | text-align: center; 25 | width: 100%; 26 | z-index: 999; 27 | } 28 | 29 | .badge { 30 | margin: 0px 5px 0px 5px; 31 | cursor: pointer; 32 | } 33 | 34 | .loading-logo { 35 | max-width: 75%; 36 | margin: auto; 37 | animation: logo-expand 2s infinite; 38 | 39 | @keyframes logo-expand { 40 | 0% { 41 | box-shadow: 0 0 0 0 rgba($turquoise, 0.5); 42 | filter: grayscale(0); 43 | } 44 | 45 | 50% { 46 | box-shadow: 0 0 0 100px rgba($turquoise, 0); 47 | filter: grayscale(1); 48 | } 49 | 50 | 75% { 51 | filter: grayscale(0); 52 | } 53 | 54 | 100% { 55 | box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); 56 | filter: grayscale(0); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/css/component-styles/CategoryList.scss: -------------------------------------------------------------------------------- 1 | .Collapsible__trigger { 2 | background: $light-gray; 3 | padding: 10px; 4 | border-radius: $radius-sm; 5 | 6 | &.is-open { 7 | &:after { 8 | content: '▼'; 9 | padding: 15px; 10 | } 11 | } 12 | 13 | &.is-closed { 14 | &:after { 15 | content: '▶'; 16 | padding: 15px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/css/component-styles/DropdownCategory.scss: -------------------------------------------------------------------------------- 1 | .dropdown-category { 2 | overflow: auto; 3 | height: 50vh; 4 | } 5 | -------------------------------------------------------------------------------- /src/css/component-styles/Header.scss: -------------------------------------------------------------------------------- 1 | .searchbar-input { 2 | border: 1px solid $light-gray; 3 | border-radius: $radius-sm; 4 | padding-left: $spacing-sm; 5 | font-size: $font-size-md; 6 | } 7 | 8 | .nav-item { 9 | position: absolute; 10 | } 11 | 12 | .saved-resource-button { 13 | background-color: $dark-gray; 14 | color: $white; 15 | font-size: $font-size-md; 16 | border-radius: $radius-sm; 17 | border: none; 18 | padding: $spacing-sm $spacing-md; 19 | margin-right: $spacing-md; 20 | margin-left: $spacing-md; 21 | width: 200px; 22 | } 23 | 24 | .has-selections { 25 | background-color: $turquoise; 26 | } 27 | -------------------------------------------------------------------------------- /src/css/component-styles/MapPage.scss: -------------------------------------------------------------------------------- 1 | .map-container { 2 | height: calc(100% - #{$nav-height}); 3 | position: absolute; 4 | width: 100%; 5 | } 6 | 7 | .static-pane { 8 | position: absolute; 9 | top: 0; 10 | right: 0; 11 | bottom: 0; 12 | left: 30%; 13 | overflow: auto; 14 | } 15 | -------------------------------------------------------------------------------- /src/css/component-styles/NotFoundPage.scss: -------------------------------------------------------------------------------- 1 | .not-found-page { 2 | max-width: 500px; 3 | padding: 20% 20px 0px 20px; 4 | text-align: center; 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | left: 0; 9 | right: 0; 10 | margin: auto; 11 | } 12 | -------------------------------------------------------------------------------- /src/css/component-styles/OrganizationCard.scss: -------------------------------------------------------------------------------- 1 | .organization-card { 2 | border-radius: $radius-sm; 3 | box-shadow: 0 4px 8px 0 $shadow-black; 4 | transition: 0.3s; 5 | width: 100%; 6 | &:hover { 7 | box-shadow: 0 8px 16px 0 $shadow-black; 8 | } 9 | } 10 | 11 | .organization-card-header { 12 | padding: 15px; 13 | background-color: $light-gray; 14 | text-align: center; 15 | overflow: hidden; 16 | border-radius: $radius-sm $radius-sm; 17 | display: flex; 18 | justify-content: space-between; 19 | } 20 | 21 | .organization-card-header-text { 22 | font-size: $font-size-lg; 23 | font-weight: bold; 24 | letter-spacing: 2px; 25 | width: calc(100% - 25px); 26 | } 27 | 28 | .organization-card-body { 29 | line-height: 1.5; 30 | display: flex; 31 | flex-direction: column; 32 | padding: $spacing-md; 33 | a { 34 | color: $light-black; 35 | &:hover { 36 | color: $blue; 37 | } 38 | } 39 | } 40 | 41 | .organization-card-subtitle { 42 | font-size: $font-size-md; 43 | font-weight: bold; 44 | line-height: $font-size-lg; 45 | text-transform: uppercase; 46 | color: $black; 47 | } 48 | 49 | .organization-card-social-media { 50 | margin: $spacing-md; 51 | color: $gray; 52 | 53 | &:hover { 54 | color: blue; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/css/component-styles/OrganizationCardSaveButton.scss: -------------------------------------------------------------------------------- 1 | .organization-card-button { 2 | &.plus { 3 | animation: button-expand 0.5s linear; 4 | 5 | @keyframes button-expand { 6 | 0% { 7 | box-shadow: 0 0 0 0 rgba($turquoise, 0.4); 8 | } 9 | 10 | 70% { 11 | box-shadow: 0 0 0 100px rgba($turquoise, 0); 12 | } 13 | 14 | 100% { 15 | box-shadow: 0 0 0 0 rgba($turquoise, 0); 16 | } 17 | } 18 | } 19 | 20 | &.minus { 21 | animation: button-expand-reverse 0.5s linear; 22 | 23 | @keyframes button-expand-reverse { 24 | 100% { 25 | box-shadow: 0 0 0 0 rgba($turquoise, 0.4); 26 | } 27 | 28 | 30% { 29 | box-shadow: 0 0 0 100px rgba($turquoise, 0); 30 | } 31 | 32 | 0% { 33 | box-shadow: 0 0 0 0 rgba($turquoise, 0); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/css/component-styles/PrintPage.scss: -------------------------------------------------------------------------------- 1 | .print-header { 2 | text-align: center; 3 | } 4 | .print-card-body { 5 | padding: 1%; 6 | border-bottom: 1px $shadow-black; 7 | width: 100%; 8 | } 9 | 10 | .print-button { 11 | border-radius: $radius-sm; 12 | margin-right: $spacing-md; 13 | } -------------------------------------------------------------------------------- /src/css/component-styles/ResultList.scss: -------------------------------------------------------------------------------- 1 | .results { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | bottom: 0; 6 | top: 50px; 7 | overflow: auto; 8 | -webkit-overflow-scrolling: touch; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/css/component-styles/SavedResource.scss: -------------------------------------------------------------------------------- 1 | .reactstrap-card { 2 | border-radius: 10px; 3 | margin: 8px 0; 4 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 5 | transition: 0.3s; 6 | } 7 | 8 | .reactstrap-card:hover { 9 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 10 | } 11 | 12 | .reactstrap-card-headline { 13 | width: calc(100% - 25px); 14 | font-size: 22px; 15 | font-weight: bold; 16 | letter-spacing: 2px; 17 | } 18 | 19 | .reactstrap-card-body-card-subtitle { 20 | font-size: 13px; 21 | font-weight: 600; 22 | letter-spacing: 1.5px; 23 | line-height: 24px; 24 | text-transform: uppercase; 25 | color: #2e3d49; 26 | } 27 | 28 | a.reactstrap-card-body-icon { 29 | color: purple; 30 | text-decoration: none; 31 | } 32 | 33 | .remove-item { 34 | cursor: pointer; 35 | margin-left: auto; 36 | 37 | text-align: center; 38 | font-size: 35px; 39 | height: 25px; 40 | width: 25px; 41 | line-height: 20px; 42 | border-radius: 50%; 43 | 44 | border: solid 1px black; 45 | } 46 | -------------------------------------------------------------------------------- /src/css/component-styles/SavedResourceContainer.scss: -------------------------------------------------------------------------------- 1 | .saved-resources-container { 2 | -webkit-overflow-scrolling: touch; 3 | overflow: auto; 4 | max-height: calc(100vh - #{$nav-height} - 50px); 5 | padding: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /src/css/component-styles/SavedResourcePanel.scss: -------------------------------------------------------------------------------- 1 | .saved-resource-panel { 2 | background-color: $dark-gray; 3 | } 4 | 5 | .saved-resource-panel-header { 6 | display: flex; 7 | justify-content: space-between; 8 | color: white; 9 | padding: 4px 10px; 10 | border-bottom: 1px solid black; 11 | } 12 | 13 | .resource-buttons { 14 | button { 15 | margin: 0 4px; 16 | } 17 | 18 | .copy-button { 19 | animation: button-expand-reverse 0.5s linear; 20 | 21 | @keyframes button-expand-reverse { 22 | 0% { 23 | box-shadow: 0 0 0 0 rgba($turquoise, 0.4); 24 | } 25 | 26 | 70% { 27 | box-shadow: 0 0 0 100px rgba($turquoise, 0); 28 | } 29 | 30 | 100% { 31 | box-shadow: 0 0 0 0 rgba($turquoise, 0); 32 | } 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/css/component-styles/SplitScreenSlidingPane.scss: -------------------------------------------------------------------------------- 1 | .sliding-pane-toggle-button { 2 | display: none; 3 | position: absolute; 4 | height: 40px; 5 | width: 100px; 6 | border: 0; 7 | top: -40px; 8 | left: 50%; 9 | background-color: $transparent-white; 10 | border-radius: $radius-sm $radius-sm 0 0; 11 | margin-left: -50px; 12 | font-size: 1.4rem; 13 | cursor: pointer; 14 | } 15 | 16 | .sliding-pane { 17 | position: absolute; 18 | top: 0; 19 | right: 70%; 20 | bottom: 0; 21 | left: 0; 22 | padding: 10px; 23 | transition: bottom 0.3s ease-in-out; 24 | } 25 | -------------------------------------------------------------------------------- /src/css/component-styles/SplitScreenTogglePane.scss: -------------------------------------------------------------------------------- 1 | .split-screen { 2 | position: fixed; 3 | width: 33%; 4 | border: 0; 5 | top: $nav-height; 6 | right: 0; 7 | background-color: white; 8 | } 9 | -------------------------------------------------------------------------------- /src/css/index.scss: -------------------------------------------------------------------------------- 1 | // import constants 2 | @import 'base-constants'; 3 | 4 | // import component styles 5 | @import '/component-styles/Admin'; 6 | @import '/component-styles/AppContainer'; 7 | @import '/component-styles/CategoryList'; 8 | @import '/component-styles/DropdownCategory'; 9 | @import '/component-styles/Header'; 10 | @import '/component-styles/MapPage'; 11 | @import '/component-styles/NotFoundPage'; 12 | @import '/component-styles/OrganizationCard'; 13 | @import '/component-styles/OrganizationCardSaveButton'; 14 | @import '/component-styles/ResultList'; 15 | @import '/component-styles/SavedResource'; 16 | @import '/component-styles/SavedResourceContainer'; 17 | @import '/component-styles/SavedResourcePanel'; 18 | @import '/component-styles/SplitScreenSlidingPane'; 19 | @import '/component-styles/SplitScreenTogglePane'; 20 | @import '/component-styles/PrintPage'; 21 | 22 | // import shared element level styles 23 | @import 'main'; 24 | // import mobile media query css 25 | @import 'mobile'; 26 | // import print media query css 27 | @import 'print'; 28 | -------------------------------------------------------------------------------- /src/css/main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Helvetica, sans-serif; 5 | } 6 | 7 | a { 8 | cursor: pointer; 9 | } 10 | 11 | .hidden { 12 | display: none !important; 13 | } 14 | 15 | .main-nav-bar { 16 | width: 100%; 17 | border-bottom: 1px solid $black; 18 | background-color: $super-light-gray; 19 | // background: linear-gradient(to right, #3a9eac, #2d7cac); 20 | // color: white; 21 | } 22 | 23 | .page { 24 | padding-top: $nav-height; 25 | } 26 | -------------------------------------------------------------------------------- /src/css/mobile.scss: -------------------------------------------------------------------------------- 1 | // media query format 2 | // @media (max-width: ) { 3 | // css rules here 4 | // } 5 | 6 | @media (max-width: $screen-size-xxlg) { 7 | } 8 | 9 | @media (max-width: $screen-size-xlg) { 10 | .card-list { 11 | grid-template-columns: repeat(3, 1fr); 12 | } 13 | } 14 | 15 | @media (max-width: $screen-size-lg) { 16 | .card-list { 17 | grid-template-columns: repeat(2, 1fr); 18 | } 19 | } 20 | 21 | @media (max-width: $screen-size-md) { 22 | .admin-pane { 23 | grid-template-columns: auto; 24 | } 25 | 26 | .card-list { 27 | grid-template-columns: repeat(1, 1fr); 28 | } 29 | 30 | .category-group-item { 31 | border-radius: 10px; 32 | } 33 | 34 | .results { 35 | width: 100%; 36 | } 37 | 38 | .saved-resources { 39 | width: 100%; 40 | } 41 | } 42 | 43 | @media (max-width: $screen-size-sm) { 44 | .sliding-pane-toggle-button { 45 | display: block; 46 | text-align: center; 47 | } 48 | 49 | .static-pane { 50 | left: 0; 51 | } 52 | 53 | .sliding-pane { 54 | background-color: $transparent-white; 55 | right: 0; 56 | bottom: -64%; 57 | z-index: 10; 58 | height: calc(70%); 59 | top: auto; 60 | 61 | &.open { 62 | height: calc(100% -40px); 63 | bottom: 0; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/css/print.scss: -------------------------------------------------------------------------------- 1 | @page { 2 | size: letter; 3 | } 4 | 5 | @media print { 6 | // print media query here 7 | } 8 | -------------------------------------------------------------------------------- /src/images/cc-logo-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforboston/communityconnect/896cbacd912b0acf331f56fbda6cc29a1ca09e64/src/images/cc-logo-home.png -------------------------------------------------------------------------------- /src/images/cc-logo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforboston/communityconnect/896cbacd912b0acf331f56fbda6cc29a1ca09e64/src/images/cc-logo-icon.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fab } from "@fortawesome/free-brands-svg-icons"; 3 | import { library } from "@fortawesome/fontawesome-svg-core"; 4 | import { fas } from "@fortawesome/free-solid-svg-icons"; 5 | import ReactDOM from "react-dom"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | import { Provider } from "react-redux"; 8 | import { Route, Switch, Redirect } from "react-router"; 9 | import configureStore from "./store/configureStore"; 10 | import AppContainer from "./App/AppContainer"; 11 | import PrintPage from "./components/PrintPage/PrintPageContainer"; 12 | import NotFoundPage from "./components/NotFoundPage/NotFoundPage"; 13 | import registerServiceWorker from "./registerServiceWorker"; 14 | 15 | import "bootstrap/dist/css/bootstrap.min.css"; 16 | import "./css/index.scss"; 17 | 18 | library.add(fab, fas); 19 | 20 | const getRoutes = store => ( 21 |
22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 45 | 46 | 47 | 48 | 49 |
50 | ); 51 | 52 | const store = configureStore(); 53 | 54 | ReactDOM.render( 55 | 56 | {getRoutes(store)} 57 | , 58 | document.getElementById("root") 59 | ); 60 | 61 | registerServiceWorker(); 62 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import resourceReducers from "./resourceReducers"; 3 | 4 | const rootReducer = combineReducers(resourceReducers); 5 | export default rootReducer; 6 | -------------------------------------------------------------------------------- /src/reducers/initialState.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isFetchingResource: true, 3 | resources: [], 4 | searchedResources: [], 5 | filteredResources: [], 6 | savedResources: [], 7 | categories: [], 8 | mapResources: [], 9 | }; 10 | -------------------------------------------------------------------------------- /src/reducers/resourceReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action/actionType"; 2 | import { getQueryResources } from "../utils/resourcesQuery"; 3 | import initialState from "./initialState"; 4 | 5 | function resourcesReducer(state = initialState.resources, action) { 6 | switch (action.type) { 7 | case types.LOAD_RESOURCE_DATA_SUCCESS: 8 | return [...state, ...action.resources]; 9 | default: 10 | return state; 11 | } 12 | } 13 | 14 | function isFetchingResourceReducer( 15 | state = initialState.isFetchingResource, 16 | action 17 | ) { 18 | switch (action.type) { 19 | case types.LOAD_RESOURCE_DATA_START: 20 | return true; 21 | case types.LOAD_RESOURCE_DATA_SUCCESS: 22 | return false; 23 | default: 24 | return state; 25 | } 26 | } 27 | 28 | function categoriesReducer(state = initialState.categories, action) { 29 | switch (action.type) { 30 | case types.LOAD_RESOURCE_DATA_SUCCESS: { 31 | const categoriesData = {}; 32 | 33 | action.resources.forEach(data => { 34 | const category = data.categories.split(","); 35 | 36 | category.forEach(cat => { 37 | categoriesData[cat] = cat.trim(); 38 | }); 39 | }); 40 | 41 | const categoryList = [...new Set(Object.values(categoriesData))]; 42 | const index = categoryList.indexOf(""); 43 | 44 | if (index > -1) { 45 | categoryList.splice(index, 1); 46 | } 47 | 48 | return [...state, ...categoryList]; 49 | } 50 | default: 51 | return state; 52 | } 53 | } 54 | 55 | function filteredResourcesReducer( 56 | state = initialState.filteredResources, 57 | action 58 | ) { 59 | switch (action.type) { 60 | case types.LOAD_RESOURCE_DATA_SUCCESS: 61 | return [...state, ...action.resources]; 62 | case types.FILTER_RESOURCES_BY_CATEGORIES: 63 | return action.filteredResource; 64 | default: 65 | return state; 66 | } 67 | } 68 | 69 | function searchedResourcesReducer( 70 | state = initialState.searchedResources, 71 | action 72 | ) { 73 | switch (action.type) { 74 | case types.LOAD_RESOURCE_DATA_SUCCESS: 75 | return [...state, ...action.resources]; 76 | case types.FILTER_RESOURCES_BY_SEARCH: 77 | return action.searchedResource; 78 | default: 79 | return state; 80 | } 81 | } 82 | 83 | function savedResourcesReducer(state = initialState.savedResources, action) { 84 | switch (action.type) { 85 | case types.LOAD_RESOURCE_DATA_SUCCESS: { 86 | const selectedResourceIds = getQueryResources(); 87 | const selectedResources = []; 88 | 89 | selectedResourceIds.forEach(selectedResourceId => { 90 | action.resources.forEach(resource => { 91 | if (resource.id === selectedResourceId) { 92 | selectedResources.push(resource); 93 | } 94 | }); 95 | }); 96 | 97 | return [...state, ...selectedResources]; 98 | } 99 | case types.ADD_SAVED_RESOURCE: 100 | return [...state, action.savedResource]; 101 | case types.REMOVE_SAVED_RESOURCE: 102 | return state.filter( 103 | resource => action.savedResourceIndex !== resource.id 104 | ); 105 | case types.CLEAR_SAVED_RESOURCES: 106 | return []; 107 | default: 108 | return state; 109 | } 110 | } 111 | 112 | export default { 113 | resources: resourcesReducer, 114 | isFetchingResource: isFetchingResourceReducer, 115 | categories: categoriesReducer, 116 | filteredResources: filteredResourcesReducer, 117 | searchedResources: searchedResourcesReducer, 118 | savedResources: savedResourcesReducer, 119 | }; 120 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | function registerValidSW(swUrl) { 22 | navigator.serviceWorker 23 | .register(swUrl) 24 | .then(registration => { 25 | // eslint-disable-next-line no-param-reassign 26 | registration.onupdatefound = () => { 27 | const installingWorker = registration.installing; 28 | 29 | installingWorker.onstatechange = () => { 30 | if (installingWorker.state === "installed") { 31 | if (navigator.serviceWorker.controller) { 32 | // At this point, the old content will have been purged and 33 | // the fresh content will have been added to the cache. 34 | // It's the perfect time to display a "New content is 35 | // available; please refresh." message in your web app. 36 | console.log("New content is available; please refresh."); 37 | } else { 38 | // At this point, everything has been precached. 39 | // It's the perfect time to display a 40 | // "Content is cached for offline use." message. 41 | console.log("Content is cached for offline use."); 42 | } 43 | } 44 | }; 45 | }; 46 | }) 47 | .catch(error => { 48 | console.error("Error during service worker registration:", error); 49 | }); 50 | } 51 | 52 | function checkValidServiceWorker(swUrl) { 53 | // Check if the service worker can be found. If it can't reload the page. 54 | fetch(swUrl) 55 | .then(response => { 56 | // Ensure service worker exists, and that we really are getting a JS file. 57 | if ( 58 | response.status === 404 || 59 | response.headers.get("content-type").indexOf("javascript") === -1 60 | ) { 61 | // No service worker found. Probably a different app. Reload the page. 62 | navigator.serviceWorker.ready.then(registration => { 63 | registration.unregister().then(() => { 64 | window.location.reload(); 65 | }); 66 | }); 67 | } else { 68 | // Service worker found. Proceed as normal. 69 | registerValidSW(swUrl); 70 | } 71 | }) 72 | .catch(() => { 73 | console.log( 74 | "No internet connection found. App is running in offline mode." 75 | ); 76 | }); 77 | } 78 | 79 | export function unregister() { 80 | if ("serviceWorker" in navigator) { 81 | navigator.serviceWorker.ready.then(registration => { 82 | registration.unregister(); 83 | }); 84 | } 85 | } 86 | 87 | export default function register() { 88 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 89 | // The URL constructor is available in all browsers that support SW. 90 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 91 | 92 | if (publicUrl.origin !== window.location.origin) { 93 | // Our service worker won't work if PUBLIC_URL is on a different origin 94 | // from what our page is served on. This might happen if a CDN is used to 95 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 96 | return; 97 | } 98 | 99 | window.addEventListener("load", () => { 100 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 101 | 102 | if (isLocalhost) { 103 | // This is running on localhost. Lets check if a service worker still exists or not. 104 | checkValidServiceWorker(swUrl); 105 | 106 | // Add some additional logging to localhost, pointing developers to the 107 | // service worker/PWA documentation. 108 | navigator.serviceWorker.ready.then(() => { 109 | console.log( 110 | "This web app is being served cache-first by a service " + 111 | "worker. To learn more, visit https://goo.gl/SC7cgQ" 112 | ); 113 | }); 114 | } else { 115 | // Is not local host. Just register service worker 116 | registerValidSW(swUrl); 117 | } 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/share-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import reduxImmutableStateInvariant from "redux-immutable-state-invariant"; 3 | import thunk from "redux-thunk"; 4 | import rootReducer from "../reducers"; 5 | 6 | export default function configureStore(initialState) { 7 | return createStore( 8 | rootReducer, 9 | initialState, 10 | applyMiddleware( 11 | thunk, 12 | // Redux middleware that spits an error 13 | // when you try to mutate your state either inside a dispatch or between dispatches. 14 | reduxImmutableStateInvariant() 15 | ) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/distance.js: -------------------------------------------------------------------------------- 1 | export default (targetLocation, myLocation) => { 2 | // return undefined if we are missing either coordinate 3 | if (!targetLocation.coordinates || !myLocation.coordinates) { 4 | return undefined; 5 | } 6 | 7 | // haversine formula is used 8 | // https://www.movable-type.co.uk/scripts/latlong.html 9 | const degreesToRadians = degrees => (degrees * Math.PI) / 180; 10 | const RADIUS_OF_EARTH = 6371000; 11 | // convert degrees to radians 12 | const latTargetLocation = degreesToRadians(targetLocation.coordinates.lat); 13 | const latMyLocation = degreesToRadians(myLocation.coordinates.lat); 14 | 15 | // calculate changes in latitude and longitude 16 | const changeInLat = degreesToRadians( 17 | myLocation.coordinates.lat - targetLocation.coordinates.lat 18 | ); 19 | 20 | const changeInLong = degreesToRadians( 21 | myLocation.coordinates.lng - targetLocation.coordinates.lng 22 | ); 23 | 24 | // a is the square of half the chord length between the points 25 | const a = 26 | Math.sin(changeInLat / 2) ** 2 + 27 | Math.cos(latTargetLocation) * 28 | Math.cos(latMyLocation) * 29 | Math.sin(changeInLong / 2) ** 2; 30 | 31 | // c is angular distance in radians 32 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 33 | const distanceInMeters = RADIUS_OF_EARTH * c; 34 | const NUM_METERS_IN_ONE_MILE = 1609.34; 35 | const metersToMiles = meters => meters / NUM_METERS_IN_ONE_MILE; 36 | // convert meters to miles and set to two decimal points 37 | const distanceInMiles = +metersToMiles(distanceInMeters).toFixed(2); 38 | 39 | return distanceInMiles; 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/resourcesQuery.js: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | import _ from "lodash"; 3 | 4 | export const getQueryResources = () => { 5 | const query = queryString.parse(window.location.search, { 6 | arrayFormat: "comma", 7 | }); 8 | 9 | return _.castArray(query.resources || []); 10 | }; 11 | export const encodeResources = resources => 12 | queryString.stringify({ resources }, { arrayFormat: "comma" }); 13 | --------------------------------------------------------------------------------