From 8197652ddc882aa0717d4b71709496bfa3c2fb58 Mon Sep 17 00:00:00 2001 From: Magnus Smari Smarason Date: Thu, 27 Mar 2025 11:15:12 +0000 Subject: [PATCH] Add remaining components: Implement tab-based species UI, CRUD operations, and supporting hooks for CITES and timeline events --- development.md | 250 ++++ package-lock.json | 23 +- package.json | 4 +- src/components/species-report.tsx | 1 - src/components/species-tabs.tsx | 1208 +------------------ src/components/species/SpeciesImage.tsx | 80 ++ src/components/species/tabs/CitesTab.tsx | 257 ++++ src/components/species/tabs/IucnTab.tsx | 120 ++ src/components/species/tabs/OverviewTab.tsx | 115 ++ src/components/species/tabs/TimelineTab.tsx | 241 ++++ src/hooks/useCitesListingCrud.ts | 173 +++ src/hooks/useTimelineEventCrud.ts | 192 +++ src/lib/api.ts | 372 +++++- src/lib/trade-validation.ts | 59 + 14 files changed, 1926 insertions(+), 1169 deletions(-) create mode 100644 development.md create mode 100644 src/components/species/SpeciesImage.tsx create mode 100644 src/components/species/tabs/CitesTab.tsx create mode 100644 src/components/species/tabs/IucnTab.tsx create mode 100644 src/components/species/tabs/OverviewTab.tsx create mode 100644 src/components/species/tabs/TimelineTab.tsx create mode 100644 src/hooks/useCitesListingCrud.ts create mode 100644 src/hooks/useTimelineEventCrud.ts create mode 100644 src/lib/trade-validation.ts diff --git a/development.md b/development.md new file mode 100644 index 0000000..5c49f80 --- /dev/null +++ b/development.md @@ -0,0 +1,250 @@ +# Arctic Species Tracker - Development Guide + +## Tech Stack + +### Frontend +- **React 18** with TypeScript for type safety and better developer experience +- **Vite** for fast development and optimized builds +- **TanStack Query** (React Query) for efficient data fetching and caching +- **Tailwind CSS** with ShadcnUI for modern, responsive design +- **Lucide Icons** for consistent iconography +- **jsPDF** for PDF report generation +- **Recharts** for data visualizations + +### Backend +- **Supabase** for: + - PostgreSQL database + - Authentication + - Real-time subscriptions + - Row Level Security (RLS) + - API management + +### Data Sources Integration +- IUCN Red List API +- CITES Species+ API +- CITES Trade Database +- iNaturalist API for species images + +## Development Process + +### Phase 1: Setup and Infrastructure (2 weeks) +1. Project initialization and repository setup +2. Development environment configuration +3. Supabase project setup and database schema design +4. CI/CD pipeline with GitHub Actions +5. Development of core components and utilities + +### Phase 2: Data Integration (3 weeks) +1. Database schema implementation +2. API integration with IUCN Red List +3. Integration with CITES Species+ API +4. CITES Trade Database connection +5. iNaturalist API integration for species images +6. Data transformation and normalization + +### Phase 3: Core Features (4 weeks) +1. Species search functionality +2. Species details view +3. CITES listings display +4. Trade data visualization +5. Timeline implementation +6. Error handling and loading states + +### Phase 4: Enhanced Features (3 weeks) +1. PDF report generation +2. Data visualization improvements +3. Advanced filtering +4. Performance optimization +5. Mobile responsiveness +6. Accessibility improvements + +### Phase 5: Testing and Refinement (2 weeks) +1. Unit testing +2. Integration testing +3. Performance testing +4. User acceptance testing +5. Bug fixes and optimizations + +## Development Timeline + +Total Development Time: 14 weeks + +```mermaid +gantt + title Arctic Species Tracker Development Timeline + dateFormat YYYY-MM-DD + section Setup + Project Setup :2024-01-01, 1w + Infrastructure :2024-01-08, 1w + section Data + Database Schema :2024-01-15, 1w + API Integration :2024-01-22, 2w + section Core + Basic Features :2024-02-05, 2w + Advanced Features :2024-02-19, 2w + section Enhancement + PDF Reports :2024-03-04, 1w + Visualizations :2024-03-11, 2w + section Testing + Testing & QA :2024-03-25, 2w +``` + +## Development Team Requirements + +### Core Team +- 1 Project Manager +- 2 Frontend Developers +- 1 Backend Developer +- 1 UI/UX Designer +- 1 QA Engineer + +### Skills Required +- React/TypeScript expertise +- PostgreSQL database design +- REST API integration +- Data visualization +- UI/UX design +- Testing methodologies + +## Deployment Strategy + +### Development Environment +- Local development using Vite +- Supabase local development +- GitHub for version control + +### Staging Environment +- Vercel preview deployments +- Supabase staging project +- Automated testing + +### Production Environment +- GitHub Pages for frontend hosting +- Supabase production instance +- Continuous deployment + +## Monitoring and Maintenance + +### Performance Monitoring +- Vercel Analytics +- Supabase Dashboard +- Error tracking with Sentry + +### Regular Maintenance +- Weekly dependency updates +- Monthly security audits +- Quarterly performance reviews + +## Future Enhancements + +### Planned Features +1. Advanced search filters +2. Batch report generation +3. Data export options +4. User accounts and preferences +5. Interactive maps +6. Real-time notifications + +### CRUD Operations Improvements +1. **Enhanced IUCN Assessment Management** + - Implement full CRUD interface for IUCN assessments + - Add validation for assessment data + - Create hooks similar to `useCitesListingCrud` for IUCN assessments + - Implement optimistic updates for better UX + +2. **Subpopulation Management** + - Create dedicated components for subpopulation CRUD + - Add geospatial data support for Arctic regions + - Implement region-specific conservation status tracking + - Add visualization for subpopulation distribution + +3. **Common Names Management** + - Build multilingual support for common names + - Add language/region tagging for names + - Implement search by any common name + - Create bulk import/export functionality + +4. **Bulk Operations** + - Develop batch processing system for multiple records + - Create CSV import/export functionality + - Implement transaction-based operations for data integrity + - Add progress tracking for bulk operations + +### Trade Data Filtering Improvements +1. **Advanced Filtering System** + - Refactor `useTradeRecordFilters` to support multiple filter types + - Implement filter composition with AND/OR logic + - Create persistent filter state with URL parameters + - Add filter presets and user-saved filters + +2. **Geographic and Quantity Filtering** + - Add geospatial filtering by country/region + - Implement trade route visualization + - Create quantity-based threshold filters + - Add time-series analysis for quantity changes + +3. **Filter UI Enhancements** + - Design multi-select filter components + - Implement date range picker for temporal filtering + - Create visual filter builder interface + - Add filter state visualization + +### Timeline Enhancement Implementation +1. **Conservation Status Change Indicators** + - Add visual flags to timeline for status changes + - Implement correlation analysis between status changes and trade + - Create hover tooltips with detailed change information + - Add filtering by change significance + +2. **Cause-Effect Analysis** + - Develop algorithms to detect potential correlations + - Implement visual connections between related events + - Create statistical analysis of trade pattern changes + - Add predictive indicators based on historical patterns + +3. **Timeline Visualization Improvements** + - Implement side-by-side species comparison + - Add zooming and scaling for timeline view + - Create custom event categories and color coding + - Implement timeline export and sharing + +### Technical Improvements +1. Bundle size optimization +2. Server-side rendering +3. Progressive Web App (PWA) +4. Automated data updates +5. Enhanced caching strategies + +## Development Best Practices + +### Code Quality +- TypeScript for type safety +- ESLint for code linting +- Prettier for code formatting +- Husky for pre-commit hooks + +### Documentation +- Comprehensive README +- API documentation +- Component documentation +- Development guides + +### Testing +- Jest for unit testing +- React Testing Library +- E2E testing with Cypress +- Regular performance audits + +## Security Considerations + +### Data Protection +- API key management +- Rate limiting +- Data encryption +- Regular security audits + +### Compliance +- GDPR compliance +- Data retention policies +- Attribution requirements +- API usage agreements diff --git a/package-lock.json b/package-lock.json index 8a2cf78..548c237 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@supabase/supabase-js": "^2.39.7", "@tanstack/react-query": "^5.24.1", "@tanstack/react-query-devtools": "^5.24.1", + "@types/uuid": "^10.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.3.1", @@ -29,7 +30,8 @@ "react-router-dom": "^6.22.1", "recharts": "^2.12.2", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "uuid": "^11.1.0" }, "devDependencies": { "@types/node": "^20.11.20", @@ -2508,6 +2510,12 @@ "license": "MIT", "optional": true }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", @@ -6522,6 +6530,19 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", diff --git a/package.json b/package.json index bb134b7..2aeabb1 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@supabase/supabase-js": "^2.39.7", "@tanstack/react-query": "^5.24.1", "@tanstack/react-query-devtools": "^5.24.1", + "@types/uuid": "^10.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.3.1", @@ -34,7 +35,8 @@ "react-router-dom": "^6.22.1", "recharts": "^2.12.2", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "uuid": "^11.1.0" }, "devDependencies": { "@types/node": "^20.11.20", diff --git a/src/components/species-report.tsx b/src/components/species-report.tsx index deb6898..2a045b9 100644 --- a/src/components/species-report.tsx +++ b/src/components/species-report.tsx @@ -3,7 +3,6 @@ import { Button } from '@/components/ui/button'; import { FileText, Loader2 } from 'lucide-react'; import { SpeciesDetails } from '@/lib/api'; import jsPDF from 'jspdf'; -import html2canvas from 'html2canvas'; import { formatDate } from '@/lib/utils'; import { useQuery } from '@tanstack/react-query'; import { getCitesTradeRecords } from '@/lib/api'; diff --git a/src/components/species-tabs.tsx b/src/components/species-tabs.tsx index 59ad7c0..b6fd4bc 100644 --- a/src/components/species-tabs.tsx +++ b/src/components/species-tabs.tsx @@ -1,132 +1,20 @@ -import { useState, useMemo } from "react"; +import { useState } from "react"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { SpeciesDetails, TimelineEvent, CitesTradeRecord } from "@/lib/api"; +import { SpeciesDetails } from "@/lib/api"; import { useQuery } from "@tanstack/react-query"; -import { getTimelineEvents, getCitesTradeRecords, getSpeciesImages } from "@/lib/api"; -import { formatDate, IUCN_STATUS_COLORS, IUCN_STATUS_FULL_NAMES, CITES_APPENDIX_COLORS } from "@/lib/utils"; -import { Loader2, Filter, BarChart3, PieChart, LineChart, ImageIcon, X } from "lucide-react"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Button } from "@/components/ui/button"; -import React from "react"; -import { - LineChart as RechartsLineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Legend, - ResponsiveContainer, - BarChart, - Bar, - PieChart as RechartsPieChart, - Pie, - Cell, -} from "recharts"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { getSpeciesImages } from "@/lib/api"; import { SpeciesReport } from './species-report'; +import { OverviewTab } from "./species/tabs/OverviewTab"; +import { CitesTab } from "./species/tabs/CitesTab"; +import { IucnTab } from "./species/tabs/IucnTab"; +import { TradeDataTab } from "./species/tabs/TradeDataTab"; +import { TimelineTab } from "./species/tabs/TimelineTab"; type SpeciesTabsProps = { species: SpeciesDetails; }; -// Helper types for visualization data -type YearlyData = { - year: number; - count: number; -}; - -type CountryData = { - country: string; - count: number; -}; - -type TermData = { - term: string; - count: number; -}; - -type PurposeData = { - purpose: string; - count: number; - description: string; -}; - -type SourceData = { - source: string; - count: number; - description: string; -}; - -type TermYearData = { - year: number; - [term: string]: number; -}; - -// Purpose code descriptions -const PURPOSE_DESCRIPTIONS: Record = { - 'T': 'Commercial', - 'Z': 'Zoo', - 'G': 'Botanical garden', - 'Q': 'Circus or travelling exhibition', - 'S': 'Scientific', - 'H': 'Hunting trophy', - 'P': 'Personal', - 'M': 'Medical', - 'E': 'Educational', - 'N': 'Reintroduction', - 'B': 'Breeding', - 'L': 'Law enforcement' -}; - -// Source code descriptions -const SOURCE_DESCRIPTIONS: Record = { - 'W': 'Wild', - 'R': 'Ranched', - 'D': 'Captive-bred (App. I)', - 'A': 'Artificially propagated (plants)', - 'C': 'Captive-bred (App. II/III)', - 'F': 'Born in captivity (F1)', - 'U': 'Unknown', - 'I': 'Confiscated/seized', - 'O': 'Pre-Convention' -}; - -// Colors for charts -const CHART_COLORS = [ - "#8884d8", "#83a6ed", "#8dd1e1", "#82ca9d", "#a4de6c", - "#d0ed57", "#ffc658", "#ff8042", "#ff6361", "#bc5090" -]; - export function SpeciesTabs({ species }: SpeciesTabsProps) { - // Debug logging for CITES listings - console.log('SpeciesTabs received species:', species?.id); - console.log('CITES listings count in component:', species?.cites_listings?.length); - - // If multiple listings exist, log them all - if (species?.cites_listings?.length > 1) { - console.log('Multiple CITES listings found:', - species.cites_listings.map(l => `${l.id}: Appendix ${l.appendix} from ${l.listing_date}`)); - } - - const { data: timelineEvents, isLoading: timelineLoading } = useQuery({ - queryKey: ["timelineEvents", species.id], - queryFn: () => getTimelineEvents(species.id), - }); - - const { data: tradeRecords, isLoading: tradeLoading, error: tradeError } = useQuery({ - queryKey: ["tradeRecords", species.id], - queryFn: () => getCitesTradeRecords(species.id), - }); - // Add image fetching query const { data: imageData } = useQuery({ queryKey: ["speciesImage", species.scientific_name], @@ -134,210 +22,6 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) { enabled: !!species.scientific_name, }); - // State for trade record filters - define these at the component level, not inside a conditional - const [yearFilter, setYearFilter] = useState("all"); - const [termFilter, setTermFilter] = useState("all"); - const [appendixFilter, setAppendixFilter] = useState("all"); - const [isImageDialogOpen, setIsImageDialogOpen] = useState(false); - - // Debug: Log CITES listings data - console.log("CITES listings count:", species.cites_listings?.length); - console.log("CITES listings data:", JSON.stringify(species.cites_listings)); - - // Log trade records data when it changes - React.useEffect(() => { - if (tradeRecords) { - console.log(`Trade records loaded: ${tradeRecords.length}`); - if (tradeRecords.length > 0) { - console.log('Sample trade record:', tradeRecords[0]); - } - } - - if (tradeError) { - console.error('Error loading trade records:', tradeError); - } - }, [tradeRecords, tradeError]); - - // Compute filtered records - const filteredRecords = useMemo(() => { - if (!tradeRecords) return []; - - return tradeRecords.filter((record: CitesTradeRecord) => { - return ( - (yearFilter === "all" || !yearFilter || record.year.toString() === yearFilter) && - (termFilter === "all" || !termFilter || record.term === termFilter) && - (appendixFilter === "all" || !appendixFilter || record.appendix === appendixFilter) - ); - }); - }, [tradeRecords, yearFilter, termFilter, appendixFilter]); - - // Prepare data for visualizations - const visualizationData = useMemo(() => { - if (!tradeRecords || tradeRecords.length === 0) return null; - - // Records by year - const yearCounts: Record = {}; - tradeRecords.forEach(record => { - yearCounts[record.year] = (yearCounts[record.year] || 0) + 1; - }); - const recordsByYear: YearlyData[] = Object.entries(yearCounts) - .map(([year, count]) => ({ year: parseInt(year), count })) - .sort((a, b) => a.year - b.year); - - // Top importers - const importerCounts: Record = {}; - tradeRecords.forEach(record => { - if (record.importer) { - importerCounts[record.importer] = (importerCounts[record.importer] || 0) + 1; - } - }); - const topImporters: CountryData[] = Object.entries(importerCounts) - .map(([country, count]) => ({ country, count })) - .sort((a, b) => b.count - a.count) - .slice(0, 10); - - // Top exporters - const exporterCounts: Record = {}; - tradeRecords.forEach(record => { - if (record.exporter) { - exporterCounts[record.exporter] = (exporterCounts[record.exporter] || 0) + 1; - } - }); - const topExporters: CountryData[] = Object.entries(exporterCounts) - .map(([country, count]) => ({ country, count })) - .sort((a, b) => b.count - a.count) - .slice(0, 10); - - // Terms traded - const termCounts: Record = {}; - tradeRecords.forEach(record => { - termCounts[record.term] = (termCounts[record.term] || 0) + 1; - }); - const termsTraded: TermData[] = Object.entries(termCounts) - .map(([term, count]) => ({ term, count })) - .sort((a, b) => b.count - a.count); - - // Trade purposes - const purposeCounts: Record = {}; - tradeRecords.forEach(record => { - if (record.purpose) { - purposeCounts[record.purpose] = (purposeCounts[record.purpose] || 0) + 1; - } - }); - const tradePurposes: PurposeData[] = Object.entries(purposeCounts) - .map(([purpose, count]) => ({ - purpose, - count, - description: PURPOSE_DESCRIPTIONS[purpose] || 'Unknown' - })) - .sort((a, b) => b.count - a.count); - - // Trade sources - const sourceCounts: Record = {}; - tradeRecords.forEach(record => { - if (record.source) { - sourceCounts[record.source] = (sourceCounts[record.source] || 0) + 1; - } - }); - const tradeSources: SourceData[] = Object.entries(sourceCounts) - .map(([source, count]) => ({ - source, - count, - description: SOURCE_DESCRIPTIONS[source] || 'Unknown' - })) - .sort((a, b) => b.count - a.count); - - // Top terms by year - const topTerms = termsTraded.slice(0, 5).map(t => t.term); - const termsByYear: Record> = {}; - - tradeRecords.forEach(record => { - if (!termsByYear[record.year]) { - termsByYear[record.year] = {}; - } - if (topTerms.includes(record.term)) { - termsByYear[record.year][record.term] = (termsByYear[record.year][record.term] || 0) + (record.quantity || 1); - } - }); - - const termQuantitiesByYear: TermYearData[] = Object.entries(termsByYear) - .map(([year, terms]) => { - const yearData: TermYearData = { year: parseInt(year) }; - topTerms.forEach(term => { - yearData[term] = terms[term] || 0; - }); - return yearData; - }) - .sort((a, b) => a.year - b.year); - - return { - recordsByYear, - topImporters, - topExporters, - termsTraded, - tradePurposes, - tradeSources, - termQuantitiesByYear, - topTerms - }; - }, [tradeRecords]); - - // Custom tooltip for pie chart - const renderCustomizedLabel = ({ cx, cy, midAngle, outerRadius, percent, payload }: any) => { - const RADIAN = Math.PI / 180; - const radius = outerRadius * 1.1; - const x = cx + radius * Math.cos(-midAngle * RADIAN); - const y = cy + radius * Math.sin(-midAngle * RADIAN); - - return ( - cx ? 'start' : 'end'} - dominantBaseline="central" - fontSize={12} - > - {`${payload.term} (${(percent * 100).toFixed(0)}%)`} - - ); - }; - - // Get unique years, terms, and appendices - filter out null, undefined, and empty values - const years = useMemo(() => { - if (!tradeRecords) return []; - const uniqueYears = [...new Set(tradeRecords.map((r: CitesTradeRecord) => r.year))] - .filter(year => year !== null && year !== undefined) - .sort((a, b) => Number(b) - Number(a)); - console.log('Unique years after filtering:', uniqueYears); - return uniqueYears; - }, [tradeRecords]); - - const terms = useMemo(() => { - if (!tradeRecords) return []; - const uniqueTerms = [...new Set(tradeRecords.map((r: CitesTradeRecord) => r.term))] - .filter(term => term !== null && term !== undefined && term !== '') - .sort(); - console.log('Unique terms after filtering:', uniqueTerms); - return uniqueTerms; - }, [tradeRecords]); - - const appendices = useMemo(() => { - if (!tradeRecords) return []; - const uniqueAppendices = [...new Set(tradeRecords.map((r: CitesTradeRecord) => r.appendix))] - .filter(appendix => appendix !== null && appendix !== undefined && appendix !== '') - .sort(); - console.log('Unique appendices after filtering:', uniqueAppendices); - return uniqueAppendices; - }, [tradeRecords]); - - // Reset filters function - const resetFilters = () => { - setYearFilter("all"); - setTermFilter("all"); - setAppendixFilter("all"); - }; - // Determine default tab based on available data const defaultTab = "overview"; // Always start with Overview tab @@ -354,853 +38,47 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) { - - Overview - 0 ? "font-semibold" : ""}> - CITES - {species.cites_listings && species.cites_listings.length > 0 && ( - {species.cites_listings.length} - )} - - IUCN - Trade Data - Timeline - - - {/* Overview Tab */} - -
- - - Species Information - Taxonomic classification and basic info - - - {/* Add image display */} -
imageData && setIsImageDialogOpen(true)} - > - {imageData ? ( - <> - {species.scientific_name} -
- {imageData.attribution} -
-
- -
- - ) : ( -
- - No image available -
- )} -
- - {/* Add Dialog for full-size image */} - - - - - {species.scientific_name} - - - - {imageData && ( -
- {species.scientific_name} -
-

{imageData.attribution}

- {imageData.license && ( -

License: {imageData.license}

- )} -
-
- )} -
-
- -
-
Scientific Name:
-
{species.scientific_name}
-
Common Name:
-
{species.common_name}
-
Kingdom:
-
{species.kingdom}
-
Phylum:
-
{species.phylum}
-
Class:
-
{species.class}
-
Order:
-
{species.order_name}
-
Family:
-
{species.family}
-
Genus:
-
{species.genus}
-
Species:
-
{species.species_name}
- {species.authority && ( - <> -
Authority:
-
{species.authority}
- - )} -
-
-
- - - - Conservation Status - Current protection and assessment - - -
-
-

