Back to projects No. 001 · Web app

Cladum

I build React apps with Redux Toolkit on real scientific data, and make the source of every value visible so nothing is taken as fact without a way to verify it.

Cladum home page: species search with live GBIF figures, featured specimens, kingdoms of the tree of life and a data source key
Year
2026
Type
Web app · SPA
Role
Front-end design and development
Stack
React · Redux Toolkit · Axios · Sass · Jest

Challenge, solution and result

The challenge

Scientific data can't be made up

GBIF brings together millions of species records, but they don't always include a common name, photo or exact location. The interface couldn't fill those gaps with assumptions.

The solution

A visible source for every value

Each value is marked as Verified · GBIF, External source or No data, with a link to the exact API query.

The result

A tested, published explorer

Search for species, walk their classification from kingdom to species and check their occurrences. 105 tests, and continuous deployment that won't publish if any of them fail.

Highlights

What it does and how it works

01

Species page with a taxonomic index

The Kingdom → Species path with the GBIF identifier for each rank, nomenclature, the common name in Spanish and a gallery with credits and licenses. If GBIF has no photos, it searches iNaturalist, accepting only exact matches with open licenses.

Panthera onca page in Cladum with the taxonomic index, photo gallery and nomenclature table
02

Resilient networking

An Axios client with timeouts, caching and typed errors. Automatic retries when the rate limit is hit (429) and cancellation when leaving a view.

03

Keyboard accessible

Search as an ARIA combobox, visible focus, a live region that announces state changes, and light and dark modes with WCAG 2.2 AA contrast.

A technical decision

One failing block doesn't take down the page

A species page combines four requests: the taxon, its common name, its photos and its occurrences. If only the photos fail, everything else should still appear.

Without a taxon there's no page, so that request goes first. The others run in parallel with Promise.allSettled, and each block explains its own "no data" state. The cancellation signal travels all the way to Axios.

features/species/speciesSlice.js · simplified
export const fetchSpecies = apiThunk('species/fetch',
  async (key, { signal }) => {
    // No taxon, no page: this error does take over the full screen
    const taxon = await getSpecies(key, { signal });

    // Everything else fails independently
    const [vernacular, photos, occ] = await Promise.allSettled([
      getVernacularNames(key, { signal }),
      getTaxonPhotos(key, taxon.canonicalName, { signal }),
      searchOccurrences({ taxonKey: key, limit: 3 }, { signal }),
    ]);

    return {
      taxon,
      photos: settled(photos) ?? [],
      photosFailed: photos.status === 'rejected',
      // …
    };
  });
Next project ALCOBA