I have recently worked on an open source map, which served well to represent points of interest from an external source. While using it, I have come to the conclusion that it can receive some updates.
This is an extension of the existing project, so I will just link the article for prerequisites and any other thoughts:
Do not load default data
While using it, I noticed that the map behaves differently because of the demo data being loaded at first render and then updated with what comes from the provided API endpoint. Therefore, the first change we will do is to create a fallback page for when we don’t have an ?api=
parameter specified and then tweak the api hook.
In Map.tsx
we will add the following code just before the return:
if (!URLParams.api) {
return (
<div className="p-4 text-center">
It appears you have not specified an <strong>?api=</strong> param. You
can use the default meanwhile:
<div className="pt-4">
<a href="?api=/cities_in_romania.json">cities_in_romania.json</a>
</div>
</div>
);
}
In the ./hooks/useGetPOI.ts
we will do nothing if we don’t have a value for the url and return the demo data if its value is provided:
useEffect(() => {
// No url was provided
if (!url) {
return;
}
// Load the default data
if (url === "/cities_in_romania.json") {
setData(mockData);
return;
}
...
Improve assets structure
For an improved assets structure, we will move the icons to their own folder and rename the images folder to marker
as it only contains the marker images:

Envelope the entries to add metadata
The map currently shows the points of interest, but they are out of context. It would be nice if it had the capacity to show some metadata, whenever the API provides it. We will update the data for that and also create a new component in the project: a button tooltip.
The data structure
We will keep the existing format, but we will also add a case for enveloped data. The approach will be the following:
interface APIEnvelope<T> {
metadata: string | string[];
records: {
latitude: number;
longitude: number;
title: string;
description?: string | string[];
};
}
Changing the hook
The envelope we prefer will be {metadata:"", records: []}
which means that some changes will also happen in the api hook and the Map.tsx
component. One bigger change mainly for code readability is that we will change the data
related variable and function names to records
:
data
will becomerecords
;setData()
will becomesetRecords()
;filteredData
will becomefilteredRecords
.
{
"metadata": "These are a few of the beautiful cities of Romania!",
"records": [
{ "title": "Bucharest", "latitude": 44.4268, "longitude": 26.1025 },
{ "title": "Timisoara", "latitude": 45.7489, "longitude": 21.2087 }
]
}
The tooltip
To show the metadata when available, we will create tooltip which will be shown on a new info button:
import { FunctionComponent, HTMLProps, PropsWithChildren } from "react";
interface TooltipProps extends HTMLProps<HTMLButtonElement> {
text: string | string[];
}
const Tooltip: FunctionComponent<PropsWithChildren<TooltipProps>> = ({
className,
children,
text,
}) => {
return (
<div className="relative inline-block group duration-300">
{children}
<div
className={`tooltip hidden group-hover:block absolute -top-2 -right-3 translate-x-full bg-gray-800 text-white text-sm py-1 px-2 opacity-90 rounded-md cursor-pointer before:content-[''] before:absolute before:top-1/2 before:right-[100%] before:-translate-y-1/2 before:border-8 before:border-y-transparent before:border-l-transparent before:border-r-gray-700 ${className}`}
>
{!Array.isArray(text)
? text
: text.map((textItem, key) => (
<div className="mb-2" key={key}>
{textItem}
</div>
))}
</div>
</div>
);
};
export default Tooltip;
More info:
The new button
In Map.tsx
, below the last button wrapper we will add:
{!!metadata && (
<ButtonWrapper className="absolute left-[10px] top-[120px]">
<Tooltip text={metadata} className="text-left w-[240px] ">
<MapButton>
<IconInfoSVG width={15} height={15} />
</MapButton>
</Tooltip>
</ButtonWrapper>
)}
Now, when hovering over the information button, we should see a tooltip opening to the right. 😸
Prettier to handle the order of tailwind classes
We will use prettier to auto arrange the tailwind classes in a predefined order in Visual Studio Code. I think this is a step that needs to be done in most of the projects using tailwind.
We already have eslint in the project, so we will only do some tweaks there.
npm install --save-dev eslint prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
npm i --save-dev prettier-plugin-tailwindcss
.eslintrc.cjs
will be updated to:
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
"plugin:jsx-a11y/recommended",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier"
],
settings: {
"react": {
"version": "detect"
},
"import/resolver": {
"node": {
"paths": [
"src"
],
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
}
},
rules: {
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": true,
"argsIgnorePattern": "^_"
}
],
"react/react-in-jsx-scope": "off",
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/click-events-have-key-events": "off"
},
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh']
}
.prettierrc
will become:
{
"trailingComma": "all",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 80,
"bracketSpacing": true,
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
More info: