Add IUCN Red List data to species information display. Includes API integration and UI updates for displaying conservation status, threats, habitats, and conservation measures.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e931b5ab-041b-42e7-baf1-50017869cef6
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/e19c6a51-7e4c-4bb8-a6a6-46dc00f0ec99/a6c5cde2-daf7-441d-bc13-69c14cf46a79.jpg
This commit is contained in:
Magnus-SmariSma
2025-03-20 22:24:19 +00:00
parent abb3d9c830
commit 71376edd20
3 changed files with 367 additions and 2 deletions

View File

@ -1,6 +1,16 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { apiClient, CITES_API_ENDPOINTS, CitesLegislation, Distribution } from "@/lib/api"; import {
apiClient,
CITES_API_ENDPOINTS,
IUCN_API_ENDPOINTS,
CitesLegislation,
Distribution,
IucnSpeciesResponse,
IucnThreatsResponse,
IucnHabitatsResponse,
IucnMeasuresResponse
} from "@/lib/api";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -57,6 +67,47 @@ export default function ResultsContainer({
}, },
enabled: !!currentSpecies?.id, enabled: !!currentSpecies?.id,
}); });
// IUCN API Queries
const { data: iucnData, isLoading: isLoadingIucnSpecies } = useQuery({
queryKey: ['/api/iucn/species', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnSpeciesByName(currentSpecies.full_name);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const { data: iucnThreats, isLoading: isLoadingIucnThreats } = useQuery({
queryKey: ['/api/iucn/threats', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnThreats(currentSpecies.full_name);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const { data: iucnHabitats, isLoading: isLoadingIucnHabitats } = useQuery({
queryKey: ['/api/iucn/habitats', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnHabitats(currentSpecies.full_name);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const { data: iucnMeasures, isLoading: isLoadingIucnMeasures } = useQuery({
queryKey: ['/api/iucn/measures', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnMeasures(currentSpecies.full_name);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const saveSpeciesMutation = useMutation({ const saveSpeciesMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {

View File

@ -9,6 +9,10 @@ interface SpeciesTabsProps {
citesLegislation: any; citesLegislation: any;
distributions: any; distributions: any;
references: any; references: any;
iucnData: any;
iucnThreats: any;
iucnHabitats: any;
iucnMeasures: any;
apiResponse: string; apiResponse: string;
isLoading: boolean; isLoading: boolean;
} }
@ -18,6 +22,10 @@ export default function SpeciesTabs({
citesLegislation, citesLegislation,
distributions, distributions,
references, references,
iucnData,
iucnThreats,
iucnHabitats,
iucnMeasures,
apiResponse, apiResponse,
isLoading isLoading
}: SpeciesTabsProps) { }: SpeciesTabsProps) {
@ -56,6 +64,7 @@ export default function SpeciesTabs({
<TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="legislation">CITES Legislation</TabsTrigger> <TabsTrigger value="legislation">CITES Legislation</TabsTrigger>
<TabsTrigger value="distribution">Distribution</TabsTrigger> <TabsTrigger value="distribution">Distribution</TabsTrigger>
<TabsTrigger value="conservation">Conservation Status</TabsTrigger>
<TabsTrigger value="references">References</TabsTrigger> <TabsTrigger value="references">References</TabsTrigger>
<TabsTrigger value="api-response">API Response</TabsTrigger> <TabsTrigger value="api-response">API Response</TabsTrigger>
</TabsList> </TabsList>
@ -266,6 +275,125 @@ export default function SpeciesTabs({
</div> </div>
</TabsContent> </TabsContent>
{/* Conservation Status Tab */}
<TabsContent value="conservation" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">IUCN Red List Status</h3>
{iucnData?.result && iucnData.result.length > 0 ? (
<div className="space-y-2">
<div className="flex items-center">
<div className={`h-4 w-4 rounded-full mr-2 ${
iucnData.result[0].category === 'EX' ? 'bg-black' :
iucnData.result[0].category === 'EW' ? 'bg-purple-800' :
iucnData.result[0].category === 'CR' ? 'bg-red-600' :
iucnData.result[0].category === 'EN' ? 'bg-orange-500' :
iucnData.result[0].category === 'VU' ? 'bg-yellow-400' :
iucnData.result[0].category === 'NT' ? 'bg-yellow-200' :
iucnData.result[0].category === 'LC' ? 'bg-green-500' :
'bg-gray-400'
}`}></div>
<span className="text-sm font-medium">
{iucnData.result[0].category === 'EX' ? 'Extinct' :
iucnData.result[0].category === 'EW' ? 'Extinct in the Wild' :
iucnData.result[0].category === 'CR' ? 'Critically Endangered' :
iucnData.result[0].category === 'EN' ? 'Endangered' :
iucnData.result[0].category === 'VU' ? 'Vulnerable' :
iucnData.result[0].category === 'NT' ? 'Near Threatened' :
iucnData.result[0].category === 'LC' ? 'Least Concern' :
iucnData.result[0].category === 'DD' ? 'Data Deficient' :
iucnData.result[0].category === 'NE' ? 'Not Evaluated' :
iucnData.result[0].category}
</span>
</div>
<p className="text-sm">Assessed: {iucnData.result[0].assessment_date}</p>
<p className="text-sm">Criteria: {iucnData.result[0].criteria || 'Not specified'}</p>
<p className="text-sm">
Population trend: {
iucnData.result[0].population_trend === 'decreasing' ? 'Decreasing ↓' :
iucnData.result[0].population_trend === 'increasing' ? 'Increasing ↑' :
iucnData.result[0].population_trend === 'stable' ? 'Stable →' :
'Unknown'
}
</p>
</div>
) : (
<p className="text-sm">No IUCN Red List data available for this species.</p>
)}
</div>
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">Habitat Types</h3>
{iucnHabitats?.result && iucnHabitats.result.length > 0 ? (
<ul className="space-y-1 text-sm">
{iucnHabitats.result.map((habitat: any, index: number) => (
<li key={index} className="flex items-start">
<span className="text-primary mr-2"></span>
<span>
{habitat.habitat}
{habitat.suitability && ` (${habitat.suitability})`}
{habitat.majorimportance === 'Yes' && ' - Major importance'}
</span>
</li>
))}
</ul>
) : (
<p className="text-sm">No habitat information available.</p>
)}
</div>
</div>
<div className="border rounded p-4 mb-4">
<h3 className="font-semibold text-gray-700 mb-2">Threats</h3>
{iucnThreats?.result && iucnThreats.result.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Threat</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Timing</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Scope</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Severity</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Score</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{iucnThreats.result.map((threat: any, index: number) => (
<tr key={index}>
<td className="px-3 py-2">{threat.title}</td>
<td className="px-3 py-2">{threat.timing || 'Unknown'}</td>
<td className="px-3 py-2">{threat.scope || 'Unknown'}</td>
<td className="px-3 py-2">{threat.severity || 'Unknown'}</td>
<td className="px-3 py-2">{threat.score || 'Unknown'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-sm">No threat information available.</p>
)}
</div>
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">Conservation Measures</h3>
{iucnMeasures?.result && iucnMeasures.result.length > 0 ? (
<ul className="space-y-1 text-sm">
{iucnMeasures.result.map((measure: any, index: number) => (
<li key={index} className="flex items-start">
<span className="text-primary mr-2"></span>
<span>
{measure.title}
{measure.year ? ` (${measure.year})` : ''}
</span>
</li>
))}
</ul>
) : (
<p className="text-sm">No conservation measures information available.</p>
)}
</div>
</TabsContent>
{/* References Tab */} {/* References Tab */}
<TabsContent value="references" className="space-y-4"> <TabsContent value="references" className="space-y-4">
<div className="border rounded p-4"> <div className="border rounded p-4">

View File

@ -1,4 +1,4 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http"; import { createServer, type Server } from "http";
import { storage } from "./storage"; import { storage } from "./storage";
import axios from "axios"; import axios from "axios";
@ -7,6 +7,7 @@ import { ZodError } from "zod";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
const CITES_BASE_URL = "https://api.speciesplus.net/api/v1"; const CITES_BASE_URL = "https://api.speciesplus.net/api/v1";
const IUCN_BASE_URL = "https://apiv3.iucnredlist.org/api/v3";
export async function registerRoutes(app: Express): Promise<Server> { export async function registerRoutes(app: Express): Promise<Server> {
// API Token routes // API Token routes
@ -212,6 +213,191 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
// IUCN Red List API routes
app.get("/api/iucn/species", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
const apiKey = process.env.IUCN_API_KEY;
if (!apiKey) {
return res.status(401).json({
success: false,
message: "IUCN API key is not configured"
});
}
try {
const response = await axios.get(`${IUCN_BASE_URL}/species/name/${name}`, {
params: { token: apiKey }
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from IUCN Red List API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN species data"
});
}
});
app.get("/api/iucn/threats", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
const apiKey = process.env.IUCN_API_KEY;
if (!apiKey) {
return res.status(401).json({
success: false,
message: "IUCN API key is not configured"
});
}
try {
const response = await axios.get(`${IUCN_BASE_URL}/threats/species/name/${name}`, {
params: { token: apiKey }
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from IUCN Red List API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN threats data"
});
}
});
app.get("/api/iucn/habitats", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
const apiKey = process.env.IUCN_API_KEY;
if (!apiKey) {
return res.status(401).json({
success: false,
message: "IUCN API key is not configured"
});
}
try {
const response = await axios.get(`${IUCN_BASE_URL}/habitats/species/name/${name}`, {
params: { token: apiKey }
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from IUCN Red List API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN habitats data"
});
}
});
app.get("/api/iucn/measures", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
const apiKey = process.env.IUCN_API_KEY;
if (!apiKey) {
return res.status(401).json({
success: false,
message: "IUCN API key is not configured"
});
}
try {
const response = await axios.get(`${IUCN_BASE_URL}/measures/species/name/${name}`, {
params: { token: apiKey }
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from IUCN Red List API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN conservation measures data"
});
}
});
const httpServer = createServer(app); const httpServer = createServer(app);
return httpServer; return httpServer;
} }