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
|
||||
**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.
|
||||
|
||||
@ -10,6 +19,8 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
||||
- Multiple common names support
|
||||
- Arctic subpopulation tracking
|
||||
- Region-specific conservation status
|
||||
- High-quality species images from iNaturalist
|
||||
- Interactive image viewer with attribution
|
||||
|
||||
- **CITES Information**
|
||||
- Complete CITES listing history
|
||||
@ -17,12 +28,15 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
||||
- Detailed trade records with filtering
|
||||
- Historical trade data analysis
|
||||
- Arctic-specific trade patterns
|
||||
- Interactive trade visualizations
|
||||
- Filterable trade records table
|
||||
|
||||
- **IUCN Assessments**
|
||||
- Latest IUCN Red List status
|
||||
- Historical assessment tracking
|
||||
- Population trend analysis
|
||||
- Arctic region-specific assessments
|
||||
- Color-coded status indicators
|
||||
|
||||
- **Timeline View**
|
||||
- Chronological view of species events
|
||||
@ -31,6 +45,13 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
||||
- Trade record history
|
||||
- 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
|
||||
|
||||
- **Frontend**: React + TypeScript + Vite
|
||||
@ -39,6 +60,8 @@ A comprehensive web application for tracking and analyzing Arctic species data,
|
||||
- **Database**: Supabase (PostgreSQL)
|
||||
- **State Management**: React Query
|
||||
- **Routing**: React Router
|
||||
- **Charts**: Recharts
|
||||
- **Image Integration**: iNaturalist API
|
||||
|
||||
## Getting Started
|
||||
|
||||
@ -86,6 +109,13 @@ The application uses the following main tables:
|
||||
- `cites_trade_records`: CITES trade data
|
||||
- `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
|
||||
|
||||
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
|
||||
- IUCN Red List for assessment data
|
||||
- iNaturalist for species images
|
||||
- Arctic Council for regional guidance
|
||||
- All contributors and maintainers
|
334
package-lock.json
generated
334
package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "arctic-species-tracker",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
@ -38,6 +39,7 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"gh-pages": "^6.3.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
@ -2611,6 +2649,13 @@
|
||||
"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": {
|
||||
"version": "10.4.21",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||
@ -2862,6 +2907,13 @@
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -3137,6 +3189,13 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "9.2.2",
|
||||
"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_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": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@ -3517,6 +3604,24 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@ -3586,6 +3691,21 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@ -3635,6 +3755,39 @@
|
||||
"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": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@ -3724,6 +3877,13 @@
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
@ -4011,6 +4171,19 @@
|
||||
"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": {
|
||||
"version": "4.5.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@ -4306,6 +4505,16 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@ -4428,6 +4637,75 @@
|
||||
"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": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
@ -5157,6 +5435,29 @@
|
||||
"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": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
@ -5332,6 +5633,29 @@
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"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": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||
@ -5403,6 +5727,16 @@
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"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": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
|
@ -3,13 +3,17 @@
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"homepage": "https://magnussmari.github.io/arctic-species-2025-frontend",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"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": {
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
@ -40,6 +44,7 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"gh-pages": "^6.3.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.2.2",
|
||||
|
@ -1,12 +1,10 @@
|
||||
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 { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getAllSpecies } from '@/lib/api';
|
||||
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 }) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@ -28,7 +26,7 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
|
||||
// Fetch search results
|
||||
const { data: searchResults, isLoading } = useQuery({
|
||||
queryKey: ['searchSpecies', debouncedTerm],
|
||||
queryFn: () => searchSpecies(debouncedTerm),
|
||||
queryFn: () => getAllSpecies(),
|
||||
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 (
|
||||
<div className="w-full max-w-2xl">
|
||||
<form onSubmit={handleSubmit} className="relative">
|
||||
@ -64,11 +68,11 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
|
||||
</form>
|
||||
|
||||
{/* Search Results */}
|
||||
{searchResults && searchResults.length > 0 && (
|
||||
{filteredResults && filteredResults.length > 0 && (
|
||||
<Card className="mt-2">
|
||||
<CardContent className="p-2">
|
||||
<ul className="divide-y divide-border">
|
||||
{searchResults.map((species) => (
|
||||
{filteredResults.map((species) => (
|
||||
<li
|
||||
key={species.id}
|
||||
className="cursor-pointer p-2 hover:bg-muted"
|
||||
@ -83,7 +87,7 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
|
@ -4,10 +4,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { SpeciesDetails, TimelineEvent, CitesTradeRecord } from "@/lib/api";
|
||||
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 { Loader2, Filter, BarChart3, PieChart, LineChart } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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";
|
||||
@ -26,8 +25,13 @@ import {
|
||||
PieChart as RechartsPieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
Sector
|
||||
} from "recharts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
type SpeciesTabsProps = {
|
||||
species: SpeciesDetails;
|
||||
@ -122,10 +126,18 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
||||
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
|
||||
const [yearFilter, setYearFilter] = useState<string>("all");
|
||||
const [termFilter, setTermFilter] = useState<string>("all");
|
||||
const [appendixFilter, setAppendixFilter] = useState<string>("all");
|
||||
const [isImageDialogOpen, setIsImageDialogOpen] = useState(false);
|
||||
|
||||
// Debug: Log CITES listings data
|
||||
console.log("CITES listings count:", species.cites_listings?.length);
|
||||
@ -270,7 +282,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
||||
}, [tradeRecords]);
|
||||
|
||||
// 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 radius = outerRadius * 1.1;
|
||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
@ -326,10 +338,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
||||
};
|
||||
|
||||
// Determine default tab based on available data
|
||||
const defaultTab =
|
||||
species.cites_listings && species.cites_listings.length > 0 ? "cites" :
|
||||
species.iucn_assessments && species.iucn_assessments.length > 0 ? "iucn" :
|
||||
"overview";
|
||||
const defaultTab = "overview"; // Always start with Overview tab
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col">
|
||||
@ -360,6 +369,66 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
||||
<CardDescription>Taxonomic classification and basic info</CardDescription>
|
||||
</CardHeader>
|
||||
<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">
|
||||
<dt className="font-semibold">Scientific Name:</dt>
|
||||
<dd className="italic">{species.scientific_name}</dd>
|
||||
@ -852,11 +921,11 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
||||
dataKey="count"
|
||||
nameKey="term"
|
||||
>
|
||||
{visualizationData.termsTraded.map((entry, index) => (
|
||||
{visualizationData.termsTraded.map((_, index) => (
|
||||
<Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(value, name, props) => [`${value} records`, props.payload.term]} />
|
||||
<Tooltip formatter={(value, _, props) => [`${value} records`, props.payload.term]} />
|
||||
</RechartsPieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@ -891,7 +960,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
||||
tickFormatter={(value) => `${value} - ${PURPOSE_DESCRIPTIONS[value] || 'Unknown'}`}
|
||||
width={150}
|
||||
/>
|
||||
<Tooltip formatter={(value, name, props) => [
|
||||
<Tooltip formatter={(value, _, props) => [
|
||||
`${value} records`,
|
||||
`${props.payload.purpose} - ${props.payload.description}`
|
||||
]} />
|
||||
@ -928,7 +997,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) {
|
||||
tickFormatter={(value) => `${value} - ${SOURCE_DESCRIPTIONS[value] || 'Unknown'}`}
|
||||
width={150}
|
||||
/>
|
||||
<Tooltip formatter={(value, name, props) => [
|
||||
<Tooltip formatter={(value, _, props) => [
|
||||
`${value} records`,
|
||||
`${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;
|
||||
};
|
||||
|
||||
// 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() {
|
||||
try {
|
||||
console.log("Fetching all species from database...");
|
||||
@ -380,3 +367,41 @@ export async function searchSpecies(query: string) {
|
||||
|
||||
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
|
||||
(async () => {
|
||||
try {
|
||||
const { data, error } = await client.from('species').select('id').limit(1);
|
||||
const { error } = await client.from('species').select('id').limit(1);
|
||||
if (error) {
|
||||
console.error('Failed to connect to Supabase:', error.message);
|
||||
} else {
|
||||
|
@ -10,7 +10,7 @@ import { queryClient } from './lib/query-client.ts'
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<BrowserRouter basename="/arctic-species-2025-frontend">
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
|
@ -51,23 +51,49 @@ export function HomePage() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto my-8 space-y-8 px-4">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-2 text-4xl font-bold">Arctic Species Tracker</h1>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
<div className="text-center max-w-3xl mx-auto">
|
||||
<h1 className="mb-4 text-5xl font-bold tracking-tight text-gray-900 dark:text-gray-100">Arctic Species Tracker</h1>
|
||||
<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
|
||||
</p>
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button onClick={handleToggleSearch} variant={showSearch ? "default" : "outline"}>
|
||||
<Search className="mr-2 h-4 w-4" />
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button
|
||||
onClick={handleToggleSearch}
|
||||
variant={showSearch ? "default" : "outline"}
|
||||
className="min-w-[160px] h-11"
|
||||
>
|
||||
<Search className="mr-2 h-5 w-5" />
|
||||
Search Species
|
||||
</Button>
|
||||
<Button onClick={() => setShowSearch(false)} variant={!showSearch && !selectedSpeciesId ? "default" : "outline"}>
|
||||
<Globe className="mr-2 h-4 w-4" />
|
||||
<Button
|
||||
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
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Results Section */}
|
||||
<div className="mt-12">
|
||||
{showSearch && !selectedSpeciesId ? (
|
||||
<div className="mx-auto flex max-w-2xl flex-col items-center space-y-6">
|
||||
<SearchForm onSelectSpecies={handleSelectSpecies} />
|
||||
@ -75,14 +101,19 @@ export function HomePage() {
|
||||
) : selectedSpeciesId ? (
|
||||
<ResultsContainer speciesId={selectedSpeciesId} onBack={handleBackToSearch} />
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{isLoading ? (
|
||||
<p className="col-span-full text-center">Loading species data...</p>
|
||||
<div className="col-span-full flex justify-center items-center py-12">
|
||||
<div className="flex items-center space-x-4 text-gray-500">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||
<p>Loading species data...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : uniqueSpecies.length > 0 ? (
|
||||
uniqueSpecies.map(species => (
|
||||
<Card
|
||||
key={species.id}
|
||||
className="cursor-pointer transition-all hover:shadow-md"
|
||||
className="cursor-pointer transition-all hover:shadow-lg hover:border-blue-200 dark:hover:border-blue-800"
|
||||
onClick={() => handleSelectSpecies(species.id)}
|
||||
>
|
||||
<CardHeader className="p-4 pb-2">
|
||||
@ -91,9 +122,9 @@ export function HomePage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0">
|
||||
<p className="text-sm text-muted-foreground">{species.primary_common_name}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
<span className="rounded-sm bg-muted px-1.5 py-0.5 text-xs">
|
||||
<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">
|
||||
{species.family}
|
||||
</span>
|
||||
</div>
|
||||
@ -101,10 +132,11 @@ export function HomePage() {
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<p className="col-span-full text-center">No species found</p>
|
||||
<p className="col-span-full text-center text-gray-500 dark:text-gray-400 py-12">No species found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import path from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/arctic-species-2025-frontend/',
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
Reference in New Issue
Block a user