feat: Enhanced UI and added species images - Added iNaturalist integration, improved header styling, project info, species cards, loading states, and dark mode support
This commit is contained in:
31
README.md
31
README.md
@ -1,4 +1,13 @@
|
|||||||
# Arctic Species 2025 Frontend
|
# Arctic Species 2025 Frontend
|
||||||
|
**Work in Progress!**
|
||||||
|
|
||||||
|
A collaborative project between:
|
||||||
|
- School of Humanities and Social Sciences
|
||||||
|
- Thomas Barry
|
||||||
|
- Dean of School
|
||||||
|
- Magnus SMari Smarason
|
||||||
|
- AI Project Manager
|
||||||
|
University of Akureyri
|
||||||
|
|
||||||
A comprehensive web application for tracking and analyzing Arctic species data, including CITES trade records, IUCN assessments, and conservation status. This project is part of the Arctic Species 2025 initiative.
|
A comprehensive web application for tracking and analyzing Arctic species data, including CITES trade records, IUCN assessments, and conservation status. This project is part of the Arctic Species 2025 initiative.
|
||||||
|
|
||||||
@ -10,6 +19,8 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
|||||||
- Multiple common names support
|
- Multiple common names support
|
||||||
- Arctic subpopulation tracking
|
- Arctic subpopulation tracking
|
||||||
- Region-specific conservation status
|
- Region-specific conservation status
|
||||||
|
- High-quality species images from iNaturalist
|
||||||
|
- Interactive image viewer with attribution
|
||||||
|
|
||||||
- **CITES Information**
|
- **CITES Information**
|
||||||
- Complete CITES listing history
|
- Complete CITES listing history
|
||||||
@ -17,12 +28,15 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
|||||||
- Detailed trade records with filtering
|
- Detailed trade records with filtering
|
||||||
- Historical trade data analysis
|
- Historical trade data analysis
|
||||||
- Arctic-specific trade patterns
|
- Arctic-specific trade patterns
|
||||||
|
- Interactive trade visualizations
|
||||||
|
- Filterable trade records table
|
||||||
|
|
||||||
- **IUCN Assessments**
|
- **IUCN Assessments**
|
||||||
- Latest IUCN Red List status
|
- Latest IUCN Red List status
|
||||||
- Historical assessment tracking
|
- Historical assessment tracking
|
||||||
- Population trend analysis
|
- Population trend analysis
|
||||||
- Arctic region-specific assessments
|
- Arctic region-specific assessments
|
||||||
|
- Color-coded status indicators
|
||||||
|
|
||||||
- **Timeline View**
|
- **Timeline View**
|
||||||
- Chronological view of species events
|
- Chronological view of species events
|
||||||
@ -31,6 +45,13 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
|||||||
- Trade record history
|
- Trade record history
|
||||||
- Arctic conservation milestones
|
- Arctic conservation milestones
|
||||||
|
|
||||||
|
- **Data Visualization**
|
||||||
|
- Trade records over time
|
||||||
|
- Top importers and exporters
|
||||||
|
- Distribution of traded terms
|
||||||
|
- Trade purposes and sources
|
||||||
|
- Interactive charts and graphs
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Frontend**: React + TypeScript + Vite
|
- **Frontend**: React + TypeScript + Vite
|
||||||
@ -39,6 +60,8 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
|||||||
- **Database**: Supabase (PostgreSQL)
|
- **Database**: Supabase (PostgreSQL)
|
||||||
- **State Management**: React Query
|
- **State Management**: React Query
|
||||||
- **Routing**: React Router
|
- **Routing**: React Router
|
||||||
|
- **Charts**: Recharts
|
||||||
|
- **Image Integration**: iNaturalist API
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@ -86,6 +109,13 @@ The application uses the following main tables:
|
|||||||
- `cites_trade_records`: CITES trade data
|
- `cites_trade_records`: CITES trade data
|
||||||
- `timeline_events`: Chronological events
|
- `timeline_events`: Chronological events
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
- **Species Images**: iNaturalist API (https://api.inaturalist.org/)
|
||||||
|
- **CITES Data**: CITES Trade Database (https://trade.cites.org/)
|
||||||
|
- **IUCN Data**: IUCN Red List (https://www.iucnredlist.org/)
|
||||||
|
- **Species Information**: Arctic Species Database
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
@ -102,5 +132,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|||||||
|
|
||||||
- CITES Secretariat for trade data
|
- CITES Secretariat for trade data
|
||||||
- IUCN Red List for assessment data
|
- IUCN Red List for assessment data
|
||||||
|
- iNaturalist for species images
|
||||||
- Arctic Council for regional guidance
|
- Arctic Council for regional guidance
|
||||||
- All contributors and maintainers
|
- All contributors and maintainers
|
334
package-lock.json
generated
334
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "arctic-species-tracker",
|
"name": "arctic-species-tracker",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"gh-pages": "^6.3.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
@ -1196,6 +1198,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.2",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.4",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-slot": "1.1.2",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-direction": {
|
"node_modules/@radix-ui/react-direction": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||||
@ -2611,6 +2649,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||||
@ -2862,6 +2907,13 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commondir": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@ -3137,6 +3189,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/email-addresses": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
@ -3505,6 +3564,34 @@
|
|||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/filename-reserved-regex": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/filenamify": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"filename-reserved-regex": "^2.0.0",
|
||||||
|
"strip-outer": "^1.0.1",
|
||||||
|
"trim-repeated": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@ -3517,6 +3604,24 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-cache-dir": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commondir": "^1.0.1",
|
||||||
|
"make-dir": "^3.0.2",
|
||||||
|
"pkg-dir": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/find-up": {
|
"node_modules/find-up": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
@ -3586,6 +3691,21 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "11.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||||
|
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@ -3635,6 +3755,39 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gh-pages": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.4",
|
||||||
|
"commander": "^13.0.0",
|
||||||
|
"email-addresses": "^5.0.0",
|
||||||
|
"filenamify": "^4.3.0",
|
||||||
|
"find-cache-dir": "^3.3.1",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"globby": "^11.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"gh-pages": "bin/gh-pages.js",
|
||||||
|
"gh-pages-clean": "bin/gh-pages-clean.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gh-pages/node_modules/commander": {
|
||||||
|
"version": "13.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||||
|
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
@ -3724,6 +3877,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
@ -4011,6 +4171,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@ -4113,6 +4286,32 @@
|
|||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-dir": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-dir/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@ -4306,6 +4505,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
@ -4428,6 +4637,75 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pkg-dir": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"find-up": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkg-dir/node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkg-dir/node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkg-dir/node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkg-dir/node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
@ -5157,6 +5435,29 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-outer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-outer/node_modules/escape-string-regexp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.0",
|
"version": "3.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||||
@ -5332,6 +5633,29 @@
|
|||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/trim-repeated": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/trim-repeated/node_modules/escape-string-regexp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||||
@ -5403,6 +5727,16 @@
|
|||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
|
@ -3,13 +3,17 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"homepage": "https://magnussmari.github.io/arctic-species-2025-frontend",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"predeploy": "npm run build",
|
||||||
|
"deploy": "gh-pages -d dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
@ -40,6 +44,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"gh-pages": "^6.3.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { searchSpecies } from '@/lib/api';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getAllSpecies } from '@/lib/api';
|
||||||
import { Search, Loader2 } from 'lucide-react';
|
import { Search, Loader2 } from 'lucide-react';
|
||||||
import { IUCN_STATUS_COLORS } from '@/lib/utils';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
|
||||||
export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string) => void }) {
|
export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string) => void }) {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@ -28,7 +26,7 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
|
|||||||
// Fetch search results
|
// Fetch search results
|
||||||
const { data: searchResults, isLoading } = useQuery({
|
const { data: searchResults, isLoading } = useQuery({
|
||||||
queryKey: ['searchSpecies', debouncedTerm],
|
queryKey: ['searchSpecies', debouncedTerm],
|
||||||
queryFn: () => searchSpecies(debouncedTerm),
|
queryFn: () => getAllSpecies(),
|
||||||
enabled: debouncedTerm.length >= 3,
|
enabled: debouncedTerm.length >= 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,6 +37,12 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter results based on search term
|
||||||
|
const filteredResults = searchResults?.filter(species =>
|
||||||
|
species.scientific_name.toLowerCase().includes(debouncedTerm.toLowerCase()) ||
|
||||||
|
species.common_name?.toLowerCase().includes(debouncedTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<form onSubmit={handleSubmit} className="relative">
|
<form onSubmit={handleSubmit} className="relative">
|
||||||
@ -64,11 +68,11 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Search Results */}
|
{/* Search Results */}
|
||||||
{searchResults && searchResults.length > 0 && (
|
{filteredResults && filteredResults.length > 0 && (
|
||||||
<Card className="mt-2">
|
<Card className="mt-2">
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<ul className="divide-y divide-border">
|
<ul className="divide-y divide-border">
|
||||||
{searchResults.map((species) => (
|
{filteredResults.map((species) => (
|
||||||
<li
|
<li
|
||||||
key={species.id}
|
key={species.id}
|
||||||
className="cursor-pointer p-2 hover:bg-muted"
|
className="cursor-pointer p-2 hover:bg-muted"
|
||||||
@ -83,7 +87,7 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{searchResults && searchResults.length === 0 && debouncedTerm.length >= 3 && (
|
{filteredResults && filteredResults.length === 0 && debouncedTerm.length >= 3 && (
|
||||||
<p className="mt-2 text-sm text-muted-foreground">No species found matching your search.</p>
|
<p className="mt-2 text-sm text-muted-foreground">No species found matching your search.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,10 +4,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { SpeciesDetails, TimelineEvent, CitesTradeRecord } from "@/lib/api";
|
import { SpeciesDetails, TimelineEvent, CitesTradeRecord } from "@/lib/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getTimelineEvents, getCitesTradeRecords } from "@/lib/api";
|
import { getTimelineEvents, getCitesTradeRecords, getSpeciesImages } from "@/lib/api";
|
||||||
import { formatDate, IUCN_STATUS_COLORS, IUCN_STATUS_FULL_NAMES, CITES_APPENDIX_COLORS } from "@/lib/utils";
|
import { formatDate, IUCN_STATUS_COLORS, IUCN_STATUS_FULL_NAMES, CITES_APPENDIX_COLORS } from "@/lib/utils";
|
||||||
import { Loader2, Filter, BarChart3, PieChart, LineChart } from "lucide-react";
|
import { Loader2, Filter, BarChart3, PieChart, LineChart, ImageIcon, X } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -26,8 +25,13 @@ import {
|
|||||||
PieChart as RechartsPieChart,
|
PieChart as RechartsPieChart,
|
||||||
Pie,
|
Pie,
|
||||||
Cell,
|
Cell,
|
||||||
Sector
|
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
type SpeciesTabsProps = {
|
type SpeciesTabsProps = {
|
||||||
species: SpeciesDetails;
|
species: SpeciesDetails;
|
||||||
@ -122,10 +126,18 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
|||||||
queryFn: () => getCitesTradeRecords(species.id),
|
queryFn: () => getCitesTradeRecords(species.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add image fetching query
|
||||||
|
const { data: imageData } = useQuery({
|
||||||
|
queryKey: ["speciesImage", species.scientific_name],
|
||||||
|
queryFn: () => getSpeciesImages(species.scientific_name),
|
||||||
|
enabled: !!species.scientific_name,
|
||||||
|
});
|
||||||
|
|
||||||
// State for trade record filters - define these at the component level, not inside a conditional
|
// State for trade record filters - define these at the component level, not inside a conditional
|
||||||
const [yearFilter, setYearFilter] = useState<string>("all");
|
const [yearFilter, setYearFilter] = useState<string>("all");
|
||||||
const [termFilter, setTermFilter] = useState<string>("all");
|
const [termFilter, setTermFilter] = useState<string>("all");
|
||||||
const [appendixFilter, setAppendixFilter] = useState<string>("all");
|
const [appendixFilter, setAppendixFilter] = useState<string>("all");
|
||||||
|
const [isImageDialogOpen, setIsImageDialogOpen] = useState(false);
|
||||||
|
|
||||||
// Debug: Log CITES listings data
|
// Debug: Log CITES listings data
|
||||||
console.log("CITES listings count:", species.cites_listings?.length);
|
console.log("CITES listings count:", species.cites_listings?.length);
|
||||||
@ -270,7 +282,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
|||||||
}, [tradeRecords]);
|
}, [tradeRecords]);
|
||||||
|
|
||||||
// Custom tooltip for pie chart
|
// Custom tooltip for pie chart
|
||||||
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent, index, payload }: any) => {
|
const renderCustomizedLabel = ({ cx, cy, midAngle, outerRadius, percent, payload }: any) => {
|
||||||
const RADIAN = Math.PI / 180;
|
const RADIAN = Math.PI / 180;
|
||||||
const radius = outerRadius * 1.1;
|
const radius = outerRadius * 1.1;
|
||||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||||
@ -326,10 +338,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Determine default tab based on available data
|
// Determine default tab based on available data
|
||||||
const defaultTab =
|
const defaultTab = "overview"; // Always start with Overview tab
|
||||||
species.cites_listings && species.cites_listings.length > 0 ? "cites" :
|
|
||||||
species.iucn_assessments && species.iucn_assessments.length > 0 ? "iucn" :
|
|
||||||
"overview";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col">
|
<div className="w-full flex flex-col">
|
||||||
@ -360,6 +369,66 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
|||||||
<CardDescription>Taxonomic classification and basic info</CardDescription>
|
<CardDescription>Taxonomic classification and basic info</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{/* Add image display */}
|
||||||
|
<div
|
||||||
|
className="relative aspect-[16/9] w-full overflow-hidden rounded-lg bg-muted mb-4 cursor-pointer hover:opacity-90 transition-opacity"
|
||||||
|
onClick={() => imageData && setIsImageDialogOpen(true)}
|
||||||
|
>
|
||||||
|
{imageData ? (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={imageData.url}
|
||||||
|
alt={species.scientific_name}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 right-0 bg-black/50 p-2 text-xs text-white">
|
||||||
|
{imageData.attribution}
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-2 right-2 bg-black/50 p-1 rounded-full">
|
||||||
|
<ImageIcon className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
|
||||||
|
<ImageIcon className="h-12 w-12" />
|
||||||
|
<span className="ml-2">No image available</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Dialog for full-size image */}
|
||||||
|
<Dialog open={isImageDialogOpen} onOpenChange={setIsImageDialogOpen}>
|
||||||
|
<DialogContent className="max-w-4xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center justify-between">
|
||||||
|
<span>{species.scientific_name}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setIsImageDialogOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{imageData && (
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={imageData.url.replace('medium', 'original')}
|
||||||
|
alt={species.scientific_name}
|
||||||
|
className="w-full h-auto rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-black/50 p-4 text-white rounded-b-lg">
|
||||||
|
<p className="text-sm">{imageData.attribution}</p>
|
||||||
|
{imageData.license && (
|
||||||
|
<p className="text-xs mt-1">License: {imageData.license}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<dl className="grid grid-cols-2 gap-2">
|
<dl className="grid grid-cols-2 gap-2">
|
||||||
<dt className="font-semibold">Scientific Name:</dt>
|
<dt className="font-semibold">Scientific Name:</dt>
|
||||||
<dd className="italic">{species.scientific_name}</dd>
|
<dd className="italic">{species.scientific_name}</dd>
|
||||||
@ -852,11 +921,11 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
|||||||
dataKey="count"
|
dataKey="count"
|
||||||
nameKey="term"
|
nameKey="term"
|
||||||
>
|
>
|
||||||
{visualizationData.termsTraded.map((entry, index) => (
|
{visualizationData.termsTraded.map((_, index) => (
|
||||||
<Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
|
<Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
|
||||||
))}
|
))}
|
||||||
</Pie>
|
</Pie>
|
||||||
<Tooltip formatter={(value, name, props) => [`${value} records`, props.payload.term]} />
|
<Tooltip formatter={(value, _, props) => [`${value} records`, props.payload.term]} />
|
||||||
</RechartsPieChart>
|
</RechartsPieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -891,7 +960,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
|||||||
tickFormatter={(value) => `${value} - ${PURPOSE_DESCRIPTIONS[value] || 'Unknown'}`}
|
tickFormatter={(value) => `${value} - ${PURPOSE_DESCRIPTIONS[value] || 'Unknown'}`}
|
||||||
width={150}
|
width={150}
|
||||||
/>
|
/>
|
||||||
<Tooltip formatter={(value, name, props) => [
|
<Tooltip formatter={(value, _, props) => [
|
||||||
`${value} records`,
|
`${value} records`,
|
||||||
`${props.payload.purpose} - ${props.payload.description}`
|
`${props.payload.purpose} - ${props.payload.description}`
|
||||||
]} />
|
]} />
|
||||||
@ -928,7 +997,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
|||||||
tickFormatter={(value) => `${value} - ${SOURCE_DESCRIPTIONS[value] || 'Unknown'}`}
|
tickFormatter={(value) => `${value} - ${SOURCE_DESCRIPTIONS[value] || 'Unknown'}`}
|
||||||
width={150}
|
width={150}
|
||||||
/>
|
/>
|
||||||
<Tooltip formatter={(value, name, props) => [
|
<Tooltip formatter={(value, _, props) => [
|
||||||
`${value} records`,
|
`${value} records`,
|
||||||
`${props.payload.source} - ${props.payload.description}`
|
`${props.payload.source} - ${props.payload.description}`
|
||||||
]} />
|
]} />
|
||||||
|
122
src/components/ui/dialog.tsx
Normal file
122
src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
@ -26,19 +26,6 @@ export type SpeciesDetails = Species & {
|
|||||||
current_cites_listing?: CitesListing;
|
current_cites_listing?: CitesListing;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug function to check for duplicate species in database
|
|
||||||
async function checkForDuplicateSpecies() {
|
|
||||||
// Direct SQL query using RPC for more complex query
|
|
||||||
const { data, error } = await supabase.rpc('get_duplicate_species', {});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error checking for duplicates:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllSpecies() {
|
export async function getAllSpecies() {
|
||||||
try {
|
try {
|
||||||
console.log("Fetching all species from database...");
|
console.log("Fetching all species from database...");
|
||||||
@ -379,4 +366,42 @@ export async function searchSpecies(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(speciesMap.values());
|
return Array.from(speciesMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSpeciesImage(speciesId: string, imageUrl: string) {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('species')
|
||||||
|
.update({ default_image_url: imageUrl })
|
||||||
|
.eq('id', speciesId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error updating species image:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSpeciesImages(scientificName: string) {
|
||||||
|
try {
|
||||||
|
console.log('Fetching images for species:', scientificName);
|
||||||
|
|
||||||
|
// First try to get from iNaturalist API
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.inaturalist.org/v1/taxa?q=${encodeURIComponent(scientificName)}&order=desc&order_by=observations_count`
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.results && data.results[0] && data.results[0].default_photo) {
|
||||||
|
console.log('Found image from iNaturalist:', data.results[0].default_photo);
|
||||||
|
return {
|
||||||
|
url: data.results[0].default_photo.medium_url,
|
||||||
|
attribution: data.results[0].default_photo.attribution,
|
||||||
|
license: data.results[0].default_photo.license_code
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('No image found for species:', scientificName);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching species images:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
@ -25,7 +25,7 @@ const client = createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
|||||||
// Test the connection
|
// Test the connection
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await client.from('species').select('id').limit(1);
|
const { error } = await client.from('species').select('id').limit(1);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Failed to connect to Supabase:', error.message);
|
console.error('Failed to connect to Supabase:', error.message);
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,7 +10,7 @@ import { queryClient } from './lib/query-client.ts'
|
|||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter basename="/arctic-species-2025-frontend">
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
@ -51,60 +51,92 @@ export function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto my-8 space-y-8 px-4">
|
<div className="container mx-auto my-8 space-y-8 px-4">
|
||||||
<div className="text-center">
|
<div className="text-center max-w-3xl mx-auto">
|
||||||
<h1 className="mb-2 text-4xl font-bold">Arctic Species Tracker</h1>
|
<h1 className="mb-4 text-5xl font-bold tracking-tight text-gray-900 dark:text-gray-100">Arctic Species Tracker</h1>
|
||||||
<p className="mb-6 text-muted-foreground">
|
<p className="mb-4 text-base font-semibold text-blue-600 dark:text-blue-400">**Work in Progress!**</p>
|
||||||
|
<div className="mb-6 text-base text-gray-600 dark:text-gray-300 space-y-1">
|
||||||
|
<p className="font-medium">A collaborative project between:</p>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<div className="font-medium">School of Humanities and Social Sciences</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p>Thomas Barry</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Dean of School</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 space-y-1">
|
||||||
|
<p>Magnus Smari Smarason</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">AI Project Manager</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 font-medium">University of Akureyri</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mb-8 text-lg text-gray-600 dark:text-gray-300">
|
||||||
Track conservation status, CITES listings, and trade data for Arctic species
|
Track conservation status, CITES listings, and trade data for Arctic species
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-2">
|
<div className="flex justify-center gap-4">
|
||||||
<Button onClick={handleToggleSearch} variant={showSearch ? "default" : "outline"}>
|
<Button
|
||||||
<Search className="mr-2 h-4 w-4" />
|
onClick={handleToggleSearch}
|
||||||
|
variant={showSearch ? "default" : "outline"}
|
||||||
|
className="min-w-[160px] h-11"
|
||||||
|
>
|
||||||
|
<Search className="mr-2 h-5 w-5" />
|
||||||
Search Species
|
Search Species
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setShowSearch(false)} variant={!showSearch && !selectedSpeciesId ? "default" : "outline"}>
|
<Button
|
||||||
<Globe className="mr-2 h-4 w-4" />
|
onClick={() => setShowSearch(false)}
|
||||||
|
variant={!showSearch && !selectedSpeciesId ? "default" : "outline"}
|
||||||
|
className="min-w-[160px] h-11"
|
||||||
|
>
|
||||||
|
<Globe className="mr-2 h-5 w-5" />
|
||||||
Browse All Species
|
Browse All Species
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSearch && !selectedSpeciesId ? (
|
{/* Search and Results Section */}
|
||||||
<div className="mx-auto flex max-w-2xl flex-col items-center space-y-6">
|
<div className="mt-12">
|
||||||
<SearchForm onSelectSpecies={handleSelectSpecies} />
|
{showSearch && !selectedSpeciesId ? (
|
||||||
</div>
|
<div className="mx-auto flex max-w-2xl flex-col items-center space-y-6">
|
||||||
) : selectedSpeciesId ? (
|
<SearchForm onSelectSpecies={handleSelectSpecies} />
|
||||||
<ResultsContainer speciesId={selectedSpeciesId} onBack={handleBackToSearch} />
|
</div>
|
||||||
) : (
|
) : selectedSpeciesId ? (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
<ResultsContainer speciesId={selectedSpeciesId} onBack={handleBackToSearch} />
|
||||||
{isLoading ? (
|
) : (
|
||||||
<p className="col-span-full text-center">Loading species data...</p>
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
) : uniqueSpecies.length > 0 ? (
|
{isLoading ? (
|
||||||
uniqueSpecies.map(species => (
|
<div className="col-span-full flex justify-center items-center py-12">
|
||||||
<Card
|
<div className="flex items-center space-x-4 text-gray-500">
|
||||||
key={species.id}
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||||
className="cursor-pointer transition-all hover:shadow-md"
|
<p>Loading species data...</p>
|
||||||
onClick={() => handleSelectSpecies(species.id)}
|
</div>
|
||||||
>
|
</div>
|
||||||
<CardHeader className="p-4 pb-2">
|
) : uniqueSpecies.length > 0 ? (
|
||||||
<CardTitle className="text-lg">
|
uniqueSpecies.map(species => (
|
||||||
<span className="italic">{species.scientific_name}</span>
|
<Card
|
||||||
</CardTitle>
|
key={species.id}
|
||||||
</CardHeader>
|
className="cursor-pointer transition-all hover:shadow-lg hover:border-blue-200 dark:hover:border-blue-800"
|
||||||
<CardContent className="p-4 pt-0">
|
onClick={() => handleSelectSpecies(species.id)}
|
||||||
<p className="text-sm text-muted-foreground">{species.primary_common_name}</p>
|
>
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
<CardHeader className="p-4 pb-2">
|
||||||
<span className="rounded-sm bg-muted px-1.5 py-0.5 text-xs">
|
<CardTitle className="text-lg">
|
||||||
{species.family}
|
<span className="italic">{species.scientific_name}</span>
|
||||||
</span>
|
</CardTitle>
|
||||||
</div>
|
</CardHeader>
|
||||||
</CardContent>
|
<CardContent className="p-4 pt-0">
|
||||||
</Card>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{species.primary_common_name}</p>
|
||||||
))
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
) : (
|
<span className="rounded-full bg-gray-100 dark:bg-gray-800 px-3 py-1 text-xs font-medium text-gray-800 dark:text-gray-200">
|
||||||
<p className="col-span-full text-center">No species found</p>
|
{species.family}
|
||||||
)}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="col-span-full text-center text-gray-500 dark:text-gray-400 py-12">No species found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import path from 'path';
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: '/arctic-species-2025-frontend/',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
Reference in New Issue
Block a user