From a5a1e2bdf57e4a320e5fdccfb73609ab8cac3845 Mon Sep 17 00:00:00 2001 From: Magnus Smari Smarason Date: Tue, 25 Mar 2025 12:28:20 +0000 Subject: [PATCH] feat: Enhanced UI and added species images - Added iNaturalist integration, improved header styling, project info, species cards, loading states, and dark mode support --- README.md | 31 +++ package-lock.json | 334 ++++++++++++++++++++++++++++++++ package.json | 7 +- src/components/search-form.tsx | 24 ++- src/components/species-tabs.tsx | 95 +++++++-- src/components/ui/dialog.tsx | 122 ++++++++++++ src/lib/api.ts | 51 +++-- src/lib/supabase.ts | 2 +- src/main.tsx | 2 +- src/pages/home.tsx | 122 +++++++----- vite.config.ts | 1 + 11 files changed, 707 insertions(+), 84 deletions(-) create mode 100644 src/components/ui/dialog.tsx diff --git a/README.md b/README.md index 672ac8b..3bc6a36 100644 --- a/README.md +++ b/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 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7aee180..22ec8b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 67080fd..aca31f2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/search-form.tsx b/src/components/search-form.tsx index 1ab539f..5144df7 100644 --- a/src/components/search-form.tsx +++ b/src/components/search-form.tsx @@ -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 (
@@ -64,11 +68,11 @@ export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string)
{/* Search Results */} - {searchResults && searchResults.length > 0 && ( + {filteredResults && filteredResults.length > 0 && (
    - {searchResults.map((species) => ( + {filteredResults.map((species) => (
  • )} - {searchResults && searchResults.length === 0 && debouncedTerm.length >= 3 && ( + {filteredResults && filteredResults.length === 0 && debouncedTerm.length >= 3 && (

    No species found matching your search.

    )}
diff --git a/src/components/species-tabs.tsx b/src/components/species-tabs.tsx index 8ff9817..81b870d 100644 --- a/src/components/species-tabs.tsx +++ b/src/components/species-tabs.tsx @@ -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("all"); const [termFilter, setTermFilter] = useState("all"); const [appendixFilter, setAppendixFilter] = useState("all"); + const [isImageDialogOpen, setIsImageDialogOpen] = useState(false); // Debug: Log CITES listings data console.log("CITES listings count:", species.cites_listings?.length); @@ -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 (
@@ -360,6 +369,66 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) { Taxonomic classification and basic info + {/* Add image display */} +
imageData && setIsImageDialogOpen(true)} + > + {imageData ? ( + <> + {species.scientific_name} +
+ {imageData.attribution} +
+
+ +
+ + ) : ( +
+ + No image available +
+ )} +
+ + {/* Add Dialog for full-size image */} + + + + + {species.scientific_name} + + + + {imageData && ( +
+ {species.scientific_name} +
+

{imageData.attribution}

+ {imageData.license && ( +

License: {imageData.license}

+ )} +
+
+ )} +
+
+
Scientific Name:
{species.scientific_name}
@@ -852,11 +921,11 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) { dataKey="count" nameKey="term" > - {visualizationData.termsTraded.map((entry, index) => ( + {visualizationData.termsTraded.map((_, index) => ( ))} - [`${value} records`, props.payload.term]} /> + [`${value} records`, props.payload.term]} />
@@ -891,7 +960,7 @@ export function SpeciesTabs({ species }: SpeciesTabsProps) { tickFormatter={(value) => `${value} - ${PURPOSE_DESCRIPTIONS[value] || 'Unknown'}`} width={150} /> - [ + [ `${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} /> - [ + [ `${value} records`, `${props.payload.source} - ${props.payload.description}` ]} /> diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..b3e6779 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 55673c9..29a6686 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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..."); @@ -379,4 +366,42 @@ 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; + } } \ No newline at end of file diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index eb98a43..0fb48ef 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -25,7 +25,7 @@ const client = createClient(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 { diff --git a/src/main.tsx b/src/main.tsx index 4a6cda0..61b05b4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,7 +10,7 @@ import { queryClient } from './lib/query-client.ts' ReactDOM.createRoot(document.getElementById('root')!).render( - + diff --git a/src/pages/home.tsx b/src/pages/home.tsx index acd07df..b919dd9 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -51,60 +51,92 @@ export function HomePage() { return (
-
-

Arctic Species Tracker

-

+

+

Arctic Species Tracker

+

**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
+
+
+

Track conservation status, CITES listings, and trade data for Arctic species

-
- -
- {showSearch && !selectedSpeciesId ? ( -
- -
- ) : selectedSpeciesId ? ( - - ) : ( -
- {isLoading ? ( -

Loading species data...

- ) : uniqueSpecies.length > 0 ? ( - uniqueSpecies.map(species => ( - handleSelectSpecies(species.id)} - > - - - {species.scientific_name} - - - -

{species.primary_common_name}

-
- - {species.family} - -
-
-
- )) - ) : ( -

No species found

- )} -
- )} + {/* Search and Results Section */} +
+ {showSearch && !selectedSpeciesId ? ( +
+ +
+ ) : selectedSpeciesId ? ( + + ) : ( +
+ {isLoading ? ( +
+
+
+

Loading species data...

+
+
+ ) : uniqueSpecies.length > 0 ? ( + uniqueSpecies.map(species => ( + handleSelectSpecies(species.id)} + > + + + {species.scientific_name} + + + +

{species.primary_common_name}

+
+ + {species.family} + +
+
+
+ )) + ) : ( +

No species found

+ )} +
+ )} +
); } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 5075d07..bab059e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,7 @@ import path from 'path'; // https://vitejs.dev/config/ export default defineConfig({ + base: '/arctic-species-2025-frontend/', plugins: [react()], resolve: { alias: {