IUCN Red List Status:

- {species.latest_assessment ? ( -
- {species.latest_assessment.status && ( - - {IUCN_STATUS_FULL_NAMES[species.latest_assessment.status] || species.latest_assessment.status} - - )} - - Assessment Year: {species.latest_assessment.year_published} - -
- ) : ( - No assessment data available - )} -
- -
-

CITES Listing:

- {species.current_cites_listing ? ( -
- - Appendix {species.current_cites_listing.appendix} - - - Listed since: {formatDate(species.current_cites_listing.listing_date)} - -
- ) : ( - No CITES listing data available - )} -
-
-
-
-
-
- - {/* CITES Tab */} - - - - CITES Listings - Convention on International Trade in Endangered Species listings - - - {species.cites_listings.length > 0 ? ( -
- {/* Current CITES status highlight */} - {species.current_cites_listing && ( -
-

Current CITES Status

-
- - Appendix {species.current_cites_listing.appendix} - - since {formatDate(species.current_cites_listing.listing_date)} -
- {species.current_cites_listing.notes && ( -

{species.current_cites_listing.notes}

- )} -
- )} - - {/* CITES listings table */} -
-

Listing History ({species.cites_listings.length} raw listings)

-
-
-                      {JSON.stringify(species.cites_listings, null, 2)}
-                    
- - - - - - - - - - - {species.cites_listings.length === 0 ? ( - - - - ) : ( - species.cites_listings.map((listing: any) => ( - - - - - - - )) - )} - -
AppendixListing DateNotesID
- No listing history available -
- - Appendix {listing.appendix} - - - {formatDate(listing.listing_date || listing.listing_dates || '')} - {listing.notes || 'N/A'}{listing.id}
-
-
- - {/* CITES info section */} -
-

What are CITES Appendices?

-
    -
  • Appendix I: Species threatened with extinction. Trade permitted only in exceptional circumstances.
  • -
  • Appendix II: Species not necessarily threatened with extinction, but trade must be controlled to avoid utilization incompatible with their survival.
  • -
  • Appendix III: Species protected in at least one country, which has asked other CITES Parties for assistance in controlling the trade.
  • -
-
-
- ) : ( -

No CITES listings available for this species.

+ + Overview + 0 ? "font-semibold" : ""}> + CITES + {species.cites_listings && species.cites_listings.length > 0 && ( + {species.cites_listings.length} )} -
-
-
+ + IUCN + Trade Data + Timeline + - {/* IUCN Tab */} - - - - IUCN Red List Assessments - International Union for Conservation of Nature assessments - - - {species.iucn_assessments.length > 0 ? ( -
- - - - - - - - - - - {species.iucn_assessments.map((assessment) => ( - - - - - - - ))} - -
YearStatusScopeMore Info
{assessment.year_published} - {assessment.status ? ( - - {IUCN_STATUS_FULL_NAMES[assessment.status] || assessment.status} - - ) : ( - 'N/A' - )} - - {assessment.scope_description || assessment.scope_code || 'Global'} - - {assessment.url ? ( - - View Assessment - - ) : ( - 'N/A' - )} -
-
- ) : ( -

No IUCN assessments available for this species.

- )} -
-
-
+ {/* Overview Tab */} + + + - {/* Trade Data Tab */} - - - - CITES Trade Records - International trade data reported to CITES - - - {tradeLoading ? ( -
- -
- ) : tradeError ? ( -
-

Error loading trade records. Please try again later.

-

{tradeError.message || 'Unknown error'}

-
- ) : tradeRecords && tradeRecords.length > 0 ? ( -
- {/* Trade Filters */} -
-
- -

Filter Trade Records

-
-
-
- - -
-
- - -
-
- - -
-
- - {/* Reset Filters Button */} - {(yearFilter !== "all" || termFilter !== "all" || appendixFilter !== "all") && ( -
- -
- )} -
- - {/* Trade summary section */} -
-

Trade Summary

-
-
-

Records

-

{filteredRecords.length}

- {filteredRecords.length !== tradeRecords.length && ( -

- of {tradeRecords.length} total -

- )} -
-
-

Year Range

-

- {filteredRecords.length > 0 - ? `${Math.min(...filteredRecords.map((r: CitesTradeRecord) => Number(r.year)))} - ${Math.max(...filteredRecords.map((r: CitesTradeRecord) => Number(r.year)))}` - : 'N/A' - } -

-
-
-

Total Quantity

-

- {filteredRecords - .reduce((sum: number, record: CitesTradeRecord) => sum + (Number(record.quantity) || 0), 0) - .toLocaleString()} -

-
-
-
+ {/* CITES Tab */} + + + - {/* Visualizations Section */} - {visualizationData && ( -
-

Trade Visualizations

- - {/* Records Over Time */} - - -
- - Records Over Time -
- Number of trade records by year -
- -
- - - - - - - - - - -
-
-
+ {/* IUCN Tab */} + + + - {/* Top Importers and Exporters */} -
- {/* Top Importers */} - - -
- - Top Importers -
- Countries importing the most specimens -
- -
- - - - - - - - - -
-
-
+ {/* Trade Data Tab */} + + + - {/* Top Exporters */} - - -
- - Top Exporters -
- Countries exporting the most specimens -
- -
- - - - - - - - - -
-
-
-
- - {/* Terms Traded */} - - -
- - Terms Traded -
- Distribution of specimen types in trade -
- -
- - - - {visualizationData.termsTraded.map((_, index) => ( - - ))} - - [`${value} records`, props.payload.term]} /> - - -
-
-
- - {/* Trade Purposes and Sources */} -
- {/* Trade Purposes */} - - -
- - Trade Purposes -
- Reasons for trade -
- -
- - - - - `${value} - ${PURPOSE_DESCRIPTIONS[value] || 'Unknown'}`} - width={150} - /> - [ - `${value} records`, - `${props.payload.purpose} - ${props.payload.description}` - ]} /> - - - -
-
-
- - {/* Trade Sources */} - - -
- - Trade Sources -
- Origin of traded specimens -
- -
- - - - - `${value} - ${SOURCE_DESCRIPTIONS[value] || 'Unknown'}`} - width={150} - /> - [ - `${value} records`, - `${props.payload.source} - ${props.payload.description}` - ]} /> - - - -
-
-
-
- - {/* Quantity of Top Terms Over Time */} - - -
- - Quantity of Top Terms Over Time -
- Trends in quantities of the most traded terms -
- -
- - - - - - - - {visualizationData.topTerms.map((term, index) => ( - - ))} - - -
-
-
-
- )} - - {/* Trade records table */} -
-

