Extend the Leaflet map project

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 become records;
  • setData() will become setRecords();
  • filteredData will become filteredRecords.
{
  "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: