SkillAgentSearch skills...

Mealternative

MERN stack restaurant finder and recipe sharing website

Install / Use

/learn @kazhala/Mealternative
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Mealternative

A fully responsive MERN stack web app for finding nearby restaurants as well as a platform for finding and sharing recipes.

Project URL: https://mealternative.com/

Introduction

This project was bootstrapped with CRA. 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.

Big credits to this blog post which helps me understand how to use the google map API.

Usage

To play around the app locally, please follow the steps below

  1. Clone the repository
  2. Go into the directory where the package.json resides
  3. Install dependencies
npm install
  1. Create the required .env file with below three variables inside it. Note: at the minimum, you will need to create your own google map api key (detailed steps and explanations are here).
cat << EOF > .env
REACT_APP_GOOGLE_MAP_API_KEY=<Your api key>
REACT_APP_BACKEND_URL=https://api.mealternative.com
REACT_APP_CLOUDINARY_URL=https://api.cloudinary.com/v1_1/kazhala/image/upload
EOF

If you also followed the backend set up, you could change the REACT_APP_BACKEND_URL to

REACT_APP_BACKEND_URL=http://localhost:8000/api
  1. Start the server
npm start
  1. Break Everything:)

Google Map

You will get $400 free credit for one year when you first create a google cloud account

Steps to set up Google Map Token

  1. Go to google app engine and create a new project. (Alternatively, you could use an existing one if you wish)
  2. Go to the google map service
  3. Enable 4 APIs. (Places, Map Javascript, Geocoding and Directions API)
  4. Navigate to the API & Service console (Credentials tab)
  5. At the top, click + Create Credentials and then click the API key
  6. Copy the api key and navigate back to the Google map service page
  7. Make sure all of the services are using the same API key (They should pick up the API key automatically). Under Google Map -> APIs
  8. Done! Now paste the copied API key to .env file mentioned in Usage -> Step4.

How it works

Load the google map

  • For center, you could use the browser api to get user current location, this will be the center of your map
const [centerMarker, setCenterMarker] = useState({});

useEffect(() => {
  // I stored it in redux, obviously you could create a state and store the lat and lng
  const locationSuccess = (pos) => {
    const crd = pos.coords;
    setCenterMarker({ lat: crd.latitude, lng: crd.longitude });
  };
  const locationError = () =>
    console.log('Please turn on location services in your phone');

  const locationOptions = {
    enableHighAccuracy: true,
    timeout: 5000,
    maximumAge: 0,
  };

  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(
      locationSuccess,
      locationError,
      locationOptions
    );
  } else {
    console.log('Sorry, your browser does not support geolocation');
  }
}, [setCenterMarker]);
  • GoogleMap component
<GoogleMapReact
  bootstrapURLKeys={{
    key: YourGoogleMapAPIKey,
    libraries: ['places', 'directions'],
  }}
  center={{ lat: centerMarker.lat, lng: centerMarker.lng }}
  defaultZoom={16}
  yesIWantToUseGoogleMapApiInternals={true}
  onGoogleApiLoaded={({ map, maps }) => handleMapApiLoaded(map, maps)}
/>
  • You will need to create a call back for onGoogleApiLoaded to init apis
const [googleMap, setGoogleMap] = useState({
  mapsApi: null,
  autoCompleteService: null,
  placesServices: null,
  directionService: null,
  geoCoderService: null,
  mapLoaded: false,
});

const handleMapApiLoaded = (map, maps) => {
  setGoogleMap({
    ...googleMap,
    mapsApi: maps,
    autoCompleteService: new maps.places.AutocompleteService(),
    placesServices: new maps.places.PlacesService(map),
    directionService: new maps.DirectionsService(),
    geoCoderService: new maps.Geocoder(),
    mapLoaded: true,
  });
};

Address auto completion

  • Import an autocompletion component from any package, I've used materialUi
  • use the autoCompleteService initialised in previouse step
