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:
@ -1,6 +1,16 @@
|
||||
import { useState, useEffect } from "react";
|
||||
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 { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -57,6 +67,47 @@ export default function ResultsContainer({
|
||||
},
|
||||
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({
|
||||
mutationFn: async () => {
|
||||
|
@ -9,6 +9,10 @@ interface SpeciesTabsProps {
|
||||
citesLegislation: any;
|
||||
distributions: any;
|
||||
references: any;
|
||||
iucnData: any;
|
||||
iucnThreats: any;
|
||||
iucnHabitats: any;
|
||||
iucnMeasures: any;
|
||||
apiResponse: string;
|
||||
isLoading: boolean;
|
||||
}
|
||||
@ -18,6 +22,10 @@ export default function SpeciesTabs({
|
||||
citesLegislation,
|
||||
distributions,
|
||||
references,
|
||||
iucnData,
|
||||
iucnThreats,
|
||||
iucnHabitats,
|
||||
iucnMeasures,
|
||||
apiResponse,
|
||||
isLoading
|
||||
}: SpeciesTabsProps) {
|
||||
@ -56,6 +64,7 @@ export default function SpeciesTabs({
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="legislation">CITES Legislation</TabsTrigger>
|
||||
<TabsTrigger value="distribution">Distribution</TabsTrigger>
|
||||
<TabsTrigger value="conservation">Conservation Status</TabsTrigger>
|
||||
<TabsTrigger value="references">References</TabsTrigger>
|
||||
<TabsTrigger value="api-response">API Response</TabsTrigger>
|
||||
</TabsList>
|
||||
@ -266,6 +275,125 @@ export default function SpeciesTabs({
|
||||
</div>
|
||||
</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 */}
|
||||
<TabsContent value="references" className="space-y-4">
|
||||
<div className="border rounded p-4">
|
||||
|
188
server/routes.ts
188
server/routes.ts
@ -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 { storage } from "./storage";
|
||||
import axios from "axios";
|
||||
@ -7,6 +7,7 @@ import { ZodError } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
|
||||
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> {
|
||||
// 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);
|
||||
return httpServer;
|
||||
}
|
||||
|
Reference in New Issue
Block a user