Trade Records

- {filteredRecords.length === 0 ? ( -
-

No records match the selected filters.

- -
- ) : ( - <> -
- - - - - - - - - - - - - - - - {filteredRecords.slice(0, 20).map((record: CitesTradeRecord) => ( - - - - - - - - - - - - ))} - -
YearAppendixTermQuantityUnitImporterExporterPurposeSource
{record.year} - - {record.appendix} - - {record.term}{record.quantity || 'N/A'}{record.unit || '-'}{record.importer || '-'}{record.exporter || '-'}{record.purpose || '-'}{record.source || '-'}
-
- {filteredRecords.length > 20 && ( -
- Showing 20 of {filteredRecords.length} records. Use the filters to narrow down results. -
- )} - - )} -
- - {/* Trade data info section */} -
-

About CITES Trade Data

-

- The CITES Trade Database, managed by UNEP-WCMC on behalf of the CITES Secretariat, contains records of trade in wildlife - listed in the CITES Appendices. Records include information on species, appendix, purpose, source, quantities, units, - and countries involved in the trade. -

-
-
- ) : ( -
-

No trade records available for this species.

- - {/* Add info about what trade records are */} -
-

About CITES Trade Records

-

- CITES trade records document international trade in wildlife listed in the CITES Appendices. - Not all species have recorded trade data, particularly if they haven't been traded internationally - or if trade reports haven't been submitted to the CITES Trade Database. -

-

- If you believe this species should have trade records, please check the following: -

-
    -
  • Verify the species is listed in a CITES Appendix
  • -
  • Check if the scientific name has been recorded differently
  • -
  • Trade may be recorded at a higher taxonomic level (family or genus)
  • -
-
-
- )} -
-
-
- - {/* Timeline Tab */} - - - - Conservation Timeline - Historical conservation and trade events - - - {timelineLoading ? ( -
- -
- ) : timelineEvents && timelineEvents.length > 0 ? ( -
- {timelineEvents.map((event: TimelineEvent) => ( -
-
-
- {formatDate(event.event_date)} - {event.status && ( - - {event.status} - - )} -
-

{event.title}

- {event.description &&

{event.description}

} -
- ))} -
- ) : ( -

No timeline events available for this species.

- )} -
-
-
-
-
- © Magnus Smari Smarason + {/* Timeline Tab */} + + + + +
+ © Magnus Smari Smarason +
- ); } diff --git a/src/components/species/SpeciesImage.tsx b/src/components/species/SpeciesImage.tsx new file mode 100644 index 0000000..938b8ec --- /dev/null +++ b/src/components/species/SpeciesImage.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { ImageIcon, X } from "lucide-react"; + +type SpeciesImageProps = { + scientificName: string; + imageData?: { + url: string; + attribution: string; + license?: string; + } | null; +}; + +export function SpeciesImage({ scientificName, imageData }: SpeciesImageProps) { + const [isImageDialogOpen, setIsImageDialogOpen] = useState(false); + + return ( + <> +
imageData && setIsImageDialogOpen(true)} + > + {imageData ? ( + <> + {scientificName} +
+ {imageData.attribution} +
+
+ +
+ + ) : ( +
+ + No image available +
+ )} +
+ + {/* Dialog for full-size image */} + + + + + {scientificName} + + + + {imageData && ( +
+ {scientificName} +
+

{imageData.attribution}

+ {imageData.license && ( +

License: {imageData.license}

+ )} +
+
+ )} +
+
+ + ); +} diff --git a/src/components/species/tabs/CitesTab.tsx b/src/components/species/tabs/CitesTab.tsx new file mode 100644 index 0000000..8207e0b --- /dev/null +++ b/src/components/species/tabs/CitesTab.tsx @@ -0,0 +1,257 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { AlertCircle, Edit, Loader2, Plus, Save, Trash2 } from "lucide-react"; +import { SpeciesDetails, CitesListing } from "@/lib/api"; +import { formatDate, CITES_APPENDIX_COLORS } from "@/lib/utils"; +import { useCitesListingCrud } from "@/hooks/useCitesListingCrud"; + +type CitesTabProps = { + species: SpeciesDetails; +}; + +export function CitesTab({ species }: CitesTabProps) { + const { + isEditingCitesListing, + isAddingCitesListing, + selectedCitesListing, + citesListingForm, + operationStatus, + handleCitesListingFormChange, + handleEditCitesListing, + handleAddCitesListing, + handleSaveCitesListing, + handleDeleteCitesListing, + handleCancelCitesListing + } = useCitesListingCrud(species.id); + + return ( + + + CITES Listings + Convention on International Trade in Endangered Species listings + + + {species.cites_listings.length > 0 ? ( +
+ {/* Current CITES status highlight */} + {species.current_cites_listing && ( +
+

Current CITES Status

+
+ + Appendix {species.current_cites_listing.appendix} + + since {formatDate(species.current_cites_listing.listing_date)} +
+ {species.current_cites_listing.notes && ( +

{species.current_cites_listing.notes}

+ )} +
+ )} + + {/* Operation status messages */} + {operationStatus.error && ( +
+ +
+

Error

+

{operationStatus.error}

+
+
+ )} + + {operationStatus.success && ( +
+

Operation completed successfully.

+
+ )} + + {/* CITES listing form */} + {(isAddingCitesListing || isEditingCitesListing) && ( +
+

+ {isAddingCitesListing ? "Add New CITES Listing" : "Edit CITES Listing"} +

+
+
+ + +
+ +
+ + handleCitesListingFormChange("listing_date", e.target.value)} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background" + /> +
+ +
+ +