One of the key aspects of creating web apps is ensuring a great user experience. However, manually filling out forms for example on the checkout page of an ecommerce website, including entering delivery addresses, can be tedious. In this tutorial, we will explore a solution that allows users to autocomplete their delivery addresses by typing a few characters.
Demo App: App Demo
Prerequisites:
. Basic knowledge of JavaScript
. A Mapbox account
. Basic knowledge of React
. Familiarity with Tailwind CSS
. Familiarity with Next.js App Router (easy to follow through, don't worry)
Let's dive into the implementation:
Open your terminal and navigate to the directory where you want to create the tutorial folder. For beginners, you can use the command
cd Desktop
Clone the repository: git clone https://github.com/de-mawo/geocoding.git
Once the files are downloaded, navigate to the geocoding directory: cd geocoding
Use the git checkout command to switch to the appropriate branch based on your preference:
For TypeScript users: git checkout ts-starter
For JavaScript users: git checkout js-starter
If you are using VSCode, you can simply type code .
in the terminal to open the current folder in the VSCode IDE.
Here is the folder structure you should see if you chose ts-starter
branch like me:
If you chose the js-starter repository, your file extensions will end with .js instead of .tsx.
In your terminal, run yarn to install all dependencies. Then, run yarn dev to start the development server. Open your browser and go to localhost:3000 to see a basic navbar.
If you haven't already, please create a Mapbox account by visiting https://account.mapbox.com/auth/signup/.
After creating the account, proceed to the Dashboard and generate an access token. This token will be used in our endpoints, as demonstrated below.
Once the token is generated, you can view its value and copy it. You also have the option to edit the token. To do so, click on "edit" and add http://localhost:3000/ as one of the URLs under the "URLs" section. When you deploy your app, remember to add another production URL.
Now that you have an access token, create a file named .env at the root level of your folder and include your token in it, like this:
REACT_APP_MAPBOX_TOKEN=pk.ejfadhssxxxxxxxxxxxxxxx
Now let's start coding by focusing on the LocationSearchForm component. Edit the component and update it to the following code.
"use client"
import { ChangeEvent, useEffect, useState } from "react";
import { HiMapPin, HiOutlinePencil } from "react-icons/hi2";
import { toast } from "react-hot-toast";
const LocationSearchForm = () => {
const [isEditing, setIsEditing] = useState<boolean>(false);
const [location, setLocation] = useState<{
latitude: number;
longitude: number;
} | null>(null);
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState<Array<{ place_name: string }>>(
[]
);
// const checknavigator = navigator.permissions
// console.log(checknavigator);
/* Reverse geocoding */
/* Get User Location on page load and if granted permission by User */
useEffect(() => {
const askForLocationPermission = () => {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation((prevLocation) => ({
...prevLocation,
latitude,
longitude,
}));
},
(error) => {
// Handle location access denied or error
toast.error("Error getting location:");
console.log(error);
}
);
};
// Check if geolocation is supported by the browser
if ("geolocation" in navigator) {
// Ask for permission
navigator.permissions
.query({ name: "geolocation" })
.then((result) => {
if (result.state === "granted") {
// Permission already granted
askForLocationPermission();
} else if (result.state === "prompt") {
// Permission not yet granted, ask the user
askForLocationPermission();
} else if (result.state === "denied") {
// Permission denied, handle accordingly
toast.error("Location access denied by the user."{ duration: 1000});
}
})
.catch((error) => {
// Handle error
console.error("Error checking location permission:", error);
});
} else {
// Geolocation is not supported
toast.error("Geolocation is not supported by this browser."{ duration: 1000});
}
}, []);
// Set location and save on local storage based on User granting location permission
useEffect(() => {
if (location) {
// search for place name using mapbox API
const endpoint = `https://api.mapbox.com/geocoding/v5/mapbox.places/${location.longitude},${location.latitude}.json?proximity=-33.9249,18.4241&country=ZA&access_token=${process.env.REACT_APP_MAPBOX_TOKEN}`;
fetch(endpoint)
.then((response) => response.json())
.then((data) => {
const place = data.features[0].place_name;
localStorage.setItem("delivery_address", place);
setQuery(place);
});
}
}, [location]);
/*Forward geocoding */
/* Look for location name being queried by user */
const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
try {
setQuery(event.target.value);
const endpoint = `https://api.mapbox.com/geocoding/v5/mapbox.places/${event.target.value}.json?proximity=-33.9249,18.4241&country=ZA&access_token=${process.env.REACT_APP_MAPBOX_TOKEN}&autocomplete=true`;
const response = await fetch(endpoint);
const results = await response.json();
// console.log(results);
setSuggestions(results?.features);
// console.log(suggestions);
} catch (error: any) {
console.log("Error fetching data: " + error.message);
}
};
/* Display location selected by User */
const handleSelectAddress = (selectedAddress: string) => {
localStorage.setItem("delivery_address", selectedAddress);
setQuery(selectedAddress);
setSuggestions([]);
setIsEditing(false);
};
return (
<div className="mx-8 md:mx-12 mt-12">
<form className="max-w-6xl mx-auto ">
<div className="relative">
{isEditing ? (
<>
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<HiMapPin
aria-hidden="true"
className="w-5 h-5 text-gray-700 "
/>
</div>
<input
type="search"
className="block w-full p-4 pl-10 text-sm text-gray-900 rounded-lg bg-gray-200 outline-none"
placeholder="Enter your address"
value={query}
onChange={handleChange}
/>
</>
) : (
<div className="flex flex-col " onClick={() => setIsEditing(true)}>
<p className="">{query}</p>
<button className="px-4 py-1 mt-2 w-24 inline-flex items-center text-green-600 bg-green-200 hover:bg-green-300 border border-green-500 focus-visible:ring-2 rounded-full ">
<HiOutlinePencil
className="mr-1 -ml-1 w-4 h-4"
fill="currentColor"
/>
Edit
</button>
</div>
)}
{suggestions?.length > 0 && (
<div className="absolute bg-gray-100 w-full shadow-sm">
{suggestions.map((suggestion, index) => (
<div
key={index}
className="flex items-center justify-between w-full p-1 cursor-pointer hover:bg-gray-200"
onClick={() => handleSelectAddress(suggestion.place_name)}
>
{suggestion.place_name}
</div>
))}
</div>
)}
</div>
</form>
</div>
);
};
export default LocationSearchForm;
This code snippet is a React component for a location search form. It provides an input field where users can search for a location. The form utilizes the Mapbox Geocoding API to provide autocomplete suggestions based on the user's input.
Our endpoint supports both required and optional parameters. You can find detailed information about these parameters at mapbox signup.
For optional parameters, I have included "proximity" to specify the default search area. This allows you to search within a specific proximity. Additionally, I have localized the country for my searches. You can refer to the country codes at country codes for more information on selecting the appropriate country code.
The component uses the following state variables:
isEditing
: A boolean variable to track whether the user is editing the location input or not.
location
: A nullable object that holds the latitude and longitude of the user's current location. This information is retrieved using the Geolocation API.
query
: The current value of the location input field.
suggestions
: An array of location suggestions based on the user's input.
The component consists of two useEffect hooks:
The first useEffect
hook runs once when the component mounts and checks for Geolocation API support. If supported, it asks for the user's location permission. If granted, it sets the location
state with the latitude and longitude of the user's current location. If the permission is denied, an error message is shown.
The second useEffect
hook runs whenever the location
state changes. It makes a request to the Mapbox Geocoding API to retrieve the place name of the user's current location. The place name is then stored in local storage and set as the initial value of the location input field.
The component also includes a handleChange
function that is triggered when the user types in the location input field. This function sends a request to the Mapbox Geocoding API with the user's input to fetch autocomplete suggestions. The suggestions are stored in the suggestions state.
When the user selects a suggested address, the handleSelectAddress
function is called. It stores the selected address in local storage, sets it as the value of the location input field, clears the suggestions, and sets isEditing
to false.
The JSX code renders the location search form, including the input field, suggestions dropdown (if there are suggestions).
Overall, this component provides a user-friendly location search form with autocomplete suggestions and the ability to detect the user's current location.
Now let's also update our LocationBtn component to include the following complete code
'use client'
import { Fragment, useState } from "react";
import { HiMapPin } from "react-icons/hi2";
import { FaChevronRight } from "react-icons/fa";
import { Dialog, Transition } from "@headlessui/react";
import LocationSearchForm from "./LocationSearchForm";
const LocationBtn = () => {
const [isOpen, setIsOpen] = useState(false);
const [showChange, setShowChange] = useState(false);
const deliveryAddress = typeof window !== "undefined" && localStorage?.getItem("delivery_address");
const openModal = () => setIsOpen(true);
const closeModal = () => {
setShowChange(false);
setIsOpen(false);
};
return (
<>
<button
onClick={openModal}
className={`flex items-center px-4 py-2 bg-slate-200 rounded-full md:max-w-sm md:rounded-lg`}
>
{" "}
<HiMapPin className="shrink-0 text-green-600" />{" "}
<span className="h-2 w-2 mx-2 bg-gray-600 shrink-0 rounded-full hidden md:block ">
{" "}
</span>{" "}
<span
className={
"truncate max-w-[8rem] text-sm text-gray-500 md:max-w-[12rem]"
}
>
{deliveryAddress ? deliveryAddress : "Enter Delivery Address"}
</span>
<FaChevronRight className=" shrink-0 text-green-600" />
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
Delivery Address
</Dialog.Title>
{showChange ? (
<div className="mt-2">
<LocationSearchForm />
</div>
) : (
<div className="flex items-center mt-8 justify-between">
<div>
<p className="truncate max-w-[10rem] md:max-w-xs">
{deliveryAddress
? deliveryAddress
: "Click change..."}
</p>{" "}
</div>
<div>
{" "}
<button
className="px-4 py-1 text-slate-600 bg-green-100 hover:bg-green-200 border border-green-500 rounded-full"
onClick={() => setShowChange(true)}
>
Change
</button>
</div>
</div>
)}
<div className="mt-12 mx-12">
<button
type="submit"
className="px-4 py-1 w-full text-white bg-green-600 hover:bg-green-500 border border-green-600 rounded-full"
onClick={closeModal}
>
Done
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
);
};
export default LocationBtn;
The LocationBtn
component is a button that displays the delivery address. It includes a modal dialog that allows the user to change the address. The address is retrieved from the browser's local storage and displayed in the button. If no address is available, a placeholder text is shown. Clicking on the button opens the modal dialog where the user can either view the current address or change it using a LocationSearchForm component. After making any changes, the user can click the "Done" button to close the dialog.
Great! Now let's move on to testing. If you click on the button, you should see a map pin in your browser's address bar.
Additionally, you should be able to edit the address and select an auto-completed option.
Please note that the location permission popup may not appear on localhost. However, once you deploy your app and update the URL permissions in your Mapbox access tokens, everything should work fine.
By clicking on the cart, you will be redirected to the /cart route where you can find the LocationBtn once again. This allows you to easily change the address displayed. This user experience eliminates the need for manual address typing, providing convenience for users.
Thank you for reading through. Watch a tutorial video
Top comments (0)