├── .DS_Store
├── .env
├── .firebaserc
├── .gitignore
├── .travis.yml
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── cypress.json
├── cypress
├── .eslintrc.json
├── fixtures
│ └── example.json
├── integration
│ └── msm
│ │ ├── save.spec.js
│ │ ├── search.spec.js
│ │ ├── share.spec.js
│ │ └── sort.spec.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── doc
└── highlighting-selections-in-a-cluster-pin.md
├── firebase.json
├── package-lock.json
├── package.json
├── public
├── MapPin.png
├── favicon.ico
├── help
│ ├── help-guide.html
│ ├── help.css
│ ├── images
│ │ ├── 1-9.png
│ │ ├── a-z.png
│ │ ├── cash_food.png
│ │ ├── cluster.png
│ │ ├── community_center.png
│ │ ├── cropped-rw-logo.png
│ │ ├── delete.png
│ │ ├── distance_menu.png
│ │ ├── drag_drop.png
│ │ ├── education.png
│ │ ├── email.png
│ │ ├── health.png
│ │ ├── housing.png
│ │ ├── image10.png
│ │ ├── image12.png
│ │ ├── job.png
│ │ ├── legal.png
│ │ ├── loc-search.png
│ │ ├── mapn-logo.png
│ │ ├── mental_health.png
│ │ ├── open_listing.png
│ │ ├── printer.png
│ │ ├── resettlement.png
│ │ ├── save_button.png
│ │ ├── saved_listing.png
│ │ ├── zoom-controls.png
│ │ └── zoom_to_fit.png
│ └── index.css
├── index.html
├── manifest.json
└── print.css
├── run-cypress
└── src
├── App.css
├── App.js
├── App.test.js
├── assets
├── distances.js
├── icon-colors.json
├── icons
│ ├── circle_question.jpg
│ ├── community-center.png
│ ├── education.png
│ ├── food-cash-assistance.png
│ ├── health.png
│ ├── highlightedProviders.png
│ ├── housing.png
│ ├── job-placement.png
│ ├── key comm ctr.svg
│ ├── key education.svg
│ ├── key food_cash.svg
│ ├── key health.svg
│ ├── key housing.svg
│ ├── key job placement.svg
│ ├── key legal.svg
│ ├── key mental health.svg
│ ├── key resettlement.svg
│ ├── legal.png
│ ├── mental-health.png
│ ├── msm_logo.svg
│ ├── pin cash.svg
│ ├── pin comm ctr.svg
│ ├── pin education.svg
│ ├── pin health.svg
│ ├── pin housing.svg
│ ├── pin job placement.svg
│ ├── pin legal.svg
│ ├── pin mental health.svg
│ ├── pin resettlement.svg
│ ├── pin-blank-clusters.png
│ ├── pin-blank-clusters.svg
│ ├── pin-blank-clusters_multi-highlights.png
│ ├── pin-blank-clusters_multi.png
│ ├── pin-cash.png
│ ├── pin-comm-ctr.png
│ ├── pin-education.png
│ ├── pin-health.png
│ ├── pin-highlight.png
│ ├── pin-highlight.svg
│ ├── pin-housing.png
│ ├── pin-hover.png
│ ├── pin-job-placement.png
│ ├── pin-legal.png
│ ├── pin-mental-health.png
│ ├── pin-mini.png
│ ├── pin-mini.svg
│ ├── pin-resettlement.png
│ └── resettlement.png
├── images.js
├── info-icon.svg
└── visa-types.json
├── components
├── AnimatedMarker
│ └── animated-marker.js
├── ClusterProviderList
│ ├── cluster-provider-list.css
│ └── cluster-provider-list.js
├── DetailsPane
│ ├── details-pane.css
│ ├── detals-pane.js
│ ├── index.js
│ ├── provider-details-info.css
│ └── provider-details-info.js
├── Dropdowns
│ ├── checkbox-dropdown.js
│ ├── expandable.css
│ ├── expandable.js
│ └── radio-button-dropdown.js
├── Map
│ ├── geocoder.container.js
│ ├── geocoder.js
│ ├── index.js
│ ├── map.container.js
│ ├── map.css
│ ├── map.js
│ ├── mapbox-gl-wrapper.js
│ └── utilities.js
├── MenuAcceptingNewFilter
│ ├── index.js
│ ├── menu-accepting-new-filter.css
│ └── menu-accepting-new-filter.js
├── MenuDistanceFilter
│ ├── index.js
│ ├── menu-distance-filter.css
│ └── menu-distance-filter.js
├── MenuDropdown
│ ├── index.js
│ ├── menu-dropdown.css
│ └── menu-dropdown.js
├── MenuDropdownItem
│ ├── index.js
│ ├── memu-dropdown-item.container.js
│ ├── menu-dropdown-item.css
│ └── menu-dropdown-item.js
├── MenuVisaFilter
│ ├── index.js
│ └── menu-visa-filter.js
├── ProviderList
│ ├── index.js
│ ├── provider-list.container.js
│ ├── provider-list.css
│ ├── provider-list.js
│ ├── sort-dropdown.css
│ └── sort-dropdown.js
├── SavedProvidersList
│ ├── index.js
│ ├── saved-providers-list.container.js
│ ├── saved-providers-list.css
│ └── saved-providers-list.js
├── TabbedMenu
│ ├── index.js
│ ├── tabbed-menu.container.js
│ ├── tabbed-menu.css
│ └── tabbed-menu.js
├── TopBar
│ ├── distance-dropdown.js
│ ├── help-menu.js
│ ├── index.js
│ ├── logo.js
│ ├── mapbox-gl-geocoder.css
│ ├── provider-type-dropdown.js
│ ├── search.js
│ ├── top-bar.container.js
│ ├── top-bar.css
│ ├── top-bar.js
│ └── visa-status-dropdown.js
└── index.js
├── index.css
├── index.js
├── provider-type-to-color.json
├── redux
├── actions.js
├── filters.js
├── highlightedProviders.js
├── hoveredProvider.js
├── mapObject.js
├── providerModels.js
├── providerModels.test.js
├── providerTypes.js
├── providers.js
├── search.js
├── selectors.js
└── store.js
├── serviceWorker.js
└── util
├── googleSheets.js
└── printJSX.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/.DS_Store
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NODE_PATH=./src
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "migrant-service-map"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | .idea/
3 | node_modules
4 | yarn.lock
5 | .vscode
6 | build
7 | .firebase
8 | .DS_Store
9 | cypress/videos
10 | cypress/screenshots
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Configures builds for all branches and deployment on the "prod" branch.
2 |
3 | language: node_js
4 | node_js: node
5 |
6 | addons:
7 | apt:
8 | packages:
9 | # Ubuntu 16+ does not install this Cypress dependency by default, so we need to install it ourselves
10 | - libgconf-2-4
11 |
12 | cache:
13 | directories:
14 | # Cache the folder with the Cypress binary
15 | - ~/.cache
16 |
17 | install: npm ci
18 | script: npm run test:all && npm run build
19 |
20 | deploy:
21 | provider: firebase
22 | edge: true
23 | on:
24 | branch: prod
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Code for Boston
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectId": "wxgyq3"
3 | }
4 |
--------------------------------------------------------------------------------
/cypress/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "cypress"
4 | ],
5 | "rules": {
6 | "cypress/no-assigning-return-values": "error",
7 | "cypress/no-unnecessary-waiting": "warn",
8 | "cypress/assertion-before-screenshot": "warn"
9 | },
10 | "env": {
11 | "cypress/globals": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "example": "fixture"
3 | }
4 |
--------------------------------------------------------------------------------
/cypress/integration/msm/save.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | context("Save", () => {
4 | before(() => {
5 | cy.visit("http://localhost:3000");
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/cypress/integration/msm/search.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | context("Search", () => {
4 | before(() => {
5 | cy.visit("http://localhost:3000");
6 | cy.get(".map-loaded", { timeout: 30000 });
7 | });
8 |
9 | it("results in concentric circles", () => {
10 | // Sanity check
11 | cy.get(".header-subtext").should(
12 | "have.text",
13 | "Use the filters in the top bar to adjust the number of results"
14 | );
15 |
16 | // Perform the search
17 | cy.get(".mapboxgl-ctrl-geocoder input").type("One Broadway");
18 |
19 | // Select a search result
20 | cy.get(".suggestions .active a").click();
21 |
22 | // Confirm that rings appear
23 | cy.get(".mapboxgl-canvas-container")
24 | .contains("0.5 mile")
25 | .should("have.class", "mapboxgl-marker");
26 | cy.get(".mapboxgl-canvas-container")
27 | .contains("1 mile")
28 | .should("have.class", "mapboxgl-marker");
29 | });
30 |
31 | it("can input a new location into the search bar", () => {
32 | // select all and type a second location
33 | cy.get(".mapboxgl-ctrl-geocoder input")
34 | .type("{selectall}Davis Square")
35 | .type(" ");
36 |
37 | // This is not necessary but it may help in some circumstances
38 | cy.wait(500);
39 |
40 | // Select a search result
41 | cy.get(".suggestions .active a").click();
42 |
43 | // Confirm that rings appear
44 | cy.get(".mapboxgl-canvas-container")
45 | .contains("0.5 mile")
46 | .should("have.class", "mapboxgl-marker");
47 | cy.get(".mapboxgl-canvas-container")
48 | .contains("1 mile")
49 | .should("have.class", "mapboxgl-marker");
50 |
51 | // Confirm that search suggestion has been added to input field
52 | cy.get(".mapboxgl-ctrl-geocoder input").should(
53 | "have.value",
54 | "Davis Square, Somerville, Massachusetts 02144, United States"
55 | );
56 | });
57 |
58 | it("can remove a reference location", () => {
59 | // clear my current location
60 | cy.get("button.geocoder-icon.geocoder-icon-close").click();
61 |
62 | // Confirm that markers don't appear
63 | cy.get(".mapboxgl-marker").should("not.exist");
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/cypress/integration/msm/share.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | context("Share", () => {
4 | before(() => {
5 | cy.visit("http://localhost:3000");
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/cypress/integration/msm/sort.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | context("Sort", () => {
4 | before(() => {
5 | cy.visit("http://localhost:3000");
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | /* eslint-disable no-unused-vars */
15 | module.exports = (on, config) => {
16 | // `on` is used to hook into various events Cypress emits
17 | // `config` is the resolved Cypress config
18 | }
19 | /* eslint-enable no-unused-vars */
20 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/doc/highlighting-selections-in-a-cluster-pin.md:
--------------------------------------------------------------------------------
1 | # Highlighting Selected Locations in a Cluster Pin
2 |
3 | Participants:
4 | * Maia
5 | * Alex
6 | * Sasha
7 | * Byron
8 | * Rachel
9 | * Eduardo
10 |
11 | We've discussed three options for handling when the user clicks on location in
12 | the list that is currently in a clustered pin on the map.
13 |
14 | * Highlight the entire cluster
15 | * "Pop out" the selected location, decreasing the number in the cluster pin.
16 | Stay out?
17 | Animated?
18 | * Removing cluster pins altogether
19 |
20 | ## Highlight the entire cluster
21 |
22 | ### How it Works
23 |
24 | Clicking on the location will cause the cluster pin the location is in to be
25 | highlighted.
26 |
27 | When the cluster pin is separated into individual pins, only the selected
28 | location remains highlighted.
29 |
30 | ### Pros
31 |
32 | * Cluster-pins behave more consistently with non-cluster pins
33 | * Simpler to implement within the time frame
34 | * Design approved
35 |
36 | ### Cons
37 |
38 | * Might appear to the service provider that they chose the entire cluster
39 | * Doesn't tell you which one is selected
40 | * Can't tell how many are selected if more than one is selected
41 |
42 | ### 10/12/19 discussion
43 |
44 | We can avoid the first two cons by animating a single provider icon when the provider is selected. When the animation stops, the marker fades out, and the containing cluster changes colors to indicate the selected icon. This should look like the provider marker appears, then "melts" back into the cluster.
45 |
46 | ## "Pop out" selected location
47 |
48 | ### How it Works
49 |
50 | Clicking on the location will remove it from the cluster pin and reveal it at
51 | its actual location. The individual pin will be highlighted.
52 |
53 | The count on the cluster pin will decrease by 1.
54 |
55 | When deselecting the location, its individual pin will be removed and the
56 | cluster pin's count will increase by 1.
57 |
58 | ### Condiderations
59 |
60 | * Should the individual pin be animated?
61 |
62 | ### Pros
63 |
64 | * Clearly identifies which item was picked and where it is on the map
65 | * Consistent visual design of what a selected icon looks like, across zoom
66 | levels
67 |
68 | ### Cons
69 |
70 | * When the location is deselected, the icon disappears, which might be confusing
71 | to the service worker using the tool.
72 | * Selected icons might get crowded without clustering
73 | * Most development time to implement
74 | * ~3 Tuesday nights and Tuesday nights only
75 | * 2 full-time days
76 | * Needs more time to finalize design
77 | * Also requires reworking
78 | * Need to rework bounding boxes for pins; pins too close together will both get
79 | clicked
80 |
81 | ## Removing cluster pins
82 |
83 | No more cluster pins! All pins are shown by default at all zoom levels. We
84 | could defer to type and range filters to limit what's shown.
85 |
86 | Since we now have the type icon inside a pin icon, it might be less noisy than
87 | it was initially.
88 |
89 | ### Pros
90 |
91 | * Simpler UX
92 |
93 | ### Cons
94 |
95 | * Need to rework highlighting
96 | * Need to rework bounding boxes for pins; pins too close together will both get
97 | clicked
98 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ],
15 | "headers": [
16 | {"source": "/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}]}
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "migrant_service_map",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^1.2.17",
7 | "@fortawesome/free-solid-svg-icons": "^5.8.1",
8 | "@fortawesome/react-fontawesome": "^0.1.4",
9 | "@mapbox/mapbox-gl-geocoder": "^2.3.0",
10 | "@turf/turf": "^5.1.6",
11 | "classnames": "^2.2.6",
12 | "concurrently": "^5.0.0",
13 | "core-js": "^3.2.1",
14 | "dot-prop-immutable": "^1.5.0",
15 | "lodash": "^4.17.11",
16 | "mapbox": "^1.0.0-beta10",
17 | "mapbox-gl": "^1.5.0",
18 | "memoize-one": "^5.1.1",
19 | "npm": "^6.13.1",
20 | "papaparse": "^5.1.0",
21 | "react": "^16.12.0",
22 | "react-beautiful-dnd": "^11.0.5",
23 | "react-dom": "^16.12.0",
24 | "react-redux": "^6.0.0",
25 | "react-scripts": "^3.2.0",
26 | "react-tabs": "^3.0.0",
27 | "react-tooltip": "^3.11.0",
28 | "redux": "^4.0.1",
29 | "reselect": "^4.0.0",
30 | "simple-flexbox": "^1.2.0",
31 | "wait-on": "^3.3.0"
32 | },
33 | "scripts": {
34 | "start": "react-scripts start",
35 | "build": "react-scripts build",
36 | "test": "react-scripts test",
37 | "test:cy": "./run-cypress",
38 | "test:cy-start-server": "./run-cypress --start-server",
39 | "test:all": "CI=true npm test && npm run test:cy-start-server",
40 | "eject": "react-scripts eject"
41 | },
42 | "eslintConfig": {
43 | "extends": "react-app"
44 | },
45 | "browserslist": [
46 | ">0.2%",
47 | "not dead",
48 | "not ie <= 11",
49 | "not op_mini all"
50 | ],
51 | "devDependencies": {
52 | "cypress": "^3.6.1",
53 | "eslint-plugin-cypress": "^2.7.0",
54 | "prettier": "^1.15.3"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/public/MapPin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/MapPin.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/favicon.ico
--------------------------------------------------------------------------------
/public/help/help.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: #1b181e;
3 | line-height: 1.4;
4 | }
5 |
6 | body > header {
7 | background-color: #0072B3;
8 | border-bottom: 1px solid #8c45cf;
9 | width: 100%;
10 | display: flex;
11 | align-items: center;
12 | padding: 10px 10px 10px 20px;
13 | }
14 | header h1 {
15 | color: white;
16 | }
17 |
18 | nav,
19 | section {
20 | margin: 3em;
21 |
22 | }
23 |
24 | .intro {
25 | display: flex;
26 | }
27 |
28 | nav {
29 | background-color: #ccc;
30 | border: 1px solid #aaa;
31 | flex: 1 1 auto;
32 | }
33 | nav h2 {
34 | padding-left: 20px;
35 | }
36 |
37 | #mapn-logo {
38 | flex: 1 1 256px;
39 | }
40 | #mapn-logo img {
41 | width: 100%;
42 | }
43 |
44 | summary {
45 | color: gray;
46 | cursor: pointer;
47 | font-weight: bold;
48 | padding: 0.5rem 0;
49 | }
50 |
51 | #icons {
52 | /* border: 1px solid #8c45cf; */
53 | border-collapse: collapse;
54 | table-layout: fixed;
55 | margin: 2rem;
56 | }
57 | #icons img {
58 | width: 40px;
59 | }
60 | th, td {
61 | border: 1px solid #ddc8f0;
62 | padding: 1rem;
63 | }
64 |
--------------------------------------------------------------------------------
/public/help/images/1-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/1-9.png
--------------------------------------------------------------------------------
/public/help/images/a-z.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/a-z.png
--------------------------------------------------------------------------------
/public/help/images/cash_food.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/cash_food.png
--------------------------------------------------------------------------------
/public/help/images/cluster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/cluster.png
--------------------------------------------------------------------------------
/public/help/images/community_center.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/community_center.png
--------------------------------------------------------------------------------
/public/help/images/cropped-rw-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/cropped-rw-logo.png
--------------------------------------------------------------------------------
/public/help/images/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/delete.png
--------------------------------------------------------------------------------
/public/help/images/distance_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/distance_menu.png
--------------------------------------------------------------------------------
/public/help/images/drag_drop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/drag_drop.png
--------------------------------------------------------------------------------
/public/help/images/education.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/education.png
--------------------------------------------------------------------------------
/public/help/images/email.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/email.png
--------------------------------------------------------------------------------
/public/help/images/health.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/health.png
--------------------------------------------------------------------------------
/public/help/images/housing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/housing.png
--------------------------------------------------------------------------------
/public/help/images/image10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/image10.png
--------------------------------------------------------------------------------
/public/help/images/image12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/image12.png
--------------------------------------------------------------------------------
/public/help/images/job.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/job.png
--------------------------------------------------------------------------------
/public/help/images/legal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/legal.png
--------------------------------------------------------------------------------
/public/help/images/loc-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/loc-search.png
--------------------------------------------------------------------------------
/public/help/images/mapn-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/mapn-logo.png
--------------------------------------------------------------------------------
/public/help/images/mental_health.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/mental_health.png
--------------------------------------------------------------------------------
/public/help/images/open_listing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/open_listing.png
--------------------------------------------------------------------------------
/public/help/images/printer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/printer.png
--------------------------------------------------------------------------------
/public/help/images/resettlement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/resettlement.png
--------------------------------------------------------------------------------
/public/help/images/save_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/save_button.png
--------------------------------------------------------------------------------
/public/help/images/saved_listing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/saved_listing.png
--------------------------------------------------------------------------------
/public/help/images/zoom-controls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/zoom-controls.png
--------------------------------------------------------------------------------
/public/help/images/zoom_to_fit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/public/help/images/zoom_to_fit.png
--------------------------------------------------------------------------------
/public/help/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --msm-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
3 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
4 | sans-serif;
5 | }
6 |
7 | body {
8 | margin: 0;
9 | padding: 0;
10 | font-family: var(--msm-font-family);
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | }
14 |
15 | * {
16 | box-sizing: border-box;
17 | }
18 |
19 | code {
20 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
21 | monospace;
22 | }
23 |
24 | /* li {
25 | list-style: none;
26 | display: flex;
27 | flex-direction: column;
28 | align-items: flex-start;
29 | justify-content: flex-start;
30 | margin-left: 0;
31 | padding-left: 0;
32 | } */
33 |
34 | /* ul {
35 | margin-left: 0;
36 | padding-left: 0;
37 | } */
38 |
39 | a,
40 | h1,
41 | h2,
42 | h3,
43 | h4,
44 | h5,
45 | h6,
46 | i {
47 | color: #8c45cf;
48 | text-decoration: none;
49 | }
50 |
51 | h2 {
52 | font-size: 1rem;
53 | font-weight: 500;
54 | padding: 0.2rem;
55 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
27 | Migrant Service Map
28 |
29 |
30 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/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": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/print.css:
--------------------------------------------------------------------------------
1 | body {
2 | -webkit-print-color-adjust: exact;
3 | }
4 |
5 | @page {
6 | size: portrait;
7 | margin: 0;
8 | }
9 |
10 | .print {
11 | width: 100%;
12 | font-family: -apple-system, BlinkMacSystemFont, sans-serif;
13 | font-size: 18px;
14 | }
15 |
16 | .print .category {
17 | width: 100%;
18 | }
19 |
20 | .print .category .header {
21 | background-color: #e2e2e4;
22 | font-weight: bold;
23 | font-size: 24px;
24 | padding: 5px 15px;
25 | margin-bottom: 30px;
26 | }
27 |
28 | .print .category .provider {
29 | padding-left: 15px;
30 | }
31 |
32 | .print .category .provider * {
33 | margin-bottom: 10px;
34 | }
35 |
36 | .print .category .provider .details > * {
37 | padding-left: 10px;
38 | }
39 |
40 | .print .category .provider .name {
41 | font-weight: bold;
42 | font-size: 22px;
43 | }
44 |
--------------------------------------------------------------------------------
/run-cypress:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Script to run cypress tests using `npm test:cy` or `npm test:cy-start-server`.
4 |
5 | # Upload the test run to the Cypress dashboard iff the record key is present.
6 | # Forks and PR builds do not have access to the record key.
7 | if [ -n "$CYPRESS_RECORD_KEY" ]; then
8 | enable_record=true
9 | else
10 | enable_record=false
11 | fi
12 |
13 | run-cypress-tests() {
14 | wait-on http://localhost:${PORT:-3000} && \
15 | $(npm bin)/cypress run --record $enable_record
16 | }
17 |
18 | if [ "$1" = "--start-server" ]; then
19 | # Run the server and cypress tests at the same time. Kill the server once the
20 | # tests complete, and exit with success only if the tests pass.
21 | concurrently \
22 | -n "server,cypress" \
23 | -k \
24 | -s first \
25 | "BROWSER=none npm start" \
26 | "./run-cypress"
27 | else
28 | run-cypress-tests
29 | fi
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | }
9 |
10 | .App-header {
11 | background-color: #282c34;
12 | min-height: 100vh;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: center;
17 | font-size: calc(10px + 2vmin);
18 | color: white;
19 | }
20 |
21 | .App-link {
22 | color: #61dafb;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from {
27 | transform: rotate(0deg);
28 | }
29 | to {
30 | transform: rotate(360deg);
31 | }
32 | }
33 |
34 | .flex-container {
35 | display: flex;
36 | flex-direction: row;
37 | align-items: flex-start;
38 | justify-items: flex-start;
39 | }
40 |
41 | .section-header {
42 | display: flex;
43 | align-items: center;
44 | justify-items: flex-end;
45 | width: 100%;
46 | flex: 1;
47 | }
48 |
49 | .section-header a,
50 | .section-header i {
51 | padding: 0.3rem;
52 | }
53 |
54 | .section-header:hover {
55 | background-color: #8c45cf;
56 | }
57 |
58 | .section-header:hover a,
59 | .section-header:hover i {
60 | color: white;
61 | }
62 |
63 | .map-container {
64 | position: relative;
65 | /* display: flex;
66 | flex-direction: row;
67 | width: 100%; */
68 | }
69 |
70 | .child-menu {
71 | display: none;
72 | max-height: 380px;
73 | overflow: scroll;
74 | }
75 |
76 | .child-menu.show {
77 | display: block;
78 | }
79 |
80 | .DropdownMenu {
81 | margin-top: 10px;
82 | margin-bottom: 10px;
83 | padding: 0.5rem;
84 | }
85 |
86 | .DropdownMenu span {
87 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
88 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
89 | }
90 |
91 | .popup {
92 | display: flex;
93 | flex-direction: column;
94 | justify-content: space-between;
95 | align-items: flex-start;
96 | width: 320px;
97 | max-height: 320px;
98 | padding: 0 16px 0 16px;
99 | }
100 |
101 | .popup > div {
102 | margin-top: 1rem;
103 | }
104 |
105 | .popup-title {
106 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
107 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
108 | color: #9778b7;
109 | font-size: 22px;
110 | font-weight: 800;
111 | text-transform: uppercase;
112 | }
113 |
114 | .popup-text {
115 | font-size: 16px;
116 | font-weight: 400;
117 | color: #909090;
118 | max-height: 6em;
119 | overflow: scroll;
120 | }
121 |
122 | .bottom-button-bar {
123 | flex: 1;
124 | display: flex;
125 | flex-direction: row-reverse;
126 | align-self: flex-end;
127 | justify-content: space-between;
128 | color: #2699fb;
129 | text-transform: uppercase;
130 | font-weight: bolder;
131 | background-color: white;
132 | }
133 | .popup .bottom-button-bar {
134 | width: 320px;
135 | }
136 |
137 | .statusToggles {
138 | cursor: pointer;
139 | display: flex;
140 | }
141 |
142 | .tooltip {
143 | background-color: #8c45cf !important;
144 | }
145 |
146 | .tooltip::after {
147 | border-bottom-color: #8c45cf !important;
148 | }
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import "core-js/features/array/flat";
2 | import "core-js/features/array/flat-map";
3 |
4 | import React from "react";
5 | import { Provider } from "react-redux";
6 | import { Map, TopBar, TabbedMenu } from "components";
7 | import ReactTooltip from "react-tooltip";
8 |
9 | import store from "redux/store";
10 |
11 | import "./App.css";
12 |
13 | export default function App() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | // Import transitively imports mapbox-gl, which tries to access window.URL.createObjectURL,
4 | // Which doesn't exist in jsdom.
5 | // TODO: Fix or mock out mapbox-gl.
6 | // import App from "./App";
7 |
8 | it.skip("renders without crashing", () => {
9 | const div = document.createElement("div");
10 | // ReactDOM.render(, div);
11 | // ReactDOM.unmountComponentAtNode(div);
12 | });
13 |
--------------------------------------------------------------------------------
/src/assets/distances.js:
--------------------------------------------------------------------------------
1 | // creates a distance constant to be used in the Map and
2 | // the DistanceDropdown so that they are consistent across
3 | // the app
4 | const distances = [0.5, 1, 3, 5];
5 |
6 | export default distances;
7 |
--------------------------------------------------------------------------------
/src/assets/icon-colors.json:
--------------------------------------------------------------------------------
1 | {
2 | "job-placement": "rgb(194,81,16)",
3 | "resettlement": "rgb(63,179,199)",
4 | "health": "rgb(246,128,128)",
5 | "mental-health": "rgb(246,128,128)",
6 | "legal": "rgb(194,83,14)",
7 | "education": "rgb(0,73,179)",
8 | "community-centers": "rgb(37,160,76)",
9 | "cash/food-assistance": "rgb(247,159,104)",
10 | "housing": "rgb(63,179,199)"
11 | }
--------------------------------------------------------------------------------
/src/assets/icons/circle_question.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/circle_question.jpg
--------------------------------------------------------------------------------
/src/assets/icons/community-center.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/community-center.png
--------------------------------------------------------------------------------
/src/assets/icons/education.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/education.png
--------------------------------------------------------------------------------
/src/assets/icons/food-cash-assistance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/food-cash-assistance.png
--------------------------------------------------------------------------------
/src/assets/icons/health.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/health.png
--------------------------------------------------------------------------------
/src/assets/icons/highlightedProviders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/highlightedProviders.png
--------------------------------------------------------------------------------
/src/assets/icons/housing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/housing.png
--------------------------------------------------------------------------------
/src/assets/icons/job-placement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/job-placement.png
--------------------------------------------------------------------------------
/src/assets/icons/key comm ctr.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/key education.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/key food_cash.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/key housing.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/assets/icons/key job placement.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/key legal.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/assets/icons/key mental health.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/assets/icons/key resettlement.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/assets/icons/legal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/legal.png
--------------------------------------------------------------------------------
/src/assets/icons/mental-health.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/mental-health.png
--------------------------------------------------------------------------------
/src/assets/icons/pin cash.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/pin comm ctr.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/assets/icons/pin education.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/pin housing.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/pin job placement.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/pin legal.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/pin mental health.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/pin resettlement.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/pin-blank-clusters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-blank-clusters.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-blank-clusters.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/src/assets/icons/pin-blank-clusters_multi-highlights.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-blank-clusters_multi-highlights.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-blank-clusters_multi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-blank-clusters_multi.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-cash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-cash.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-comm-ctr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-comm-ctr.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-education.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-education.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-health.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-health.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-highlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-highlight.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-highlight.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/src/assets/icons/pin-housing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-housing.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-hover.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-job-placement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-job-placement.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-legal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-legal.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-mental-health.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-mental-health.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-mini.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-mini.png
--------------------------------------------------------------------------------
/src/assets/icons/pin-mini.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/src/assets/icons/pin-resettlement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/pin-resettlement.png
--------------------------------------------------------------------------------
/src/assets/icons/resettlement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeforboston/migrant_service_map/b5ddce4c48546ed14f8fbb9344a9bdc48ea0ac9e/src/assets/icons/resettlement.png
--------------------------------------------------------------------------------
/src/assets/images.js:
--------------------------------------------------------------------------------
1 | const typeImages = [
2 | {
3 | type: "job-placement",
4 | image: require("./icons/pin-job-placement.png")
5 | },
6 | {
7 | type: "community-centers",
8 | image: require("./icons/pin-comm-ctr.png")
9 | },
10 | {
11 | type: "cash/food-assistance",
12 | image: require("./icons/pin-cash.png")
13 | },
14 | {
15 | type: "education",
16 | image: require("./icons/pin-education.png")
17 | },
18 | {
19 | type: "resettlement",
20 | image: require("./icons/pin-resettlement.png")
21 | },
22 | {
23 | type: "housing",
24 | image: require("./icons/pin-housing.png")
25 | },
26 | {
27 | type: "health",
28 | image: require("./icons/pin-health.png")
29 | },
30 | {
31 | type: "mental-health",
32 | image: require("./icons/pin-mental-health.png")
33 | },
34 | {
35 | type: "legal",
36 | image: require("./icons/pin-legal.png")
37 | },
38 | {
39 | type: "highlighted",
40 | image: require("./icons/pin-highlight.png")
41 | },
42 | {
43 | type: "hovered",
44 | image: require("./icons/pin-hover.png")
45 | },
46 | {
47 | type: "clusters",
48 | image: require("./icons/pin-blank-clusters.png")
49 | },
50 | {
51 | type: "clusters-multi",
52 | image: require("./icons/pin-blank-clusters_multi.png")
53 | },
54 | {
55 | type: "clusters-multi-highlighted",
56 | image: require("./icons/pin-blank-clusters_multi-highlights.png")
57 | },
58 | {
59 | type: "pin-mini",
60 | image: require("./icons/pin-mini.png")
61 | }
62 | ];
63 |
64 | const keyImages = [
65 | {
66 | type: "job-placement",
67 | image: require("./icons/key job placement.svg")
68 | },
69 | {
70 | type: "community-centers",
71 | image: require("./icons/key comm ctr.svg")
72 | },
73 | {
74 | type: "cash/food-assistance",
75 | image: require("./icons/key food_cash.svg")
76 | },
77 | {
78 | type: "education",
79 | image: require("./icons/key education.svg")
80 | },
81 | {
82 | type: "resettlement",
83 | image: require("./icons/key resettlement.svg")
84 | },
85 | {
86 | type: "housing",
87 | image: require("./icons/key housing.svg")
88 | },
89 | {
90 | type: "health",
91 | image: require("./icons/key health.svg")
92 | },
93 | {
94 | type: "mental-health",
95 | image: require("./icons/key mental health.svg")
96 | },
97 | {
98 | type: "legal",
99 | image: require("./icons/key legal.svg")
100 | },
101 | ];
102 |
103 | export { keyImages };
104 |
105 | export default typeImages;
106 |
--------------------------------------------------------------------------------
/src/assets/info-icon.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/assets/visa-types.json:
--------------------------------------------------------------------------------
1 | [
2 | "Temporary Agricultural Worker H-2A",
3 | "H-1B",
4 | "Permanent Resident Card (I-551)",
5 | "Advance Parole (I-512)",
6 | "Demo Type 1 (D1)",
7 | "Demo Type 2 (D2)",
8 | "Demo Type 3 (D3)",
9 | "Demo Type 4 (D4)",
10 | "Demo Type 5 (D5)",
11 | "Demo Type 6 (D6)"
12 | ]
--------------------------------------------------------------------------------
/src/components/AnimatedMarker/animated-marker.js:
--------------------------------------------------------------------------------
1 | import typeImages from "assets/images";
2 | import mapboxgl from "mapbox-gl";
3 | import { bboxPolygon, point, booleanPointInPolygon } from "@turf/turf";
4 |
5 | class AnimatedMarker {
6 | constructor(provider) {
7 | this.provider = provider;
8 | this.markerElement = this.createMarkerElement(this.provider.id, this.provider.typeId);
9 | this.element = this.markerElement.element;
10 | this.markerIcon = this.markerElement.icon;
11 | this.markerIconHighlight = this.markerElement.highlight;
12 | this.marker = new mapboxgl.Marker({
13 | element: this.element
14 | }).setLngLat(provider.coordinates);
15 | }
16 |
17 | isInView = (map) => {
18 | const mapBoundsArray = map.getBounds().toArray().flat();
19 | const poly = bboxPolygon(mapBoundsArray);
20 | const pt = point(this.provider.coordinates);
21 | return booleanPointInPolygon(pt, poly);
22 | };
23 |
24 | bounceIcon = () => {
25 | this.markerIcon.classList.add("bounceOn");
26 | this.markerIconHighlight.classList.add("bounceOn");
27 | this.markerIconHighlight.classList.add("highlightOn");
28 | this.markerIcon.addEventListener("animationend", this.remove);
29 | };
30 |
31 | addTo(map) {
32 | this.marker.addTo(map);
33 | if (this.isInView(map)) {
34 | this.bounceIcon()
35 | } else {
36 | map.once('moveend', this.bounceIcon)
37 | }
38 | }
39 |
40 | remove = () => {
41 | this.markerIcon.removeEventListener("animationend", this.remove);
42 | this.marker.remove();
43 | };
44 |
45 | createMarkerElement = (providerId, typeId) => {
46 | const element = document.createElement("div");
47 | element.id = `marker-${providerId}`;
48 | element.className = "marker";
49 | const icon = document.createElement("img");
50 | icon.id = `marker-icon-${providerId}`;
51 | icon.src = this.getPin(typeId);
52 | icon.className = "baseState";
53 | const highlight = document.createElement("img");
54 | highlight.src = require("../../assets/icons/pin-highlight.svg");
55 | highlight.className = "marker-highlight";
56 | const highlightContainer = document.createElement("div");
57 | highlightContainer.className = "marker-highlight-container";
58 | highlightContainer.appendChild(highlight);
59 | element.appendChild(icon);
60 | element.appendChild(highlightContainer);
61 | return { element, icon, highlight };
62 | };
63 |
64 | getPin = typeId => {
65 | const pinImage = typeImages.filter(item => item.type === typeId);
66 | return pinImage[0].image;
67 | };
68 | }
69 |
70 | export { AnimatedMarker };
71 |
--------------------------------------------------------------------------------
/src/components/ClusterProviderList/cluster-provider-list.css:
--------------------------------------------------------------------------------
1 | #clusterList-inner {
2 | background-color: white;
3 | padding: 15px;
4 | border: 1px solid #D7ADFF;
5 | border-radius: 8px;
6 | }
7 |
8 | .image {
9 | display: inline-block;
10 | width: 20px;
11 | height: 20px;
12 | vertical-align: top;
13 | }
14 |
15 | .item {
16 | color: #8c45cf;
17 | font-weight: 400;
18 | font-size: 15px;
19 | vertical-align: top;
20 | display: inline-block;
21 | width: 20em;
22 | overflow: hidden;
23 | white-space: nowrap;
24 | text-overflow: ellipsis;
25 | padding: 0px 6px;
26 | line-height: 1.5em;
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/ClusterProviderList/cluster-provider-list.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import './cluster-provider-list.css'
3 | import { keyImages } from "../../assets/images.js";
4 |
5 | class ClusterList extends Component {
6 |
7 | getImage = typeId => {
8 | return keyImages.filter(image => image.type === typeId)
9 | }
10 |
11 | render() {
12 | const maxProviders = 9
13 | const listLength = this.props.list.length
14 | const firstNineProviders = this.props.list.slice(0, maxProviders)
15 | return (
16 |
17 |
18 | {this.props.list.for}
19 | {firstNineProviders.map(
20 | (item, i) => {
21 | return (
22 |
23 |
[0].image})
24 |
{item.name}
25 |
26 | )
27 | }
28 | )}
29 | {listLength > maxProviders &&
30 |
31 |
{listLength - maxProviders} more...
32 |
}
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | export { ClusterList }
40 |
--------------------------------------------------------------------------------
/src/components/DetailsPane/details-pane.css:
--------------------------------------------------------------------------------
1 | .details-pane .provider-info {
2 | margin: 0 0 10px 0;
3 | overflow: hidden;
4 | padding: 5px 0;
5 | }
6 |
7 | .details-pane .provider-details-info,
8 | .details-pane .popup-text {
9 | padding-left: 5px;
10 | }
11 |
12 | .details-pane .missions {
13 | white-space: nowrap;
14 | overflow: hidden;
15 | text-overflow: ellipsis;
16 | font-size: 15px;
17 | padding: 10px 0px 10px 0px;
18 | cursor: pointer;
19 | }
20 |
21 | .details-pane .missions.expanded {
22 | white-space: initial;
23 | }
24 |
25 | .details-pane .missions-expander {
26 | float: right;
27 | font-weight: bold;
28 | font-size: 10px;
29 | color: royalblue;
30 | cursor: pointer;
31 | width: 100%;
32 | text-align: center;
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/DetailsPane/detals-pane.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Row } from "simple-flexbox";
3 | import "./details-pane.css";
4 | import ProviderDetailsInfo from "./provider-details-info";
5 |
6 | export default class DetailsPane extends React.Component {
7 | state = { isMissionTextExpanded: false };
8 |
9 | onMissionTextExpanderClicked = e => {
10 | e.stopPropagation();
11 | const { isMissionTextExpanded } = this.state;
12 |
13 | this.setState({ isMissionTextExpanded: !isMissionTextExpanded });
14 | };
15 |
16 | render() {
17 | const {
18 | provider: { email, address, website, telephone, mission },
19 | flyToProvider
20 | } = this.props;
21 | const { isMissionTextExpanded } = this.state;
22 | return (
23 |
24 |
25 |
26 |
31 | {email}
32 |
33 |
34 |
35 |
36 | {website}
37 |
38 |
39 |
40 |
41 |
46 | {address || "address"}
47 |
48 |
49 | {telephone}
50 |
51 |
52 |
this.onMissionTextExpanderClicked(e)}
55 | >
56 | {mission}
57 |
58 |
this.onMissionTextExpanderClicked(e)}
61 | >
62 | SHOW {isMissionTextExpanded ? "LESS" : "MORE"}
63 |
64 |
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/DetailsPane/index.js:
--------------------------------------------------------------------------------
1 | import DetailsPane from "./detals-pane";
2 |
3 | export default DetailsPane;
4 |
--------------------------------------------------------------------------------
/src/components/DetailsPane/provider-details-info.css:
--------------------------------------------------------------------------------
1 | .provider-details-info {
2 | display: flex;
3 | font-size: 10px;
4 | min-width: 0px;
5 | flex-grow: 1;
6 | flex-shrink: 1;
7 | flex-basis: 50%;
8 | margin: 5px;
9 | }
10 |
11 | .provider-details-info .ellipsis {
12 | white-space: nowrap;
13 | text-overflow: ellipsis;
14 | overflow: hidden;
15 | }
16 |
17 | .provider-details-info .provider-details-icon {
18 | margin-right: 0.5em;
19 | }
20 |
21 | .provider-details-icon {
22 | color: var(--msm-purple)
23 | }
24 |
25 | .provider-details-info .providers-detail-content {
26 | color: royalblue;
27 | text-overflow: ellipsis;
28 | overflow: hidden;
29 | cursor: pointer;
30 | }
31 |
32 | .provider-details-info .providers-detail-content a:link {
33 | color: royalblue;
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/DetailsPane/provider-details-info.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Column } from "simple-flexbox";
3 | import "./provider-details-info.css";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faEnvelope, faMapPin, faGlobe, faPhone } from "@fortawesome/free-solid-svg-icons";
6 |
7 | export default class ProviderDetailsInfo extends React.Component {
8 | iconsByType = {
9 | "email": faEnvelope,
10 | "address": faMapPin,
11 | "website": faGlobe,
12 | "phone": faPhone,
13 | }
14 |
15 | onAddressClicked = (e) => {
16 | e.stopPropagation();
17 | if (this.props.onClick) {
18 | this.props.onClick();
19 | }
20 | };
21 |
22 | render() {
23 | const { label, type, ellipsis } = this.props;
24 | return (
25 |
26 |
31 |
36 |
37 | {label.toUpperCase()}
38 |
39 | {this.props.children}
40 |
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/Dropdowns/checkbox-dropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Expandable from "./expandable";
3 |
4 | export default class CheckBoxDropdown extends React.Component {
5 | optionsMappings = {};
6 |
7 | constructor(props) {
8 | super(props);
9 | const { options = [] } = props;
10 | options.forEach(option => (this.optionsMappings[option.id] = false));
11 | }
12 |
13 | render() {
14 | const {
15 | className,
16 | options,
17 | footer,
18 | header,
19 | expanded,
20 | setExpanded,
21 | visibleTypes,
22 | onChange = () => {}
23 | } = this.props;
24 | const inputDiv = options.map((option, index) => {
25 | const { display, id } = option;
26 | return (
27 |
28 | {
35 | this.optionsMappings[id] = checked;
36 | onChange(
37 | id,
38 | options.filter(option => this.optionsMappings[option.id])
39 | );
40 | }}
41 | />
42 |
45 |
46 | );
47 | });
48 |
49 | return (
50 |
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Dropdowns/expandable.css:
--------------------------------------------------------------------------------
1 | .expandable-container {
2 | /* width: 100%; */
3 | color: #8c45cf;
4 | overflow: visible;
5 | position: relative;
6 | z-index: 100;
7 | }
8 |
9 | .expandable-container .expanded {
10 | transition: all 1s ease-out;
11 | }
12 |
13 | .expandable-container .expandable-content-wrapper {
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: stretch;
17 | padding: 0 1rem 1rem 1rem;
18 | border-radius: 2px;
19 | border: 1px solid #d7adff;
20 | box-shadow: 1px 3px 4px rgba(96, 1, 185, 0.25);
21 | }
22 |
23 | .expandable-container .expandable-content-wrapper.expanded {
24 | height: auto;
25 | }
26 |
27 | .expandable-container .expandable-header {
28 | padding: 0 5px;
29 | height: 100%;
30 | }
31 |
32 | .expandable-container .dropdown-input-wrapper {
33 | display: flex;
34 | justify-content: flex-start;
35 | margin-top: 0.5rem;
36 | }
37 |
38 | .help-container a:hover h2,
39 | .expandable-container .dropdown-input-wrapper:hover {
40 | color: #202020;
41 | background-color: #ead3ff;
42 | }
43 |
44 | .expandable-container .dropdown-input-wrapper .expandable-label {
45 | width: 100%;
46 | cursor: pointer;
47 | }
48 |
49 | .expandable-container .expanded-content {
50 | height: 0px;
51 | opacity: 0;
52 | display: flex;
53 | flex-direction: column;
54 | pointer-events: none;
55 | }
56 |
57 | .expandable-container .expanded-content.expanded {
58 | height: auto;
59 | max-height: 300px;
60 | opacity: 1;
61 | pointer-events: auto;
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/Dropdowns/expandable.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./expandable.css";
3 |
4 | export default function Expandable({
5 | className,
6 | content,
7 | footer,
8 | header,
9 | expanded = false,
10 | setExpanded,
11 | onSelect = () => {}
12 | }) {
13 | return (
14 | setExpanded(true)}
17 | onMouseLeave={() => setExpanded(false)}
18 | >
19 |
24 |
setExpanded(!expanded)}
26 | className="expandable-header"
27 | >
28 | {header}
29 |
30 |
34 | {content}
35 |
36 |
37 | {footer}
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Dropdowns/radio-button-dropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Expandable from "./expandable";
3 |
4 | export default class RadioButtonDropdown extends React.Component {
5 | render() {
6 | const {
7 | className,
8 | options,
9 | header,
10 | selected,
11 | expanded,
12 | setExpanded,
13 | onChange = () => {}
14 | } = this.props;
15 | const inputDiv = options.map((option, index) => {
16 | const { value, text } = option;
17 | return (
18 |
19 | onChange(value)}
25 | checked={value === selected}
26 | />
27 |
30 |
31 | );
32 | });
33 |
34 | return (
35 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/Map/geocoder.container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import Geocoder from "./geocoder";
4 | import { setSearchResult, clearSearchResult } from "redux/actions";
5 |
6 | const GeocoderContainer = props => {
7 | return ;
8 | };
9 |
10 | const mapStateToProps = state => {
11 | return {
12 | searchProximityCoordinates: state.search.coordinates,
13 | };
14 | };
15 |
16 | const mapDispatchToProps = dispatch => {
17 | return {
18 | setSearchResult: (searchCoordinates, mapboxId, searchText) => {
19 | dispatch(setSearchResult(searchCoordinates, mapboxId, searchText));
20 | },
21 | clearSearchResult: () => {
22 | dispatch(clearSearchResult());
23 | },
24 | };
25 | };
26 |
27 | export default connect(
28 | mapStateToProps,
29 | mapDispatchToProps
30 | )(GeocoderContainer);
31 |
--------------------------------------------------------------------------------
/src/components/Map/geocoder.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import mapboxgl from "./mapbox-gl-wrapper";
3 | import MapboxGeocoder from "@mapbox/mapbox-gl-geocoder";
4 |
5 | const SPECIAL_NO_RESULTS_ID = "notfound.0";
6 |
7 | // Approximate bounding box of Massachusetts.
8 | const boundingBox = [-73.56055, 41.158671, -69.80923, 42.994435];
9 |
10 | export default class Geocoder extends Component {
11 | constructor(props) {
12 | super(props);
13 |
14 | this.geocoderContainerRef = React.createRef();
15 | }
16 |
17 | componentDidMount() {
18 | const geocoder = (this.geocoder = new MapboxGeocoder({
19 | accessToken: mapboxgl.accessToken,
20 | proximity: {
21 | longitude: this.props.searchProximityCoordinates[0],
22 | latitude: this.props.searchProximityCoordinates[1]
23 | },
24 | placeholder: "Location",
25 | marker: false,
26 | flyTo: false,
27 | bbox: boundingBox
28 | }));
29 |
30 | const searchBox = geocoder.onAdd(null);
31 | searchBox.className += " msm-map-search-box";
32 | this.geocoderContainerRef.current.appendChild(searchBox);
33 |
34 | geocoder.on("results", ev => {
35 | /* Fun hack to show "no results found" in the search box. This solution depends on the implementation of
36 | * this specific version of the geocoder.
37 | *
38 | * You can see that the response passed to the 'results' event is then used to set the dropdown result:
39 | * https://github.com/mapbox/mapbox-gl-geocoder/blob/d2db50aede1ef6777083435f2dc533d5e1846a7e/lib/index.js#L203
40 | *
41 | * Typeahead instances render suggestions via method getItemValue:
42 | * https://github.com/tristen/suggestions/blob/9328f1f3d21598c40014892e3e0329027dd2b538/src/suggestions.js#L221
43 | *
44 | * Geocoder overrides getItemValue to look at the "place_name" property:
45 | * https://github.com/mapbox/mapbox-gl-geocoder/blob/d2db50aede1ef6777083435f2dc533d5e1846a7e/lib/index.js#L103
46 | *
47 | * Geocoder API response object documentation:
48 | * https://docs.mapbox.com/api/search/#geocoding-response-object
49 | */
50 | if (!ev.features || !ev.features.length) {
51 | ev.features = [
52 | {
53 | id: SPECIAL_NO_RESULTS_ID,
54 | place_name: "No search results"
55 | }
56 | ];
57 | }
58 | });
59 |
60 | geocoder.on("result", ev => {
61 | // ev.result contains id, place_name, text
62 |
63 | // Check for no results.
64 | if (ev.result.id === SPECIAL_NO_RESULTS_ID) {
65 | geocoder._clear();
66 | return;
67 | };
68 |
69 | const {
70 | geometry: { coordinates: searchCoordinates },
71 | id: mapboxId,
72 | text: searchText
73 | } = ev.result;
74 |
75 | this.props.setSearchResult(searchCoordinates, mapboxId, searchText);
76 | });
77 |
78 | geocoder.on("clear", ev => {
79 | this.props.clearSearchResult();
80 | });
81 | }
82 |
83 | componentWillUnmount() {
84 | this.geocoder.onRemove();
85 | }
86 |
87 | render() {
88 | return ;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/Map/index.js:
--------------------------------------------------------------------------------
1 | import Map from "./map.container";
2 |
3 | export { default as Geocoder } from "./geocoder.container";
4 | export { point, transformTranslate, circle } from "@turf/turf";
5 | export { getHighlightedProviders, getProvidersSorted } from "redux/selectors";
6 | export { displayProviderInformation } from "redux/actions.js";
7 | export default Map;
8 |
--------------------------------------------------------------------------------
/src/components/Map/map.container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import {
4 | initializeVisaFilter,
5 | displayProviderInformation,
6 | selectProvider
7 | } from "redux/actions";
8 | import { getMapProviders } from "redux/selectors";
9 | import Map from "./map";
10 |
11 | const MapContainer = props => {
12 | return ;
13 | };
14 |
15 | const mapStateToProps = state => {
16 | return {
17 | visibleProviders: getMapProviders(state),
18 | loadedProviderTypeIds: state.providerTypes.allIds,
19 | highlightedProviders: state.highlightedProviders,
20 | filters: state.filters,
21 | search: state.search,
22 | hoveredProvider: state.hoveredProvider,
23 | selectProviderId: state.providers.selectProviderId,
24 | selectProviderKey: state.providers.selectProviderKey
25 | };
26 | };
27 |
28 | const mapDispatchToProps = dispatch => {
29 | return {
30 | initializeVisaFilter: visas => {
31 | dispatch(initializeVisaFilter(visas));
32 | },
33 | displayProviderInformation: id => {
34 | dispatch(displayProviderInformation(id));
35 | },
36 | selectProvider: id => {
37 | dispatch(selectProvider(id))
38 | }
39 | };
40 | };
41 |
42 | export default connect(
43 | mapStateToProps,
44 | mapDispatchToProps
45 | )(MapContainer);
46 |
--------------------------------------------------------------------------------
/src/components/Map/map.css:
--------------------------------------------------------------------------------
1 | .map {
2 | /* Cross-browser compatability issues:
3 | ** Proper map display in Chrome, Safari, and Firefox
4 | ** requires '-webkit-fill-available' + absolute positioning
5 | ** (prevents map height collapsing to 0)
6 | */
7 | height: 100%; /* take up full height of flex'ed */
8 | height: -webkit-fill-available;
9 | width: 100%;
10 | z-index: 0;
11 | position: absolute;
12 | }
13 |
14 | .grow {
15 | overflow: visible;
16 | }
17 |
18 | .distance-marker {
19 | justify-content: center;
20 | align-items: center;
21 | text-align: center;
22 | display: flex;
23 |
24 | padding: 5px;
25 | border-radius: 5px;
26 |
27 | font-family: var(--msm-font-family);
28 | font-weight: bold;
29 | font-size: 16px;
30 | line-height: normal;
31 |
32 | background-color: #d561b5;
33 | color: white;
34 | box-shadow: 0px 3px 6px #0000005e;
35 | /* Creates a border around the text. */
36 | text-shadow: -1px 0 #971172, 0 1px #971172, 1px 0 #971172, 0 -1px #971172;
37 | }
38 |
39 | .marker {
40 | cursor: pointer;
41 | }
42 |
43 | .baseState {
44 | display: block;
45 | width: 40%;
46 | height: 40%;
47 | transform-origin: bottom;
48 | margin: auto;
49 | cursor: pointer;
50 | }
51 |
52 | .marker-highlight-container {
53 | position: relative;
54 | height: 70px;
55 | width: 100%;
56 | margin: 0% 0 -100% 0;
57 | -webkit-transform-origin: bottom;
58 | transform-origin: bottom;
59 | padding-bottom: 100%;
60 | }
61 |
62 | .marker-highlight {
63 | display: block;
64 | position: absolute;
65 | width: 100%;
66 | height: 62%;
67 | -webkit-transform-origin: bottom;
68 | transform-origin: bottom;
69 | margin: auto;
70 | bottom: 100%;
71 | opacity: 0.5;
72 | }
73 |
74 | .highlightOn {
75 | animation: highlightColor linear 3s;
76 | }
77 |
78 | .bounceOn {
79 | animation: changeheight ease 1s 2 both;
80 | }
81 |
82 | .bounceOff {
83 | animation: shimmy linear 1.5s forwards;
84 | }
85 |
86 | @keyframes highlightColor {
87 | 0% {
88 | opacity: 0;
89 | }
90 | 100% {
91 | opacity: 1;
92 | }
93 | }
94 |
95 | @keyframes changeheight {
96 | 0% {
97 | transform: scale(1);
98 | }
99 | 20%,
100 | 25% {
101 | transform: scale(1.2);
102 | }
103 | 50% {
104 | transform: scale(1);
105 | }
106 | 90%,
107 | 100% {
108 | transform: scale(1);
109 | }
110 | }
111 |
112 | .mapboxgl-popup-content {
113 | font-family: inherit;
114 | font-weight: 400;
115 | font-size: 1.2em;
116 | color: white;
117 | background: #8c45cf;
118 | border: 2px solid #8c45cf;
119 | }
120 |
121 | .mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
122 | -webkit-align-self: center;
123 | align-self: center;
124 | border-top: none;
125 | border-bottom-color: #8c45cf;
126 | }
127 |
128 | .mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
129 | -webkit-align-self: flex-start;
130 | align-self: flex-start;
131 | border-top: none;
132 | border-left: none;
133 | border-bottom-color: #8c45cf;
134 | }
135 |
136 | .mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
137 | -webkit-align-self: flex-end;
138 | align-self: flex-end;
139 | border-top: none;
140 | border-right: none;
141 | border-bottom-color: #8c45cf;
142 | }
143 |
144 | .mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
145 | -webkit-align-self: center;
146 | align-self: center;
147 | border-bottom: none;
148 | border-top-color: #8c45cf;
149 | }
150 |
151 | .mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
152 | -webkit-align-self: flex-start;
153 | align-self: flex-start;
154 | border-bottom: none;
155 | border-left: none;
156 | border-top-color: #8c45cf;
157 | }
158 |
159 | .mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
160 | -webkit-align-self: flex-end;
161 | align-self: flex-end;
162 | border-bottom: none;
163 | border-right: none;
164 | border-top-color: #8c45cf;
165 | }
166 |
167 | .mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
168 | -webkit-align-self: center;
169 | align-self: center;
170 | border-left: none;
171 | border-right-color: #8c45cf;
172 | }
173 |
174 | .mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
175 | -webkit-align-self: center;
176 | align-self: center;
177 | border-right: none;
178 | border-left-color: #8c45cf;
179 | }
180 |
--------------------------------------------------------------------------------
/src/components/Map/mapbox-gl-wrapper.js:
--------------------------------------------------------------------------------
1 | import mapboxgl from "mapbox-gl";
2 |
3 | mapboxgl.accessToken =
4 | "pk.eyJ1IjoicmVmdWdlZXN3ZWxjb21lIiwiYSI6ImNqZ2ZkbDFiODQzZmgyd3JuNTVrd3JxbnAifQ.UY8Y52GQKwtVBXH2ssbvgw";
5 |
6 | export default mapboxgl;
7 |
--------------------------------------------------------------------------------
/src/components/Map/utilities.js:
--------------------------------------------------------------------------------
1 | import mapboxgl from "./mapbox-gl-wrapper";
2 | import memoizeOne from "memoize-one";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import { faMapMarkerAlt, faMapMarker } from "@fortawesome/free-solid-svg-icons";
5 | import ReactDOM from "react-dom";
6 | import React from "react";
7 |
8 | const convertProvidersToGeoJSON = providers => {
9 | return providers.map(provider => ({
10 | type: "Feature",
11 | geometry: {
12 | type: "Point",
13 | coordinates: provider.coordinates
14 | },
15 | properties: provider
16 | }));
17 | };
18 |
19 | const createCenterMarker = () => {
20 | const centerMarker = document.createElement("div");
21 | ReactDOM.render(
22 |
23 |
29 |
30 | ,
31 | centerMarker
32 | );
33 | return centerMarker;
34 | };
35 |
36 | const createDistanceMarker = distance => {
37 | const markerElement = document.createElement("div");
38 | markerElement.className = "distance-marker";
39 | markerElement.id = "marker-" + distance + "-miles";
40 | markerElement.innerText = distance + (distance > 1 ? " miles" : " mile");
41 | return markerElement;
42 | };
43 |
44 | const removeDistanceMarkers = markerArray => {
45 | // const distanceMarkers = Array.from(document.getElementsByClassName("distanceMarker"));
46 | return markerArray.map(marker => marker.remove());
47 | };
48 |
49 | const getProviderBoundingBox = (providersById, providerIds) => {
50 | return getBoundingBox(
51 | lookupProviders(providersById, providerIds).map(
52 | provider => provider.coordinates
53 | )
54 | );
55 | };
56 |
57 | const getBoundingBox = allCoordinates => {
58 | const bounds = new mapboxgl.LngLatBounds();
59 | allCoordinates.forEach(c => bounds.extend(c));
60 | return bounds;
61 | };
62 |
63 | /** Looks up all providers in the given map with an id in the given array. */
64 | const lookupProviders = (providersById, ids) =>
65 | filterProviderIds(providersById, ids).map(id => providersById[id]);
66 |
67 | /** Filters the given ids down to just those that appear in the map from id's to providers. */
68 | const filterProviderIds = (providersById, ids) =>
69 | ids.filter(id => providersById.hasOwnProperty(id));
70 |
71 | /** Converts an array of providers to a map from id to provider */
72 | const providersById = memoizeOne(providers => {
73 | const byId = {};
74 | providers.map(provider => (byId[provider.id] = provider));
75 | return byId;
76 | });
77 |
78 | export {
79 | convertProvidersToGeoJSON,
80 | createCenterMarker,
81 | createDistanceMarker,
82 | removeDistanceMarkers,
83 | getProviderBoundingBox,
84 | getBoundingBox,
85 | lookupProviders,
86 | filterProviderIds,
87 | providersById
88 | };
89 |
--------------------------------------------------------------------------------
/src/components/MenuAcceptingNewFilter/index.js:
--------------------------------------------------------------------------------
1 | import MenuAcceptingNewFilter from "./menu-accepting-new-filter";
2 |
3 | export default MenuAcceptingNewFilter;
4 |
--------------------------------------------------------------------------------
/src/components/MenuAcceptingNewFilter/menu-accepting-new-filter.css:
--------------------------------------------------------------------------------
1 | ul.dropdown-list {
2 | display: block;
3 | /* flex-wrap: wrap; */
4 | width: 100%;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/MenuAcceptingNewFilter/menu-accepting-new-filter.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "../ProviderList/provider-list.css";
3 | import Toggle from "../toggle.js";
4 | class MenuAcceptingNewFilter extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | toggleOn: false
9 | };
10 | }
11 | toggleToggle = () => {
12 | let toggleOn = this.state.toggleOn;
13 | toggleOn = !toggleOn;
14 | this.setState({ toggleOn });
15 | };
16 |
17 | render() {
18 | const backerColor = this.state.toggleOn ? "#8c45cf" : "white";
19 | const backerSecondaryColor = this.state.toggleOn ? "white" : "gray";
20 | return (
21 |
34 |
46 | NEW
47 |
48 |
Accepting New Clients
49 |
53 |
54 | );
55 | }
56 | }
57 |
58 | export default MenuAcceptingNewFilter;
59 |
60 | //
61 | //
62 | //
Distance Filter
63 | //
64 | //
65 | //
87 | //
88 |
--------------------------------------------------------------------------------
/src/components/MenuDistanceFilter/index.js:
--------------------------------------------------------------------------------
1 | import MenuDistanceFilter from "./menu-distance-filter";
2 |
3 | export default MenuDistanceFilter;
4 |
--------------------------------------------------------------------------------
/src/components/MenuDistanceFilter/menu-distance-filter.css:
--------------------------------------------------------------------------------
1 | ul.dropdown-list {
2 | display: block;
3 | /* flex-wrap: wrap; */
4 | width: 100%;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/MenuDistanceFilter/menu-distance-filter.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "components/ProviderList/provider-list.css";
3 |
4 | class DistanceFilter extends Component {
5 | render() {
6 | const distances = [1, 2, 5];
7 | return (
8 |
9 |
10 |
Distance Filter
11 |
12 |
13 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default DistanceFilter;
46 |
--------------------------------------------------------------------------------
/src/components/MenuDropdown/index.js:
--------------------------------------------------------------------------------
1 | import MenuDropdown from "./menu-dropdown";
2 |
3 | export default MenuDropdown;
4 |
--------------------------------------------------------------------------------
/src/components/MenuDropdown/menu-dropdown.css:
--------------------------------------------------------------------------------
1 | .dropdown-menu {
2 | width: 100%;
3 | }
4 | .dropdown-menu.closed {
5 | padding-bottom: 8px;
6 | }
7 |
8 | .dropdown-menu div {
9 | flex: 1 0 auto;
10 | }
11 | .dropdown-menu svg {
12 | align-self: center;
13 | flex: 0 0 16px;
14 | transition: 0.2s transform ease-out;
15 | }
16 | .dropdown-menu.closed svg {
17 | transform: rotate(-90deg);
18 | }
--------------------------------------------------------------------------------
/src/components/MenuDropdown/menu-dropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Row } from "simple-flexbox";
3 | import "./menu-dropdown.css";
4 |
5 | let triangle =
6 |
7 | export default function MenuDropdown({
8 | text,
9 | children,
10 | collapsed,
11 | collapsible,
12 | handleToggle
13 | }) {
14 | return (
15 | <>
16 |
20 | {text}
21 | {collapsible && triangle}
22 |
23 | {(!collapsible || !collapsed) && children}
24 | >
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/MenuDropdownItem/index.js:
--------------------------------------------------------------------------------
1 | import MenuDropdownItem from "./memu-dropdown-item.container";
2 |
3 | export default MenuDropdownItem;
4 |
--------------------------------------------------------------------------------
/src/components/MenuDropdownItem/memu-dropdown-item.container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import {
4 | setHoveredProvider
5 | } from "redux/actions";
6 | import DropdownMenuItem from "./menu-dropdown-item";
7 |
8 | const MenuDropdownItemContainer = props => {
9 | return ;
10 | };
11 |
12 | const mapDispatchToProps = dispatch => {
13 | return {
14 | setHovered: id => {
15 | dispatch(setHoveredProvider(id));
16 | }
17 | };
18 | };
19 |
20 | export default connect(
21 | null,
22 | mapDispatchToProps
23 | )(MenuDropdownItemContainer);
24 |
--------------------------------------------------------------------------------
/src/components/MenuDropdownItem/menu-dropdown-item.css:
--------------------------------------------------------------------------------
1 | /* .details-pane .provider-info {
2 | border: solid 1px #e2d5ed;
3 | margin: 5px 0;
4 | overflow: hidden;
5 | padding: 5px 0;
6 | background-color: #f0ebfb;
7 | }
8 | .popup-info,
9 | .popup-text {
10 | padding-left: 5px;
11 | }
12 | .details-pane .popup-text {
13 | white-space: nowrap;
14 | overflow: hidden;
15 | text-overflow: ellipsis;
16 | }
17 | */
18 | .provider-card {
19 | display: flex;
20 | width: 100%;
21 | flex-flow: row nowrap;
22 | background-color: #FFFFFF;
23 | border: 2px solid #C8A2EB;
24 | border-radius: 4px;
25 | box-shadow: 1px 3px 4px rgba(96, 1, 185, 0.25);
26 | outline: none;
27 | user-select: none;
28 | overflow: hidden;
29 | }
30 |
31 | .provider-card * {
32 | -webkit-user-select: text; /* Chrome all / Safari all */
33 | -moz-user-select: text; /* Firefox all */
34 | -ms-user-select: text; /* IE 10+ */
35 | user-select: text; /* Likely future */
36 | }
37 |
38 | .provider-card.card-expanded {
39 | background: #F9F2FF;
40 | border: 2px solid #8C45CF;
41 | box-sizing: border-box;
42 | box-shadow: 1px 3px 4px rgba(184, 147, 217, 0.4);
43 | }
44 |
45 | .provider-card.card-expanded:hover {
46 | box-shadow: 3px 5px 8px rgba(184, 147, 217, 0.8);
47 | }
48 |
49 | .provider-card.card-wrapped {
50 | background: #FFFFFF;
51 | border: 2px solid #DCCFE8;
52 | box-shadow: 1px 3px 4px rgba(96, 1, 185, 0.25);
53 | }
54 |
55 | .provider-card.card-wrapped:hover {
56 | border: 2px solid #C8A2EB;
57 | box-shadow: 1px 3px 4px rgba(110, 2, 207, 0.4);
58 | }
59 |
60 | .provider-card.card-expanded .card-draggable-icon {
61 | color: #A36DD6;
62 | }
63 |
64 | .provider-card.card-expanded:hover .card-draggable-icon {
65 | color: #8C45CF;
66 | }
67 |
68 | .provider-card.card-wrapped .card-draggable-icon {
69 | color: #DCCFE8;
70 | }
71 |
72 | .provider-card.card-wrapped:hover .card-draggable-icon {
73 | color: #D4A5FF;
74 | }
75 |
76 | .provider-card.card-dragging .card-draggable-icon {
77 | color: #8C45CF;
78 | }
79 |
80 | .provider-card.active {
81 | background: #EFDFFD;
82 | border: 2px solid #B799D2;
83 | box-sizing: border-box;
84 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25), inset 0px 3px 4px rgba(89, 0, 168, 0.4) !important;
85 | }
86 |
87 | .provider-card.card-dragging:active {
88 | background: #EFDFFD;
89 | border: 2px solid #8C45CF;
90 | box-sizing: border-box;
91 | box-shadow: 3px 5px 8px rgba(184, 147, 217, 0.8) !important;
92 | }
93 |
94 | .card-layout {
95 | display: flex;
96 | flex-flow: column nowrap;
97 | flex-grow: 1;
98 | flex-shrink: 0;
99 | padding: 10px;
100 | width: 100%;
101 | }
102 |
103 | .provider-card.saved .card-layout{
104 | padding-left: 41px;
105 | margin-left: -31px;
106 | }
107 |
108 | .card-draggable-icon {
109 | align-self: center;
110 | padding: 10px 10px;
111 | flex: 0 0 auto;
112 | color: #C8A2EB
113 | }
114 | /*
115 | .provider-card.search {
116 | background-color: #f5fbff;
117 | }
118 | */
119 | .provider-card .card-container {
120 | position: relative;
121 | display: flex;
122 | flex-direction: row;
123 | justify-content: space-between;
124 | align-items: center;
125 | padding-right: 80px;
126 | }
127 |
128 | .card-header {
129 | display: flex;
130 | width: 100%;
131 | flex-direction: column;
132 | }
133 |
134 | h5.wrapped {
135 | margin: 0 0 5px 0;
136 | font-size: 16px;
137 | color: #000;
138 | overflow: hidden;
139 | text-overflow: ellipsis;
140 | white-space: nowrap;
141 | cursor: pointer;
142 | height: 18px;
143 | }
144 |
145 | h5.expanded {
146 | margin: 0 0 5px 0;
147 | font-size: 16px;
148 | color: #000;
149 | text-overflow: ellipsis;
150 | cursor: pointer;
151 | }
152 |
153 | .wrapped-info {
154 | display: flex;
155 | flex-direction: row;
156 | flex-wrap: nowrap;
157 | justify-content: space-between;
158 | align-items: center;
159 | }
160 |
161 | .prov-type {
162 | height: 30px;
163 | display: flex;
164 | flex-direction: row;
165 | align-items: center;
166 | }
167 |
168 | .prov-type.wrapped {
169 | width: 100px;
170 | }
171 | .prov-type.expanded {
172 | width: 180px;
173 | }
174 |
175 | .prov-type p {
176 | font-size: 10px;
177 | margin: 5px 0;
178 | text-transform: uppercase;
179 | overflow: hidden;
180 | text-overflow: ellipsis;
181 | white-space: nowrap;
182 | }
183 |
184 | .wrapped-info svg:first-of-type {
185 | margin-right: 5px;
186 | }
187 | .wrapped-info .wrapped-icons svg {
188 | font-size: 12px;
189 | margin: 3px;
190 | }
191 |
192 | .wrapped-info:last-child {
193 | min-width: 60px;
194 | font-size: 12px;
195 | color: #7e7e7e;
196 | }
197 |
198 | .wrapped-icons {
199 | color: #7e7e7e;
200 | display: none;
201 | }
202 |
203 | .save-button-container {
204 | position: absolute;
205 | top: -10px;
206 | right: -10px;
207 | }
208 |
209 | .button {
210 | display: flex;
211 | justify-content: center;
212 | align-items: center;
213 | height: 73px;
214 | width: 70px;
215 | border: 0;
216 | flex-direction: column;
217 | cursor: pointer;
218 | outline: none;
219 | }
220 |
221 | .remoteButton {
222 | border: none;
223 | outline: none;
224 | display: flex;
225 | justify-content: center;
226 | align-items: center;
227 | flex-direction: column;
228 | height: 73px;
229 | width: 70px;
230 | cursor: pointer;
231 | background-color: transparent;
232 | font-size: 12px;
233 | font-weight: bold;
234 | background-color: #E08963;
235 | color: #FFF;
236 | }
237 |
238 | .remoteButton > svg {
239 | margin-bottom: 0.25em;
240 | }
241 |
242 | .button.saved {
243 | background-color: #382E77;
244 | color: #fff;
245 | }
246 |
247 | .button.unsaved {
248 | background-color: #6563E0;
249 | color: #fff;
250 | }
251 |
--------------------------------------------------------------------------------
/src/components/MenuDropdownItem/menu-dropdown-item.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Fragment } from "react";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import {
5 | faFolderPlus,
6 | faFolderOpen,
7 | faEnvelopeOpenText,
8 | faGlobeEurope,
9 | faMapMarkedAlt,
10 | faPhone,
11 | faTrashAlt,
12 | faBriefcase,
13 | faGraduationCap,
14 | faMoneyBillWave,
15 | faLandmark,
16 | faFileContract,
17 | faHospital,
18 | faBed,
19 | faBalanceScale,
20 | faGripVertical,
21 | faUsers
22 | } from "@fortawesome/free-solid-svg-icons";
23 | import "./menu-dropdown-item.css";
24 | import DetailsPane from "components/DetailsPane";
25 | import providerTypeToColor from "provider-type-to-color.json";
26 |
27 | const isPresent = value => value && value !== "n/a";
28 |
29 | export const cardIconMappings = {
30 | "Job Placement": faBriefcase,
31 | "Community Center": faUsers,
32 | Education: faGraduationCap,
33 | "Cash/Food Assistance": faMoneyBillWave,
34 | Resettlement: faLandmark,
35 | "Mental Health": faFileContract,
36 | Health: faHospital,
37 | Housing: faBed,
38 | Legal: faBalanceScale
39 | };
40 |
41 | export default class DropdownMenuItem extends React.Component {
42 |
43 | state = { isActive: false }
44 |
45 | toggleSave = e => {
46 | e.stopPropagation();
47 | this.props.toggleSavedStatus();
48 | }
49 |
50 | setActive = (e) => {
51 | this.setState({isActive: true})
52 | }
53 |
54 | unsetActive = (e) => {
55 | this.setState({isActive: false})
56 | }
57 |
58 | setHovered = isHovered => {
59 | const { provider, setHovered } = this.props;
60 | if (isHovered) {
61 | setHovered(provider.id);
62 | } else {
63 | setHovered(null);
64 | }
65 | };
66 |
67 | render() {
68 | const { provider, isSaved, isHighlighted, isDragging, flyToProvider } = this.props;
69 | const {isActive} = this.state;
70 | const inSavedMenu = !!this.props.inSavedMenu;
71 | const isExpanded = isHighlighted;
72 | const activeClass = isActive ? "active" : "";
73 | const dragClass = isDragging ? "dragging" : "resting";
74 | const expandClass = isExpanded ? "expanded" : "wrapped";
75 | const menuClass = inSavedMenu ? "saved" : "search";
76 | const cardIcon = cardIconMappings[provider.typeName];
77 | return (
78 | {
84 | this.setHovered(true);
85 | }}
86 | onMouseLeave={() => {
87 | this.setHovered(false);
88 | this.unsetActive();
89 | }}
90 | >
91 | {inSavedMenu && (
92 |
95 |
)}
96 |
97 |
98 |
99 |
100 | {provider.name}
101 |
102 |
103 |
104 |
108 |
{provider.typeName}
109 |
110 | {!isExpanded && (
111 |
112 | {isPresent(provider.email) && (
113 |
114 | )}
115 | {isPresent(provider.website) && (
116 |
117 | )}
118 | {!!provider.coordinates.length && (
119 |
120 | )}
121 | {isPresent(provider.telephone) && (
122 |
123 | )}
124 |
125 | )}
126 |
127 | {provider.distance ? (
128 |
{Math.round(provider.distance * 10) / 10} mi away
129 | ) : null}
130 |
131 |
132 |
133 |
134 | {inSavedMenu ? (
135 |
143 | ) : (
144 |
162 | )}
163 |
164 |
165 | {isExpanded &&
}
166 |
167 |
168 | );
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/components/MenuVisaFilter/index.js:
--------------------------------------------------------------------------------
1 | import MenuVisaFilter from "./menu-visa-filter";
2 |
3 | export default MenuVisaFilter;
4 |
--------------------------------------------------------------------------------
/src/components/MenuVisaFilter/menu-visa-filter.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "../ProviderList/provider-list.css";
3 |
4 | class MenuVisaFilter extends Component {
5 | render() {
6 | const VISA_TYPES = ["visa1", "visa2", "visa3"];
7 |
8 | return (
9 |
10 |
11 |
Visa Status
12 |
Current Visa Status
13 |
14 |
37 |
38 | );
39 | }
40 | }
41 |
42 | export default MenuVisaFilter;
43 |
--------------------------------------------------------------------------------
/src/components/ProviderList/index.js:
--------------------------------------------------------------------------------
1 | import ProviderList from "./provider-list.container";
2 | export default ProviderList;
3 |
--------------------------------------------------------------------------------
/src/components/ProviderList/provider-list.container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import {
4 | saveProvider,
5 | displayProviderInformation,
6 | changeSortOrder,
7 | changeSortDirection,
8 | flyToProvider,
9 | zoomToFit,
10 | selectProvider
11 | } from "redux/actions";
12 | import { getProvidersSorted } from "redux/selectors.js";
13 | import ProviderList from "./provider-list";
14 |
15 | const VISA_TYPES = ["visa1", "visa2", "visa3"];
16 |
17 | const ProviderListContainer = props => {
18 | return ;
19 | };
20 |
21 | const mapStateToProps = state => {
22 | return {
23 | providersList: getProvidersSorted(state),
24 | savedProviders: state.providers.savedProviders,
25 | incomingState: state.providers.sortMethod,
26 | sortDirection: state.providers.sortDirection,
27 | visaTypes: VISA_TYPES,
28 | highlightedProviders: state.highlightedProviders,
29 | filters: state.filters,
30 | providerTypes: state.providerTypes.allIds
31 | };
32 | };
33 |
34 | const mapDispatchToProps = dispatch => {
35 | return {
36 | saveProvider: id => {
37 | dispatch(saveProvider(id));
38 | },
39 | displayProviderInformation: id => {
40 | dispatch(displayProviderInformation(id));
41 | },
42 | changeSortOrder: value => {
43 | dispatch(changeSortOrder(value));
44 | },
45 | changeSortDirection: direction => {
46 | dispatch(changeSortDirection(direction));
47 | },
48 | flyToProvider: id => {
49 | dispatch(flyToProvider(id));
50 | },
51 | zoomToFit: () => {
52 | dispatch(zoomToFit());
53 | },
54 | selectProvider: id => {
55 | dispatch(selectProvider(id))
56 | }
57 | };
58 | };
59 |
60 | export default connect(
61 | mapStateToProps,
62 | mapDispatchToProps
63 | )(ProviderListContainer);
64 |
--------------------------------------------------------------------------------
/src/components/ProviderList/provider-list.css:
--------------------------------------------------------------------------------
1 | h2 {
2 | margin: 2px 2px 0 0;
3 | font-weight: 900;
4 | text-transform: uppercase;
5 | letter-spacing: 0.4px;
6 | }
7 |
8 | p {
9 | margin: 2px 0 6px 2px;
10 | font-size: 14px;
11 | line-height: 12px;
12 | font-weight: bold;
13 | color: gray;
14 | transition: all 0.1s ease;
15 | }
16 |
17 | label {
18 | margin-left: 10px;
19 | }
20 |
21 | .service-providers {
22 | display: flex;
23 | flex-direction: column;
24 | height: 100%;
25 | }
26 |
27 | .service-providers .tab-header {
28 | background-color: white;
29 | z-index: 2;
30 | padding: 20px;
31 | }
32 |
33 | .service-providers .tab-header .header-text {
34 | margin: 0px;
35 | }
36 |
37 | .service-providers .tab-header .header-subtext {
38 | color: #7e7e7e;
39 | font-size: 12px;
40 | margin-bottom: 10px;
41 | }
42 |
43 | .providers-list {
44 | overflow-y: auto;
45 | /* giving relative positioning to this element allows it to be
46 | ** the offsetParent for its (grand)child 's containing provider info
47 | ** and eliminate the need to add any offset correction to scrollTop
48 | ** when responding to a map click on a provider icon
49 | **/
50 | position: relative;
51 | scroll-behavior: smooth;
52 | margin: 0;
53 | padding: 8px 16px 8px 16px;
54 | height: -webkit-fill-available;
55 | flex: auto;
56 | }
57 |
58 | @media (prefers-reduced-motion: reduce) {
59 | .providers-list {
60 | scroll-behavior: auto;
61 | }
62 | }
63 |
64 | .providers-sublist li:last-child {
65 | padding-bottom: 16px;
66 | }
67 |
68 | .providers-sublist {
69 | width: 100%;
70 | }
71 |
72 | .search-item-container {
73 | margin: 0.5em 0 0 0;
74 | }
75 |
76 | .dropdown-list-container.expanded p {
77 | color: white;
78 | }
79 |
80 | .top-nav {
81 | overflow: visible;
82 | height: 10%;
83 | background-color: white;
84 | z-index: 8;
85 | }
86 |
87 | .upper-bar {
88 | position: absolute;
89 | z-index: 20;
90 | background-color: white;
91 | border-bottom: 1px solid #8c45cf;
92 | width: 100%;
93 | }
94 |
95 | .dropdown-row {
96 | display: flex;
97 | width: 100%;
98 | padding: 0 15px;
99 | }
100 |
101 | .dropdown-list-container {
102 | flex: 1;
103 | margin: 0 8px;
104 | z-index: 10;
105 | background-color: white;
106 | border: 1px solid #8c45cf;
107 | box-shadow: 2px 1px 2px #8c45cf;
108 | }
109 |
110 | .placeholder {
111 | box-shadow: none;
112 | border: none;
113 | }
114 |
115 | .dropdown-list {
116 | display: flex;
117 | flex-direction: column;
118 | height: 0;
119 | padding-top: 0px;
120 | text-align: left;
121 | opacity: 0;
122 | background-color: white;
123 | transition: all 0.1s ease-out;
124 | }
125 |
126 | .dropdown-list-container.expanded .dropdown-list {
127 | flex: 1;
128 | padding-top: 6px;
129 | height: auto;
130 | opacity: 1;
131 | width: 100%;
132 | }
133 |
134 | .dropdown-list-item {
135 | height: 0px;
136 | padding: 0px 5px;
137 | color: black;
138 | transition: all 0.1s ease-out;
139 | }
140 | .dropdown-list-container.expanded .dropdown-list-item {
141 | height: 30px;
142 | padding: 2px 5px;
143 | }
144 |
145 | .dropdown-list-header {
146 | width: 100%;
147 | padding: 0 5px;
148 | color: #8c45cf;
149 | background-color: white;
150 | transition: all 0.1s ease-out;
151 | }
152 |
153 | .dropdown-list-container.expanded .dropdown-list-header {
154 | background-color: #8c45cf;
155 | }
156 | .dropdown-list-container.expanded .dropdown-list-header h2 {
157 | color: white;
158 | }
159 |
160 | .dropdown-button {
161 | height: 0;
162 | width: 100%;
163 | margin: 0 24px;
164 | padding: 0 10px;
165 | border-radius: 0;
166 | transition: all 0.1s ease-out;
167 | }
168 |
169 | .dropdown-list-container.expanded .dropdown-button {
170 | border: 1px solid #8c45cf;
171 | height: 100%;
172 | margin: 12px 24px;
173 | padding: 10px;
174 | }
175 |
176 | .dropdown-menu {
177 | color: gray;
178 | font-weight: bold;
179 | margin: 0px 2px;
180 | }
181 |
182 | .dropdown-menu.expanded {
183 | color: #8c45cf;
184 | }
185 |
--------------------------------------------------------------------------------
/src/components/ProviderList/sort-dropdown.css:
--------------------------------------------------------------------------------
1 | .sort-container {
2 | display: flex;
3 | flex-direction: row;
4 | box-sizing: content-box;
5 | /* Height should match the height of the sort icon. */
6 | height: 48px;
7 | /* padding-top: 24px;
8 | padding-left: 16px;
9 | padding-right: 16px;
10 | padding-bottom: 8px; */
11 | /* Changed padding to match SavedProvidersList padding */
12 | padding: 17px 17px 18px;
13 | position: sticky;
14 | top: 0;
15 | background: white;
16 | color: #8c45cf;
17 | z-index: 2;
18 | border-bottom: 1px solid hsla(271, 34%, 86%, 0.5);
19 | /* border-radius: 0 8px 8px 8px; */
20 | box-shadow: 1px 3px 4px rgba(184, 147, 217, 0.2);
21 | flex: none;
22 | }
23 | .sort-container .expandable-container {
24 | flex: 1 1 auto;
25 | margin-right: 5px;
26 | }
27 | .sort-container .expandable-container .expanded {
28 | transition: all 0.08s ease-out;
29 | }
30 | .side-menu .expandable-content .expandable-header {
31 | padding: 0;
32 | }
33 | .side-menu .expandable-header {
34 | padding: 0.75rem;
35 | text-transform: uppercase;
36 | }
37 | .side-menu .expandable-header h4 {
38 | margin: 0;
39 | padding: 0;
40 | }
41 | .expandable-content-wrapper.sort-by {
42 | box-shadow: 1px 1px 2px #aaa;
43 | flex: 1 1 auto;
44 | padding: 0;
45 | cursor: pointer;
46 | }
47 |
48 | .radio-container {
49 | color: gray;
50 | padding: 0.75rem;
51 | background-color: white;
52 | }
53 | .radio-container:hover {
54 | background-color: #d2e9ff;
55 | }
56 | .radio-container.selected {
57 | background-color: #bedefd;
58 | }
59 | .radio-container input {
60 | display: none;
61 | }
62 | .radio-container label {
63 | margin: 0;
64 | cursor: pointer;
65 | }
66 |
67 | .providers-list {
68 | padding-left: 16px;
69 | padding-right: 16px;
70 | }
71 |
72 | .sort-container-icon {
73 | align-self: center;
74 | margin: 5px;
75 | cursor: pointer;
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/ProviderList/sort-dropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactTooltip from "react-tooltip";
3 | import Expandable from "../Dropdowns/expandable";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import {
6 | faSortNumericDown,
7 | faSortNumericUp,
8 | faSortAlphaUp,
9 | faSortAlphaDown,
10 | faCompressArrowsAlt
11 | } from "@fortawesome/free-solid-svg-icons";
12 | import "./sort-dropdown.css";
13 |
14 | export default class SortDropdown extends React.Component {
15 | state = {
16 | expanded: false
17 | };
18 |
19 | getSortIcon = (sortDirection, incomingState) => {
20 | if (incomingState === "Distance") {
21 | if (sortDirection === "asc") {
22 | return faSortNumericUp;
23 | }
24 | return faSortNumericDown;
25 | } else {
26 | if (sortDirection === "asc") {
27 | return faSortAlphaUp;
28 | }
29 | return faSortAlphaDown;
30 | }
31 | };
32 |
33 | componentDidMount() {
34 | ReactTooltip.rebuild();
35 | }
36 |
37 | render() {
38 | const { expanded } = this.state;
39 | const {
40 | className,
41 | handleChange,
42 | changeDirection,
43 | header,
44 | incomingState,
45 | options,
46 | sortDirection,
47 | zoomToFit
48 | } = this.props;
49 |
50 | let inputDiv = options.map((option, index) => (
51 | handleChange(option)}
57 | >
58 | {}}
65 | />
66 |
69 |
70 | ));
71 | let wrappedHeader = (
72 |
73 | {header}: {incomingState}
74 |
75 | );
76 |
77 | return (
78 |
79 | this.setState({ expanded: false })}
84 | expanded={expanded}
85 | setExpanded={expanded => this.setState({ expanded })}
86 | />
87 | changeDirection()}
91 | className="sort-container-icon"
92 | data-tip="Change sort direction"
93 | />
94 |
101 |
102 | );
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/components/SavedProvidersList/index.js:
--------------------------------------------------------------------------------
1 | import SaveProvidersList from "./saved-providers-list.container";
2 |
3 | export default SaveProvidersList;
4 |
--------------------------------------------------------------------------------
/src/components/SavedProvidersList/saved-providers-list.container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import {
4 | displayProviderInformation,
5 | saveProvider,
6 | reorderSavedProviders,
7 | flyToProvider,
8 | } from "redux/actions";
9 | import { getSavedProviders } from "redux/selectors.js";
10 | import SavedProvidersList from "./saved-providers-list";
11 | import { DragDropContext } from "react-beautiful-dnd";
12 |
13 | const reorder = (list, startIndex, endIndex) => {
14 | const result = Array.from(list);
15 | const [removed] = result.splice(startIndex, 1);
16 | result.splice(endIndex, 0, removed);
17 |
18 | return result;
19 | };
20 |
21 | const SavedProvidersListContainer = props => {
22 | const onDragEnd = result => {
23 | // dropped outside the list
24 | if (!result.destination) {
25 | return;
26 | }
27 |
28 | const items = reorder(
29 | props.savedProviders,
30 | result.source.index,
31 | result.destination.index
32 | );
33 | const providerIds = items.map(item => item.id);
34 | props.reorderSavedProviders(providerIds);
35 | };
36 |
37 | return (
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const mapStateToProps = state => {
45 | return {
46 | searchCenter: state.search.currentLocation
47 | ? state.search.history[state.search.currentLocation].searchText
48 | : null,
49 | savedProviders: getSavedProviders(state),
50 | highlightedProviders: state.highlightedProviders
51 | };
52 | };
53 |
54 | const mapDispatchToProps = dispatch => {
55 | return {
56 | saveProvider: id => {
57 | dispatch(saveProvider(id));
58 | },
59 | flyToProvider: id => {
60 | dispatch(flyToProvider(id));
61 | },
62 | displayProviderInformation: id => {
63 | dispatch(displayProviderInformation(id));
64 | },
65 | reorderSavedProviders: ids => {
66 | dispatch(reorderSavedProviders(ids));
67 | },
68 | };
69 | };
70 |
71 | export default connect(
72 | mapStateToProps,
73 | mapDispatchToProps
74 | )(SavedProvidersListContainer);
75 |
--------------------------------------------------------------------------------
/src/components/SavedProvidersList/saved-providers-list.css:
--------------------------------------------------------------------------------
1 | .search-center {
2 | color: #7e7e7e;
3 | font-size: 12px;
4 | }
5 |
6 | .saved-list {
7 | display: flex;
8 | flex-direction: column;
9 | height: 100%;
10 | }
11 |
12 | .header-container {
13 | position: sticky;
14 | top: 0;
15 | background: white;
16 | z-index: 2;
17 | padding: 18px;
18 | border-radius: 0 8px 0 0;
19 | box-shadow: 0 3px 4px rgba(184, 147, 217, 0.2);
20 | }
21 |
22 | .saved-content-container {
23 | overflow-y: auto;
24 | position: relative;
25 | margin: 0;
26 | padding: 16px 16px 8px 16px;
27 | flex: auto;
28 | }
29 |
30 | .saved-draggable {
31 | margin: 0 0 0.5em 0;
32 | }
33 |
34 | .saved-list header {
35 | display: flex;
36 | justify-content: space-between;
37 | }
38 |
39 | .saved-list h3 {
40 | color: #8c45cf;
41 | margin: 0px;
42 | }
43 |
44 | .remove-button {
45 | color: #333;
46 | display: flex;
47 | flex-direction: row-reverse;
48 | font-weight: bold;
49 | cursor: pointer;
50 | text-transform: uppercase;
51 | }
52 |
53 | .print-email-btn {
54 | height: 20px;
55 | background-color: #8c45cf;
56 | color: white;
57 | border-radius: 5px;
58 | border: none;
59 | transition: .3s;
60 | margin-left: 5px;
61 | }
62 |
63 | .print-email-btn:hover {
64 | background-color: #a16fd1;
65 | cursor: pointer;
66 | }
67 |
68 | .print-email-btn:focus {
69 | outline: none;
70 | }
71 |
72 | .email-icon, .print-icon {
73 | color: #8c45cf;
74 | cursor: pointer;
75 | }
76 |
77 | .print-icon {
78 | margin-right: 16px;
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/SavedProvidersList/saved-providers-list.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MenuDropdownItem } from "..";
3 | import { printJSX } from "util/printJSX";
4 | import "./saved-providers-list.css";
5 | import { Droppable, Draggable } from "react-beautiful-dnd";
6 | import _ from "lodash";
7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8 | import ReactTooltip from "react-tooltip";
9 | import { faPrint, faEnvelope } from "@fortawesome/free-solid-svg-icons";
10 |
11 | export default class SavedProvidersList extends React.Component {
12 | state = {};
13 |
14 | toProviderDiv = provider => {
15 | const {
16 | id,
17 | address,
18 | email,
19 | mission,
20 | name,
21 | telephone,
22 | typeName,
23 | website
24 | } = provider;
25 | return (
26 |
27 |
{name}
28 |
29 | {typeName}
30 |
31 |
32 |
Address: {address}
33 |
Email: {email}
34 |
Phone: {telephone}
35 |
38 |
{mission}
39 |
40 |
41 | );
42 | };
43 |
44 | printSavedProviders(providers) {
45 | const printPage = (
46 |
47 |
{_.map(providers, this.toProviderDiv)}
48 |
49 | );
50 | printJSX(printPage);
51 | }
52 |
53 | emailSavedProviders(providers) {
54 | const email = providers.map(provider => {
55 | const { name, address, website, telephone, email } = provider;
56 | return [
57 | name,
58 | `Address: ${address}`,
59 | `Website: ${website}`,
60 | `Phone: ${telephone}`,
61 | `Email: ${email}`
62 | ].join("\n");
63 | }).join("\n\n");
64 |
65 | const uriEncodedBody = encodeURIComponent(email);
66 |
67 | let myWindow;
68 |
69 | function openWin() {
70 | myWindow = window.open("mailto:?&body=" + uriEncodedBody);
71 | setTimeout(closeWin, 3000);
72 | }
73 |
74 | // closeWin shuts new tab after 3 seconds if email is
75 | // opened in email application e.g. Outlook, Mail, and keeps new tab open if
76 | // redirected to browser email application e.g. Gmail
77 |
78 | function closeWin() {
79 | try {
80 | // Without try block, "if (myWindow.location)" would cause
81 | // cross site error after redirect
82 | if (myWindow.location.href) {
83 | myWindow.close();
84 | }
85 | } catch {
86 | return;
87 | }
88 | }
89 |
90 | openWin();
91 | }
92 |
93 | componentDidMount() {
94 | ReactTooltip.rebuild();
95 | }
96 |
97 | render() {
98 | const {
99 | savedProviders,
100 | saveProvider,
101 | searchCenter,
102 | highlightedProviders,
103 | displayProviderInformation,
104 | flyToProvider
105 | } = this.props;
106 | return (
107 |
108 |
109 |
128 |
129 | Showing proximity to {searchCenter}
130 |
131 |
132 |
133 |
138 | {provided => {
139 | return (
140 |
145 | {savedProviders.map((provider, index) => (
146 |
151 | {provided => (
152 | displayProviderInformation(provider.id)}
158 | >
159 | saveProvider(provider.id)}
164 | flyToProvider={() => flyToProvider(provider.id)}
165 | isHighlighted={highlightedProviders.includes(
166 | provider.id
167 | )}
168 | inSavedMenu={true}
169 | />
170 |
171 | )}
172 |
173 | ))}
174 | {provided.placeholder}
175 |
176 | );
177 | }}
178 |
179 |
180 | );
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/components/TabbedMenu/index.js:
--------------------------------------------------------------------------------
1 | import TabbedMenu from "./tabbed-menu.container";
2 |
3 | export default TabbedMenu;
4 |
--------------------------------------------------------------------------------
/src/components/TabbedMenu/tabbed-menu.container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { selectTab } from "../../redux/actions";
4 | import TabbedMenu from "./tabbed-menu";
5 |
6 | const TabbedMenuContainer = props => {
7 | return ;
8 | };
9 |
10 | const mapStateToProps = state => {
11 | return {
12 | savedProviderCount: state.providers.savedProviders.length,
13 | selectedTabIndex: state.search.selectedTabIndex
14 | };
15 | };
16 |
17 | const dispatchToProps = dispatch => {
18 | return {
19 | selectTab: index => {
20 | dispatch(selectTab(index));
21 | }
22 | };
23 | };
24 |
25 | export default connect(
26 | mapStateToProps,
27 | dispatchToProps
28 | )(TabbedMenuContainer);
29 |
--------------------------------------------------------------------------------
/src/components/TabbedMenu/tabbed-menu.css:
--------------------------------------------------------------------------------
1 | .side-menu {
2 | position: absolute;
3 | top: 0;
4 | left: 20px;
5 | padding: 20px 0;
6 | z-index: 1;
7 | display: flex;
8 | flex-direction: column;
9 | width: 400px;
10 | height: 100%;
11 | }
12 |
13 | .react-tabs__tab-list {
14 | display: flex;
15 | flex: initial;
16 | margin: 0;
17 | padding: 0;
18 | border: 0;
19 | }
20 |
21 | .react-tabs__tab {
22 | border: none;
23 | background: #aaa;
24 | color: white;
25 | border-radius: 8px 8px 0 0;
26 | flex: 1 1 auto;
27 | text-align: center;
28 | -moz-user-select: none;
29 | -khtml-user-select: none;
30 | user-select: none;
31 | }
32 | .react-tabs__tab--selected {
33 | background: white;
34 | border: none;
35 | box-shadow: 0 0 6px grey;
36 | }
37 |
38 | .react-tabs__tab h3 {
39 | margin: 8px 20px;
40 | text-transform: uppercase;
41 | color: white;
42 | }
43 | .react-tabs__tab--selected h3 {
44 | color: #8c45cf;
45 | }
46 |
47 | .react-tabs__tab-panel {
48 | background-color: white;
49 | flex: auto;
50 | min-height: 0;
51 | box-shadow: 0 0 6px grey;
52 | height: 100%;
53 | }
54 |
55 | .selected-tab-panel {
56 | display: flex;
57 | flex-direction: column;
58 | }
59 |
60 | #provider-counter {
61 | font-size: 14px;
62 | background-color: #939393;
63 | border-radius: 50%;
64 | color: white;
65 | display: block;
66 | position: absolute;
67 | right: 1em;
68 | top: 50%;
69 | transform: translateY(-50%);
70 | padding: .1em .5em .15em .4em;
71 | }
72 | .react-tabs__tab--selected #provider-counter {
73 | background-color: #bbb;
74 | }
--------------------------------------------------------------------------------
/src/components/TabbedMenu/tabbed-menu.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
3 | import { ProviderList, SavedProvidersList } from "components";
4 |
5 | import "react-tabs/style/react-tabs.css";
6 | import "./tabbed-menu.css";
7 |
8 | function Counter({count}) {
9 | if (count > 0) {
10 | return {count}
11 | } else {
12 | return null;
13 | }
14 | }
15 |
16 | const TabbedMenu = ({ savedProviderCount, selectedTabIndex, selectTab }) => {
17 | return (
18 | selectTab(index)}
22 | selectedTabPanelClassName="selected-tab-panel"
23 | >
24 |
25 |
26 | Service Providers
27 |
28 |
29 |
30 | Saved
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default TabbedMenu;
47 |
--------------------------------------------------------------------------------
/src/components/TopBar/distance-dropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import RadioButtonDropdown from "../Dropdowns/radio-button-dropdown";
3 | import distances from "assets/distances";
4 | import { Row, Column } from "simple-flexbox";
5 |
6 | const distanceText = (distance) => distance ? `${distance} mile${distance === 1 ? "" : "s"}` : 'None Selected';
7 |
8 | export default class DistanceDropdown extends React.Component {
9 | state = { expanded: false };
10 |
11 | clearDistance = event => {
12 | event.stopPropagation();
13 | const { onClear = () => {} } = this.props;
14 | onClear();
15 | this.setState({ expanded: false });
16 | };
17 |
18 | render() {
19 | const { className, currentDistance, onChange } = this.props;
20 | const { expanded } = this.state;
21 | const options = distances.map(distance => {
22 | if (distance < 0) {
23 | // "null" clears the filter
24 | return { value: null, text: "None" };
25 | } else {
26 | return {
27 | value: distance,
28 | text: distanceText(distance),
29 | };
30 | }
31 | });
32 | return (
33 | this.setState({expanded})}
40 | header={
41 | <>
42 |
43 |
44 | Distance
45 | {distanceText(currentDistance)}
46 |
47 |
51 | clear
52 |
53 |
54 | >
55 | }
56 | />
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/TopBar/help-menu.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import iconImage from "../../assets/info-icon.svg";
3 | import "./top-bar.css";
4 |
5 | let helpWindowReference = {};
6 |
7 | class Link extends Component {
8 | constructor(props) {
9 | super(props);
10 | let { href } = props;
11 | helpWindowReference[href] = null;
12 | this.handleClick = href => {
13 | if (
14 | helpWindowReference[href] == null ||
15 | helpWindowReference[href].closed
16 | ) {
17 | helpWindowReference[href] = window.open(href);
18 | } else {
19 | helpWindowReference[href].focus();
20 | }
21 | };
22 | }
23 |
24 | render() {
25 | return (
26 | {
29 | this.handleClick(this.props.href);
30 | e.preventDefault();
31 | }}
32 | >
33 | {this.props.title}
34 |
35 | );
36 | }
37 | }
38 |
39 | class HelpMenu extends Component {
40 | helpLinks = [
41 | { title: "Help Guide", url: "/help/help-guide.html" },
42 | { title: "Send Feedback", url: "https://forms.gle/1eUKSKSRyi1xTaD6A" },
43 | {
44 | title: "Update Provider Info",
45 | url: "https://forms.gle/2Ro2nWhGzfHmnx3m9"
46 | }
47 | ];
48 |
49 | render() {
50 | return (
51 |
52 |

53 |
54 | {this.helpLinks.map((link, i) => (
55 |
56 | ))}
57 |
58 |
59 | );
60 | }
61 | }
62 | export default HelpMenu;
63 |
--------------------------------------------------------------------------------
/src/components/TopBar/index.js:
--------------------------------------------------------------------------------
1 | import TopBar from "./top-bar.container";
2 | export default TopBar;
3 |
--------------------------------------------------------------------------------
/src/components/TopBar/logo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import logo from "../../assets/icons/msm_logo.svg";
3 |
4 | export default class Logo extends React.Component {
5 | render() {
6 | return (
7 |
12 | );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/TopBar/mapbox-gl-geocoder.css:
--------------------------------------------------------------------------------
1 | /*
2 | This file is a copy + edit of this mapbox css:
3 | https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v3.1.6/mapbox-gl-geocoder.css
4 |
5 | Do not use this file to store other css. If we update mapbox, we may need to update this file
6 | to match.
7 | */
8 |
9 | /* Basics */
10 | .mapboxgl-ctrl-geocoder,
11 | .mapboxgl-ctrl-geocoder *,
12 | .mapboxgl-ctrl-geocoder *:after,
13 | .mapboxgl-ctrl-geocoder *:before {
14 | -webkit-box-sizing: border-box;
15 | -moz-box-sizing: border-box;
16 | box-sizing: border-box;
17 | }
18 | .mapboxgl-ctrl-geocoder {
19 | font-size: 15px;
20 | line-height: 20px;
21 | font-family: var(--msm-font-family);
22 | position: relative;
23 | background-color: white;
24 | width: 33.3333%;
25 | min-width: 240px;
26 | max-width: 360px;
27 | z-index: 1;
28 | border-radius: 3px;
29 | }
30 |
31 | .mapboxgl-ctrl-geocoder input[type="text"] {
32 | font-size: 14px;
33 | font-family: var(--msm-font-family);
34 | width: 100%;
35 | font-weight: 700;
36 | border: 0;
37 | background-color: transparent;
38 | margin: 0;
39 | color: grey;
40 | padding: 0px 10px 0px 40px;
41 | text-overflow: ellipsis;
42 | white-space: nowrap;
43 | overflow: hidden;
44 | }
45 | .mapboxgl-ctrl-geocoder input:focus {
46 | color: rgba(0, 0, 0, 0.75);
47 | outline: 0;
48 | box-shadow: none;
49 | outline: thin dotted\8;
50 | }
51 |
52 | .mapboxgl-ctrl-geocoder .geocoder-icon-search {
53 | position: absolute;
54 | top: 0px;
55 | left: 10px;
56 | }
57 | .mapboxgl-ctrl-geocoder button {
58 | padding: 0;
59 | margin: 0;
60 | background-color: #fff;
61 | border: none;
62 | cursor: pointer;
63 | }
64 | .mapboxgl-ctrl-geocoder .geocoder-pin-right * {
65 | background-color: #fff;
66 | z-index: 2;
67 | position: absolute;
68 | right: 10px;
69 | top: 0px;
70 | display: none;
71 | }
72 |
73 | .mapboxgl-ctrl-geocoder,
74 | .mapboxgl-ctrl-geocoder ul {
75 | box-shadow: 1px 3px 4px rgba(96, 1, 185, 0.25);
76 | }
77 |
78 | /* Suggestions */
79 | .mapboxgl-ctrl-geocoder ul {
80 | box-sizing: content-box;
81 | background-color: #fdfaff;
82 | box-shadow: 1px 3px 4px rgba(96, 1, 185, 0.25);
83 | border-radius: 0 0 8px 8px;
84 | border: 1px solid #d7adff;
85 | border-top: 0px;
86 | left: 0;
87 | list-style: none;
88 | margin-left: -1px;
89 | padding: 0;
90 | position: absolute;
91 | width: 100%;
92 | top: 0.5rem;
93 | z-index: 1000;
94 | overflow: hidden;
95 | font-size: 12px;
96 | }
97 |
98 | .mapboxgl-ctrl-bottom-left .mapboxgl-ctrl-geocoder ul,
99 | .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl-geocoder ul {
100 | top: auto;
101 | bottom: 100%;
102 | }
103 | .mapboxgl-ctrl-geocoder ul > li > a {
104 | clear: both;
105 | cursor: default;
106 | display: block;
107 | padding: 5px 10px;
108 | white-space: nowrap;
109 | overflow: hidden;
110 | text-overflow: ellipsis;
111 | /* border-bottom: 1px solid rgba(0, 0, 0, 0.1); */
112 | color: #404040;
113 | }
114 | .mapboxgl-ctrl-geocoder ul > li {
115 | border-bottom: 1px solid rgba(0, 0, 0, 0.1);
116 | }
117 | .mapboxgl-ctrl-geocoder ul > li:last-child > a {
118 | border-bottom: none;
119 | }
120 | .mapboxgl-ctrl-geocoder ul > li:last-child {
121 | border-bottom: none;
122 | }
123 | .mapboxgl-ctrl-geocoder ul > li > a:hover {
124 | color: #202020;
125 | background-color: #ead3ff;
126 | text-decoration: none;
127 | cursor: pointer;
128 | width: 100%;
129 | }
130 | .mapboxgl-ctrl-geocoder ul > li.active > a {
131 | color: #202020;
132 | background-color: #d7adff;
133 | text-decoration: none;
134 | cursor: pointer;
135 | width: 100%;
136 | }
137 |
138 | @-webkit-keyframes rotate {
139 | from {
140 | -webkit-transform: rotate(0deg);
141 | }
142 | to {
143 | -webkit-transform: rotate(360deg);
144 | }
145 | }
146 | @-moz-keyframes rotate {
147 | from {
148 | -moz-transform: rotate(0deg);
149 | }
150 | to {
151 | -moz-transform: rotate(360deg);
152 | }
153 | }
154 | @-ms-keyframes rotate {
155 | from {
156 | -ms-transform: rotate(0deg);
157 | }
158 | to {
159 | -ms-transform: rotate(360deg);
160 | }
161 | }
162 | @keyframes rotate {
163 | from {
164 | transform: rotate(0deg);
165 | }
166 | to {
167 | transform: rotate(360deg);
168 | }
169 | }
170 |
171 | /* icons */
172 | .geocoder-icon {
173 | display: inline-block;
174 | width: 20px;
175 | height: 20px;
176 | vertical-align: middle;
177 | background-repeat: no-repeat;
178 | }
179 | .geocoder-icon-search {
180 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmVyc2lvbj0iMS4xIj4NCiAgPHBhdGggZD0iTTguNSA0QzYgNCA0IDYgNCA4LjUgNCAxMSA2IDEzIDguNSAxMyA5LjQgMTMgMTAuMiAxMi44IDEwLjggMTIuM0wxMC45IDEyLjMgMTQuMyAxNS43QzE0LjUgMTUuOSAxNC43IDE2IDE1IDE2IDE1LjYgMTYgMTYgMTUuNiAxNiAxNSAxNiAxNC43IDE1LjkgMTQuNSAxNS43IDE0LjNMMTIuMyAxMC45IDEyLjMgMTAuOEMxMi44IDEwLjIgMTMgOS40IDEzIDguNSAxMyA2IDExIDQgOC41IDR6TTguNSA1LjVDMTAuMiA1LjUgMTEuNSA2LjggMTEuNSA4LjUgMTEuNSAxMC4yIDEwLjIgMTEuNSA4LjUgMTEuNSA2LjggMTEuNSA1LjUgMTAuMiA1LjUgOC41IDUuNSA2LjggNi44IDUuNSA4LjUgNS41eiIgZmlsbD0iIzAwMCIvPg0KPC9zdmc+);
181 | margin-left: 3px;
182 | }
183 | .geocoder-icon-close {
184 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgdmVyc2lvbj0iMS4xIiBoZWlnaHQ9IjIwIiB3aWR0aD0iMjAiPg0KICA8cGF0aCBkPSJtNSA1IDAgMS41IDMuNSAzLjUtMy41IDMuNSAwIDEuNSAxLjUgMCAzLjUtMy41IDMuNSAzLjUgMS41IDAgMC0xLjUtMy41LTMuNSAzLjUtMy41IDAtMS41LTEuNSAwLTMuNSAzLjUtMy41LTMuNS0xLjUgMHoiIGZpbGw9IiMwMDAiLz4NCjwvc3ZnPg==);
185 | /* position: relative; */
186 | }
187 | .geocoder-icon-loading {
188 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiB2aWV3Qm94PSIwIDAgMjAgMjAiPjxwYXRoIGQ9Im0xMCAyIDAgMy4zYzIuNiAwIDQuNyAyLjEgNC43IDQuN2wzLjMgMGMwLTQuNC0zLjYtOC04LTh6IiBmaWxsPSIjMDAwIi8+PHBhdGggZD0iTTEwIDJDNi44IDIgMy43IDQuMSAyLjYgNy4xIDEuNCAxMCAyLjEgMTMuNiA0LjUgMTUuOGMyLjQgMi40IDYuNCAyLjkgOS40IDEuMiAyLjUtMS40IDQuMi00LjIgNC4yLTctMS4xIDAtMi4yIDAtMy4zIDAgMC4xIDIuMi0xLjcgNC4zLTMuOCA0LjZDOC43IDE1IDYuNCAxMy44IDUuNyAxMS43IDQuOCA5LjcgNS42IDcuMSA3LjYgNiA4LjMgNS42IDkuMSA1LjMgMTAgNS4zYzAtMS4xIDAtMi4yIDAtMy4zeiIgc3R5bGU9ImZpbGw6IzAwMDtvcGFjaXR5OjAuMiIvPjwvc3ZnPg==);
189 | -webkit-animation: rotate 400ms linear infinite;
190 | -moz-animation: rotate 400ms linear infinite;
191 | -ms-animation: rotate 400ms linear infinite;
192 | animation: rotate 400ms linear infinite;
193 | }
194 |
--------------------------------------------------------------------------------
/src/components/TopBar/provider-type-dropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import CheckBoxDropdown from "../Dropdowns/checkbox-dropdown";
3 | import { Row, Column } from "simple-flexbox";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { cardIconMappings } from "../../components/MenuDropdownItem/menu-dropdown-item.js";
6 | import providerTypeToColor from "provider-type-to-color.json";
7 |
8 | const defaultSubheaderText = "Not Selected";
9 | export default class ProviderTypeDropdown extends React.Component {
10 | state = {
11 | expanded: false
12 | };
13 |
14 | onCheckboxChanged = (changedOption, selectedValues) => {
15 | const { onChange } = this.props;
16 | onChange(changedOption);
17 | };
18 |
19 | clearProviderTypes = event => {
20 | event.stopPropagation();
21 | const { onChange = () => {} } = this.props;
22 | onChange(undefined);
23 | };
24 |
25 | selectAllProviderTypes = event => {
26 | event.stopPropagation();
27 | const { onChange = () => {} } = this.props;
28 | const { providerTypes } = this.props;
29 |
30 | providerTypes.allIds.forEach(providerType => {
31 | onChange(providerType);
32 | });
33 | };
34 |
35 | setExpanded = expanded => {
36 | this.setState({
37 | expanded
38 | });
39 | };
40 |
41 | render() {
42 | const { expanded } = this.state;
43 | const { className, providerTypes } = this.props;
44 | let subheaderText = defaultSubheaderText;
45 | let selectOrClearCommand =
46 | providerTypes.visible.length > 0 ? "clear all" : "select all";
47 |
48 | if (providerTypes.visible.length === 1) {
49 | const selectedProviderType =
50 | providerTypes.byId[providerTypes.visible[0]].name;
51 | subheaderText = selectedProviderType;
52 | } else {
53 | subheaderText = providerTypes.visible.length + " Selected";
54 | }
55 |
56 | return (
57 | a.localeCompare(b))
61 | .map(id => ({
62 | id,
63 | display: (
64 |
65 | {" "}
70 | {providerTypes.byId[id].name}
71 |
72 | )
73 | }))}
74 | expanded={expanded}
75 | setExpanded={this.setExpanded}
76 | onChange={this.onCheckboxChanged}
77 | visibleTypes={providerTypes.visible}
78 | header={
79 | <>
80 |
81 |
82 | PROVIDER TYPE
83 | {subheaderText}
84 |
85 | 0
89 | ? this.clearProviderTypes
90 | : this.selectAllProviderTypes
91 | }
92 | >
93 | {selectOrClearCommand}
94 |
95 |
96 | >
97 | }
98 | />
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/TopBar/search.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Column } from "simple-flexbox";
3 | import "./mapbox-gl-geocoder.css";
4 | import { Geocoder } from "components/Map";
5 |
6 | export default class Search extends React.Component {
7 | render() {
8 | const { className } = this.props;
9 | return (
10 | <>
11 |
12 |
13 |
14 |
21 | NEAR
22 |
23 |
24 |
25 |
26 |
27 | >
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/TopBar/top-bar.container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import {
4 | toggleProviderVisibility,
5 | saveProvider,
6 | clearDistanceFilter,
7 | changeDistanceFilter,
8 | changeVisaFilter,
9 | clearVisaFilter,
10 | displayProviderInformation
11 | } from "redux/actions";
12 | import TopBar from "./top-bar";
13 |
14 | const TopBarContainer = props => {
15 | return ;
16 | };
17 |
18 | const mapStateToProps = state => {
19 | return {
20 | providerTypes: state.providerTypes,
21 | distance: state.filters.distance,
22 | };
23 | };
24 |
25 | const mapDispatchToProps = dispatch => {
26 | return {
27 | toggleProviderVisibility: providerType => {
28 | dispatch(toggleProviderVisibility(providerType));
29 | },
30 | saveProvider: id => {
31 | dispatch(saveProvider(id));
32 | },
33 | clearDistanceFilter: () => {
34 | dispatch(clearDistanceFilter());
35 | },
36 | changeDistanceFilter: distance => {
37 | dispatch(changeDistanceFilter(distance));
38 | },
39 | clearVisaFilter: () => {
40 | dispatch(clearVisaFilter());
41 | },
42 | changeVisaFilter: visa => {
43 | dispatch(changeVisaFilter(visa));
44 | },
45 | displayProviderInformation: id => {
46 | dispatch(displayProviderInformation(id));
47 | }
48 | };
49 | };
50 |
51 | export default connect(
52 | mapStateToProps,
53 | mapDispatchToProps
54 | )(TopBarContainer);
55 |
--------------------------------------------------------------------------------
/src/components/TopBar/top-bar.css:
--------------------------------------------------------------------------------
1 | .top-bar {
2 | overflow: visible;
3 | z-index: 8;
4 | background-color: #0072b3;
5 | border-bottom: 1px solid #8c45cf;
6 | width: 100%;
7 | display: flex;
8 | flex: 0 0 auto;
9 | margin: -70px 0 0 0;
10 | position: fixed;
11 | padding: 10px 10px 10px 20px;
12 | justify-content: stretch;
13 | height: 70px;
14 | }
15 |
16 | .top-bar > * {
17 | flex: 1;
18 | }
19 | .top-bar .help-container {
20 | flex: 0 1 auto; /* help item doesn't grow */
21 | }
22 |
23 | .top-bar .top-bar-item {
24 | background-color: #fdfaff;
25 | border: 1px solid #d7adff;
26 | box-shadow: 1px 3px 4px rgba(96, 1, 185, 0.25);
27 | margin: 0 8px;
28 | height: 100%;
29 | border-radius: 2px;
30 | cursor: default;
31 | }
32 |
33 | #logo {
34 | display: flex;
35 | justify-content: center;
36 | flex: 0 0 191px;
37 | }
38 |
39 | #logo a {
40 | text-align: center;
41 | }
42 |
43 | #logo img {
44 | height: 110%;
45 | margin-top: -2%;
46 | }
47 |
48 | #help-icon {
49 | height: 40px;
50 | width: 40px;
51 | }
52 |
53 | .mapboxgl-ctrl-geocoder {
54 | height: 100%;
55 | border-radius: 0px;
56 | max-width: none !important;
57 | }
58 |
59 | .dropdown-row {
60 | display: flex;
61 | margin-top: 0;
62 | }
63 |
64 | .mapboxgl-ctrl-geocoder {
65 | border-radius: 8px !important;
66 | }
67 |
68 | button.geocoder-icon.geocoder-icon-close {
69 | background-color: #fdfaff;
70 | }
71 |
72 | .msm-map-search-box {
73 | box-shadow: none !important;
74 | width: 100% !important;
75 | background-color: transparent;
76 | }
77 |
78 | .help-container {
79 | display: flex;
80 | flex-direction: column;
81 | justify-content: center;
82 | position: relative;
83 | z-index: 110;
84 | }
85 |
86 | .help-links {
87 | /* display: none; */
88 | position: absolute;
89 | right: -6px;
90 | top: 110%;
91 | visibility: hidden;
92 | text-align: right;
93 | width: 14em;
94 |
95 | background-color: #fdfaff;
96 | border: 1px solid #d7adff;
97 | box-shadow: 1px 3px 4px rgba(96, 1, 185, 0.25);
98 | border-radius: 3px;
99 | padding: 0.3em;
100 | }
101 | .help-links::before {
102 | content: "";
103 | width: 0;
104 | border-color: transparent transparent white transparent;
105 | border-style: solid;
106 | border-width: 0 14px 10px;
107 | position: absolute;
108 | bottom: 100%;
109 | z-index: 200;
110 | right: 12px;
111 | }
112 | .help-container:hover .help-links {
113 | /* display: block; */
114 | visibility: visible;
115 | }
116 |
117 | .help-icon {
118 | color: var(--msm-purple);
119 | }
120 |
121 | .dropdown-row ul {
122 | display: flex;
123 | flex-direction: row;
124 | }
125 |
126 | .clear-icon-container {
127 | padding: 10px;
128 | cursor: pointer;
129 | }
130 |
131 | @media only screen and (max-width: 1187px) {
132 | .responsive-disappear {
133 | display: none;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/TopBar/top-bar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "components/ProviderList/provider-list.css";
3 | import Logo from "./logo";
4 | import ProviderTypeDropdown from "./provider-type-dropdown";
5 | import Search from "./search";
6 | import DistanceDropdown from "./distance-dropdown";
7 | import HelpMenu from "./help-menu";
8 | import "./top-bar.css";
9 | // import VisaStatusDropdown from "./visa-status-dropdown";
10 |
11 | class TopBar extends Component {
12 | render() {
13 | const {
14 | distance,
15 | changeDistanceFilter,
16 | clearDistanceFilter,
17 | providerTypes,
18 | toggleProviderVisibility
19 | // visaTypes
20 | // changeVisaFilter,
21 | } = this.props;
22 | const topBarItemClass = "top-bar-item";
23 |
24 | return (
25 |
26 | {/*
*/}
31 |
32 |
37 |
38 |
44 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default TopBar;
53 |
--------------------------------------------------------------------------------
/src/components/TopBar/visa-status-dropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import CheckBoxDropdown from "../Dropdowns/checkbox-dropdown";
3 |
4 | const defaultSubheaderText = "Current Visa Status";
5 | const numberOptionsBeforeViewmore = 3
6 | export default class VisaStatusDropdown extends React.Component {
7 | state = {
8 | viewAllOptions: false,
9 | expanded: false
10 | };
11 |
12 | onCheckboxChanged = (option, selectedValues) => {
13 | const { onChange } = this.props;
14 | onChange(option);
15 | };
16 |
17 | onSeeMore = () => {
18 | this.setState({
19 | viewAllOptions: true
20 | });
21 | };
22 |
23 | setExpanded = expanded => {
24 | this.setState({
25 | viewAllOptions: !expanded ? false : this.state.viewAllOptions,
26 | expanded
27 | });
28 | };
29 |
30 | render() {
31 | let { className, visaTypes } = this.props;
32 | const { viewAllOptions, expanded } = this.state;
33 | let subheaderText = defaultSubheaderText;
34 |
35 | if (visaTypes.visible.length === 1) {
36 | subheaderText = visaTypes.visible[0];
37 | } else {
38 | subheaderText = visaTypes.visible.length + " Selected";
39 | }
40 |
41 | const preferredVisaTypes = viewAllOptions ? visaTypes.allVisas : visaTypes.allVisas.slice(0, numberOptionsBeforeViewmore)
42 | const footer = See More
;
43 | const footerShown = visaTypes.allVisas.length > numberOptionsBeforeViewmore ? (viewAllOptions ? null : footer) : null;
44 | return (
45 | ({
48 | id: visaType,
49 | display: visaType
50 | }))}
51 | onChange={this.onCheckboxChanged}
52 | visibleTypes={visaTypes.visible}
53 | header={
54 | <>
55 | VISA STATUS
56 | {subheaderText}
57 | >
58 | }
59 | footer={footerShown}
60 | expanded={expanded}
61 | setExpanded={this.setExpanded}
62 | />
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import DetailsPane from "./DetailsPane";
2 | import Map from "./Map";
3 | import TabbedMenu from "./TabbedMenu";
4 | import ProviderList from "./ProviderList";
5 | import MenuDistanceFilter from "./MenuDistanceFilter";
6 | import MenuVisaFilter from "./MenuVisaFilter";
7 | import MenuDropdown from "./MenuDropdown";
8 | import MenuDropdownItem from "./MenuDropdownItem";
9 | import SavedProvidersList from "./SavedProvidersList";
10 | import TopBar from "./TopBar";
11 |
12 | export {
13 | DetailsPane,
14 | Map,
15 | TabbedMenu,
16 | ProviderList,
17 | MenuDistanceFilter,
18 | MenuVisaFilter,
19 | MenuDropdown,
20 | MenuDropdownItem,
21 | SavedProvidersList,
22 | TopBar
23 | };
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --msm-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
3 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
4 | sans-serif;
5 | --msm-purple: #8c45cf;
6 | }
7 |
8 | body {
9 | margin: 0;
10 | padding: 0;
11 | font-family: var(--msm-font-family);
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | * {
17 | box-sizing: border-box;
18 | -webkit-user-select: none; /* Chrome all / Safari all */
19 | -moz-user-select: none; /* Firefox all */
20 | -ms-user-select: none; /* IE 10+ */
21 | user-select: none; /* Likely future */
22 | }
23 | html,
24 | body,
25 | div#root {
26 | height: 100%;
27 | overflow: hidden;
28 | }
29 | div#root {
30 | display: flex;
31 | flex-direction: column;
32 | padding: 70px 0 0 0;
33 | }
34 | main {
35 | flex: 1 0 auto;
36 | position: relative;
37 | }
38 |
39 | code {
40 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
41 | monospace;
42 | }
43 |
44 | li {
45 | list-style: none;
46 | display: flex;
47 | flex-direction: column;
48 | align-items: flex-start;
49 | justify-content: flex-start;
50 | margin-left: 0;
51 | padding-left: 0;
52 | }
53 |
54 | ul {
55 | margin-left: 0;
56 | padding-left: 0;
57 | }
58 |
59 | a,
60 | h1,
61 | h2,
62 | h3,
63 | h4,
64 | h5,
65 | h6,
66 | i {
67 | color: var(--msm-purple);
68 | text-decoration: none;
69 | }
70 |
71 | h2 {
72 | font-size: 1rem;
73 | font-weight: 500;
74 | padding: 0.2rem;
75 | }
76 |
77 | nav {
78 | padding: 0;
79 | max-height: 3rem;
80 | display: flex;
81 | /* background-color: #8c45cf; */
82 | }
83 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 | import * as serviceWorker from "./serviceWorker";
6 | import { getProvidersFromSheet, providersSheetUrl } from "util/googleSheets";
7 |
8 | import { initializeVisaFilter } from "./redux/actions";
9 | import store from "./redux/store";
10 |
11 | import VISA_TYPES from "./assets/visa-types";
12 |
13 | store.dispatch(initializeVisaFilter(VISA_TYPES));
14 |
15 | getProvidersFromSheet(providersSheetUrl);
16 |
17 | ReactDOM.render(, document.getElementById("root"));
18 |
19 | // If you want your app to work offline and load faster, you can change
20 | // unregister() to register() below. Note this comes with some pitfalls.
21 | // Learn more about service workers: http://bit.ly/CRA-PWA
22 | serviceWorker.unregister();
23 |
--------------------------------------------------------------------------------
/src/provider-type-to-color.json:
--------------------------------------------------------------------------------
1 | {
2 | "Job Placement": "#B55023",
3 | "Community Center": "#E2484C",
4 | "Cash/Food Assistance": "#B55023",
5 | "Education": "#915656",
6 | "Resettlement": "#1C6BA5",
7 | "Health": "#1845A4",
8 | "Mental Health": "#1C6BA5",
9 | "Housing": "#906D6E",
10 | "Legal": "#1845A4"
11 | }
12 |
--------------------------------------------------------------------------------
/src/redux/actions.js:
--------------------------------------------------------------------------------
1 | export const INITIALIZE_PROVIDERS = "INITIALIZE_PROVIDERS";
2 | export const INITIALIZE_VISA = "INITIALIZE_VISA";
3 | export const TOGGLE_TYPE = "TOGGLE_TYPE";
4 | export const CHANGE_DISTANCE = "CHANGE_DISTANCE";
5 | export const CLEAR_DISTANCE = "CLEAR_DISTANCE";
6 | export const CHANGE_VISA = "CHANGE_VISA";
7 | export const CLEAR_VISA = "CLEAR_VISA";
8 | export const HIGHLIGHT_PROVIDER = "HIGHLIGHT_PROVIDER";
9 | export const SET_SEARCH_RESULT = "SET_SEARCH_RESULT";
10 | export const CLEAR_SEARCH_RESULT = "CLEAR_SEARCH_RESULT";
11 | export const FILTER_PROVIDERS = "FILTER_PROVIDERS";
12 | export const FILTER_NAME = "FILTER_NAME";
13 | export const SAVE_PROVIDER = "SAVE_PROVIDER";
14 | export const SELECT_TAB = "SELECT_TAB";
15 | export const REORDER_SAVED_PROVIDERS = "REORDER_SAVED_PROVIDERS";
16 | export const CHANGE_SORT_ORDER = "CHANGE_SORT_ORDER";
17 | export const CHANGE_SORT_DIRECTION = "CHANGE_SORT_DIRECTION";
18 | export const SET_MAP_OBJECT = "SET_MAP_OBJECT";
19 | export const FLY_TO_PROVIDER = "FLY_TO_PROVIDER";
20 | export const ZOOM_TO_FIT = "ZOOM_TO_FIT";
21 | export const SELECT_NEW_PROVIDER = "SELECT_NEW_PROVIDER";
22 | export const HOVERED_PROVIDER = "HOVERED_PROVIDER";
23 |
24 | /**
25 | * Returns a new number each time it's called, useful to differentiate actions that would
26 | * otherwise contain the same data.
27 | */
28 | const nextActionKey = (() => {
29 | let actionKey = 0;
30 | return () => actionKey++;
31 | })();
32 |
33 | export const initializeProviders = providers => {
34 | // TODO WHEN ASYNC async dispatch => {
35 | // return await dispatch({
36 | return {
37 | type: INITIALIZE_PROVIDERS,
38 | payload: providers
39 | };
40 | //});
41 | };
42 |
43 | export function setMapObject(mapObject) {
44 | return {
45 | type: SET_MAP_OBJECT,
46 | mapObject
47 | };
48 | }
49 |
50 | export function displayProviderInformation(providerId) {
51 | return {
52 | type: HIGHLIGHT_PROVIDER,
53 | providerId
54 | };
55 | }
56 |
57 | export function toggleProviderVisibility(providerType) {
58 | return {
59 | type: TOGGLE_TYPE,
60 | payload: providerType
61 | };
62 | }
63 |
64 | export function changeDistanceFilter(distance) {
65 | return {
66 | type: CHANGE_DISTANCE,
67 | distance
68 | };
69 | }
70 |
71 | export function clearDistanceFilter() {
72 | return {
73 | type: CLEAR_DISTANCE
74 | };
75 | }
76 |
77 | export function initializeVisaFilter(visas) {
78 | return {
79 | type: INITIALIZE_VISA,
80 | visas
81 | };
82 | }
83 |
84 | export function changeVisaFilter(visa) {
85 | return {
86 | type: CHANGE_VISA,
87 | visa
88 | };
89 | }
90 |
91 | export function clearVisaFilter() {
92 | return {
93 | type: CLEAR_VISA
94 | };
95 | }
96 |
97 | export function setSearchResult(searchCoordinates, mapboxId, searchText) {
98 | return {
99 | type: SET_SEARCH_RESULT,
100 | coordinates: searchCoordinates,
101 | mapboxId,
102 | text: searchText,
103 | key: nextActionKey()
104 | };
105 | }
106 |
107 | export function clearSearchResult() {
108 | return {
109 | type: CLEAR_SEARCH_RESULT,
110 | key: nextActionKey()
111 | };
112 | }
113 |
114 | export function setFilteredProviders(providers) {
115 | return {
116 | type: FILTER_PROVIDERS,
117 | providers
118 | };
119 | }
120 |
121 | export function filterByName(name) {
122 | return {
123 | type: FILTER_NAME,
124 | name
125 | };
126 | }
127 |
128 | export function saveProvider(id) {
129 | return {
130 | type: SAVE_PROVIDER,
131 | id
132 | };
133 | }
134 |
135 | export function selectTab(index) {
136 | return {
137 | type: SELECT_TAB,
138 | index
139 | };
140 | }
141 |
142 | export function reorderSavedProviders(ids) {
143 | return {
144 | type: REORDER_SAVED_PROVIDERS,
145 | ids
146 | };
147 | }
148 |
149 | export function changeSortOrder(id) {
150 | return {
151 | type: CHANGE_SORT_ORDER,
152 | id
153 | };
154 | }
155 |
156 | export function changeSortDirection(direction) {
157 | return {
158 | type: CHANGE_SORT_DIRECTION,
159 | direction
160 | };
161 | }
162 |
163 | export function flyToProvider(id) {
164 | return {
165 | type: FLY_TO_PROVIDER,
166 | key: nextActionKey(),
167 | id
168 | };
169 | }
170 |
171 | export function zoomToFit() {
172 | return {
173 | type: ZOOM_TO_FIT,
174 | key: nextActionKey()
175 | };
176 | }
177 |
178 | export function selectProvider(id) {
179 | return {
180 | type: SELECT_NEW_PROVIDER,
181 | key: nextActionKey(),
182 | id
183 | };
184 | }
185 |
186 |
187 | export function setHoveredProvider(id) {
188 | return {
189 | type: HOVERED_PROVIDER,
190 | key: nextActionKey(),
191 | id
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/redux/filters.js:
--------------------------------------------------------------------------------
1 | import {
2 | CHANGE_DISTANCE,
3 | CLEAR_DISTANCE,
4 | INITIALIZE_VISA,
5 | CHANGE_VISA,
6 | CLEAR_VISA
7 | } from "./actions";
8 |
9 | const INITIAL_VISA_STATE = {
10 | allVisas: [],
11 | visible: [],
12 | };
13 |
14 | export default function filters(state = { visa: INITIAL_VISA_STATE }, action) {
15 | // TO ADD LATER: visa status, accepting clients
16 | switch (action.type) {
17 | case CLEAR_DISTANCE: // both init and 'clear' button
18 | return { ...state, distance: null };
19 | case CHANGE_DISTANCE:
20 | return { ...state, distance: action.distance };
21 | case INITIALIZE_VISA:
22 | return { ...state, visa: {
23 | allVisas: action.visas,
24 | visible: state.visa.visible
25 | } };
26 | case CLEAR_VISA: // both init and 'clear' button
27 | return { ...state, visa: INITIAL_VISA_STATE };
28 | case CHANGE_VISA:
29 | return changeVisa(state, action.visa);
30 | default:
31 | return state;
32 | }
33 | }
34 |
35 | function changeVisa(state, visa) {
36 | if (state.visa.visible.includes(visa)) {
37 | const removedVisible = state.visa.visible.filter(typeId => visa !== typeId);
38 | return { ...state, visa: {
39 | allVisas: state.visa.allVisas,
40 | visible: removedVisible
41 | } };
42 | }
43 | return { ...state, visa: {
44 | allVisas: state.visa.allVisas,
45 | visible: [ ...state.visa.visible, visa]
46 | } };
47 | }
--------------------------------------------------------------------------------
/src/redux/highlightedProviders.js:
--------------------------------------------------------------------------------
1 | import { HIGHLIGHT_PROVIDER } from "./actions";
2 | import dotProp from "dot-prop-immutable";
3 |
4 | export default function highlightedProviders(state = [], action) {
5 | switch (action.type) {
6 | case HIGHLIGHT_PROVIDER: {
7 | const providerIndex = state.findIndex(id => id === action.providerId);
8 | if (providerIndex > -1) {
9 | return dotProp.delete(state, providerIndex);
10 | } else {
11 | return [action.providerId, ...state];
12 | }
13 | }
14 | default:
15 | return state;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/redux/hoveredProvider.js:
--------------------------------------------------------------------------------
1 | import { HOVERED_PROVIDER } from "./actions";
2 |
3 | export default function hoveredProvider(state = null, action) {
4 | if (action.type === HOVERED_PROVIDER) {
5 | state = action.id;
6 | }
7 | return state;
8 | }
9 |
--------------------------------------------------------------------------------
/src/redux/mapObject.js:
--------------------------------------------------------------------------------
1 | import { SET_MAP_OBJECT } from "./actions";
2 |
3 | const INITIAL_STATE = {
4 | mapObject: {}
5 | };
6 |
7 | export default function mapObject(state = INITIAL_STATE, action) {
8 | switch (action.type) {
9 | case SET_MAP_OBJECT:
10 | return action.mapObject;
11 | default:
12 | return state;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/redux/providerModels.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | /**
4 | * Represents any error generated while loading a provider.
5 | */
6 | class InvalidProviderError extends Error {
7 | constructor(messageOrCause) {
8 | const message = String(messageOrCause);
9 | super(message);
10 | this.name = this.constructor.name;
11 | if (messageOrCause instanceof Error) {
12 | this.stack = `Unexpected error\n${messageOrCause.stack}`;
13 | }
14 | }
15 | }
16 |
17 | /**
18 | * List of all provider types recognized by the app.
19 | * Provider type names are matched to these values.
20 | */
21 | const allProviderTypes = {
22 | jobPlacement: { name: "Job Placement", id: "job-placement" },
23 | resettlement: { name: "Resettlement", id: "resettlement" },
24 | health: { name: "Health", id: "health" },
25 | mentalHealth: { name: "Mental Health", id: "mental-health" },
26 | legal: { name: "Legal", id: "legal" },
27 | education: { name: "Education", id: "education" },
28 | communityCenter: { name: "Community Center", id: "community-centers" },
29 | cashFoodAssistance: {
30 | name: "Cash/Food Assistance",
31 | id: "cash/food-assistance"
32 | },
33 | housing: { name: "Housing", id: "housing" }
34 | };
35 |
36 | const allTypeIds = Object.values(allProviderTypes).map(t => t.id);
37 | const typeIdsByNormalizedTypeName = {
38 | ..._.zipObject(allTypeIds, allTypeIds),
39 | "community-center": allProviderTypes.communityCenter.id
40 | };
41 | const typeNamesByTypeId = _.fromPairs(
42 | Object.values(allProviderTypes).map(t => [t.id, t.name])
43 | );
44 |
45 | const check = (condition, why, what) => {
46 | if (!condition) {
47 | throw new InvalidProviderError(`${why}: ${what}`);
48 | }
49 | };
50 |
51 | const loadString = (name, string) => {
52 | check(typeof string === "string", `${name} must be a string`, string);
53 | check(string.length, `${name} must be nonempty`, string);
54 | return string;
55 | };
56 |
57 | const loadTypeId = typeName => {
58 | const normalizedTypeName = String(typeName)
59 | .toLowerCase()
60 | .replace(/ /, "-"),
61 | typeId = typeIdsByNormalizedTypeName[normalizedTypeName];
62 | if (!typeId) {
63 | throw new InvalidProviderError(
64 | `Could not match type name ${typeName} to type ID`
65 | );
66 | }
67 | return typeId;
68 | };
69 |
70 | const loadTypeName = typeName => {
71 | // The type name is valid if it matches an ID.
72 | let id;
73 | try {
74 | id = loadTypeId(typeName);
75 | } catch (e) {
76 | throw new InvalidProviderError(`Invalid type name ${typeName}`);
77 | }
78 | // Since multiple capitalizations can map to the same ID, return a "canonical" name for the type.
79 | if (typeNamesByTypeId[id]) {
80 | return typeNamesByTypeId[id];
81 | } else {
82 | throw new InvalidProviderError(`Invalid type name ${typeName}`);
83 | }
84 | };
85 |
86 | const loadCoordinates = c => {
87 | check(!_.isNil(c), "coordinates must be defined", c);
88 | const coordinates = Array.from(c);
89 | check(coordinates.length === 2, "Expected [lon, lat]", coordinates);
90 | check(
91 | typeof coordinates[0] === "number" &&
92 | coordinates[0] >= -180 &&
93 | coordinates[0] <= 180,
94 | "Expected [lon, lat]",
95 | coordinates
96 | );
97 | check(
98 | typeof coordinates[1] === "number" &&
99 | coordinates[1] >= -90 &&
100 | coordinates[1] <= 90,
101 | "Expected [lon, lat]",
102 | coordinates
103 | );
104 | return coordinates;
105 | };
106 |
107 | /**
108 | * Validates and creates objects representing service providers and organizes them by type.
109 | */
110 | class ProviderBuilder {
111 | constructor() {
112 | this._loadedProviders = [];
113 | }
114 |
115 | /**
116 | * Adds a provider to the overall set of providers. All fields are required except those with default values.
117 | *
118 | * @throws InvalidProviderError if any of the fields are invalid.
119 | */
120 | addProvider({
121 | coordinates,
122 | address,
123 | email = "n/a",
124 | mission,
125 | name,
126 | telephone = "n/a",
127 | timestamp,
128 | typeName,
129 | website = "n/a"
130 | }) {
131 | let provider;
132 | try {
133 | provider = {
134 | coordinates: loadCoordinates(coordinates),
135 | address: loadString("address", address),
136 | email: loadString("email", email),
137 | name: loadString("name", name),
138 | mission: loadString("mission", mission),
139 | telephone: loadString("telephone", telephone),
140 | website: loadString("website", website), // check URL?
141 | timestamp: loadString("timestamp", timestamp), // check can parse to Date?
142 | typeName: loadTypeName(typeName),
143 | typeId: loadTypeId(typeName)
144 | };
145 | } catch (e) {
146 | if (e instanceof InvalidProviderError) {
147 | throw e;
148 | }
149 | throw new InvalidProviderError(e);
150 | }
151 | this._loadedProviders.push(provider);
152 | }
153 |
154 | _getUniqueProviders() {
155 | return _.uniqWith(
156 | this._loadedProviders,
157 | (p1, p2) =>
158 | _.isEqual(p1.coordinates, p2.coordinates) &&
159 | _.isEqual(p1.name, p2.name) &&
160 | _.isEqual(p1.typeId, p2.typeId)
161 | );
162 | }
163 |
164 | _getProvidersById(providers) {
165 | const providersById = {};
166 | let id = 0;
167 | providers.forEach(provider => {
168 | provider.id = id++;
169 | providersById[provider.id] = provider;
170 | });
171 | return providersById;
172 | }
173 |
174 | _getProviderTypesByTypeId(providers) {
175 | const providersByTypeId = _.groupBy(providers, provider => provider.typeId);
176 | const providerTypesByTypeId = {};
177 | let providersOfSameType;
178 | for (providersOfSameType of Object.values(providersByTypeId)) {
179 | const typeId = providersOfSameType[0].typeId;
180 | providerTypesByTypeId[typeId] = {
181 | id: typeId,
182 | name: providersOfSameType[0].typeName,
183 | providers: providersOfSameType.map(provider => provider.id)
184 | };
185 | }
186 | return providerTypesByTypeId;
187 | }
188 |
189 | /**
190 | * Returns an object with providers and providerTypes, as used to initialize the app.
191 | *
192 | * This should be called once, after adding all providers.
193 | */
194 | build() {
195 | const uniqueProviders = this._getUniqueProviders(),
196 | providersById = this._getProvidersById(uniqueProviders),
197 | providerTypesByTypeId = this._getProviderTypesByTypeId(uniqueProviders);
198 |
199 | return {
200 | providers: { byId: providersById },
201 | providerTypes: {
202 | allIds: Object.keys(providerTypesByTypeId),
203 | byId: providerTypesByTypeId
204 | }
205 | };
206 | }
207 | }
208 |
209 | export { InvalidProviderError, allProviderTypes, ProviderBuilder };
210 |
--------------------------------------------------------------------------------
/src/redux/providerTypes.js:
--------------------------------------------------------------------------------
1 | import { INITIALIZE_PROVIDERS, TOGGLE_TYPE } from "./actions";
2 |
3 | const INITIAL_STATE = {
4 | allIds: [],
5 | byId: {},
6 | visible: []
7 | };
8 |
9 | export default function providerTypes(state = INITIAL_STATE, action) {
10 | switch (action.type) {
11 | case INITIALIZE_PROVIDERS:
12 | return initialProviders(state, action.payload);
13 | case TOGGLE_TYPE:
14 | return setVisibleTypes(state, action.payload);
15 | default:
16 | return state;
17 | }
18 | }
19 |
20 | function initialProviders(state, payload) {
21 | return {
22 | ...state,
23 | allIds: payload.providerTypes.allIds,
24 | byId: payload.providerTypes.byId
25 | };
26 | }
27 |
28 | function setVisibleTypes(state, type) {
29 | // If visible contains type removes it
30 | if (typeof type === 'undefined') {
31 | return { ...state, visible: [] };
32 | }
33 | if (state.visible.includes(type)) {
34 | const removedVisible = state.visible.filter(typeId => type !== typeId);
35 | return { ...state, visible: removedVisible };
36 | }
37 | return { ...state, visible: [...state.visible, type] };
38 | }
39 |
--------------------------------------------------------------------------------
/src/redux/providers.js:
--------------------------------------------------------------------------------
1 | import {
2 | INITIALIZE_PROVIDERS,
3 | SAVE_PROVIDER,
4 | CHANGE_SORT_ORDER,
5 | CHANGE_SORT_DIRECTION,
6 | REORDER_SAVED_PROVIDERS,
7 | SELECT_NEW_PROVIDER
8 | } from "./actions";
9 |
10 | const INITIAL_STATE = {
11 | allIds: [],
12 | byId: {},
13 | sortMethod: "Provider Type",
14 | sortDirection: "desc",
15 | savedProviders: []
16 | };
17 |
18 | export default function providers(state = INITIAL_STATE, action) {
19 | switch (action.type) {
20 | case INITIALIZE_PROVIDERS:
21 | return initialProviders(state, action.payload);
22 | case SAVE_PROVIDER:
23 | return saveProvider(state, action.id);
24 | case CHANGE_SORT_ORDER:
25 | return {
26 | ...state,
27 | sortMethod: action.id
28 | };
29 | case CHANGE_SORT_DIRECTION:
30 | return {
31 | ...state,
32 | sortDirection: action.direction
33 | };
34 | case REORDER_SAVED_PROVIDERS:
35 | return {
36 | ...state,
37 | savedProviders: action.ids
38 | };
39 | case SELECT_NEW_PROVIDER:
40 | return {
41 | ...state,
42 | selectProviderId: action.id,
43 | selectProviderKey: action.key
44 | }
45 | default:
46 | return state;
47 | }
48 | }
49 |
50 | function initialProviders(state, payload) {
51 | return {
52 | ...state,
53 | allIds: payload.providers.allIds,
54 | byId: payload.providers.byId
55 | };
56 | }
57 |
58 | function saveProvider(state, id) {
59 | return state.savedProviders.includes(id)
60 | ? {
61 | //deletes if true
62 | ...state,
63 | savedProviders: state.savedProviders.filter(p => p !== id)
64 | } //adds if false
65 | : { ...state, savedProviders: [id, ...state.savedProviders] };
66 | }
67 |
--------------------------------------------------------------------------------
/src/redux/search.js:
--------------------------------------------------------------------------------
1 | import {
2 | SELECT_TAB,
3 | FLY_TO_PROVIDER,
4 | ZOOM_TO_FIT,
5 | CLEAR_SEARCH_RESULT,
6 | SET_SEARCH_RESULT,
7 | } from "./actions";
8 |
9 | const INITIAL_STATE = {
10 | mapCenter: [-71.066954, 42.359947],
11 | coordinates: [-71.066954, 42.359947],
12 | selectedTabIndex: 0,
13 | currentLocation: "default", //references item in history object once user submits search string
14 | history: {
15 | default: {
16 | coordinates: [-71.066954, 42.359947],
17 | searchText: "center of Boston"
18 | }
19 | }
20 | };
21 |
22 | export default function search(state = INITIAL_STATE, action) {
23 | switch (action.type) {
24 | case SELECT_TAB:
25 | return { ...state, selectedTabIndex: action.index };
26 | case SET_SEARCH_RESULT:
27 | return {
28 | ...state,
29 | searchKey: action.key,
30 | coordinates: action.coordinates,
31 | currentLocation: action.mapboxId,
32 | selectedTabIndex: 0,
33 | history: {
34 | ...state.history,
35 | [action.mapboxId]: {
36 | coordinates: action.coordinates,
37 | searchText: action.text
38 | }
39 | }
40 | };
41 | case CLEAR_SEARCH_RESULT:
42 | return {
43 | ...INITIAL_STATE,
44 | searchKey: action.key,
45 | selectedTabIndex: state.selectedTabIndex
46 | }
47 | case FLY_TO_PROVIDER:
48 | return {
49 | ...state,
50 | flyToProviderId: action.id,
51 | flyToProviderKey: action.key
52 | };
53 | case ZOOM_TO_FIT:
54 | return {
55 | ...state,
56 | zoomToFitKey: action.key
57 | };
58 | default:
59 | return state;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/redux/selectors.js:
--------------------------------------------------------------------------------
1 | import { distance, point } from "@turf/turf";
2 | import { createSelector } from "reselect";
3 |
4 | const getProviderTypesById = state => state.providerTypes.byId;
5 | const getVisibleProviderTypes = state => state.providerTypes.visible;
6 | const getProvidersById = state => state.providers.byId;
7 | const getDistance = state => state.filters.distance;
8 | const getSavedProvidersIds = state => state.providers.savedProviders;
9 | const getHighlightedProvidersList = state => state.highlightedProviders;
10 | const getSearchCoordinates = state =>
11 | state.search.history[state.search.currentLocation];
12 | // const getSearchCoordinates = state => state.search.currentLocation ? state.search.history[state.search.currentLocation] : null; // TODO: separate coordinates and searched location
13 | const getSortMethod = state => state.providers.sortMethod;
14 | const getSortDirection = state => state.providers.sortDirection;
15 | const getResultCase = state =>
16 | state.search.selectedTabIndex === 1 ? "saved" : "search";
17 |
18 | export const getProvidersSorted = createSelector(
19 | [
20 | getProviderTypesById,
21 | getVisibleProviderTypes,
22 | getProvidersById,
23 | getDistance,
24 | getSearchCoordinates,
25 | // visa status,
26 | // accepting new clients,
27 | getSortMethod,
28 | getSortDirection
29 | ],
30 | (
31 | providerTypesById,
32 | visibleProviderTypes,
33 | providersById,
34 | distance,
35 | searchCoordinates,
36 | sortMethod,
37 | sortDirection
38 | ) => {
39 | // for each provider type that is active, get providers belonging to it that are near the search location
40 | // TODO: limit based on new client status and visa type
41 | let groupedByProviderType = visibleProviderTypes.map(id => {
42 | let providerType = providerTypesById[id];
43 | let providers = providerType.providers.map(
44 | provId => providersById[provId]
45 | );
46 | const options = { units: "miles" };
47 | let providersWithDistances = calculateProviderDistances(
48 | providers,
49 | searchCoordinates,
50 | options
51 | );
52 | let nearbyProviders = distance
53 | ? getProvidersWithinDistance(providersWithDistances, distance)
54 | : providersWithDistances;
55 | return {
56 | ...providerType,
57 | providers: sortProvidersByDistance(nearbyProviders)
58 | };
59 | });
60 |
61 | // sort and return an array of grouped providers
62 | // (for distance and alphabetical, it's a single-object array)
63 | let flatList = groupedByProviderType.reduce(
64 | (result, type) => result.concat(type.providers),
65 | [] // result needs to be initialized to empty array
66 | );
67 | switch (sortMethod) {
68 | case "Distance":
69 | return [
70 | {
71 | id: "distance-sort",
72 | name: getDistanceSortText(sortDirection),
73 | providers: sortProvidersByDistance(flatList, sortDirection)
74 | }
75 | ];
76 | case "Name":
77 | return [
78 | {
79 | id: "alphabetical",
80 | name: "By name",
81 | providers: sortProvidersByName(flatList, sortDirection)
82 | }
83 | ];
84 | case "Provider Type":
85 | return sortProvidersByType(groupedByProviderType, sortDirection);
86 |
87 | default:
88 | return groupedByProviderType;
89 | }
90 | }
91 | );
92 |
93 | export const getSavedProviders = createSelector(
94 | [getSavedProvidersIds, getProvidersById, getSearchCoordinates],
95 | (savedProvidersIds, providersById, searchCoordinates) => {
96 | if (!searchCoordinates) {
97 | // no distance information included
98 | return savedProvidersIds.map(id => providersById[id]);
99 | }
100 | return savedProvidersIds.map(id => {
101 | const provDistance = distance(
102 | // TODO use coordinates from search history when provider was saved
103 | point(providersById[id].coordinates),
104 | point(searchCoordinates.coordinates),
105 | { units: "miles" }
106 | );
107 | return {
108 | ...providersById[id],
109 | distance: provDistance
110 | };
111 | });
112 | }
113 | );
114 |
115 | export const getHighlightedProviders = createSelector(
116 | [getProvidersById, getHighlightedProvidersList],
117 | (providersById, highlightedProvidersList) => {
118 | return highlightedProvidersList.map(id => providersById[id]);
119 | }
120 | );
121 |
122 | export const getSearchResultProviders = createSelector(
123 | [getProvidersSorted],
124 | searchResults => {
125 | let resultProvidersById = {};
126 | searchResults.forEach(category =>
127 | category.providers.forEach(
128 | provider => (resultProvidersById[provider.id] = provider)
129 | )
130 | );
131 | return Object.values(resultProvidersById);
132 | }
133 | );
134 |
135 | export const getMapProviders = createSelector(
136 | [getResultCase, getSearchResultProviders, getSavedProviders],
137 | (resultCase, searchProviders, savedProviders) =>
138 | resultCase === "saved" ? savedProviders : searchProviders
139 | );
140 |
141 | function calculateProviderDistances(providers, refLocation, options) {
142 | var referencePoint = point(refLocation.coordinates);
143 | return providers.map(provider => {
144 | // New object with the distance attached
145 | return {
146 | ...provider,
147 | distance: distance(point(provider.coordinates), referencePoint, options)
148 | };
149 | });
150 | }
151 |
152 | function getDistanceSortText(sortDirection) {
153 | return sortDirection === "desc"
154 | ? "Closest to farthest"
155 | : "Farthest to closest";
156 | }
157 |
158 | function getProvidersWithinDistance(providers, maxDistance) {
159 | return providers.filter(
160 | provider => maxDistance && provider.distance < maxDistance
161 | );
162 | }
163 |
164 | function sortProvidersByDistance(providerArray, direction) {
165 | // Sort the list by distance
166 | return providerArray.sort((a, b) => {
167 | if (direction === "asc") {
168 | return a.distance < b.distance ? 1 : -1;
169 | }
170 | return a.distance > b.distance ? 1 : -1;
171 | });
172 | }
173 |
174 | function sortProvidersByName(providerArray, direction) {
175 | return providerArray.sort((a, b) => {
176 | if (direction === "asc") {
177 | return a.name < b.name ? 1 : -1;
178 | }
179 | return a.name > b.name ? 1 : -1;
180 | });
181 | }
182 |
183 | function sortProvidersByType(prividersByType, direction) {
184 | return prividersByType.sort((a, b) => {
185 | if (direction === "asc") {
186 | return a.id < b.id ? 1 : -1;
187 | }
188 | return a.id > b.id ? 1 : -1;
189 | });
190 | }
191 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers } from "redux";
2 |
3 | import providers from "./providers";
4 | import providerTypes from "./providerTypes";
5 | import filters from "./filters";
6 | import search from "./search";
7 | import highlightedProviders from "./highlightedProviders";
8 | import mapObject from "./mapObject";
9 | import hoveredProvider from "./hoveredProvider";
10 |
11 | export default createStore(
12 | combineReducers({
13 | highlightedProviders,
14 | providers,
15 | providerTypes,
16 | filters,
17 | search,
18 | mapObject,
19 | hoveredProvider
20 | }),
21 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
22 | );
23 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener("load", () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | "This web app is being served cache-first by a service " +
46 | "worker. To learn more, visit http://bit.ly/CRA-PWA"
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === "installed") {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | "New content is available and will be used when all " +
74 | "tabs for this page are closed. See http://bit.ly/CRA-PWA."
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log("Content is cached for offline use.");
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error("Error during service worker registration:", error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get("content-type");
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf("javascript") === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | "No internet connection found. App is running in offline mode."
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ("serviceWorker" in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/util/googleSheets.js:
--------------------------------------------------------------------------------
1 | import Papa from "papaparse";
2 | import { initializeProviders } from "redux/actions";
3 | import store from "../redux/store";
4 | import { ProviderBuilder } from "../redux/providerModels";
5 | import _ from "lodash";
6 |
7 | const providersSheetUrl = "https://docs.google.com/spreadsheets/d/1Rd_U5KrD5LOoYgCO91WwJCxbji2FVbDoBqHa5qy2b0M/gviz/tq?tqx=out:csv&sheet=Form+Responses+1";
8 |
9 | const column = (prefix, fieldName) => ({
10 | prefix,
11 | fieldName
12 | });
13 | const requiredColumns = [
14 | column("validated by", "validatedBy"),
15 | column("timestamp", "timestamp"),
16 | column("organization name", "name"),
17 | column("website", "website"),
18 | column("type of service", "typeName"),
19 | column("mission", "mission"),
20 | column("telephone", "telephone"),
21 | column("email", "email"),
22 | column("address", "address"),
23 | column("longitude", "longitude"),
24 | column("latitude", "latitude")
25 | ];
26 |
27 | /**
28 | * Converts spreadsheet data (an array of row objects) to an array of objects with
29 | * fields matching the expected columns in the spreadsheet, as specified in `requiredColumns`.
30 | *
31 | * This function is responsible for validating that the spreadsheet has the correct format.
32 | *
33 | * @returns An array of objects with fields matching required spreadsheet columns. Fields are not
34 | * created for empty or whitespace-only cells.
35 | * @throws InvalidSpreadsheetFormat if provider fields cannot be matched to spreadsheet columns.
36 | */
37 | const getRawProviders = (rowObjects, columnNames) => {
38 | const normalizedColumnNames = columnNames.map(name =>
39 | name.toLowerCase().trim()
40 | );
41 |
42 | const fieldsByColumnName = {};
43 | requiredColumns.forEach(column => {
44 | const columnIndex = _.findIndex(normalizedColumnNames, columnName =>
45 | columnName.startsWith(column.prefix)
46 | );
47 | if (columnIndex === -1) {
48 | throw Error(
49 | `Couldn't find required column "${column.prefix}" among ${columnNames}`
50 | );
51 | }
52 | fieldsByColumnName[columnNames[columnIndex]] = column.fieldName;
53 | });
54 |
55 | return rowObjects.map(row => {
56 | const rawProvider = {};
57 | _.forOwn(fieldsByColumnName, (fieldName, columnName) => {
58 | const value = row[columnName].trim();
59 | if (value) {
60 | rawProvider[fieldName] = value;
61 | }
62 | });
63 | return rawProvider;
64 | });
65 | };
66 |
67 | /**
68 | * Builds a provider store from raw providers.
69 | *
70 | * @param rawProviders spreadsheet data organized into objects with fields matching
71 | * `requiredColumns`, as returned by `getRawProviders`.
72 | * @returns An object with fields:
73 | * providerStore: A provider store as returned by `ProviderBuilder.build`
74 | * loadErrors: An array of {index, error} objects indicating the providers
75 | * that couldn't be loaded.
76 | */
77 | const buildProviderStore = rawProviders => {
78 | const providerBuilder = new ProviderBuilder();
79 | const loadErrors = [];
80 | rawProviders.forEach((fields, index) => {
81 | try {
82 | providerBuilder.addProvider({
83 | coordinates: [
84 | parseFloat(fields.longitude),
85 | parseFloat(fields.latitude)
86 | ],
87 | name: fields.name,
88 | address: fields.address,
89 | email: fields.email,
90 | mission: fields.mission,
91 | telephone: fields.telephone,
92 | timestamp: fields.timestamp,
93 | typeName: fields.typeName,
94 | website: fields.website
95 | });
96 | } catch (error) {
97 | loadErrors.push({ index, error });
98 | }
99 | });
100 | return {
101 | providerStore: providerBuilder.build(),
102 | loadErrors
103 | };
104 | };
105 |
106 | const loadSpreadsheet = url => {
107 | return new Promise((resolve, reject) => {
108 | Papa.parse(url, {
109 | download: true,
110 | skipEmptyLines: "greedy",
111 | header: true,
112 | error: error => reject(error),
113 | complete: result =>
114 | resolve({
115 | rowObjects: result.data,
116 | parseErrors: result.errors,
117 | columnNames: result.meta.fields
118 | })
119 | });
120 | });
121 | };
122 |
123 | const alertLoadFailure = url => {
124 | window.alert(
125 | `An error occurred while loading the provider spreadsheet at ${url}. Please check developer tools for more information.`
126 | );
127 | };
128 |
129 | const getProvidersFromSheet = async url => {
130 | try {
131 | var { rowObjects, parseErrors, columnNames } = await loadSpreadsheet(url);
132 | } catch (e) {
133 | console.error(`Error while loading spreadsheet from ${url}`, e);
134 | alertLoadFailure(url);
135 | return;
136 | }
137 |
138 | if (Array.isArray(parseErrors) && parseErrors.length > 0) {
139 | console.error(
140 | `Errors while parsing CSV from ${url}: ${parseErrors.map(error =>
141 | JSON.stringify(error, null, " ")
142 | )}`
143 | );
144 | alertLoadFailure(url);
145 | return;
146 | }
147 |
148 | try {
149 | var rawProviders = getRawProviders(rowObjects, columnNames);
150 | } catch (e) {
151 | console.error(`Couldn't parse spreadsheet from ${url}, bad format`, e);
152 | alertLoadFailure(url);
153 | return;
154 | }
155 |
156 | const { providerStore, loadErrors } = buildProviderStore(rawProviders);
157 |
158 | if (loadErrors.length) {
159 | const googleSpreadsheetIndex = rawProviderIndex => rawProviderIndex + 2;
160 | const badSpreadsheetRows = loadErrors.map(({ index }) =>
161 | googleSpreadsheetIndex(index)
162 | );
163 | const allRowErrors = loadErrors
164 | .map(
165 | ({ error, index }) => `Row ${googleSpreadsheetIndex(index)}: ${error}`
166 | )
167 | .join("\n");
168 | console.warn(
169 | `Couldn't load providers from rows [${badSpreadsheetRows}] of ${url}. Errors:\n${allRowErrors}`
170 | );
171 | }
172 |
173 | store.dispatch(initializeProviders(providerStore));
174 | };
175 |
176 | export { getProvidersFromSheet, providersSheetUrl };
177 |
--------------------------------------------------------------------------------
/src/util/printJSX.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom";
2 | const printIFrame = document.createElement("iframe");
3 | const printContentDivId = "printContent";
4 | document.body.appendChild(printIFrame);
5 | printIFrame.contentWindow.document.open();
6 | printIFrame.contentWindow.document.write(``);
7 | printIFrame.contentWindow.document.close();
8 | printIFrame.display = "none";
9 | const printContentDiv = printIFrame.contentWindow.document.getElementById(
10 | printContentDivId
11 | );
12 |
13 | const cssLink = document.createElement("link");
14 | cssLink.href = "print.css";
15 | cssLink.rel = "stylesheet";
16 | cssLink.type = "text/css";
17 | printIFrame.contentWindow.document.head.appendChild(cssLink);
18 |
19 | function printJSX(contents) {
20 | const { contentWindow } = printIFrame;
21 | ReactDOM.render(contents, printContentDiv, () => {
22 | contentWindow.focus();
23 | contentWindow.print();
24 | });
25 | }
26 |
27 | export { printJSX };
28 |
--------------------------------------------------------------------------------