My latest pet project is Mapa Especial, a map of specialty coffee shops in São Paulo. The city's specialty coffee scene is very decentralized and hard to navigate, so I thought I'd solve this problem myself.
The website is free and open-source, built upon the foundation of Leaflet, Next.js, and React. It is completely static — you can host it for free on GitHub pages.
This article explains the major design decisions I made when building Mapa Especial — from branding to application architecture.
The idea
In much of Europe, manual coffee brewing is the primary distinguishing factor for specialty coffee shops. If a place serves Hario V60, they likely brew specialty beans. That is not the case in Brazil, where filter coffee is the nationwide standard. Even diners that use pre-ground supermarket-grade beans have a brewing cone behind the counter. Looks can be deceiving, too: businesses that advertise themselves as 'coffee shops' are often just brunch restaurants.
São Paulo has a rich specialty coffee scene, with new spots opening up every couple months. But the aforementioned cultural differences make Google Maps next to useless when you look for good coffee places. That's why I decided to launch Mapa Especial — for people like me, who want to tell 'specialty' coffee shops from the rest.
The branding
Like São Paulo itself, the color scheme had to be both urban and tropical.
- The grey background represents the everpresent concrete.
- The dark green is the colour of the palm leaves.
- The orange is the colour of the sabiá-laranjeira, the official bird of the São Paulo state.
Once I decided on the color scheme, I became interested in further exploring the avian theme. Sabiás are very cute, and can make for a good mascot.
It soon became clear that the bird's shape vaguely resembles the famous piso paulista — the trademark pavement pattern of the São Paulo city.
All that's left is to give one of these birds a coffee cup — and we have the branding!
The logo can shrink and grow:
It neatly fits on business cards and stickers alike:
The mapping engine
The Leaflet mapping engine was the obvious choice: few libraries beat it in terms of functionality, and none in terms of the available help content. The companion react-leaflet library, however, changes the API enough to warrant a comprehensive documentation of its own — which it currently lacks. Many questions I had could only be answered by looking at other open-source projects or StackOverflow.
The compatibility between the recent versions of nextjs
and react-leaflet
is not perfect, either. To reliably program buttons that overlay the map, I had to define a GenericLeafletControl
that references native leaflet
methods.
The leaflet-active-area plugin helps me shrink and expand the active area of the map. Since there are two modal windows that overlay the map, sometimes it is necessary to adjust the active area to account for the space that these windows take up.
Data storage
Leaflet natively supports the GeoJSON data format for marker placement. The react-leaflet
compatibility layer even offers a <GeoJSON />
component to streamline the process. That said, I simply couldn't get it to work. I suspect this is another nextjs
compatibility issue.
For the sake of simplicity, I kept the GeoJSON data format. It seems to be more or less common, and I didn't want to reinvent the wheel. I did, however, implement custom logic that parses this data and generates markers as necessary.
Marker data is spread across two files: cafes.json
and shops.json
. The LeafletLayer
component creates a Leaflet map layer for each. Since there are many roasters that sell beans to multiple coffee shops, roaster data is stored separately (roasters.json
). A future update will, hopefully, include a separate page with the list of confirmed specialty roasters in the area.
State management and filtering
The application's main UI includes two modal windows: the filter window and the active item window. When you first open the map, the application loads their default states from JSON files (defaultFilterState.json
and defaultDetailState.json
). In this configuration, the filter form is empty and the active item window is hidden.
Filtering is simple. When the filter changes, React re-renders the marker layers. As these layers initialize, they only load data for establishments that match the filter query. Since all the data is stored on the client, the changes are instant and do not slow the app down.
Fun fact: The application refreshes the map the moment you change filter settings. However, this isn't apparent to smartphone users: in the mobile version, the filter window obscures the map. To improve the website's UX I added an 'apply filter' button to the mobile interface. The button does nothing but has a positive psychological effect.
The state of the active item window is identical in structure to a data entry from cafes.json
or shops.json
. When you click the "view more" button on a marker, the application populates the window with that marker's data. When you close the detail window, its state is reset.
Other fun discoveries
⦿ The react-select library does not do SSR and it took me an embarrassingly long time to figure out why it keeps throwing errors when I run the dev server.
⦿ Want to display the rating of a place on Google Maps but don't want to pay for the place card API? Just crop the iframe:
What's Next?
The website is functional, but there's always room for improvement. If you'd like to contribute to the project, visit the GitHub page and open a new PR or issue.