Building a simple Full-Stack Restaurant Finder App with React, Redux, Node.js, and Google Places API (Part 2)
Tags: react, typescript, redux, googleplaces
Published: 2024-06-19 18:27:01



**Part 2: Building a Dynamic Frontend with React, Redux Toolkit, and Google Maps API**

**Introduction:**
Welcome to the second part of our tutorial on building a full-stack "Restaurant Finder" application. 
In this blog post, we will focus on developing the frontend components using React and Redux Toolkit. Our frontend will provide users with an intuitive interface to explore nearby restaurants, view detailed information such as ratings and photos, and seamlessly navigate to locations using Google Maps integration.

Throughout this tutorial, we'll cover essential frontend development concepts including 
- state management with Redux Toolkit, 
- integrating Google Maps for interactive map displays, and 
- creating reusable components for a consistent user experience. 

By the end of this guide, you'll have a complete understanding of how to implement a simple and responsive frontend for our "Restaurant Finder" application.


**Step 1: Setting Up React App**
- Initialize Project:

-> Create a new directory for your client and navigate into it.
Run `npx create-react-app client --template typescript` to create a new React app with TypeScript.

- Install Dependencies:

Run `npm install redux react-redux @reduxjs/toolkit axios tailwindcss daisyui @vis.gl/react-google-maps`

- Configure Tailwind CSS:

-> Create a tailwind.config.js file and configure Tailwind CSS:

```
// tailwind.config.js
module.exports = {
  purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [require('daisyui')],
};
```

- Setup Tailwind in CSS:
-> Add the following to src/index.css:

```
@tailwind base;
@tailwind components;
@tailwind utilities;
```
**Step 2: Creating Components**
- Header Component:
-> Create a Header.tsx file in src/components and add the following:

```
import React from "react";

const Header: React.FC = () => {
  return (
    <header className=" mx-1 my-5 p-4 flex justify-between items-center">
      <div className="text-sm font-semibold bg-neutral-700 rounded-md p-1">
        <span className="text-white mr-1">Restaurant</span>
        <span className=" w-12 h-8 rounded bg-neutral-100 px-1 text-neutral-700 font-bold">
          Finder
        </span>
      </div>
      <p className=" text-sm font-semibold mx-2 px-1">
        "Good food is the foundation of genuine happiness !"
      </p>
    </header>
  );
};

export default Header;
```
- Footer Component:
-> Create a Footer.tsx file in src/components and add the following:

```
import React from "react";

const Footer: React.FC = () => {
  const year = new Date().getFullYear();
  return (
    <footer className=" text-neutral-300 p-1 m-0 flex justify-center items-center font-thin text-xs">
      <p className="">
        <span>@ {year} © Your Name.</span>
      </p>
    </footer>
  );
};

export default Footer;
```
- PlaceAutocomplete Component:
-> Create a PlaceAutocomplete.tsx file in src/components and add the following:

```
import React from "react";
import { useGoogleAutocomplete } from "@vis.gl/react-google-maps";
import { useDispatch } from "react-redux";
import { fetchRestaurants } from "../redux/restaurantSlice";

const PlaceAutocomplete: React.FC = () => {
  const dispatch = useDispatch();

  const {
    value,
    suggestions: { status, data },
    setValue,
    clearSuggestions,
  } = useGoogleAutocomplete({
    apiKey: process.env.REACT_APP_GOOGLE_PLACES_API_KEY,
    debounce: 300,
    minLength: 3,
  });

  const handleSelect = ({ description }) => {
    setValue(description, false);
    clearSuggestions();

    const geocoder = new window.google.maps.Geocoder();
    geocoder.geocode({ address: description }, (results, status) => {
      if (status === "OK") {
        const { lat, lng } = results[0].geometry.location;
        dispatch(fetchRestaurants({ lat: lat(), lng: lng() }));
      }
    });
  };

  return (
    <div className="relative">
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Enter a place"
        className="w-full px-4 py-2 border rounded"
      />
      {status === "OK" && (
        <ul className="absolute z-10 w-full bg-white border rounded shadow-md mt-1">
          {data.map((suggestion) => (
            <li
              key={suggestion.place_id}
              onClick={() => handleSelect(suggestion)}
              className="px-4 py-2 cursor-pointer hover:bg-gray-200"
            >
              {suggestion.description}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default PlaceAutocomplete;

```
- RestaurantList Component:
-> Create a RestaurantList.tsx file in src/components and add the following:

```
import React from "react";
import { useSelector } from "react-redux";
import { RootState } from "../redux/store";
import RestaurantCard from "./RestaurantCard";

const RestaurantList: React.FC = () => {
  const restaurants = useSelector(
    (state: RootState) => state.restaurants.restaurants
  );

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
      {restaurants.map((restaurant) => (
        <RestaurantCard key={restaurant.place_id} restaurant={restaurant} />
      ))}
    </div>
  );
};

export default RestaurantList;
```
- RestaurantCard Component:
-> Create a RestaurantCard.tsx file in src/components and add the following:

