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.
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.
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.
Each value is marked as Verified · GBIF, External source or No data, with a link to the exact API query.
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.
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.
An Axios client with timeouts, caching and typed errors. Automatic retries when the rate limit is hit (429) and cancellation when leaving a view.
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 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.
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',
// …
};
});