Creating a list of climbing gyms with React + Material-UI

Rumen Manev
7 min readMay 22, 2020

So I’ve been trying to learn coding on and off over the years, which basically means I’ve done a dozen online courses and learned a bunch of things in theory, but never in practice.

That’s the problem with programming courses — they teach you some basic operations, they show you how to do some loops and if statements and that’s it. Now go build a web app.

Anyway, some time ago I built this map of indoor climbing gyms, using vanilla JavaScript and Leaflet.js, which was really fun, so I decided to do a somewhat similar take on this idea, but also very different.

I decided to lose the map and instead focus on creating a detailed list of indoor climbing gyms that people can filter through, depending on which city they’re in right now. The use case I imagined is something I’ve experienced in the past.

Whenever I’m in a new city for a longer period of time I search for the indoor climbing gyms in that area to decide where I want to boulder at. Of course, I’m doing that with Google Maps, which does a good enough job, so why would someone use this platform instead, I hear you ask. Shut up.

My main goal with this project was to practice React. I’ve used it before to build my own personal website and also to help out LeaveMeAlove a bit with their admin panel, so I was eager to get better at it.

As usual, everything starts with Create-React-App

npx create-react-app my-app
cd my-app
npm start

Next, I install React Router for creating paths to the different pages.

npm install react-router-dom

For some extra swag, I install React Spring, to create a fade-in effect of my content when loading the page.

npm install react-spring

React Spring is a very cool library that you can use to create really awesome animations, but I’m basically using it as an accountant would use a 16-inch MacBook Pro:

const Gyms = () => {const fade = useSpring({opacity: 1, from: {opacity: 0}, delay: 400})return (<animated.div style={fade}><h1>Hello World!</h1></animated.div>);}

Looking at my package.json file, I’ve installed quite a few other packages (that I don’t really need), while trying to visualize a map, but we’ll get to that in a bit.

Choosing a framework

Time to decide what CSS framework I should use for this.

Bootstrap is documented excellently, but it’s pretty basic and I want to learn something new.

Bulma is a good contender, but the documentation is a bit weird and I’m not feeling it — maybe I’ll use it for the next project.

Tailwind looks very promising! It’s new, it’s sexy and I heard the creator, Adam Wathan talk about it in a podcast recently. However, opening up a Smashing Magazine article about it, I saw this:

“When you need to get started with a mini-project that has a very short deadline (especially something a few users would be using or only yourself), then Tailwind CSS is not the best option.”

Fair enough, maybe that’s not the right tool for my purpose. But I’ll definitely try it out in the next project!

Wait a minute, I’ve used Materialize in the past and I really liked both the ease-of-use and style of it. I’m sure someone already translated it to React components. Yes, they did and it’s called Material-UI. OK, let’s go with that.

npm install @material-ui/core

Straight away I use the Material-UI Grid and Paper components to create the list of gyms. I’m also adding GridListTile and GridListTileBar to add a short description of the gyms inside their own info card. I’m not adding screenshots of it, both because this is pretty basic stuff and because I’m embarrassed about my patchwork code. But hey, it works.

Search functionality

The first interesting challenge I bumped into was creating a search field above the list of gyms. The idea was to filter the list by name of the gym, city, address, or any other information available. The most logical I could think of was to search by the city, but why not have all options, eh? When I think about it, the field should probably be called Filter and not Search, since that’s what it really does.

Here’s how it looks like.

...
import data from ‘./data’;

