Datos científicos que no se pueden inventar
GBIF reúne millones de registros de especies, pero no siempre trae nombre común, foto o ubicación exacta. La interfaz no podía rellenar esos huecos con suposiciones.
Construyo apps en React con Redux Toolkit sobre datos científicos reales, y hago visible de dónde sale cada dato para que nada se dé por cierto sin poder verificarlo.
GBIF reúne millones de registros de especies, pero no siempre trae nombre común, foto o ubicación exacta. La interfaz no podía rellenar esos huecos con suposiciones.
Cada valor se marca como Verificado · GBIF, Fuente externa o Sin dato, con un enlace a la consulta exacta de la API.
Busca especies, recorre su clasificación de reino a especie y consulta sus ocurrencias. 105 pruebas y despliegue continuo que no publica si alguna falla.
La ruta Reino → Especie con el identificador GBIF de cada rango, nomenclatura, nombre común en español y una galería con autoría y licencia. Si GBIF no tiene fotos, se buscan en iNaturalist solo con coincidencia exacta y licencia abierta.
Cliente Axios con tiempo máximo, caché y errores tipados. Reintento automático ante el límite de consultas (429) y cancelación al salir de una vista.
Buscador como combobox ARIA, foco visible, región viva que anuncia los cambios de estado y modo claro y oscuro con contraste WCAG 2.2 AA.
Una ficha junta cuatro consultas: el taxón, su nombre común, sus fotos y sus ocurrencias. Si solo falla la de fotos, el resto debe verse igual.
Sin taxón no hay ficha, así que esa consulta va primero. Las demás corren en paralelo con Promise.allSettled y cada bloque explica su propio «sin dato». La señal de cancelación viaja hasta Axios.
export const fetchSpecies = apiThunk('species/fetch',
async (key, { signal }) => {
// Sin taxón no hay ficha: este error sí es de pantalla completa
const taxon = await getSpecies(key, { signal });
// El resto falla por separado
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',
// …
};
});