├── .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 |
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 | Visit the current spreadsheet
72 | Click File and select Make a Copy
73 | Click OK
74 | When viewing your copy, click SHARE in the upper-right hand corner.
75 | Click "Get shareable link" in the upper-right hand corner of the modal.
76 | Ensure that "Anyone with the link can view" is selected.
77 | Copy link
78 | Click done
79 | Click File and select "Publish to the web"
80 | Click Publish
81 | Open "src/googlesheetApi.js" in the codebase
82 | Replace "revere_key" with a portion of the URL in your clipboard
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 |
31 | You need to enable JavaScript to run this app.
32 |
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 |
100 | Clear
101 |
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 |
18 | {this.props.sortOptions.map((sortOption, i) => (
19 |
24 | {sortOption.key}
25 |
26 | ))}
27 |
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 |
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 |
25 |
26 |
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 |
90 |
91 |
92 | )}
93 | />
94 | (
98 |
106 | Admin View
107 |
108 | )}
109 | />
110 | (
114 |
122 | Map View
123 |
124 | )}
125 | />
126 | (
129 |
134 | Saved Resources {savedResources.length}
135 |
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 |
148 | Cancel
149 | {" "}
150 |
156 | Continue
157 |
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 |
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 |
14 | Go Home
15 | {" "}
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 |
24 |
25 |
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 |
24 |
25 |
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 | {
46 | navigator.clipboard.writeText(tmpUrl);
47 | setBtnTitle("Resource URL Copied to Clipboard");
48 | setBtnIcon(faCheck);
49 | }}
50 | >
51 |
52 |
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 |
--------------------------------------------------------------------------------