const handleAutoCompleteUpdate = (searchValue, callBack) => {
  const searchQuery = {
    input: searchValue,
    location: new mapsApi.LatLng(centerMarker.lat, centerMarker.lng), // mapsApi is from the previous step
    radius: 100000, // in Meters. 100km
  };
  // if there is input, perform google autoCompleteService request
  searchQuery.input &&
    autoCompleteService.getQueryPredictions(searchQuery, (response) => {
      // The name of each GoogleMaps place suggestion is in the "description" field
      if (response) {
        const dataSource = response.map((resp) => resp.description);
        // set the autoCompletion's options
        callBack(dataSource);
      }
    });
};

// This is the autocompletion src that will be presented in the dropdown
const [autoSrc, setAutoSrc] = useState([]);

// the onChange handler for the autocompletion component
const handleChange = (e) => {
  handleAutoCompleteUpdate(e.target.value, (dataSource) =>
    setAutoSrc(dataSource)
  );
};
  • The autocompletion component for reference
import { Autocomplete } from '@material-ui/lab';
...
<Autocomplete
  options={autoSrc}
  loading={determineLoading()}
  onOpen={() => setOpen(true)}
  onClose={() => setOpen(false)}
  open={open}
  disableOpenOnFocus
  onChange={handleSelect} // This is the value when user select an item in the dropdown
  renderInput={(params) => (
    <TextField
      {...params}
      label='Location center'
      variant='outlined'
      fullWidth
      placeholder='Add address'
      value={value} // This value is set through handleSelect, not handleChange
      onChange={handleChange} // This is the value when user type in the textfiled, it only updates the autoSrc
      size='small'
      InputLabelProps={{
        shrink: true,
      }}
    />
  )}
/>;
  • Updating the center location in the map
const updateCenterMarker = (address) => {
  // decode the address to latlng
  geoCoderService.geocode({ address }, (response) => {
    if (!response[0]) {
      console.error("Can't find the address");
      setError("Can't find the address");
      // if empty, set to original location
      setCenterMarker({ lat, lng });
      return;
    }
    const { location } = response[0].geometry;
    setCenterMarker({ lat: location.lat(), lng: location.lng() });
  });
};
Demo

Finding restaurants

  • Basic search using google api
const [resultRestaurantList, setResultRestaurantList] = useState([]);

const handleRestaurantSearch = (searchQuery) => {
  // 1. Create places request (if no search query, just search all restaurant)
  // rankBy cannot be used with radius at the same time
  // rankBy and radius doesn't seem to work with textSearch, keep it for future reference
  const placesRequest = {
    location: new mapsApi.LatLng(centerMarker.lat, centerMarker.lng), // mapsApi from previous step initialising google map
    type: ['restaurant', 'cafe'],
    query: searchQuery ? searchQuery : 'restaurant',
    // radius: '500',
    // rankBy: mapsApi.places.RankBy.DISTANCE
  };

  // perform textSearch based on query passed in ('chinese', 'thai', etc)
  placesServices.textSearch(
    placesRequest,
    (locationResults, status, paginationInfo) => {
      if (status !== 'OK') {
        console.error('No results found', status);
      } else {
        setResultRestaurantList([...locationResults]);
      }
    }
  );
};
  • Add pagination to the result

    By default, google would return 20 results, with extra 2 page pagination up to 60 results

const [nextPage, setNextPage] = useState(null);
const [resultRestaurantList, setResultRestaurantList] = useState([]);

const handleRestaurantSearch = (searchQuery) => {
  const placesRequest = {
    location: new mapsApi.LatLng(centerMarker.lat, centerMarker.lng),
    type: ['restaurant', 'cafe'],
    query: searchQuery ? searchQuery : 'restaurant',
  };

  placesServices.textSearch(
    placesRequest,
    (locationResults, status, paginationInfo) => {
      if (status !== 'OK') {
        console.error('No results found', status);
      } else {
        // store nextPage information to state
        setNextPage(paginationInfo);
        // update state results, without clearing the result when paginating
        setResultRestaurantList((prevList) => {
          const newList = [...prevList, ...tempResultList];
          return newList;
        });
      }
    }
  );
};

const getNextPage = () => {
  if (nextPage.hasNextPage) {
    nextPage.nextPage();
  }
};
  • Add distance rest
View on GitHub
GitHub Stars52
CategoryDevelopment
Updated3mo ago
Forks17

Languages

JavaScript

Security Score

82/100

Audited on Dec 18, 2025

No findings