{"id":19019467,"url":"https://github.com/kazhala/mealternative","last_synced_at":"2025-04-23T05:17:55.310Z","repository":{"id":36919739,"uuid":"228312279","full_name":"kazhala/mealternative","owner":"kazhala","description":"MERN stack restaurant finder and recipe sharing website","archived":false,"fork":false,"pushed_at":"2020-06-05T16:49:03.000Z","size":72621,"stargazers_count":50,"open_issues_count":1,"forks_count":16,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-23T05:17:44.795Z","etag":null,"topics":["food","google-maps","mern-stack","react","react-google-maps","reactjs"],"latest_commit_sha":null,"homepage":"https://mealternative.com/map","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kazhala.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-12-16T05:50:12.000Z","updated_at":"2025-02-19T12:21:46.000Z","dependencies_parsed_at":"2022-08-08T18:16:49.714Z","dependency_job_id":null,"html_url":"https://github.com/kazhala/mealternative","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kazhala%2Fmealternative","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kazhala%2Fmealternative/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kazhala%2Fmealternative/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kazhala%2Fmealternative/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kazhala","download_url":"https://codeload.github.com/kazhala/mealternative/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250372944,"owners_count":21419724,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["food","google-maps","mern-stack","react","react-google-maps","reactjs"],"created_at":"2024-11-08T20:12:36.279Z","updated_at":"2025-04-23T05:17:55.288Z","avatar_url":"https://github.com/kazhala.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mealternative\n\nA fully responsive MERN stack web app for finding nearby restaurants as well as a platform for finding and sharing recipes.\n\nProject URL: https://mealternative.com/\n\n- Find restaurants using [google-map-react](https://github.com/google-map-react/google-map-react)\n- Styled with [MaterialUi](https://material-ui.com/)\n- State managed by [Redux](https://redux.js.org/introduction/getting-started/) and [Redux-Saga](https://redux-saga.js.org/)\n- For backend related information you could find it in backend [repo](https://github.com/kazhala/mealternative-backend)\n\n![](https://user-images.githubusercontent.com/43941510/77802302-aca09380-70ce-11ea-8877-fe0fde5d0a22.png)\n\n## Introduction\n\nThis project was bootstrapped with [CRA](https://github.com/facebook/create-react-app). It's not built for production usage, I've built this website mainly to refresh my knowledge on the MERN stack as well as finding some restaurants from time to time. I hope you could steal and find something useful from this repo and website.\n\nBig credits to this blog [post](https://medium.com/javascript-in-plain-english/building-a-react-ice-cream-finder-app-with-the-google-maps-api-7e39339e0261) which helps me understand how to use the google map API.\n\n## Usage\n\nTo play around the app locally, please follow the steps below\n\n1. Clone the repository\n2. Go into the directory where the package.json resides\n3. Install dependencies\n\n```bash\nnpm install\n```\n\n4. Create the required .env file with below three variables inside it.\n   Note: at the minimum, you will need to create your own google map api key (detailed steps and explanations are [here](#google-map)).\n\n```bash\ncat \u003c\u003c EOF \u003e .env\nREACT_APP_GOOGLE_MAP_API_KEY=\u003cYour api key\u003e\nREACT_APP_BACKEND_URL=https://api.mealternative.com\nREACT_APP_CLOUDINARY_URL=https://api.cloudinary.com/v1_1/kazhala/image/upload\nEOF\n```\n\nIf you also followed the backend set up, you could change the `REACT_APP_BACKEND_URL` to\n\n```bash\nREACT_APP_BACKEND_URL=http://localhost:8000/api\n```\n\n5. Start the server\n\n```bash\nnpm start\n```\n\n6. Break Everything:)\n\n## Google Map\n\n\u003e You will get \\$400 free credit for one year when you first create a google cloud account\n\n### Steps to set up Google Map Token\n\n1. Go to google app engine and create a new project. (Alternatively, you could use an existing one if you wish)\n2. Go to the google map service\n   ![](https://user-images.githubusercontent.com/43941510/77834703-a4ae2580-719a-11ea-9ee8-8d199698cb91.png)\n3. Enable 4 APIs. (Places, Map Javascript, Geocoding and Directions API)\n   ![](https://user-images.githubusercontent.com/43941510/77834791-5ea59180-719b-11ea-932e-3844ac34a966.png)\n4. Navigate to the API \u0026 Service console (Credentials tab)\n   ![](https://user-images.githubusercontent.com/43941510/77834996-4cc4ee00-719d-11ea-92e1-60a91dff2fbe.png)\n5. At the top, click + Create Credentials and then click the API key\n6. Copy the api key and navigate back to the Google map service page\n7. Make sure all of the services are using the same API key (They should pick up the API key automatically). Under Google Map -\u003e APIs\n   ![](https://user-images.githubusercontent.com/43941510/77835168-526f0380-719e-11ea-8bae-6ffd7ed9ec9e.png)\n8. Done! Now paste the copied API key to .env file mentioned in Usage -\u003e Step4.\n\n### How it works\n\n#### Load the google map\n\n- For center, you could use the browser api to get user current location, this will be the center of your map\n\n```javascript\nconst [centerMarker, setCenterMarker] = useState({});\n\nuseEffect(() =\u003e {\n  // I stored it in redux, obviously you could create a state and store the lat and lng\n  const locationSuccess = (pos) =\u003e {\n    const crd = pos.coords;\n    setCenterMarker({ lat: crd.latitude, lng: crd.longitude });\n  };\n  const locationError = () =\u003e\n    console.log('Please turn on location services in your phone');\n\n  const locationOptions = {\n    enableHighAccuracy: true,\n    timeout: 5000,\n    maximumAge: 0,\n  };\n\n  if (navigator.geolocation) {\n    navigator.geolocation.getCurrentPosition(\n      locationSuccess,\n      locationError,\n      locationOptions\n    );\n  } else {\n    console.log('Sorry, your browser does not support geolocation');\n  }\n}, [setCenterMarker]);\n```\n\n- GoogleMap component\n\n```javascript\n\u003cGoogleMapReact\n  bootstrapURLKeys={{\n    key: YourGoogleMapAPIKey,\n    libraries: ['places', 'directions'],\n  }}\n  center={{ lat: centerMarker.lat, lng: centerMarker.lng }}\n  defaultZoom={16}\n  yesIWantToUseGoogleMapApiInternals={true}\n  onGoogleApiLoaded={({ map, maps }) =\u003e handleMapApiLoaded(map, maps)}\n/\u003e\n```\n\n- You will need to create a call back for `onGoogleApiLoaded` to init apis\n\n```javascript\nconst [googleMap, setGoogleMap] = useState({\n  mapsApi: null,\n  autoCompleteService: null,\n  placesServices: null,\n  directionService: null,\n  geoCoderService: null,\n  mapLoaded: false,\n});\n\nconst handleMapApiLoaded = (map, maps) =\u003e {\n  setGoogleMap({\n    ...googleMap,\n    mapsApi: maps,\n    autoCompleteService: new maps.places.AutocompleteService(),\n    placesServices: new maps.places.PlacesService(map),\n    directionService: new maps.DirectionsService(),\n    geoCoderService: new maps.Geocoder(),\n    mapLoaded: true,\n  });\n};\n```\n\n#### Address auto completion\n\n- Import an autocompletion component from any package, I've used materialUi\n- use the autoCompleteService initialised in previouse step\n\n```javascript\nconst handleAutoCompleteUpdate = (searchValue, callBack) =\u003e {\n  const searchQuery = {\n    input: searchValue,\n    location: new mapsApi.LatLng(centerMarker.lat, centerMarker.lng), // mapsApi is from the previous step\n    radius: 100000, // in Meters. 100km\n  };\n  // if there is input, perform google autoCompleteService request\n  searchQuery.input \u0026\u0026\n    autoCompleteService.getQueryPredictions(searchQuery, (response) =\u003e {\n      // The name of each GoogleMaps place suggestion is in the \"description\" field\n      if (response) {\n        const dataSource = response.map((resp) =\u003e resp.description);\n        // set the autoCompletion's options\n        callBack(dataSource);\n      }\n    });\n};\n\n// This is the autocompletion src that will be presented in the dropdown\nconst [autoSrc, setAutoSrc] = useState([]);\n\n// the onChange handler for the autocompletion component\nconst handleChange = (e) =\u003e {\n  handleAutoCompleteUpdate(e.target.value, (dataSource) =\u003e\n    setAutoSrc(dataSource)\n  );\n};\n```\n\n- The autocompletion component for reference\n\n```javascript\nimport { Autocomplete } from '@material-ui/lab';\n...\n\u003cAutocomplete\n  options={autoSrc}\n  loading={determineLoading()}\n  onOpen={() =\u003e setOpen(true)}\n  onClose={() =\u003e setOpen(false)}\n  open={open}\n  disableOpenOnFocus\n  onChange={handleSelect} // This is the value when user select an item in the dropdown\n  renderInput={(params) =\u003e (\n    \u003cTextField\n      {...params}\n      label='Location center'\n      variant='outlined'\n      fullWidth\n      placeholder='Add address'\n      value={value} // This value is set through handleSelect, not handleChange\n      onChange={handleChange} // This is the value when user type in the textfiled, it only updates the autoSrc\n      size='small'\n      InputLabelProps={{\n        shrink: true,\n      }}\n    /\u003e\n  )}\n/\u003e;\n```\n\n- Updating the center location in the map\n\n```javascript\nconst updateCenterMarker = (address) =\u003e {\n  // decode the address to latlng\n  geoCoderService.geocode({ address }, (response) =\u003e {\n    if (!response[0]) {\n      console.error(\"Can't find the address\");\n      setError(\"Can't find the address\");\n      // if empty, set to original location\n      setCenterMarker({ lat, lng });\n      return;\n    }\n    const { location } = response[0].geometry;\n    setCenterMarker({ lat: location.lat(), lng: location.lng() });\n  });\n};\n```\n\n##### Demo\n\n![](../assets/address-demo.gif?raw=true)\n\n#### Finding restaurants\n\n- Basic search using google api\n\n```javascript\nconst [resultRestaurantList, setResultRestaurantList] = useState([]);\n\nconst handleRestaurantSearch = (searchQuery) =\u003e {\n  // 1. Create places request (if no search query, just search all restaurant)\n  // rankBy cannot be used with radius at the same time\n  // rankBy and radius doesn't seem to work with textSearch, keep it for future reference\n  const placesRequest = {\n    location: new mapsApi.LatLng(centerMarker.lat, centerMarker.lng), // mapsApi from previous step initialising google map\n    type: ['restaurant', 'cafe'],\n    query: searchQuery ? searchQuery : 'restaurant',\n    // radius: '500',\n    // rankBy: mapsApi.places.RankBy.DISTANCE\n  };\n\n  // perform textSearch based on query passed in ('chinese', 'thai', etc)\n  placesServices.textSearch(\n    placesRequest,\n    (locationResults, status, paginationInfo) =\u003e {\n      if (status !== 'OK') {\n        console.error('No results found', status);\n      } else {\n        setResultRestaurantList([...locationResults]);\n      }\n    }\n  );\n};\n```\n\n- Add pagination to the result\n  \u003e By default, google would return 20 results, with extra 2 page pagination up to 60 results\n\n```javascript\nconst [nextPage, setNextPage] = useState(null);\nconst [resultRestaurantList, setResultRestaurantList] = useState([]);\n\nconst handleRestaurantSearch = (searchQuery) =\u003e {\n  const placesRequest = {\n    location: new mapsApi.LatLng(centerMarker.lat, centerMarker.lng),\n    type: ['restaurant', 'cafe'],\n    query: searchQuery ? searchQuery : 'restaurant',\n  };\n\n  placesServices.textSearch(\n    placesRequest,\n    (locationResults, status, paginationInfo) =\u003e {\n      if (status !== 'OK') {\n        console.error('No results found', status);\n      } else {\n        // store nextPage information to state\n        setNextPage(paginationInfo);\n        // update state results, without clearing the result when paginating\n        setResultRestaurantList((prevList) =\u003e {\n          const newList = [...prevList, ...tempResultList];\n          return newList;\n        });\n      }\n    }\n  );\n};\n\nconst getNextPage = () =\u003e {\n  if (nextPage.hasNextPage) {\n    nextPage.nextPage();\n  }\n};\n```\n\n- Add distance restriction to our search and sorting capability\n  \u003e radius and rankBy setting doesn't work well with textSearch api, we could implement something our own\n\n```javascript\nconst [nextPage, setNextPage] = useState(null);\nconst [resultRestaurantList, setResultRestaurantList] = useState([]);\n// format the distance for sorting later\nconst calculateDistance = (restaurantLocation, centerLocation) =\u003e {\n  return mapsApi.geometry.spherical.computeDistanceBetween(\n    restaurantLocation,\n    centerLocation\n  );\n};\n\n// Note: add the queryRadius to parameter (in km)\nconst handleRestaurantSearch = (searchQuery, queryRadius) =\u003e {\n  const placesRequest = {\n    location: new mapsApi.LatLng(centerMarker.lat, centerMarker.lng),\n    type: ['restaurant', 'cafe'],\n    query: searchQuery ? searchQuery : 'restaurant',\n  };\n\n  placesServices.textSearch(\n    placesRequest,\n    (locationResults, status, paginationInfo) =\u003e {\n      if (status !== 'OK') {\n        console.error('No results found', status);\n      } else {\n        // temp list to keep current result, only update state once\n        let tempResultList = [];\n        for (let i = 0; i \u003c locationResults.length; i++) {\n          // distance check, see if it's in range\n          if (\n            calculateDistance(\n              locationResults[i].geometry.location,\n              placesRequest.location\n            ) \u003c\n            queryRadius * 1000\n          ) {\n            // add an attribute for sorting\n            locationResults[i].distance = calculateDistance(\n              locationResults[i].geometry.location,\n              placesRequest.location\n            );\n            tempResultList.push(locationResults[i]);\n          }\n        }\n        setNextPage(paginationInfo);\n        setResultRestaurantList((prevList) =\u003e {\n          const newList = [...prevList, ...tempResultList];\n          return newList;\n        });\n      }\n    }\n  );\n};\n```\n\n##### Check out full example [here](https://github.com/kazhala/mealternative/blob/master/src/Routes/Map/MapContainer.js)\n\n\u003e Include sorting, fetch restaurant details and much more\n\n##### Demo\n\n![](../assets/search-demo.gif?raw=true)\n\n## Structure\n\n![](https://user-images.githubusercontent.com/43941510/77801667-672f9680-70cd-11ea-9921-5ecb0eaf089f.png)\n\n\u003e The folder structure follows the same folder structure we were using at my intern. It's not the best but there are some positives.\n\n### App\n\nAll the HOC components all handled in [index.js](https://github.com/kazhala/mealternative/blob/master/src/index.js) while the [app.js](https://github.com/kazhala/mealternative/blob/master/src/App/App.js) is mainly used for handling react-router switch and the root layout(mobile sidebar, app bar etc).\n\n### Common\n\nCommon components shared between routes. [PageSpinner.js](https://github.com/kazhala/mealternative/blob/master/src/Common/Spinner/PageSpinner.js), modal, [SnackBar](https://github.com/kazhala/mealternative/blob/master/src/Common/InfoModal/SuccessSnack.js) etc.\n\n### Hooks\n\nCustom hooks folder. [useInfiniteLoad.js](https://github.com/kazhala/mealternative/blob/master/src/Hooks/useInfiniteLoad.js), [useScreenSize](https://github.com/kazhala/mealternative/blob/master/src/Hooks/useScreenSize.js) etc.\n\n### Redux\n\n![](https://user-images.githubusercontent.com/43941510/77802995-3b61e000-70d0-11ea-9245-cbd16fdac9ad.png)\n\n- store.js (the standard store.js that creates a redux store)\n- reducer.main.js and saga.main.js (the root of saga and reducers)\n- Individual reducers\n  - action.js (action creators)\n  - index.js (export purpose only)\n  - operation.js (saga helper functions, async calls to backend)\n  - reducer.js\n  - sagas.js (saga listener and saga worker)\n  - types.js (action type, eliminate typo erros)\n\n### Routes\n\n![](https://user-images.githubusercontent.com/43941510/77803602-9ea04200-70d1-11ea-90a1-5b57c61a2e3a.png)\n\n- Routes.js (export purpose only)\n- Individual routes\n  - Container (Redux connection and most of the logic are handled in the container)\n  - Style.js (MaterialUi useStyle hook)\n  - Root component (No logic, view only, handles the root layout and style for the route)\n  - \\_components (sub-components of the route, some of it main contains local logic only related to the component itself)\n\n## Deployment and Hosting\n\n### Frontend\n\nThe frontend of this project is hosted on an AWS s3 bucket and distributed through CloudFront. [Here](https://github.com/kazhala/AWSCloudFormationStacks/blob/master/Hosting_frontend_S3.yaml) is the custom frontend deployment cloudformation template.\n\n### Backend\n\nThe backend of this project is hosted on AWS ec2 instance through elastic beanstalk and distributed through CloudFront. [Here](https://github.com/kazhala/AWSCloudFormationStacks/blob/master/Hosting_backend_nodejs.yaml) is the custom backend deployment cloudformation template.\n\n### Using the template\n\n1. Register a domain through AWS Route53 or create a hosted zone in Route53 and import the domain\n2. Register an SSL certificate in us-east-1 region(Cloudfront requirement)\n3. Create the Cloudformation stack using the frontend template and enter your registered domain as the bucket name and your SSL certificate Arn\n4. After the stack is created, the frontend should be live.\n5. Backend template usage is [here](https://github.com/kazhala/mealternative-backend)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkazhala%2Fmealternative","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkazhala%2Fmealternative","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkazhala%2Fmealternative/lists"}