DEV Community

Cover image for Building a simple Full-Stack Restaurant Finder App with React, Redux, Node.js, and Google Places API (Part 2)
Vaisakh
Vaisakh

Posted on

Building a simple Full-Stack Restaurant Finder App with React, Redux, Node.js, and Google Places API (Part 2)

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')],
};
Enter fullscreen mode Exit fullscreen mode
  • Setup Tailwind in CSS: -> Add the following to src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!

Part 1: Building the Backend

GitHub Repo

React components and hooks for the Google Maps JavaScript API.
React components and hooks for the Google Maps JavaScript API.

Image description

📚 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! 🌟

Top comments (0)