```
import React from "react";
import { Restaurant } from "../redux/restaurantSlice";

interface RestaurantCardProps {
  restaurant: Restaurant;
}

const RestaurantCard: React.FC<RestaurantCardProps> = ({ restaurant }) => {
  return (
    <div className="bg-white p-4 rounded shadow">
      {restaurant.photoUrl && (
        <img
          src={restaurant.photoUrl}
          alt={restaurant.name}
          className="w-full h-48 object-cover rounded mb-4"
        />
      )}
      <h3 className="text-lg font-semibold mb-2">{restaurant.name}</h3>
      <p className="text-sm text-gray-600 mb-2">{restaurant.vicinity}</p>
      <p className="text-sm text-gray-600 mb-2">
        Rating: {restaurant.rating} ({restaurant.user_ratings_total} reviews)
      </p>
      <p className="text-sm text-gray-600 mb-2">
        Distance: {restaurant.distance.toFixed(2)} km
      </p>
      <a
        href={`https://www.google.com/maps/place/?q=place_id:${restaurant.place_id}`}
        target="_blank"
        rel="noopener noreferrer"
        className="text-blue-500 hover:underline"
      >
        View on Google Maps
      </a>
    </div>
  );
};

export default RestaurantCard;
```
**Step 3: Setting Up Redux**
- Create Redux Store:
-> Create a redux/store.ts file and add the following:

```
import { configureStore } from "@reduxjs/toolkit";
import restaurantReducer from "./restaurantSlice";

const store = configureStore({
  reducer: {
    restaurants: restaurantReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;
```

- src/redux/hooks.ts

```
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```

- Create Restaurant Slice:
-> Create a redux/restaurantSlice.ts file and add the following:

```
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

export interface Restaurant {
  name: string;
  vicinity: string;
  rating: number;
  user_ratings_total: number;
  distance: number;
  photoUrl: string | null;
  place_id: string;
}

interface RestaurantState {
  restaurants: Restaurant[];
  status: "idle" | "loading" | "succeeded" | "failed";
  error: string | null;
}

const initialState: RestaurantState = {
  restaurants: [],
  status: "idle",
  error: null,
};

export const fetchRestaurants = createAsyncThunk(
  "restaurants/fetchRestaurants",
  async ({ lat, lng }: { lat: number; lng: number }) => {
    const response = await axios.get("http://localhost:3001/api/places", {
      params: { lat, lng },
    });
    return response.data;
  }
);

const restaurantSlice = createSlice({
  name: "restaurants",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchRestaurants.pending, (state) => {
        state.status = "loading";
      })
      .addCase(fetchRestaurants.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.restaurants = action.payload;
      })
      .addCase(fetchRestaurants.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message || null;
      });
  },
});

export default restaurantSlice.reducer;
```
- Configure Store Provider:
-> Wrap your app with the Redux provider in src/index.tsx:

```
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/store";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();
```
**Step 4: Assembling the App**
- Create App Component:
-> Update src/App.tsx to include all components:

```
import React from "react";
import Header from "./components/Header";
import Footer from "./components/Footer";
import PlaceAutocomplete from "./components/PlaceAutocomplete";
import RestaurantList from "./components/RestaurantList";

const App: React.FC = () => {
  return (
    <div className="flex flex-col min-h-screen">
      <Header />
      <div className="flex-grow flex flex-col items-center p-4">
        <PlaceAutocomplete />
        <RestaurantList />
      </div>
      <Footer />
    </div>
  );
};

export default App;
```
**Run the Frontend:**
-> Navigate to the client directory and run npm start to start the React app.
-> Open a browser and navigate to http://localhost:3000 to see the application in action.

**Step 5: Testing the App**
- Functionality Testing:

-> Enter a location in the search bar and verify the list of restaurants updates accordingly.
-> Check that the restaurant cards display all relevant information and links to Google Maps.

Code Quality:
Ensure your code follows best practices and is well-structured.

**Project Structure**

client
├── src/
│   ├── components/
│   │   ├── Footer.tsx
│   │   ├── Header.tsx
│   │   ├── PlaceAutocomplete.tsx
│   │   ├── RestaurantItem.tsx
│   │   ├── RestaurantList.tsx
│   ├── images/
│   │   ├── def-restaurant.jpg
│   │   ├── menuplate.jpg
│   ├── redux/
│   │   ├── hooks.ts
│   │   ├── store.ts
│   │   ├── restaurantsSlice.ts
│   ├── App.tsx
│   ├── index.tsx
│   ├── .env
│   ├── package.json
server
├── server.js
├── .env

**_Great job! _**
You have successfully built a user-friendly frontend for the "Restaurant Finder" app. Your React application is now equipped with features like location-based restaurant search, and it integrates seamlessly with the backend you built earlier.

With both the backend and frontend completed, you have a full-stack application ready for deployment. Feel free to explore further enhancements, such as adding more filters or improving the UI. 
_Happy coding!_


{% cta https://dev.to/vb_nair/building-a-simple-full-stack-restaurant-finder-app-with-react-redux-nodejs-and-google-places-api-1acd %} Part 1: Building the Backend {% endcta %}

{% cta https://github.com/vbnair/restaurantFinder.git %} GitHub Repo {% endcta %}

React components and hooks for the Google Maps JavaScript API.
{% cta https://visgl.github.io/react-google-maps/ %} React components and hooks for the Google Maps JavaScript API. {% endcta %}


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nrn2grk587asi16kcx2s.png)

📚 Explore and Learn!
This project is a gateway to exploring n learning, and planning to add on further iterations to enhance and expand. created it for exploration and showcase the integration of various technologies. Dive in, experiment, and enjoy the journey! 🌟