├── .eslintrc.js ├── .gitignore ├── .storybook └── main.js ├── .travis.yml ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── places.json └── robots.txt ├── src ├── App.css ├── App.js ├── App.test.js ├── components │ ├── AutoComplete.js │ ├── GoogleMap.js │ ├── Marker.js │ └── SearchBox.js ├── const │ └── la_center.js ├── examples │ ├── AutoComplete.js │ ├── Heatmap.js │ ├── Main.js │ ├── MarkerInfoWindow.js │ ├── MarkerInfoWindowGmapsObj.js │ └── Searchbox.js ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js ├── setupTests.js └── stories │ ├── Autocomplete │ └── Base.stories.js │ ├── ClickableMarkers │ └── Base.stories.js │ ├── Heatmap │ └── Base.stories.js │ ├── Main │ └── Base.stories.js │ └── Searchbox │ └── Base.stories.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 6 | "import/no-extraneous-dependencies": [ 7 | "error", 8 | { 9 | devDependencies: [ 10 | "src/stories/**", 11 | ] 12 | } 13 | ] 14 | }, 15 | "env": { 16 | "es6": true, 17 | "jest": true, 18 | "browser": true, 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /storybook-static 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | build-storybook.log* 26 | 27 | .env.* 28 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.js'], 3 | addons: [ 4 | '@storybook/preset-create-react-app', 5 | '@storybook/addon-actions', 6 | '@storybook/addon-links', 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | cache: 5 | directories: 6 | - node_modules 7 | before_script: 8 | - git diff --exit-code # make sure that yarn.lock didn't change 9 | script: 10 | - yarn run lint 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Examples for [google-map-react library](https://github.com/google-map-react/google-map-react) · [![Build Status](https://travis-ci.org/google-map-react/google-map-react-examples.svg?branch=master)](https://travis-ci.org/google-map-react/google-map-react-examples) 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Stack 6 | 7 | - [React](https://facebook.github.io/react) rendering 8 | - [React-router](https://reacttraining.com/react-router/) for routing 9 | - [Styled components](https://www.styled-components.com/) for styling our components 10 | 11 | ## Examples 12 | 13 | - [Main](https://google-map-react.github.io/google-map-react-examples/?path=/story/main-examples--base) ([source](https://github.com/google-map-react/google-map-react-examples/blob/master/src/examples/Main.js)) 14 | - [Heatmap](https://google-map-react.github.io/google-map-react-examples/?path=/story/heatmap-examples--base) ([source](https://github.com/google-map-react/google-map-react-examples/blob/master/src/examples/Heatmap.js)) 15 | - [Searchbox](https://google-map-react.github.io/google-map-react-examples/?path=/story/searchbox-examples--base) ([source](https://github.com/google-map-react/google-map-react-examples/blob/master/src/examples/Searchbox.js)) 16 | - [Autocomplete](https://google-map-react.github.io/google-map-react-examples/?path=/story/autocomplete-examples--base) ([source](https://github.com/google-map-react/google-map-react-examples/blob/master/src/examples/Searchbox.js)) 17 | - [Marker and Info Window using React Component](https://google-map-react.github.io/google-map-react-examples/?path=/story/markerinfo-examples--custom-component) ([source](https://github.com/google-map-react/google-map-react-examples/blob/master/src/examples/MarkerInfoWindow.js)) 18 | - [Marker and Info Window using Google Maps API Object](https://google-map-react.github.io/google-map-react-examples/?path=/story/markerinfo-examples--default-marker) ([source](https://github.com/google-map-react/google-map-react-examples/blob/master/src/examples/MarkerInfoWindowGmapsObj.js)) 19 | 20 | ## Getting started 21 | 22 | ### Installation 23 | 24 | Although I prefer yarn, you can use either yarn or npm: 25 | 26 | - `npm install` or simply `yarn` 27 | 28 | ### Run development 29 | 30 | - `npm start` or `yarn start` 31 | 32 | Runs the app in development mode.
33 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 34 | 35 | The page will automatically reload if you make changes to the code.
36 | You will see the build errors and lint warnings in the console. 37 | 38 | ### Run production build 39 | 40 | - `npm run build` or `yarn build` 41 | 42 | Builds the app for production to the `build` folder.
43 | It correctly bundles React in production mode and optimizes the build for the best performance. 44 | 45 | The build is minified and the filenames include the hashes.
46 | By default, it also [includes a service worker](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#making-a-progressive-web-app) so that your app loads from local cache on future visits. 47 | 48 | Your app is ready to be deployed. 49 | 50 | ### Deploy to GH pages 51 | 52 | - `npm run deploy` or `yarn deploy` 53 | 54 | This will push a branch named `gh-pages` to the repository, which will be used by GH to render the latest version of our site. 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-map-react-examples", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://itsmichaeldiego.github.io/google-map-react-examples/", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.0", 8 | "@testing-library/react": "^10.4.5", 9 | "@testing-library/user-event": "^12.0.11", 10 | "google-map-react": "^2.0.1", 11 | "lodash.isempty": "^4.4.0", 12 | "prop-types": "^15.6.1", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-scripts": "3.4.1", 16 | "styled-components": "^5.1.1" 17 | }, 18 | "scripts": { 19 | "lint": "eslint src", 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject", 24 | "predeploy": "npm run build-storybook", 25 | "deploy": "gh-pages -d storybook-static", 26 | "storybook": "start-storybook -p 9009 -s public", 27 | "build-storybook": "build-storybook -s public", 28 | "chromatic": "npx chromatic --project-token=2mchcgpjqg2" 29 | }, 30 | "devDependencies": { 31 | "@storybook/addon-actions": "^5.3.19", 32 | "@storybook/addon-links": "^5.3.19", 33 | "@storybook/addons": "^5.3.19", 34 | "@storybook/preset-create-react-app": "^3.1.3", 35 | "@storybook/react": "^5.3.19", 36 | "babel-eslint": "^10.0.1", 37 | "chromatic": "^5.0.0", 38 | "eslint-config-airbnb": "^18.2.0", 39 | "eslint-plugin-import": "^2.9.0", 40 | "eslint-plugin-jsx-a11y": "^6.0.3", 41 | "eslint-plugin-react": "^7.7.0", 42 | "gh-pages": "^3.1.0" 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-map-react/google-map-react-examples/73385d4434d8d70737613d61123cab63e197e5c7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-map-react/google-map-react-examples/73385d4434d8d70737613d61123cab63e197e5c7/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-map-react/google-map-react-examples/73385d4434d8d70737613d61123cab63e197e5c7/public/logo512.png -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/places.json: -------------------------------------------------------------------------------- 1 | { 2 | "html_attributions": [], 3 | "next_page_token": "CpQCDwEAAJwdqLU_q4CMC9j-DOFIiwmwpP8ICAF_lHHKpWrNLfYsLS0hubKFTbau5sp7Tc_vC2bftXqZSRfq4uMk34-nE1f3weBRv77ZvsUhQn1MvJCfW6pNFA6zrDP_lPLBQZVrGVePIGhW8g485WnZdAHUyKpr4jjJ6hiOh2R6MxVBTYHyRuy5mDa-NGx4zQXDsHGSejOPH6BmPUoyfb-2MmRo1kcYABkBZRAaXpDmTtoXLmX8GxELbMOOAjEJ8Vzxdk-4GQkzoDYB1YwHBR1JXzxlawX0DdUyaJKjLWGjvt8GOwF8d9KVTF0r528jDCheFjvRgxOGMeYq1IrUx9lBofHH4sAX2plD6BGumN40mdBkDKVMEhC8pyLxL6t51mflBEc_WkF6GhTsH0kHwldrZYDnyUZ78TU9zeiiqg", 4 | "results": [{ 5 | "formatted_address": "3818 Sunset Blvd, Los Angeles, CA 90026, USA", 6 | "geometry": { 7 | "location": { 8 | "lat": 34.091158, 9 | "lng": -118.2795188 10 | }, 11 | "viewport": { 12 | "northeast": { 13 | "lat": 34.09258172989272, 14 | "lng": -118.2780556701073 15 | }, 16 | "southwest": { 17 | "lat": 34.08988207010728, 18 | "lng": -118.2807553298927 19 | } 20 | } 21 | }, 22 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 23 | "id": "1113d3dd7339f965caae39387dd808a9e877bc2e", 24 | "name": "Flore Vegan", 25 | "opening_hours": { 26 | "open_now": false, 27 | "weekday_text": [] 28 | }, 29 | "photos": [{ 30 | "height": 2336, 31 | "html_attributions": [ 32 | "Roman Roze" 33 | ], 34 | "photo_reference": "CmRaAAAA69UVwaJnUQXQUSSX9IfB3b29opNIohkexsGAGoHTD5Lyg24lhpBtaiNlrgihstR-k7Su9Vgbc8-eE5qHEdeLVY1QTfiuyS9TPp3e2GMM_grW2FtrgrFQGtMJSeJ336cPEhCVHYfFzoOgrrKdXlk34rJiGhSXSv_XG1q1CtOrWJjWQrxJmLvIPg", 35 | "width": 3504 36 | }], 37 | "place_id": "ChIJj62e80jHwoARusJT4mjohWw", 38 | "price_level": 2, 39 | "rating": 4.6, 40 | "reference": "CmRbAAAAM9_YQ6Dt9T69zucczidzOd6HU2vmzaXvTG-lJ89KyBlJVBJ0aEfar2Exre4iDWKHjExUshYSpAXEzA-YqotVnOt4XznKY_vkD520XK5nzFz5v5IefUe6FDBqZPzYlxRDEhAgQvwzNjwC49WWlyoMKza5GhS6r-VIl1lXdMl_JEW67yL7fPkZdg", 41 | "types": [ 42 | "restaurant", 43 | "food", 44 | "point_of_interest", 45 | "establishment" 46 | ] 47 | }, 48 | { 49 | "formatted_address": "1700 Sunset Blvd, Los Angeles, CA 90026, USA", 50 | "geometry": { 51 | "location": { 52 | "lat": 34.0771192, 53 | "lng": -118.2587199 54 | }, 55 | "viewport": { 56 | "northeast": { 57 | "lat": 34.07856197989273, 58 | "lng": -118.2573112201073 59 | }, 60 | "southwest": { 61 | "lat": 34.07586232010728, 62 | "lng": -118.2600108798928 63 | } 64 | } 65 | }, 66 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 67 | "id": "d8bc8d867ccf72cbc552e100238a0820628963ee", 68 | "name": "Sage Plant Based Bistro and Brewery Echo Park", 69 | "opening_hours": { 70 | "open_now": false, 71 | "weekday_text": [] 72 | }, 73 | "photos": [{ 74 | "height": 3024, 75 | "html_attributions": [ 76 | "Anurag Singhai" 77 | ], 78 | "photo_reference": "CmRaAAAAW6v0EyZa77vrG5Aq8zbnU1sR7pljqfoDpSxXhISFgWoYWLkFY5hh7YCFbzYLj1XzflTJOrCXJa-q5jPT4L0vMY8cjXrhCzB5y7Z--qJTWOO_NxaRUBbB5QhpxyUT-R6tEhBru_ZZ_xcCxFueYrRxI6pFGhRgiExnWWOhU2Ii_NnW6M8R4xs4IQ", 79 | "width": 4032 80 | }], 81 | "place_id": "ChIJ6T9ggBvHwoARc3aegK3PBe0", 82 | "price_level": 2, 83 | "rating": 4.6, 84 | "reference": "CmRbAAAAy5CT3sE8bRlADeIeMYtC7NRdE58vKOCjOvhZNUs0QwBD7kSS6WIfo3wmxvt4EGZm5TJ6WdqOCRSnnnFoXAVpz9F3EyZPosUDCN2LJIvtYxwS3BYkwh6uVQRSj-OPLak6EhBRf8xOpcuc5WWWBgGNbGGbGhSYiomy4iNV-g_KzzqNrpymm3MxQw", 85 | "types": [ 86 | "restaurant", 87 | "food", 88 | "point_of_interest", 89 | "establishment" 90 | ] 91 | }, 92 | { 93 | "formatted_address": "8284 Melrose Ave, Los Angeles, CA 90046, USA", 94 | "geometry": { 95 | "location": { 96 | "lat": 34.083527, 97 | "lng": -118.370157 98 | }, 99 | "viewport": { 100 | "northeast": { 101 | "lat": 34.08495787989272, 102 | "lng": -118.3688093201073 103 | }, 104 | "southwest": { 105 | "lat": 34.08225822010727, 106 | "lng": -118.3715089798927 107 | } 108 | } 109 | }, 110 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 111 | "id": "8529e7504ec45e2f380310882a3bd05fe050d701", 112 | "name": "Crossroads Kitchen", 113 | "opening_hours": { 114 | "open_now": false, 115 | "weekday_text": [] 116 | }, 117 | "photos": [{ 118 | "height": 446, 119 | "html_attributions": [ 120 | "Crossroads Vegan Restaurant" 121 | ], 122 | "photo_reference": "CmRaAAAAeNvmtrTLNo95XHaHxur4Lc3CSCbtl8zUk1cbjlFdDSpMupFJ0LXRP7iaqHQeP0N8srb3P9NzwI0jrASIvj7dn-8b0KkmjvgeAmETi9sDAEwQVOui2yIJd5yzzJN3LD_bEhCCaBWZ_g1fJHyzVzz9c18eGhTSCsqJIwmOF_xRWfhPBoFUO86y1A", 123 | "width": 720 124 | }], 125 | "place_id": "ChIJhZy9Gba-woARQLEEpEN46uw", 126 | "price_level": 3, 127 | "rating": 4.5, 128 | "reference": "CmRbAAAAU-AZsWP9B3I6f7bIB2lho68meak73rt_fxdoJug-tXoqRil2guPjWxQmycp2dPFKXZNG5azOwT-rEy9vJtXQf44uVj45j_ohS6ietOXClyyAppqZt_ngGVISdtcNvrFaEhDYSDnUEsrd7GxTol1wHfJ4GhRsQAnur588F9jnXy0S4IYaiaEAQA", 129 | "types": [ 130 | "bar", 131 | "restaurant", 132 | "food", 133 | "point_of_interest", 134 | "establishment" 135 | ] 136 | }, 137 | { 138 | "formatted_address": "4319 Sunset Blvd, Los Angeles, CA 90029, USA", 139 | "geometry": { 140 | "location": { 141 | "lat": 34.0951843, 142 | "lng": -118.283107 143 | }, 144 | "viewport": { 145 | "northeast": { 146 | "lat": 34.09646662989272, 147 | "lng": -118.2818451701072 148 | }, 149 | "southwest": { 150 | "lat": 34.09376697010727, 151 | "lng": -118.2845448298927 152 | } 153 | } 154 | }, 155 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 156 | "id": "287049135d7d1ab1bfd2de8d7654afcc97cfee77", 157 | "name": "My Vegan Gold", 158 | "opening_hours": { 159 | "open_now": false, 160 | "weekday_text": [] 161 | }, 162 | "photos": [{ 163 | "height": 667, 164 | "html_attributions": [ 165 | "Sayamon Riddang" 166 | ], 167 | "photo_reference": "CmRaAAAABpfiLbCymEfhpZDl0vsV5ZjC7UDVT-GQAryHJkQ1rZHakstWk4Cugb0HrxU3dsdqbjxvKU9gxVHsZjxToVuMwRw_dHWdwIPFNl-VGYtipa8Z6Uzf6RzmS2tDje45jzTREhD1E_zkXr3Z6v8wWLzDDxwlGhS8VB8VsaT96ycvEwdkjhzmNKSfDg", 168 | "width": 1000 169 | }], 170 | "place_id": "ChIJh6ttFUzHwoAR9p8Wi_B8AbI", 171 | "price_level": 2, 172 | "rating": 4.6, 173 | "reference": "CmRbAAAAUR_MLHOu0gPb-77cvoygt4blzXX2PtoI7PRfh8mqqOgx8MPgpclf8PcfCXkEvAfDxu4wafz5l1jBDTPTntDz3Epf0dJxilKvshezCkwMXdlIoZjBZwW8n6BMU5WKGCjXEhBd8OVC5aJ7Qc_S3skTq1KnGhQNTUFaUZAnY5r37iUSLCT59zfhrw", 174 | "types": [ 175 | "restaurant", 176 | "food", 177 | "point_of_interest", 178 | "establishment" 179 | ] 180 | }, 181 | { 182 | "formatted_address": "4442, 1769 Hillhurst Ave, Los Angeles, CA 90027, United States", 183 | "geometry": { 184 | "location": { 185 | "lat": 34.1033401, 186 | "lng": -118.2875469 187 | }, 188 | "viewport": { 189 | "northeast": { 190 | "lat": 34.10468952989272, 191 | "lng": -118.2861405701073 192 | }, 193 | "southwest": { 194 | "lat": 34.10198987010727, 195 | "lng": -118.2888402298927 196 | } 197 | } 198 | }, 199 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 200 | "id": "1401d50571af0bd7fc0267e6a3635bfb3d3dbda2", 201 | "name": "Green Leaves Vegan", 202 | "opening_hours": { 203 | "open_now": false, 204 | "weekday_text": [] 205 | }, 206 | "photos": [{ 207 | "height": 3168, 208 | "html_attributions": [ 209 | "Carl Afable" 210 | ], 211 | "photo_reference": "CmRaAAAAz4NOaoeJczV-BFF7HYE-unBeYl8bud0vmkVtYxCv-lKLYo1YnAgcf-Cisr4BQypE9WbaUwm157PYyxLkWkQ1CwVcP7bXtV6QAdNegOXB8QlWAEPR95Q6lm4NQ71B3Vm2EhD2nTXqDhitEm0BrjyRh3JxGhRYCJXLJCF4AWJiBXk5fcdcjylX_g", 212 | "width": 4752 213 | }], 214 | "place_id": "ChIJ2RKN_rHAwoARIGWYxAgzzyQ", 215 | "price_level": 1, 216 | "rating": 4.5, 217 | "reference": "CmRbAAAAtM6n50gnrmJYk2qR_1uslm5ILIFgla8_RVtkm5pLzg9pMV-UcNQ3LzYSc6iM2vm6rbIzSvmjYMvWZ0Qy0AnVyIhb0kYHi1HpNFbYF3aD5cxH2hyHuB9qbn_OY_7vTpWEEhDTEJ002c2Pt-QTBNerb9iOGhRnv28Ti7KlI3gYVtBKG5XKOju9Zg", 218 | "types": [ 219 | "restaurant", 220 | "food", 221 | "point_of_interest", 222 | "establishment" 223 | ] 224 | }, 225 | { 226 | "formatted_address": "707 E 10th St, Los Angeles, CA 90021, USA", 227 | "geometry": { 228 | "location": { 229 | "lat": 34.035798, 230 | "lng": -118.251288 231 | }, 232 | "viewport": { 233 | "northeast": { 234 | "lat": 34.03709272989273, 235 | "lng": -118.2499997201073 236 | }, 237 | "southwest": { 238 | "lat": 34.03439307010728, 239 | "lng": -118.2526993798928 240 | } 241 | } 242 | }, 243 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 244 | "id": "01c75569ea2c5ee7ae29e91990ff4d872bc1ca5b", 245 | "name": "The Vegan Joint", 246 | "opening_hours": { 247 | "open_now": false, 248 | "weekday_text": [] 249 | }, 250 | "photos": [{ 251 | "height": 680, 252 | "html_attributions": [ 253 | "The Vegan Joint (Downtown)" 254 | ], 255 | "photo_reference": "CmRaAAAAGdHRzCGTwQ0_tc-fWo9wfezr51jbxPQ7IH1Gsc9OCRxoIijxg110YoNAaCTQSefXtCVdiH2xYcifKokVtnwxp01tE6J888NxT_MA1zpzxiM5ZM3MgJ9OxeJERDyE8jT0EhBSS5kv0YX7tmAxiUFHPOEXGhT2ZyiB6uRAfzpKchXvfOxyvA7S2Q", 256 | "width": 1906 257 | }], 258 | "place_id": "ChIJL8eKojLGwoAR-inFlT4uF8Q", 259 | "rating": 4.5, 260 | "reference": "CmRbAAAAPj4xP0HMJoDhx-P7C2JZTRSxu44XKoALQqupq2WaiqDjqiqi7g1ua7imgENWEH_fXu8XS5XxAmpkYV4XcDWfbLeZFuOKbgNGIvxQhXBhL-215hJQvoHFodeO9_lCGliNEhC6QZwCTKktRp33eibHgQJyGhRZf9Z_PHxTO-dxhjCFmIY8KQUYlQ", 261 | "types": [ 262 | "restaurant", 263 | "food", 264 | "point_of_interest", 265 | "establishment" 266 | ] 267 | }, 268 | { 269 | "formatted_address": "2135 Sunset Blvd, Los Angeles, CA 90026, USA", 270 | "geometry": { 271 | "location": { 272 | "lat": 34.0776068, 273 | "lng": -118.2646526 274 | }, 275 | "viewport": { 276 | "northeast": { 277 | "lat": 34.07886997989272, 278 | "lng": -118.2632818701073 279 | }, 280 | "southwest": { 281 | "lat": 34.07617032010727, 282 | "lng": -118.2659815298927 283 | } 284 | } 285 | }, 286 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 287 | "id": "946525c8b38a73842b88f67b1786746dce63fc27", 288 | "name": "Elf Cafe", 289 | "opening_hours": { 290 | "open_now": false, 291 | "weekday_text": [] 292 | }, 293 | "photos": [{ 294 | "height": 1365, 295 | "html_attributions": [ 296 | "ZAGAT" 297 | ], 298 | "photo_reference": "CmRaAAAAXeyh-4q2GinyRWob20cRuMTPB9uwOMr4jceLbiGh1sUI3I3dU2tqPBg0fIALpeBD4h5cfCTyULBTj6vSqNEcDUcPGFnjOaDO3BYQALO94_WCtbjkY83fQkdM_BPFlm2oEhDshmrB_41OPbyEVGJ4fPUVGhSYvebsAZyOFjSgUoJm_QQAhDFhzA", 299 | "width": 2048 300 | }], 301 | "place_id": "ChIJEy4mphDHwoARejZ9zUN_d4U", 302 | "price_level": 2, 303 | "rating": 4.7, 304 | "reference": "CmRbAAAAwxF-jK7VtlDfSJigYrgWtGrgNXRvw-4pU62YULylvTajhxW9lwzvmYKmKKx4ZO0SBMR1TOqVE-rPlhlh7aZI5NfcL8RItn_IX1BxS7X9X2ode26MDbVwICFFC_qziC-MEhA4i-3OikQ9eHbTeu-L27G1GhRLgk421tk5hnWDbhrEbtK-S3Ap4A", 305 | "types": [ 306 | "restaurant", 307 | "food", 308 | "point_of_interest", 309 | "establishment" 310 | ] 311 | }, 312 | { 313 | "formatted_address": "4114 Santa Monica Blvd, Los Angeles, CA 90029, USA", 314 | "geometry": { 315 | "location": { 316 | "lat": 34.0919263, 317 | "lng": -118.2820544 318 | }, 319 | "viewport": { 320 | "northeast": { 321 | "lat": 34.09333132989273, 322 | "lng": -118.2807559701073 323 | }, 324 | "southwest": { 325 | "lat": 34.09063167010728, 326 | "lng": -118.2834556298927 327 | } 328 | } 329 | }, 330 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 331 | "id": "d358acb483e9bacaf445de016f78473908253696", 332 | "name": "Bulan Thai Vegetarian Kitchen", 333 | "opening_hours": { 334 | "open_now": false, 335 | "weekday_text": [] 336 | }, 337 | "photos": [{ 338 | "height": 3024, 339 | "html_attributions": [ 340 | "jeromy robert" 341 | ], 342 | "photo_reference": "CmRaAAAAbjH5vQk8Ese5ppONFAIwbunnNnhcEEkKtRrn5yttTYvxHxjbgbGDXIv7cNSfQMU7wICcox5NT__xnaKwJi-e3zr5230Z4t8TcReOcVoV8Ewjfn5g7AL4AK0gksctR-KqEhD6iw1PbCByU1zD7iD3lxPXGhT5AoSLQ7BHIzymU94-0xSvRMo9yA", 343 | "width": 4032 344 | }], 345 | "place_id": "ChIJAaqI1E7HwoARRCxBLPw048Y", 346 | "price_level": 2, 347 | "rating": 4.6, 348 | "reference": "CmRbAAAAbLDZZu-oOjRmRi7hsQpZELuusRQZEk6lE1YJL5r7pnP3RtWaSLlA_p9uYp0QIalfzXhS0605iRnNsEr_WDhos1HxubW4h0sPKz96GPUKeULOux319b2pYHqQCYwYUmOMEhCEXh-YmfKFafd1EtkAYkB2GhSbxBpYLCWz45RTP0YaWm6eeJecwQ", 349 | "types": [ 350 | "restaurant", 351 | "food", 352 | "point_of_interest", 353 | "establishment" 354 | ] 355 | }, 356 | { 357 | "formatted_address": "1047 S Fairfax Ave, Los Angeles, CA 90019, USA", 358 | "geometry": { 359 | "location": { 360 | "lat": 34.0568525, 361 | "lng": -118.3646369 362 | }, 363 | "viewport": { 364 | "northeast": { 365 | "lat": 34.05817752989272, 366 | "lng": -118.3632100701073 367 | }, 368 | "southwest": { 369 | "lat": 34.05547787010728, 370 | "lng": -118.3659097298927 371 | } 372 | } 373 | }, 374 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 375 | "id": "6f73d8dfd2f3dc8b08d127ea3d0691b33030973b", 376 | "name": "Rahel Ethiopian Vegan Cuisine", 377 | "opening_hours": { 378 | "open_now": false, 379 | "weekday_text": [] 380 | }, 381 | "photos": [{ 382 | "height": 2988, 383 | "html_attributions": [ 384 | "Edna Banayat" 385 | ], 386 | "photo_reference": "CmRaAAAAsltyy0_FJzHaxUaRfy3ks0qgLs8N0x2Y7dZqKDw21NUi7D9STyFscfcUKiL9z9E2LXC3jrIqq6vikR6j0hb8_L7kCDr1WjgVUamyzOw3GdXSmaDVEQrOWDkBSGWGCvSAEhA9zXJDgrZrtdC90Jah1VWnGhRr8wcVDTal8O1R7S6WOQ0482phCw", 387 | "width": 5312 388 | }], 389 | "place_id": "ChIJqeeNwhW5woARdmN2K2Q107A", 390 | "price_level": 2, 391 | "rating": 4.6, 392 | "reference": "CmRbAAAAoNTyxGrtvDexCku_OpmL47Akpq5T587ncWV1Rp8xWIc7YQlqk75_0h2_nB0bduMRadfyHsW6i6SXFUNicU0BWvaGGy3Mtta6Omd-9lePm-ZN2EQBbuT05kFBNHkWbs9zEhCfXjvnfKzALRwRyDKEeQr-GhRIXuOSJHoM7g6L-V83WDXl3mxUgQ", 393 | "types": [ 394 | "restaurant", 395 | "food", 396 | "point_of_interest", 397 | "establishment" 398 | ] 399 | }, 400 | { 401 | "formatted_address": "4664, 10438 National Blvd, Los Angeles, CA 90034, United States", 402 | "geometry": { 403 | "location": { 404 | "lat": 34.0285781, 405 | "lng": -118.4115541 406 | }, 407 | "viewport": { 408 | "northeast": { 409 | "lat": 34.02999047989272, 410 | "lng": -118.4102540701073 411 | }, 412 | "southwest": { 413 | "lat": 34.02729082010728, 414 | "lng": -118.4129537298927 415 | } 416 | } 417 | }, 418 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 419 | "id": "92cc1f89e314ba9e518f1d7aa345a57441affe39", 420 | "name": "The Vegan Joint (West LA)", 421 | "opening_hours": { 422 | "open_now": false, 423 | "weekday_text": [] 424 | }, 425 | "photos": [{ 426 | "height": 421, 427 | "html_attributions": [ 428 | "The Vegan Joint (West LA)" 429 | ], 430 | "photo_reference": "CmRaAAAAN3v7CUQbGdsnPNSZ26qvp11iqa78bJjwe_P9pr0rA0meuKU916gHo0OktysWHWB-ZhFRtiVGYQeTm6mQUVChAxglotv9Tgv4VTRAIFdy_-WeLSvkQz-KhOf-mtvmfObfEhCOC0m29jY1Dvm0Xz_l2s1sGhT-9vWMge8CoQrb5wpFAtLrUB8phg", 431 | "width": 750 432 | }], 433 | "place_id": "ChIJ74X8ncu7woARzxiCmkqeeXc", 434 | "price_level": 2, 435 | "rating": 4.4, 436 | "reference": "CmRbAAAAh4e7HwMd83IQ1TW182b2n_qdhUDULWHH33HQjJOT_DGzaFV2HyLLEnN4z07DGn_-S0GsA2mwy3hvI-GFIKW1hM-VO8MOaJkiFMfWW347qlVy6iYmltY0DzU_-eA02rHPEhC0AGDxyCgGEH-2PrMsG0_ZGhTFDKatE5VBRZkTdWAaYfzSrs3QDg", 437 | "types": [ 438 | "restaurant", 439 | "food", 440 | "point_of_interest", 441 | "establishment" 442 | ] 443 | }, 444 | { 445 | "formatted_address": "3655 S Grand Ave, Los Angeles, CA 90007, USA", 446 | "geometry": { 447 | "location": { 448 | "lat": 34.017339, 449 | "lng": -118.278469 450 | }, 451 | "viewport": { 452 | "northeast": { 453 | "lat": 34.01859817989273, 454 | "lng": -118.2769120701073 455 | }, 456 | "southwest": { 457 | "lat": 34.01589852010728, 458 | "lng": -118.2796117298927 459 | } 460 | } 461 | }, 462 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 463 | "id": "da772e983e3e6dbc0cd042042311ab79839d1f28", 464 | "name": "Azla Ethiopian Vegan Cuisine", 465 | "opening_hours": { 466 | "open_now": false, 467 | "weekday_text": [] 468 | }, 469 | "photos": [{ 470 | "height": 3024, 471 | "html_attributions": [ 472 | "Benjamin Rubin" 473 | ], 474 | "photo_reference": "CmRaAAAARwfVFSLVUmk_dVkBaY43M8sPng1XdXX7Is882vzG7KcFewp2dMMlYqwKQSGyfAOSWZ3vJrgBUEypUwJESuqoxRneTLjp9W-XcQ4pQByDVabpYbT5r5y006esU3aTeHvIEhDXtTfa4Kh1z_-u4D_BFJT2GhSQDwL9zFfvKbwRuGmmcui08SdiLg", 475 | "width": 4032 476 | }], 477 | "place_id": "ChIJmb4BkgrIwoAR-PS33wrjk_M", 478 | "rating": 4.8, 479 | "reference": "CmRbAAAA5lMrGEVyK5VhjDMMJZ2tUR0Poo9zQt4wZZBcW46e3CfsYUN0INPZKeeKERZJwkhcogLK-FsKAZI92jncCYN4RZQYqtyiJONx3I6VWIrTXGI9rOt01dSTtBCZEKtag0AcEhBlaMFWX2bnU8gMjCv3Fs1AGhSs8gJhrAa-jQNLRX7pxzIqBJ_HQg", 480 | "types": [ 481 | "restaurant", 482 | "food", 483 | "point_of_interest", 484 | "establishment" 485 | ] 486 | }, 487 | { 488 | "formatted_address": "8101½ Beverly Blvd, Los Angeles, CA 90048, USA", 489 | "geometry": { 490 | "location": { 491 | "lat": 34.0764288, 492 | "lng": -118.3661624 493 | }, 494 | "viewport": { 495 | "northeast": { 496 | "lat": 34.07760962989272, 497 | "lng": -118.3648135201073 498 | }, 499 | "southwest": { 500 | "lat": 34.07490997010728, 501 | "lng": -118.3675131798927 502 | } 503 | } 504 | }, 505 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 506 | "id": "1924e9b97b3101ea1d18ac33191e8c2d8fffcbc2", 507 | "name": "Araya's Vegetarian Place", 508 | "opening_hours": { 509 | "open_now": false, 510 | "weekday_text": [] 511 | }, 512 | "photos": [{ 513 | "height": 3088, 514 | "html_attributions": [ 515 | "Simon Catford" 516 | ], 517 | "photo_reference": "CmRaAAAAyNe-nZSiUqUrfU3RKs7KqCG2-yoYpRixjGySI1LQgkDKkmRycyaz8RNyaVTYD8bqIf8FpmH7MOvC84sF6gVTsOBKI3IUGeyx54Uxntf8IkZOx4MjDr-Gdh_e40hXYKePEhApDNZUIBhEwnQHrWJqcBA1GhSN3DbanyozQB0pl8wVQucBZbsg1g", 518 | "width": 4160 519 | }], 520 | "place_id": "ChIJV-aT-jS5woAROYkaRCGf2s8", 521 | "price_level": 2, 522 | "rating": 4.6, 523 | "reference": "CmRbAAAAeSSwdHRB2eK9ro7pBWjJZeiDbH12E8AhhMyUsosVZeCMtzmNgPNfRgbS96NRPgdbWrudWjLJrKcomMqCyH8PWpA8rhSKHf7Kj7u7rgBv0r3b4kowdzJ4VUe8WyNoGQ_VEhBVcix6m4C5QlEf-G3y7AveGhTqHZ55gdeHglCukJFsCIpkzMR9Cw", 524 | "types": [ 525 | "restaurant", 526 | "food", 527 | "point_of_interest", 528 | "establishment" 529 | ] 530 | }, 531 | { 532 | "formatted_address": "4507 S Centinela Ave, Los Angeles, CA 90066, USA", 533 | "geometry": { 534 | "location": { 535 | "lat": 33.9925942, 536 | "lng": -118.4232475 537 | }, 538 | "viewport": { 539 | "northeast": { 540 | "lat": 33.99401072989273, 541 | "lng": -118.4218230701073 542 | }, 543 | "southwest": { 544 | "lat": 33.99131107010729, 545 | "lng": -118.4245227298927 546 | } 547 | } 548 | }, 549 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 550 | "id": "8f5ac5552a0e53f7ae627916c3d1d48b45030f54", 551 | "name": "LA Vegan", 552 | "opening_hours": { 553 | "open_now": false, 554 | "weekday_text": [] 555 | }, 556 | "photos": [{ 557 | "height": 3644, 558 | "html_attributions": [ 559 | "Eric Estenzo" 560 | ], 561 | "photo_reference": "CmRaAAAAdcatw_mFed_48beFNIsyCrjLRenujvkj0YkhN5PX8oJQAr013xQEGat49VQ1GmrvEMRTV8CIeWtBOYSwzIWUrD7kyY1c3sgmx-0y_ecKUIuSlxhlbDtjQeCa3U8SV5yGEhAGwkRlZ2kuFsZ5uBKLh79CGhRKQDVIo1Z_ijdaHPLZqjuRU8mcew", 562 | "width": 7012 563 | }], 564 | "place_id": "ChIJZUzGG266woARXsRdJc2silw", 565 | "price_level": 2, 566 | "rating": 4.6, 567 | "reference": "CmRbAAAAXBx1MbFAy5X5HJqg1yMWtv10pIpp5Qeq2lCD7y7dhJM5B8iHXgysRGXgjsD5eTeKnxuaM26srTPC3Ib7g1TfBAnaIcVVw7K0-JEiuRzs-dBnuj_1ey5A0r3zvXOQpcN0EhB18uIS3_Rj_T61b2Sfo0ecGhSmROJ3Of8T44ckuMzOqKUJsZng8Q", 568 | "types": [ 569 | "restaurant", 570 | "food", 571 | "point_of_interest", 572 | "establishment" 573 | ] 574 | }, 575 | { 576 | "formatted_address": "8393 Beverly Blvd, Los Angeles, CA 90048, USA", 577 | "geometry": { 578 | "location": { 579 | "lat": 34.0764345, 580 | "lng": -118.3730332 581 | }, 582 | "viewport": { 583 | "northeast": { 584 | "lat": 34.07760002989272, 585 | "lng": -118.3716821701073 586 | }, 587 | "southwest": { 588 | "lat": 34.07490037010728, 589 | "lng": -118.3743818298927 590 | } 591 | } 592 | }, 593 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 594 | "id": "1d6756ade69dac8cf5ce42aeeeb793535b4504e8", 595 | "name": "Vegan Glory", 596 | "opening_hours": { 597 | "open_now": false, 598 | "weekday_text": [] 599 | }, 600 | "photos": [{ 601 | "height": 570, 602 | "html_attributions": [ 603 | "Thom Uber" 604 | ], 605 | "photo_reference": "CmRaAAAA1YSeQpoJrDP6swpc1vw2b364pPMsjw2s2DVZYV4AWfENCwY02zHen8VpfI27Vnh8762bVFs4kda28R2sH354xKhju0vBIEM4yRlQr_G1-NvcywFtMQE7mBVtgegTo6ADEhBBLf_S0InHdmZ-SVC1KX8zGhQS9hpDvsrqVNDPNuWKLSPHy34XoA", 606 | "width": 760 607 | }], 608 | "place_id": "ChIJyYMRL0u5woARId-DVapUFNk", 609 | "rating": 4.4, 610 | "reference": "CmRbAAAActbey6zjHroEmUk5ZVCyD6LR0-RHIuWbMAJW5v7PaA-V-pijZFT9MgDjuoCOYAy7asUfIXfLiPxli2mdJtUGTCsUW6qw91uHaZR6XQv62bUFwmM9qM6IB6gOcv4T5BKbEhD9Jo87T4pNpyFSgCZWcyMOGhT2Au7pVgKuofn6_B7s4vSFUNhXPA", 611 | "types": [ 612 | "restaurant", 613 | "food", 614 | "point_of_interest", 615 | "establishment" 616 | ] 617 | }, 618 | { 619 | "formatted_address": "1253 Vine St # 9, Los Angeles, CA 90038, USA", 620 | "geometry": { 621 | "location": { 622 | "lat": 34.093981, 623 | "lng": -118.327638 624 | }, 625 | "viewport": { 626 | "northeast": { 627 | "lat": 34.09533472989273, 628 | "lng": -118.3257989701073 629 | }, 630 | "southwest": { 631 | "lat": 34.09263507010728, 632 | "lng": -118.3284986298927 633 | } 634 | } 635 | }, 636 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 637 | "id": "a869aba570e8103a6ae09ae71335a38e99b11e01", 638 | "name": "Doomie's Home Cookin'", 639 | "opening_hours": { 640 | "open_now": false, 641 | "weekday_text": [] 642 | }, 643 | "photos": [{ 644 | "height": 2400, 645 | "html_attributions": [ 646 | "Joe Gebis" 647 | ], 648 | "photo_reference": "CmRaAAAA20MBConiFPlO2-6BuoTBSGSAUBJaYoX0_m0xj_MNE2BPaqhDDSNtUXMTcmiKKZw0umwGkTaBt-Y3kNfKwKfRAdNnW9byHMuvElqBWXZTPSPuTIN_4xJKt1tKsosLuIfBEhB_ZCYjBhFWDue7-Z-UcfFjGhRyD45yAH2035Ifu15OklnylBIfhA", 649 | "width": 3200 650 | }], 651 | "place_id": "ChIJ56wYlja_woAR7f2m0YbS8hk", 652 | "price_level": 2, 653 | "rating": 4.4, 654 | "reference": "CmRbAAAAObVl81DX-3CYhyQv0itdfPojjR7bwG0eAv2JpZb9ZYoVBNg9F0znMXC72bBsDctx7ogiPKPvagqMNtEFkHYG7aLMk5VO-xtR8sfDAgeEiHHZV24oStUWs4aY-xqrU8W4EhAok2aSJPg0eRFD7nXeJpzzGhRPGtkJYZeCZHvy9UctDGItmFdbKg", 655 | "types": [ 656 | "restaurant", 657 | "food", 658 | "point_of_interest", 659 | "establishment" 660 | ] 661 | }, 662 | { 663 | "formatted_address": "710 W 1st St, Los Angeles, CA 90012, USA", 664 | "geometry": { 665 | "location": { 666 | "lat": 34.056385, 667 | "lng": -118.2508724 668 | }, 669 | "viewport": { 670 | "northeast": { 671 | "lat": 34.05794337989273, 672 | "lng": -118.2493326201073 673 | }, 674 | "southwest": { 675 | "lat": 34.05524372010728, 676 | "lng": -118.2520322798927 677 | } 678 | } 679 | }, 680 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 681 | "id": "53b4341b80e3596c05431abaa9b42b3fb0adf66d", 682 | "name": "Âu Lạc LA", 683 | "opening_hours": { 684 | "open_now": false, 685 | "weekday_text": [] 686 | }, 687 | "photos": [{ 688 | "height": 3036, 689 | "html_attributions": [ 690 | "Patrick Donnelly" 691 | ], 692 | "photo_reference": "CmRaAAAAmofVPSjKMklO3J2mpcRl_kHAwl710EqByuQjaP8rjOtmj08yJKKr1xcJICtaZ-quKiiG2aYUhFru5JF1eYeDHx-pILIPOmKspUN07oGjhMwKnUa73wikXvM-FVJuJcRMEhDLVub-Rx6D4N3iiN0JAtHhGhQQ2Un8dIsylPeLN9IcRcerUif-1w", 693 | "width": 4048 694 | }], 695 | "place_id": "ChIJe0sPnFLGwoARxFDUYFEjhuQ", 696 | "price_level": 2, 697 | "rating": 4.5, 698 | "reference": "CmRbAAAAHb0M5CcLv12TZJiEq0QRs5nexZ-fBdKvzQmTE8w3j_-YHX15hSy7WYb3EuweeyxhuIBirsVr4uBMtSUP4KoClwz1l6uE0KgO5jHySb6a9Qi5_Hoju3iN55xLA38dCGPTEhBqVAmmstFHL-wzx6LKFLkWGhTdLVJpc_s2vuSY-OL3rJzZStmuFg", 699 | "types": [ 700 | "bar", 701 | "restaurant", 702 | "food", 703 | "point_of_interest", 704 | "establishment" 705 | ] 706 | }, 707 | { 708 | "formatted_address": "2870 Rowena Ave, Los Angeles, CA 90039, USA", 709 | "geometry": { 710 | "location": { 711 | "lat": 34.107701, 712 | "lng": -118.2667943 713 | }, 714 | "viewport": { 715 | "northeast": { 716 | "lat": 34.10916667989272, 717 | "lng": -118.2653993701072 718 | }, 719 | "southwest": { 720 | "lat": 34.10646702010727, 721 | "lng": -118.2680990298927 722 | } 723 | } 724 | }, 725 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 726 | "id": "199ee04e9dbcb33b16d3aed792b6c0cc0e441d8d", 727 | "name": "Little Pine", 728 | "opening_hours": { 729 | "open_now": false, 730 | "weekday_text": [] 731 | }, 732 | "photos": [{ 733 | "height": 563, 734 | "html_attributions": [ 735 | "Karen TenEyck" 736 | ], 737 | "photo_reference": "CmRaAAAAoYVVT_rhyzFXzPVGgWSkRNXU5Fqm7AC5BcX7obilkGQX_cqVfu74Jb4Dm77gWDtvF3uov5Oeu06JutufKGQwbWlBJmdH0O3xCDoNA-t5uPOHjvZu26A2WYA3JtaMR4TzEhAbM1Fsz-8UUaZOh37fbEs3GhRdnpshI2u0QbwbL0KbAa_IiNPPdg", 738 | "width": 1000 739 | }], 740 | "place_id": "ChIJjc_P0c3AwoARBWVMPN_YVRU", 741 | "price_level": 2, 742 | "rating": 4.7, 743 | "reference": "CmRbAAAAugyZhEPl2BK5sW7_AnhEZv-d-21pR6GzooTVD3RJFsDF9hecZMusog36IO34t7wuFczZWyD8kEiVKilj4Sp9buBdtO077raxcOkVC6--BXKuhGrh2Lkq5dhR2pLXNLXZEhBKcQzGE58Xr82Fz7Oy--MaGhTT7qBjkenQ3jVinJpgqASlYR5c0g", 744 | "types": [ 745 | "restaurant", 746 | "food", 747 | "point_of_interest", 748 | "establishment" 749 | ] 750 | }, 751 | { 752 | "formatted_address": "Little Tokyo Shopping Center, 333 South Alameda St #310, Los Angeles, CA 90013, USA", 753 | "geometry": { 754 | "location": { 755 | "lat": 34.0450139, 756 | "lng": -118.2388682 757 | }, 758 | "viewport": { 759 | "northeast": { 760 | "lat": 34.04636372989273, 761 | "lng": -118.2375183701073 762 | }, 763 | "southwest": { 764 | "lat": 34.04366407010728, 765 | "lng": -118.2402180298928 766 | } 767 | } 768 | }, 769 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 770 | "id": "bdc0c9353ceb91395214f3f84f0016059271acd7", 771 | "name": "Shojin", 772 | "opening_hours": { 773 | "open_now": false, 774 | "weekday_text": [] 775 | }, 776 | "photos": [{ 777 | "height": 2336, 778 | "html_attributions": [ 779 | "Roman Roze" 780 | ], 781 | "photo_reference": "CmRaAAAALLVT3JKv4U1gRmbItfHhzOIdYRIibKmLMWThQa0gYU-umKbevliJUIogQL_Hm1_6oWGlutmR4kuzd6ikrpM4_eIeLSprpxAlHiEmi3FVNN5XvnBxfV8xWj6ggQLNM8DxEhDscXYyU5r1aKlYTCVLG61tGhQsd9XLF9OR7UL9XZo_eOm1Y98hCw", 782 | "width": 3504 783 | }], 784 | "place_id": "ChIJyy_0pjnGwoARi47oqNbdedU", 785 | "price_level": 3, 786 | "rating": 4.6, 787 | "reference": "CmRbAAAAORx-wW_TP5eWz0P93UBThqK-FdOLxos-ptjGRGlmiFaOf6ySc43rGF1ENCNilSFQL0Q-7RtFawCJMdvApBCTEyHZnRIIx4Jp4cRIcvSxRBJzB-S3aWfAglqv4L_sO7zsEhAjm-hxEpM5a-8fqVVcqJAbGhQDjg_IeMxUnePapODPTZPxOis_Lw", 788 | "types": [ 789 | "restaurant", 790 | "food", 791 | "point_of_interest", 792 | "establishment" 793 | ] 794 | }, 795 | { 796 | "formatted_address": "2520 Glendale Blvd, Los Angeles, CA 90039, USA", 797 | "geometry": { 798 | "location": { 799 | "lat": 34.1031997, 800 | "lng": -118.2586152 801 | }, 802 | "viewport": { 803 | "northeast": { 804 | "lat": 34.10455672989272, 805 | "lng": -118.2575728201073 806 | }, 807 | "southwest": { 808 | "lat": 34.10185707010727, 809 | "lng": -118.2602724798928 810 | } 811 | } 812 | }, 813 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 814 | "id": "52be338abc98361f3c8664a7d4e853a11332afec", 815 | "name": "by CHLOE. Silver Lake", 816 | "opening_hours": { 817 | "open_now": false, 818 | "weekday_text": [] 819 | }, 820 | "photos": [{ 821 | "height": 2988, 822 | "html_attributions": [ 823 | "Jeannette G." 824 | ], 825 | "photo_reference": "CmRaAAAA4Yb-teP-cLR0wHYMwtNvoo-wPlgVsj444v-VTgdwi63Gshd-PV5ZMhVPQSN2v5yPM-lxvZQW7Redm4wNKPLmGku5MNs0yhMuBysvXO6gpJ6kF4PGCepMIcZQ409LpBD_EhD8ZeAMspzsx_RnDHSQkuDlGhQ3-VOGj2K85VfvxsOCAyIS4HdVnw", 826 | "width": 5312 827 | }], 828 | "place_id": "ChIJ4fYRfivHwoAR_2DlpZxO5_8", 829 | "price_level": 2, 830 | "rating": 4.5, 831 | "reference": "CmRbAAAALsJqmhwH-aLZPy6SNAbLNzQRQ3qfT92yX3k6t13Ynq9K5hQatkTEBPhTr-pqcF3RznWRRYDNUoyPSoT8irbse6j2VZUHsgvo7wvPPVSTeYaeB3gZYXNaFnJmcqyUUU4CEhAhjByzVT-NNxUGH3pcyuS-GhTSUc8pvZUzjDie_3LXHrkObrtDzA", 832 | "types": [ 833 | "restaurant", 834 | "food", 835 | "point_of_interest", 836 | "establishment" 837 | ] 838 | }, 839 | { 840 | "formatted_address": "639 N Larchmont Blvd, Los Angeles, CA 90004, USA", 841 | "geometry": { 842 | "location": { 843 | "lat": 34.0828183, 844 | "lng": -118.3241586 845 | }, 846 | "viewport": { 847 | "northeast": { 848 | "lat": 34.08422687989272, 849 | "lng": -118.3225729201073 850 | }, 851 | "southwest": { 852 | "lat": 34.08152722010728, 853 | "lng": -118.3252725798928 854 | } 855 | } 856 | }, 857 | "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", 858 | "id": "21f5bb524cea1c8698187b817298300df290ad1b", 859 | "name": "Cafe Gratitude", 860 | "opening_hours": { 861 | "open_now": false, 862 | "weekday_text": [] 863 | }, 864 | "photos": [{ 865 | "height": 1365, 866 | "html_attributions": [ 867 | "Cafe Gratitude" 868 | ], 869 | "photo_reference": "CmRaAAAAX_eBNo4lUv8qGy5th3upd5Qp_Gx9NEQ55wfy0Yb7l1L_0M_gIwQqPb55RiJivmK_Wu3_CiyjBwEVGeeQCGRPHanVPYIG7_fQGV3_o2QgPSgQOAEfeKvuvdeUFGLX2TMzEhANtSJNyyzGbezwvsEmgvklGhSNKWwiEgOHaw28SqiWmqm1aAWDJQ", 870 | "width": 2048 871 | }], 872 | "place_id": "ChIJjRPL7sm4woARFcIwlfejAGA", 873 | "price_level": 2, 874 | "rating": 4.5, 875 | "reference": "CmRbAAAANXEcxEtfEYz2V5OIwdFEuyw35xZZ7SpF9AVuYFUqha3tf8CiZbuYrXLCG_y-twfXpcRiYyN0kXDfwmjA-uPZQaPbfpriBY397myHuiFF1A1ESI-5Hcb1y6UiRger5g3tEhCRq3Ml_vdNqA8AGua9iLXrGhTz1KC9902YirqAu7KLW4Bq7FYOEQ", 876 | "types": [ 877 | "cafe", 878 | "restaurant", 879 | "food", 880 | "point_of_interest", 881 | "establishment" 882 | ] 883 | } 884 | ], 885 | "status": "OK" 886 | } 887 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit 12 | {' '} 13 | src/App.js 14 | {' '} 15 | and save to reload. 16 |

17 | 23 | Learn React 24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/AutoComplete.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Wrapper = styled.div` 5 | position: relative; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | padding: 20px; 10 | `; 11 | 12 | class AutoComplete extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.clearSearchBox = this.clearSearchBox.bind(this); 16 | } 17 | 18 | componentDidMount({ map, mapApi } = this.props) { 19 | const options = { 20 | // restrict your search to a specific type of result 21 | // types: ['geocode', 'address', 'establishment', '(regions)', '(cities)'], 22 | // restrict your search to a specific country, or an array of countries 23 | // componentRestrictions: { country: ['gb', 'us'] }, 24 | }; 25 | this.autoComplete = new mapApi.places.Autocomplete( 26 | this.searchInput, 27 | options, 28 | ); 29 | this.autoComplete.addListener('place_changed', this.onPlaceChanged); 30 | this.autoComplete.bindTo('bounds', map); 31 | } 32 | 33 | componentWillUnmount({ mapApi } = this.props) { 34 | mapApi.event.clearInstanceListeners(this.searchInput); 35 | } 36 | 37 | onPlaceChanged = ({ map, addplace } = this.props) => { 38 | const place = this.autoComplete.getPlace(); 39 | 40 | if (!place.geometry) return; 41 | if (place.geometry.viewport) { 42 | map.fitBounds(place.geometry.viewport); 43 | } else { 44 | map.setCenter(place.geometry.location); 45 | map.setZoom(17); 46 | } 47 | 48 | addplace(place); 49 | this.searchInput.blur(); 50 | }; 51 | 52 | clearSearchBox() { 53 | this.searchInput.value = ''; 54 | } 55 | 56 | render() { 57 | return ( 58 | 59 | { 61 | this.searchInput = ref; 62 | }} 63 | type="text" 64 | onFocus={this.clearSearchBox} 65 | placeholder="Enter a location" 66 | /> 67 | 68 | ); 69 | } 70 | } 71 | 72 | export default AutoComplete; 73 | -------------------------------------------------------------------------------- /src/components/GoogleMap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import GoogleMapReact from 'google-map-react'; 5 | 6 | const Wrapper = styled.main` 7 | width: 100%; 8 | height: 100%; 9 | `; 10 | 11 | const GoogleMap = ({ children, ...props }) => ( 12 | 13 | 19 | {children} 20 | 21 | 22 | ); 23 | 24 | GoogleMap.propTypes = { 25 | children: PropTypes.oneOfType([ 26 | PropTypes.node, 27 | PropTypes.arrayOf(PropTypes.node), 28 | ]), 29 | }; 30 | 31 | GoogleMap.defaultProps = { 32 | children: null, 33 | }; 34 | 35 | export default GoogleMap; 36 | -------------------------------------------------------------------------------- /src/components/Marker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const Wrapper = styled.div` 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | width: 18px; 10 | height: 18px; 11 | background-color: #000; 12 | border: 2px solid #fff; 13 | border-radius: 100%; 14 | user-select: none; 15 | transform: translate(-50%, -50%); 16 | cursor: ${(props) => (props.onClick ? 'pointer' : 'default')}; 17 | &:hover { 18 | z-index: 1; 19 | } 20 | `; 21 | 22 | const Marker = ({ text, onClick }) => ( 23 | 27 | ); 28 | 29 | Marker.defaultProps = { 30 | onClick: null, 31 | }; 32 | 33 | Marker.propTypes = { 34 | onClick: PropTypes.func, 35 | text: PropTypes.string.isRequired, 36 | }; 37 | 38 | export default Marker; 39 | -------------------------------------------------------------------------------- /src/components/SearchBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Wrapper = styled.div` 5 | position: relative; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | padding: 20px; 10 | `; 11 | 12 | class SearchBox extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.clearSearchBox = this.clearSearchBox.bind(this); 16 | } 17 | 18 | componentDidMount({ map, mapApi } = this.props) { 19 | this.searchBox = new mapApi.places.SearchBox(this.searchInput); 20 | this.searchBox.addListener('places_changed', this.onPlacesChanged); 21 | this.searchBox.bindTo('bounds', map); 22 | } 23 | 24 | componentWillUnmount({ mapApi } = this.props) { 25 | mapApi.event.clearInstanceListeners(this.searchInput); 26 | } 27 | 28 | onPlacesChanged = ({ map, addplace } = this.props) => { 29 | const selected = this.searchBox.getPlaces(); 30 | const { 0: place } = selected; 31 | if (!place.geometry) return; 32 | if (place.geometry.viewport) { 33 | map.fitBounds(place.geometry.viewport); 34 | } else { 35 | map.setCenter(place.geometry.location); 36 | map.setZoom(17); 37 | } 38 | 39 | addplace(selected); 40 | this.searchInput.blur(); 41 | }; 42 | 43 | clearSearchBox() { 44 | this.searchInput.value = ''; 45 | } 46 | 47 | render() { 48 | return ( 49 | 50 | { 52 | this.searchInput = ref; 53 | }} 54 | type="text" 55 | onFocus={this.clearSearchBox} 56 | placeholder="Enter a location" 57 | /> 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default SearchBox; 64 | -------------------------------------------------------------------------------- /src/const/la_center.js: -------------------------------------------------------------------------------- 1 | export default [34.0522, -118.2437]; 2 | -------------------------------------------------------------------------------- /src/examples/AutoComplete.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import isEmpty from 'lodash.isempty'; 3 | 4 | // components: 5 | import Marker from '../components/Marker'; 6 | 7 | // examples: 8 | import GoogleMap from '../components/GoogleMap'; 9 | import AutoComplete from '../components/AutoComplete'; 10 | 11 | // consts 12 | import LOS_ANGELES_CENTER from '../const/la_center'; 13 | 14 | class Autocomplete extends Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | mapApiLoaded: false, 20 | mapInstance: null, 21 | mapApi: null, 22 | places: [], 23 | }; 24 | } 25 | 26 | apiHasLoaded = (map, maps) => { 27 | this.setState({ 28 | mapApiLoaded: true, 29 | mapInstance: map, 30 | mapApi: maps, 31 | }); 32 | }; 33 | 34 | addPlace = (place) => { 35 | this.setState({ places: [place] }); 36 | }; 37 | 38 | render() { 39 | const { 40 | places, mapApiLoaded, mapInstance, mapApi, 41 | } = this.state; 42 | return ( 43 | <> 44 | {mapApiLoaded && ( 45 | 46 | )} 47 | this.apiHasLoaded(map, maps)} 56 | > 57 | {!isEmpty(places) 58 | && places.map((place) => ( 59 | 65 | ))} 66 | 67 | 68 | ); 69 | } 70 | } 71 | 72 | export default Autocomplete; 73 | -------------------------------------------------------------------------------- /src/examples/Heatmap.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import isEmpty from 'lodash.isempty'; 3 | 4 | // examples: 5 | import GoogleMap from '../components/GoogleMap'; 6 | 7 | // consts 8 | import LOS_ANGELES_CENTER from '../const/la_center'; 9 | 10 | class Heatmap extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | places: [], 16 | }; 17 | } 18 | 19 | componentDidMount() { 20 | fetch('places.json') 21 | .then((response) => response.json()) 22 | .then((data) => this.setState({ places: data.results })); 23 | } 24 | 25 | render() { 26 | const { places } = this.state; 27 | const data = places.map((place) => ({ 28 | lat: place.geometry.location.lat, 29 | lng: place.geometry.location.lng, 30 | weight: Math.floor(Math.random() * Math.floor(5)), 31 | })); 32 | const heatmapData = { 33 | positions: data, 34 | options: { 35 | radius: 20, 36 | opacity: 1, 37 | }, 38 | }; 39 | 40 | return ( 41 | <> 42 | {!isEmpty(places) && ( 43 | 52 | )} 53 | 54 | ); 55 | } 56 | } 57 | 58 | export default Heatmap; 59 | -------------------------------------------------------------------------------- /src/examples/Main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import isEmpty from 'lodash.isempty'; 3 | 4 | // components: 5 | import Marker from '../components/Marker'; 6 | 7 | // examples: 8 | import GoogleMap from '../components/GoogleMap'; 9 | 10 | // consts 11 | import LOS_ANGELES_CENTER from '../const/la_center'; 12 | 13 | // Return map bounds based on list of places 14 | const getMapBounds = (map, maps, places) => { 15 | const bounds = new maps.LatLngBounds(); 16 | 17 | places.forEach((place) => { 18 | bounds.extend(new maps.LatLng( 19 | place.geometry.location.lat, 20 | place.geometry.location.lng, 21 | )); 22 | }); 23 | return bounds; 24 | }; 25 | 26 | // Re-center map when resizing the window 27 | const bindResizeListener = (map, maps, bounds) => { 28 | maps.event.addDomListenerOnce(map, 'idle', () => { 29 | maps.event.addDomListener(window, 'resize', () => { 30 | map.fitBounds(bounds); 31 | }); 32 | }); 33 | }; 34 | 35 | // Fit map to its bounds after the api is loaded 36 | const apiIsLoaded = (map, maps, places) => { 37 | // Get bounds by our places 38 | const bounds = getMapBounds(map, maps, places); 39 | // Fit map to bounds 40 | map.fitBounds(bounds); 41 | // Bind the resize listener 42 | bindResizeListener(map, maps, bounds); 43 | }; 44 | 45 | class Main extends Component { 46 | constructor(props) { 47 | super(props); 48 | 49 | this.state = { 50 | places: [], 51 | }; 52 | } 53 | 54 | componentDidMount() { 55 | fetch('places.json') 56 | .then((response) => response.json()) 57 | .then((data) => this.setState({ places: data.results })); 58 | } 59 | 60 | render() { 61 | const { places } = this.state; 62 | return ( 63 | <> 64 | {!isEmpty(places) && ( 65 | apiIsLoaded(map, maps, places)} 70 | > 71 | {places.map((place) => ( 72 | 78 | ))} 79 | 80 | )} 81 | 82 | ); 83 | } 84 | } 85 | 86 | export default Main; 87 | -------------------------------------------------------------------------------- /src/examples/MarkerInfoWindow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import isEmpty from 'lodash.isempty'; 4 | 5 | // examples: 6 | import GoogleMap from '../components/GoogleMap'; 7 | 8 | // consts: [34.0522, -118.2437] 9 | import LOS_ANGELES_CENTER from '../const/la_center'; 10 | 11 | // InfoWindow component 12 | const InfoWindow = (props) => { 13 | const { place } = props; 14 | const infoWindowStyle = { 15 | position: 'relative', 16 | bottom: 150, 17 | left: '-45px', 18 | width: 220, 19 | backgroundColor: 'white', 20 | boxShadow: '0 2px 7px 1px rgba(0, 0, 0, 0.3)', 21 | padding: 10, 22 | fontSize: 14, 23 | zIndex: 100, 24 | }; 25 | 26 | return ( 27 |
28 |
29 | {place.name} 30 |
31 |
32 | 33 | {place.rating} 34 | {' '} 35 | 36 | 37 | {String.fromCharCode(9733).repeat(Math.floor(place.rating))} 38 | 39 | 40 | {String.fromCharCode(9733).repeat(5 - Math.floor(place.rating))} 41 | 42 |
43 |
44 | {place.types[0]} 45 |
46 |
47 | {'$'.repeat(place.price_level)} 48 |
49 |
50 | {place.opening_hours.open_now ? 'Open' : 'Closed'} 51 |
52 |
53 | ); 54 | }; 55 | 56 | // Marker component 57 | const Marker = ({ show, place }) => { 58 | const markerStyle = { 59 | border: '1px solid white', 60 | borderRadius: '50%', 61 | height: 10, 62 | width: 10, 63 | backgroundColor: show ? 'red' : 'blue', 64 | cursor: 'pointer', 65 | zIndex: 10, 66 | }; 67 | 68 | return ( 69 | <> 70 |
71 | {show && } 72 | 73 | ); 74 | }; 75 | 76 | class MarkerInfoWindow extends Component { 77 | constructor(props) { 78 | super(props); 79 | 80 | this.state = { 81 | places: [], 82 | }; 83 | } 84 | 85 | componentDidMount() { 86 | fetch('places.json') 87 | .then((response) => response.json()) 88 | .then((data) => { 89 | data.results.forEach((result) => { 90 | result.show = false; // eslint-disable-line no-param-reassign 91 | }); 92 | this.setState({ places: data.results }); 93 | }); 94 | } 95 | 96 | // onChildClick callback can take two arguments: key and childProps 97 | onChildClickCallback = (key) => { 98 | this.setState((state) => { 99 | const index = state.places.findIndex((e) => e.id === key); 100 | state.places[index].show = !state.places[index].show; // eslint-disable-line no-param-reassign 101 | return { places: state.places }; 102 | }); 103 | }; 104 | 105 | render() { 106 | const { places } = this.state; 107 | 108 | return ( 109 | <> 110 | {!isEmpty(places) && ( 111 | 117 | {places.map((place) => ( 118 | 125 | ))} 126 | 127 | )} 128 | 129 | ); 130 | } 131 | } 132 | 133 | InfoWindow.propTypes = { 134 | place: PropTypes.shape({ 135 | name: PropTypes.string, 136 | formatted_address: PropTypes.string, 137 | rating: PropTypes.number, 138 | types: PropTypes.arrayOf(PropTypes.string), 139 | price_level: PropTypes.number, 140 | opening_hours: PropTypes.shape({ 141 | open_now: PropTypes.bool, 142 | }), 143 | }).isRequired, 144 | }; 145 | 146 | Marker.propTypes = { 147 | show: PropTypes.bool.isRequired, 148 | place: PropTypes.shape({ 149 | name: PropTypes.string, 150 | formatted_address: PropTypes.string, 151 | rating: PropTypes.number, 152 | types: PropTypes.arrayOf(PropTypes.string), 153 | price_level: PropTypes.number, 154 | opening_hours: PropTypes.shape({ 155 | open_now: PropTypes.bool, 156 | }), 157 | }).isRequired, 158 | }; 159 | 160 | export default MarkerInfoWindow; 161 | -------------------------------------------------------------------------------- /src/examples/MarkerInfoWindowGmapsObj.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import isEmpty from 'lodash.isempty'; 3 | 4 | // examples: 5 | import GoogleMap from '../components/GoogleMap'; 6 | 7 | // consts: [34.0522, -118.2437] 8 | import LOS_ANGELES_CENTER from '../const/la_center'; 9 | 10 | const getInfoWindowString = (place) => ` 11 |
12 |
13 | ${place.name} 14 |
15 |
16 | 17 | ${place.rating} 18 | 19 | ${String.fromCharCode(9733).repeat(Math.floor(place.rating))}${String.fromCharCode(9733).repeat(5 - Math.floor(place.rating))} 20 |
21 |
22 | ${place.types[0]} 23 |
24 |
25 | ${'$'.repeat(place.price_level)} 26 |
27 |
28 | ${place.opening_hours.open_now ? 'Open' : 'Closed'} 29 |
30 |
`; 31 | 32 | // Refer to https://github.com/google-map-react/google-map-react#use-google-maps-api 33 | const handleApiLoaded = (map, maps, places) => { 34 | const markers = []; 35 | const infowindows = []; 36 | 37 | places.forEach((place) => { 38 | markers.push(new maps.Marker({ 39 | position: { 40 | lat: place.geometry.location.lat, 41 | lng: place.geometry.location.lng, 42 | }, 43 | map, 44 | })); 45 | 46 | infowindows.push(new maps.InfoWindow({ 47 | content: getInfoWindowString(place), 48 | })); 49 | }); 50 | 51 | markers.forEach((marker, i) => { 52 | marker.addListener('click', () => { 53 | infowindows[i].open(map, marker); 54 | }); 55 | }); 56 | }; 57 | 58 | class MarkerInfoWindowGmapsObj extends Component { 59 | constructor(props) { 60 | super(props); 61 | 62 | this.state = { 63 | places: [], 64 | }; 65 | } 66 | 67 | componentDidMount() { 68 | fetch('places.json') 69 | .then((response) => response.json()) 70 | .then((data) => { 71 | data.results.forEach((result) => { 72 | result.show = false; // eslint-disable-line no-param-reassign 73 | }); 74 | this.setState({ places: data.results }); 75 | }); 76 | } 77 | 78 | render() { 79 | const { places } = this.state; 80 | 81 | return ( 82 | <> 83 | {!isEmpty(places) && ( 84 | handleApiLoaded(map, maps, places)} 90 | /> 91 | )} 92 | 93 | ); 94 | } 95 | } 96 | 97 | export default MarkerInfoWindowGmapsObj; 98 | -------------------------------------------------------------------------------- /src/examples/Searchbox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import isEmpty from 'lodash.isempty'; 3 | 4 | // components: 5 | import Marker from '../components/Marker'; 6 | 7 | // examples: 8 | import GoogleMap from '../components/GoogleMap'; 9 | import SearchBox from '../components/SearchBox'; 10 | 11 | // consts 12 | import LOS_ANGELES_CENTER from '../const/la_center'; 13 | 14 | class Searchbox extends Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | mapApiLoaded: false, 20 | mapInstance: null, 21 | mapApi: null, 22 | places: [], 23 | }; 24 | } 25 | 26 | apiHasLoaded = (map, maps) => { 27 | this.setState({ 28 | mapApiLoaded: true, 29 | mapInstance: map, 30 | mapApi: maps, 31 | }); 32 | }; 33 | 34 | addPlace = (place) => { 35 | this.setState({ places: place }); 36 | }; 37 | 38 | render() { 39 | const { 40 | places, mapApiLoaded, mapInstance, mapApi, 41 | } = this.state; 42 | return ( 43 | <> 44 | {mapApiLoaded && } 45 | this.apiHasLoaded(map, maps)} 54 | > 55 | {!isEmpty(places) 56 | && places.map((place) => ( 57 | 63 | ))} 64 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | export default Searchbox; 71 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /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 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // This optional code is used to register a service worker. 3 | // register() is not called by default. 4 | 5 | // This lets the app load faster on subsequent visits in production, and gives 6 | // it offline capabilities. However, it also means that developers (and users) 7 | // will only see deployed updates on subsequent visits to a page, after all the 8 | // existing tabs open on the page have been closed, since previously cached 9 | // resources are updated in the background. 10 | 11 | // To learn more about the benefits of this model and instructions on how to 12 | // opt-in, read https://bit.ly/CRA-PWA 13 | 14 | const isLocalhost = Boolean(window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)); 19 | 20 | export function register(config) { 21 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 22 | // The URL constructor is available in all browsers that support SW. 23 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 24 | if (publicUrl.origin !== window.location.origin) { 25 | // Our service worker won't work if PUBLIC_URL is on a different origin 26 | // from what our page is served on. This might happen if a CDN is used to 27 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 28 | return; 29 | } 30 | 31 | window.addEventListener('load', () => { 32 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 33 | 34 | if (isLocalhost) { 35 | // This is running on localhost. Let's check if a service worker still exists or not. 36 | checkValidServiceWorker(swUrl, config); 37 | 38 | // Add some additional logging to localhost, pointing developers to the 39 | // service worker/PWA documentation. 40 | navigator.serviceWorker.ready.then(() => { 41 | console.log('This web app is being served cache-first by a service ' + 42 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'); 43 | }); 44 | } else { 45 | // Is not localhost. Just register service worker 46 | registerValidSW(swUrl, config); 47 | } 48 | }); 49 | } 50 | } 51 | 52 | function registerValidSW(swUrl, config) { 53 | navigator.serviceWorker 54 | .register(swUrl) 55 | .then((registration) => { 56 | registration.onupdatefound = () => { 57 | const installingWorker = registration.installing; 58 | if (installingWorker == null) { 59 | return; 60 | } 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the updated precached content has been fetched, 65 | // but the previous service worker will still serve the older 66 | // content until all client tabs are closed. 67 | console.log('New content is available and will be used when all ' + 68 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'); 69 | 70 | // Execute callback 71 | if (config && config.onUpdate) { 72 | config.onUpdate(registration); 73 | } 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // "Content is cached for offline use." message. 78 | console.log('Content is cached for offline use.'); 79 | 80 | // Execute callback 81 | if (config && config.onSuccess) { 82 | config.onSuccess(registration); 83 | } 84 | } 85 | } 86 | }; 87 | }; 88 | }) 89 | .catch((error) => { 90 | console.error('Error during service worker registration:', error); 91 | }); 92 | } 93 | 94 | function checkValidServiceWorker(swUrl, config) { 95 | // Check if the service worker can be found. If it can't reload the page. 96 | fetch(swUrl, { 97 | headers: { 'Service-Worker': 'script' }, 98 | }) 99 | .then((response) => { 100 | // Ensure service worker exists, and that we really are getting a JS file. 101 | const contentType = response.headers.get('content-type'); 102 | if ( 103 | response.status === 404 || 104 | (contentType != null && contentType.indexOf('javascript') === -1) 105 | ) { 106 | // No service worker found. Probably a different app. Reload the page. 107 | navigator.serviceWorker.ready.then((registration) => { 108 | registration.unregister().then(() => { 109 | window.location.reload(); 110 | }); 111 | }); 112 | } else { 113 | // Service worker found. Proceed as normal. 114 | registerValidSW(swUrl, config); 115 | } 116 | }) 117 | .catch(() => { 118 | console.log('No internet connection found. App is running in offline mode.'); 119 | }); 120 | } 121 | 122 | export function unregister() { 123 | if ('serviceWorker' in navigator) { 124 | navigator.serviceWorker.ready 125 | .then((registration) => { 126 | registration.unregister(); 127 | }) 128 | .catch((error) => { 129 | console.error(error.message); 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/stories/Autocomplete/Base.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import AutoComplete from '../../examples/AutoComplete'; 5 | 6 | export default { 7 | title: 'AutoComplete Examples', 8 | }; 9 | 10 | export const Base = () => ( 11 | 12 | 13 | 14 | ); 15 | 16 | const Wrapper = styled.section` 17 | width: 100vw; 18 | height: 100vh; 19 | `; 20 | -------------------------------------------------------------------------------- /src/stories/ClickableMarkers/Base.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import MarkerInfoWindow from '../../examples/MarkerInfoWindow'; 5 | import MarkerInfoWindowGmapsObj from '../../examples/MarkerInfoWindowGmapsObj'; 6 | 7 | export default { 8 | title: 'MarkerInfo Examples', 9 | }; 10 | 11 | export const DefaultMarker = () => ( 12 | 13 | 14 | 15 | ); 16 | 17 | export const CustomComponent = () => ( 18 | 19 | 20 | 21 | ); 22 | 23 | const Wrapper = styled.section` 24 | width: 100vw; 25 | height: 100vh; 26 | `; 27 | -------------------------------------------------------------------------------- /src/stories/Heatmap/Base.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Heatmap from '../../examples/Heatmap'; 5 | 6 | export default { 7 | title: 'Heatmap Examples', 8 | }; 9 | 10 | export const Base = () => ( 11 | 12 | 13 | 14 | ); 15 | 16 | const Wrapper = styled.section` 17 | width: 100vw; 18 | height: 100vh; 19 | `; 20 | -------------------------------------------------------------------------------- /src/stories/Main/Base.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Main from '../../examples/Main'; 5 | 6 | export default { 7 | title: 'Main Examples', 8 | }; 9 | 10 | export const Base = () => ( 11 | 12 |
13 | 14 | ); 15 | 16 | const Wrapper = styled.section` 17 | width: 100vw; 18 | height: 100vh; 19 | `; 20 | -------------------------------------------------------------------------------- /src/stories/Searchbox/Base.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Searchbox from '../../examples/Searchbox'; 5 | 6 | export default { 7 | title: 'Searchbox Examples', 8 | }; 9 | 10 | export const Base = () => ( 11 | 12 | 13 | 14 | ); 15 | 16 | const Wrapper = styled.section` 17 | width: 100vw; 18 | height: 100vh; 19 | `; 20 | --------------------------------------------------------------------------------