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 { 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 () => {
|
||||||
|
@ -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">
|
||||||
|
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 { 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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user