const SearchGymList = () => {
const [searchText, setSearchText] = useState("");const excludeColumns = ["img", "email"];const handleChange = value => {setSearchText(value);filterData(value);};const filterData = (value) => {const lowercasedValue = value.toLowerCase().trim();if (lowercasedValue === "") setData(tileData);else {const filteredData = tileData.filter(item => {return Object.keys(item).some(key =>excludeColumns.includes(key) ? false : item[key].toString().toLowerCase().includes(lowercasedValue));});setData(filteredData);}}return (<TextFieldid="filled-basic"label="Search for your city"variant="filled"value={searchText}onChange={e => handleChange(e.target.value)}/>
...

The actual gym information is in a separate JSON file and accessed through an import. I’m creating a local state, which will allow me to use whatever the user has typed into the Search field.

I’m creating an array to exclude the items in the JSON file that I don’t want to be included in the search — “img” and “email”.

The handleChange function is used later in the Search form to accept the value the user types in.

The main part is played by the high-order function filter(). The rest was kind of tweaking things until it worked. Pardon my inability to explain my code in theory. Remember — this is not my trade.

I’m sure this is something basic for a professional web developer, but just seeing this filter work made me enormously happy.

Dynamic pages

My next goal was to create dynamic pages of each climbing gym that users could go through by clicking on an Info icon on any card from the list. Remember I installed React Router, so that’s what I’m using to create my paths. This is what my App.js looks like.

function App() {return (<Router><div className=”App”><Nav /><Switch><Route path=’/’ exact component={Home} /><Route path=’/about’ component={About} /><Route path=’/:id’ component={GymDetails} /></Switch><Footer /></div></Router>);}

The “:id” in the 3rd path is what creates the dynamic pages. But that’s not everything. To complete the dynamic path, inside the “GymDetails” component I added a “match” props.

const GymDetails = ({match}) => {const [data] = useState(tileData);const currentGym = data.filter(gym => gym.title === match.params.id)return (<animated.div style={fade}><div className='gymInfo'><img className='gymImg' src={currentGym[0].img} alt={currentGym[0].title} /><h1>{match.params.id}</h1><p>{currentGym[0].address}</p><p><a href={currentGym[0].website}>{currentGym[0].website}</a></p><p>{currentGym[0].email}</p></div></animated.div>);}

This “match” allows you to pass the id of the item you’ve clicked on from the gym list on the homepage. However, the “match” only gives you the id and some other params, which don’t serve my purpose — I want to display the gym details from my JSON file, such as an image, address, contact details, etc.

So to do that, I created another variable called “currentGym” and used filter() again to compare the gym name with the id I get through the match props.

Mind you, this is not something I saw from someone else, so I’m really not sure it’s the appropriate way to do it (if you know, please do tell). But, again, it works. And it made me really happy to see it work.

Adding a map

At the time of writing this, I still haven’t figured out how to add a dynamic map next to the gym details inside the individual gym pages. That means, to display a marker inside a map window, depending on the gym address from the JSON file.

The problem is this. It’s easy to do the above if you have the coordinates of the gyms (latitude and longitude). However, I only have the address. Translating an address into geographic coordinates is what’s known as geocoding. Google Map’s API does this, but it’s paid. There are a few other APIs that do this. But they’re paid.

I’m not exaggerating when I say I went through a multi-day-long rabbit hole of searching all kinds of map frameworks and libraries, mainly around the OpenStreetMap ecosystem. The closest I’ve gotten to figuring it out is a thing called Nominatim, which supposedly does exactly what I’m looking for, but I still haven’t figured out how to make it work.

In theory, I need to fetch coordinates from Nominatim of an address, that I dynamically load from each of my JSON entries.

After fetching, I will get another JSON with geo-information. From it, I’ll need to get an array of a thing called “bbox”, which is basically a set of 4 coordinates that determine a location in OpenStreetMap.

Then, I need to turn this “bbox” array into a string and pass it into the local state. After that, I have to add that state to an iFrame with an OpenStreetMap URL, which will then display the correct location on the page.

I must say, typing this all down really clears things up.

Future plan

I don’t really have a fixed roadmap for this project. The idea was to try out specific tools with something I’m interested in. That tends to make things more exciting when you finally see something work. I guess it’ll be good to display more dynamic information about climbing gyms. Maybe add a booking functionality.

I’m really curious about Node.js and using a database like MongoDB (MERN stack anyone?), so maybe I’ll try and move the local JSON file into MongoDB Atlas and create a REST API to fetch the data from there (using Node and Express).

Anyway, sorry to all the actual devs, who are probably cringing really hard reading all this. There might be a small chance, someone else is building something similar and would find some of the information here useful. I hope I’ve saved you at least a few hours of going through obscure libraries.

If you are building something similar and want to share or have some advice on how I should do things in the future, do let me know in the comments!

Thank you for reading!

--

--