Initial commit: Arctic Species 2025 Frontend

This commit is contained in:
Magnus Smari Smarason
2025-03-25 11:59:55 +00:00
parent 0f958ef08c
commit 436b661acd
107 changed files with 4898 additions and 14243 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
VITE_SUPABASE_URL=your_supabase_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key

36
.gitignore vendored
View File

@ -1,6 +1,36 @@
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# production
build
dist
# misc
.DS_Store
server/public
vite.config.ts.*
*.tar.gz
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Supabase
.supabase
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

42
.replit
View File

@ -1,42 +0,0 @@
modules = ["nodejs-20", "bash", "web"]
run = "npm run dev"
hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
[nix]
channel = "stable-24_05"
[deployment]
deploymentTarget = "autoscale"
run = ["npm", "run", "start"]
build = ["npm", "run", "build"]
[[ports]]
localPort = 5000
externalPort = 80
[workflows]
runButton = "Project"
[[workflows.workflow]]
name = "Project"
mode = "parallel"
author = "agent"
[[workflows.workflow.tasks]]
task = "workflow.run"
args = "Start application"
[[workflows.workflow]]
name = "Start application"
author = "agent"
[workflows.workflow.metadata]
agentRequireRestartOnSave = false
[[workflows.workflow.tasks]]
task = "packager.installForAll"
[[workflows.workflow.tasks]]
task = "shell.exec"
args = "npm run dev"
waitForPort = 5000

263
README.md
View File

@ -1,227 +1,98 @@
# Species Information System
# Arctic Species 2025 Frontend
A comprehensive web application that integrates with multiple biodiversity APIs to provide detailed information about species, their conservation status, habitats, threats, and protection measures.
## Overview
This application allows users to search for species using the CITES+ API and IUCN Red List API, view detailed information about them, and save this information for future reference. The system provides a unified interface to access and compare data from multiple sources, making it easier for researchers, conservationists, and wildlife enthusiasts to gather comprehensive information about species.
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.
## Features
- **API Integration**:
- CITES+ API for species listings, legislation, distributions, and references
- IUCN Red List API for conservation status, threats, habitats, and conservation measures
- (Coming soon) GBIF for occurrence data
- (Coming soon) Wikidata for additional taxonomic information
- (Coming soon) SBDI Bioatlas for biodiversity data
- **Arctic Species Search & Details**
- Search by scientific name or common name
- Detailed species information including taxonomy
- Multiple common names support
- Arctic subpopulation tracking
- Region-specific conservation status
- **Search Functionality**:
- Search species by scientific name (Latin name)
- View recent search history
- API status indicators to confirm connections
- **CITES Information**
- Complete CITES listing history
- Current CITES status
- Detailed trade records with filtering
- Historical trade data analysis
- Arctic-specific trade patterns
- **Detailed Species Information**:
- Basic taxonomic data (kingdom, phylum, class, order, family, genus)
- CITES listing status and appendices
- Geographic distribution
- Scientific references
- Conservation status (IUCN Red List category)
- Threats facing the species
- Habitat information
- Conservation measures
- **IUCN Assessments**
- Latest IUCN Red List status
- Historical assessment tracking
- Population trend analysis
- Arctic region-specific assessments
- **Data Management**:
- Save species information for offline access
- View previously saved species
- In-memory storage (with option for PostgreSQL database)
- **Timeline View**
- Chronological view of species events
- CITES listing changes
- IUCN assessment updates
- Trade record history
- Arctic conservation milestones
## Technical Stack
## Tech Stack
- **Frontend**:
- React with TypeScript
- TanStack Query for data fetching
- Shadcn UI components
- Tailwind CSS for styling
- React Hook Form for form handling
- **Backend**:
- Node.js with Express
- RESTful API architecture
- Axios for API requests
- Zod for validation
- In-memory storage (with PostgreSQL option)
## API Configuration
### CITES+ API
The application requires a CITES+ API token for authentication. To obtain a token:
1. Visit [https://api.speciesplus.net/documentation](https://api.speciesplus.net/documentation)
2. Register for an account
3. Generate an API token from your account dashboard
4. Enter the token in the application's authentication panel
### IUCN Red List API
The application supports both IUCN Red List API v3 and v4 versions:
#### IUCN API v3 (Legacy)
To use the v3 API:
1. Visit [https://apiv3.iucnredlist.org/api/v3/docs](https://apiv3.iucnredlist.org/api/v3/docs)
2. Register for an account and request an API key
3. The key will be automatically configured in the application's environment variables (`IUCN_API_KEY`)
#### IUCN API v4 (Recommended)
For enhanced functionality with the v4 API:
1. Visit [https://apiv3.iucnredlist.org/api/v4/docs](https://apiv3.iucnredlist.org/api/v4/docs)
2. Register for an account and request access to the v4 API
3. Generate a bearer token for the v4 API
4. Enter the token in the application's authentication panel (IUCN tab)
The application will intelligently use v4 if available, with automatic fallback to v3 when needed.
- **Frontend**: React + TypeScript + Vite
- **UI Components**: Shadcn/ui
- **Styling**: Tailwind CSS
- **Database**: Supabase (PostgreSQL)
- **State Management**: React Query
- **Routing**: React Router
## Getting Started
### Prerequisites
- Node.js 18.x or higher
- NPM 8.x or higher
### Installation
- Node.js (v18 or higher)
- npm or yarn
- Supabase account and project
### Environment Setup
1. Clone the repository:
```
git clone https://github.com/yourusername/species-information-system.git
cd species-information-system
```bash
git clone https://github.com/yourusername/arctic-species-2025-frontend.git
cd arctic-species-2025-frontend
```
2. Install dependencies:
```
```bash
npm install
```
3. Set up environment variables:
Create a `.env` file in the root directory and add:
3. Create a `.env` file in the root directory with your Supabase credentials:
```
IUCN_API_KEY=your_iucn_api_key
VITE_SUPABASE_URL=your_supabase_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
```
4. Start the development server:
```
```bash
npm run dev
```
5. Open your browser and navigate to `http://localhost:5000`
The application will be available at `http://localhost:5173`
### Using the Application
## Database Schema
1. **API Authentication**:
- Click the "API Token" button to open the authentication panel
- In the CITES tab, enter your CITES+ API token
- In the IUCN tab, enter your IUCN v4 bearer token (if available)
- The API status indicators will show if both APIs are successfully connected, including which version of the IUCN API is active
The application uses the following main tables:
2. **Searching for Species**:
- Enter a scientific name (e.g., "Panthera tigris") in the search box
- Select a species from the search results
- `species`: Core species information
- `common_names`: Alternative names for species
- `subpopulations`: Species subpopulation data
- `iucn_assessments`: IUCN Red List assessments
- `cites_listings`: CITES listing history
- `cites_trade_records`: CITES trade data
- `timeline_events`: Chronological events
3. **Viewing Species Information**:
- Navigate through the tabs to see different categories of information
- CITES Legislation: View protection status under CITES
- Distribution: See geographical range of the species
- References: View scientific publications about the species
- IUCN Status: View conservation status and population trend
- Threats: View factors threatening the species
- Habitats: View habitat information
- Conservation Measures: View protection and conservation efforts
## Contributing
4. **Saving Species**:
- Click the "Save Species" button to store information locally
- Access saved species from the sidebar
## API Status Indicators
The application provides real-time status indicators for both APIs:
- **CITES API**: Shows connection status to the CITES+ API
- **IUCN API**: Shows connection status to the IUCN Red List API
Hovering over each indicator will show additional details about the connection status.
## Error Handling
The application includes comprehensive error handling for API connections:
- Clear visual feedback when APIs are not connected
- Detailed error messages in tooltips
- Fallback to saved data when APIs are unavailable
## Data Processing
To improve reliability and prevent API errors, the application processes scientific names before querying the IUCN API:
- Scientific names are simplified to genus and species components
- This prevents 414 (URI too long) errors when querying with subspecies names
## Development
### Project Structure
```
/client
/src
/components - UI components
/hooks - Custom React hooks
/lib - Utility functions and API client
/pages - Application pages
/server
/routes.ts - API endpoints
/storage.ts - Data storage implementation
/shared
/schema.ts - Shared data schemas
```
### Adding a New API
To add a new biodiversity API:
1. Define API endpoints and response types in `client/src/lib/api.ts`
2. Add server routes in `server/routes.ts`
3. Update the API status component in `client/src/components/api-status.tsx`
4. Add new data display components as needed
5. Update the species tabs in `client/src/components/species-tabs.tsx`
### Testing
Run tests with:
```
npm test
```
## Limitations and Known Issues
- Some species may not have data in all API sources
- API rate limits may apply (refer to API documentation for details)
- IUCN API v3 may return 414 errors for very long scientific names (this is minimized in the implementation)
- IUCN API v4 requires a separate bearer token authentication
## API Version Handling
The application intelligently manages API versions with the following approach:
- **IUCN API Version Selection**:
- The system first attempts to use IUCN API v4 if a bearer token is available
- If v4 returns an error or isn't available, the system automatically falls back to v3
- All API responses include an `apiVersion` field indicating which version was used
- **Version-Specific Authentication**:
- CITES+ API: Uses a token-based authentication via query parameter
- IUCN v3: Uses an API key via environment variable and query parameter
- IUCN v4: Uses OAuth 2.0 Bearer token authentication via headers
## Future Enhancements
- Integration with GBIF, Wikidata, and SBDI Bioatlas
- Offline mode with IndexedDB storage
- Export functionality for research data
- Visualization tools for geographic distribution
- User accounts for saving preferences and searches
- Bulk data import/export features
- Mobile application version
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## License
@ -229,11 +100,7 @@ This project is licensed under the MIT License - see the LICENSE file for detail
## Acknowledgments
- CITES+ API for providing species trade and protection data
- IUCN Red List API for conservation status data
- All contributors to the biodiversity data sources used in this application
## Contact
For questions, feedback, or contributions, please reach out to:
[Your Contact Information]
- CITES Secretariat for trade data
- IUCN Red List for assessment data
- Arctic Council for regional guidance
- All contributors and maintainers

View File

@ -1,26 +0,0 @@
11:28:51 PM [express] serving on port 5000
11:29:02 PM [express] GET /api/cites/status 401 in 2ms :: {"success":false,"connected":false,"message…
11:29:02 PM [express] GET /api/species 200 in 0ms :: {"success":true,"species":[]}
11:29:02 PM [express] GET /api/searches 200 in 1ms :: {"success":true,"searches":[]}
IUCN API check failed: getaddrinfo ENOTFOUND apiv4.iucnredlist.org
11:29:02 PM [express] GET /api/iucn/status 500 in 93ms :: {"success":false,"connected":false,"message…
11:29:03 PM [express] GET /api/cites/status 401 in 1ms :: {"success":false,"connected":false,"message…
11:29:03 PM [express] GET /api/species 304 in 1ms :: {"success":true,"species":[]}
11:29:03 PM [express] GET /api/searches 200 in 1ms :: {"success":true,"searches":[]}
IUCN API check failed: getaddrinfo ENOTFOUND apiv4.iucnredlist.org
11:29:03 PM [express] GET /api/iucn/status 500 in 28ms :: {"success":false,"connected":false,"message…
11:29:32 PM [express] POST /api/token 200 in 8787ms :: {"success":true,"token":{"id":1,"token":"vypRg…
11:29:32 PM [express] GET /api/token 200 in 1ms :: {"token":"vypRgcZfUGD4nCr8jt9Qkwtt","iucnToken":nu…
11:29:43 PM [express] GET /api/species/search 200 in 4033ms :: {"success":true,"data":{"pagination":{…
IUCN API habitats lookup failed: getaddrinfo ENOTFOUND apiv4.iucnredlist.org
11:29:43 PM [express] GET /api/iucn/habitats 500 in 13ms :: {"success":false,"message":"Error from IU…
IUCN API threats lookup failed: getaddrinfo ENOTFOUND apiv4.iucnredlist.org
11:29:43 PM [express] GET /api/iucn/threats 500 in 8ms :: {"success":false,"message":"Error from IUCN…
IUCN API species lookup failed: getaddrinfo ENOTFOUND apiv4.iucnredlist.org
11:29:43 PM [express] GET /api/iucn/species 500 in 6ms :: {"success":false,"message":"Error from IUCN…
11:29:43 PM [express] GET /api/species/8084 200 in 136ms :: {"success":true,"data":{"cites_listings":…
IUCN API conservation measures lookup failed: getaddrinfo ENOTFOUND apiv4.iucnredlist.org
11:29:43 PM [express] GET /api/iucn/measures 500 in 6ms :: {"success":false,"message":"Error from IUC…
11:29:43 PM [express] GET /api/species/8084 200 in 290ms :: {"success":true,"data":[{"id":19400,"iso_…
11:29:44 PM [express] GET /api/species/8084 200 in 309ms :: {"success":true,"data":[{"id":6292,"citat…

View File

@ -1,378 +0,0 @@
API V4
v4
OAS 3.0
/api-docs/v4/openapi.yaml
Authorize
Assessment
Return assessment data relating to a specific assessment_id. You do not need to supply a latest assessment_id; you can therefore return data on latest and historic assessments if the assessment_id is known. You can gather a list of current and historic assessment_id for a specific species by using api/v4/taxa/scientific_name/{scientific_name}
GET
/api/v4/assessment/{assessment_id}
Retrieves an assessment
Biogeographical Realms
Return the latest assessments for a biogeographical realm.
GET
/api/v4/biogeographical_realms/
Returns a list of biogeographic realm codes
GET
/api/v4/biogeographical_realms/{code}
Returns a collection of assessments for a biogeographical realm code
Comprehensive Groups
Return the latest assessments for a comprehensive group
GET
/api/v4/comprehensive_groups/
Returns a list of comprehensive groups
GET
/api/v4/comprehensive_groups/{name}
Returns a collection of assessments for a comprehensive group name
Conservation Actions
Description coming soon...
GET
/api/v4/conservation_actions/
returns a list of conservation actions
GET
/api/v4/conservation_actions/{code}
returns a collection of assessments for a conservation action code
Countries
Return the latest assessments for a given country.
GET
/api/v4/countries/
Returns a list of countries by ISO alpha-2 code
GET
/api/v4/countries/{code}
Returns a collection of assessments for a given country ISO alpha-2 code
FAOs
Documentation coming soon..
GET
/api/v4/faos/
Returns a list of FAOs
GET
/api/v4/faos/{code}
Returns a collection of assessments for an FAO code
Growth Forms
Return the latest assessments for a given growth form.
GET
/api/v4/growth_forms/
Returns a list of growth forms
GET
/api/v4/growth_forms/{code}
Returns a collection of assessments for a given growth form code
Green Status
Return the latest Green Status assessments.
GET
/api/v4/green_status/all
Returns a list of all Green Status assessments
Habitats
Return the latest assessments for a given habitat (e.g. Forest - Temperate or Marine Intertidal). These habitat codes correspond to the IUCN Red List Habitats Classification Scheme (v3.1)
GET
/api/v4/habitats/
Returns a list of habitat codes.
GET
/api/v4/habitats/{code}
Returns a collection of assessments for a given habitat code
Information
Endpoints for supplying general API-specific information.
GET
/api/v4/information/api_version
Returns the current version number of the IUCN Red List of Threatened Species API
GET
/api/v4/information/red_list_version
Returns the current IUCN Red List of Threatened Species version
Population Trends
Return a list of the latest assessments based on a population trend (i.e. increasing, decreasing stable or unknown)
GET
/api/v4/population_trends/
Returns a list of population trends
GET
/api/v4/population_trends/{code}
Returns a collection of assessments for a given population trend code
Red List Categories
Return a list of the latest assessments for a given category. (Not Evaluated, Data Deficient, Least Concern, Near Threatened, Vulnerable, Endangered, Critically Endangered, Extinct in the Wild and Extinct).
GET
/api/v4/red_list_categories/
Returns a list of Red List categories
GET
/api/v4/red_list_categories/{code}
Returns a collection of assessments for a given Red List category code
Research
Return a list of the latest assessments for a given research category.
GET
/api/v4/research/
Returns a list of habitat codes.
GET
/api/v4/research/{code}
Returns a collection of assessments for a given research code
Scopes
Return latest assessments for a given geographical assessment scope (e.g. Global, Mediterranean)
GET
/api/v4/scopes/
returns a list of scopes
GET
/api/v4/scopes/{code}
Returns a collection of assessments for a given scope code
Statistics
Return aggregated statistics.
GET
/api/v4/statistics/count
Return count of the number of species with assessments
Stresses
Return a list of the latest assessments based on the stresses species are subject to (e.g. Ecosystem degradation, species disturbance etc.)
GET
/api/v4/stresses/
Returns a list of stressors
GET
/api/v4/stresses/{code}
returns a collection of assessments for a given stress code
Systems
Return assessments belonging to broad systems (e.g. terrestrial, freshwater or marine)
GET
/api/v4/systems/
Returns a list of systems
GET
/api/v4/systems/{code}
Returns a collection of assessments for a given system code
Taxa
Return summary assessment data at the species level and other higher taxonomic levels (kingdom to family, inclusive). When querying at the species level, the API returns the latest and historic assessment information.
GET
/api/v4/taxa/sis/{sis_id}
Returns a collection of assessments for a given SIS id
GET
/api/v4/taxa/scientific_name
Returns a collection of assessments for a given genus_name and species_name (i.e. Latin binomial) and optional infra_name (i.e. Latin trinomial)
GET
/api/v4/taxa/kingdom/
Returns a list of all kingdom names
GET
/api/v4/taxa/kingdom/{kingdom_name}
Returns a collection of the latests assessments for a given kingdom_name
GET
/api/v4/taxa/phylum/
Returns a list of all phylum names
GET
/api/v4/taxa/phylum/{phylum_name}
Returns a collection of the latests assessments for a given phylum_name
GET
/api/v4/taxa/class/
Returns a list of all class names
GET
/api/v4/taxa/class/{class_name}
Returns a collection of the latests assessments for a given class_name
GET
/api/v4/taxa/order/
Returns a list of all order names
GET
/api/v4/taxa/order/{order_name}
Returns a collection of the latests assessments for a given order_name
GET
/api/v4/taxa/family/
Returns a list of all family names
GET
/api/v4/taxa/family/{family_name}
Returns a collection of the latests assessments for a given family_name
GET
/api/v4/taxa/possibly_extinct
Returns a collection of the all latest global assessments for taxa that are possibly extinct
GET
/api/v4/taxa/possibly_extinct_in_the_wild
Returns a collection of the all latest global assessments for taxa that are possibly extinct in the wild
Threats
Return a list of the latest assessments which are subject to a specific threat (e.g. energy production and mining, climate change and severe weather)
GET
/api/v4/threats/
Returns a list of threats
GET
/api/v4/threats/{code}
Returns a collection of assessments for a given threat code
Use and Trade
Return a list of the latest assessments which are subject to a specific uses and trade.
GET
/api/v4/use_and_trade/
Returns a list of use and trades
GET
/api/v4/use_and_trade/{code}
Returns a collection of assessments for a given use and t

View File

@ -1,48 +0,0 @@
Feedback
Species+
Species+API DocumentationSign InRegister an Account
Species+/CITES Checklist API Documentation
Application Programming Interface (API) to support CITES Parties to increase the accuracy and efficiency of curating CITES species data for permitting purposes.
Getting Started
Signing up for API access
You can sign up for an API account here. Once you have signed up, visit Sign in to log in to the API Dashboard and retrieve your generated token.
Authenticating your requests
Your authentication token needs to be passed into every request you make via the HTTP header below:
X-Authentication-Token: 8QW6Qgh57sBG2k0gtt
Tokens can be manually regenerated from the API Dashboard
Tools for testing the API
There are a number of free tools available that allow you to test the API before you start integrating it in your systems. For example, curl is a popular command-line tool that could be used for this purpose:
curl -i "https://api.speciesplus.net/api/v1/taxon_concepts.xml?name=Mammalia" -H "X-Authentication-Token:YOUR_TOKEN_HERE"
Please note: we now support encrypted connections, please switch your integrations to use https://api.speciesplus.net.
There are also a number of tools that can be installed as an add-on to your browser; they can be found for example by searching for "rest client" in the add-ons repository for your browser.
Formats
All endpoint can return both JSON and XML data. The default is JSON, if you would like to receive XML data, you can add .xml to the endpoint as below:
https://api.speciesplus.net/api/v1/taxon_concepts.xml
Optional parameters
Whereas authentication is passed via a HTTP header, other parameters for refining your response data are provided via the query string. These are detailed below and where appropriate in the documentation for each endpoint. Parameters can be combined.
API Calls
CITES Legislation
HTTP Verb / Endpoint Description
[GET] api/v1/taxon_concepts/:taxon_concept_id/cites_legislation Lists current CITES appendix listings and reservations, CITES quotas, and CITES suspensions for a given taxon concept
Distributions
HTTP Verb / Endpoint Description
[GET] api/v1/taxon_concepts/:taxon_concept_id/distributions Lists distributions for a given taxon concept
EU Legislation
HTTP Verb / Endpoint Description
[GET] api/v1/taxon_concepts/:taxon_concept_id/eu_legislation Lists current EU annex listings, SRG opinions, and EU suspensions for a given taxon concept
References
HTTP Verb / Endpoint Description
[GET] api/v1/taxon_concepts/:taxon_concept_id/references Lists references for a given taxon concept
Taxon Concepts
HTTP Verb / Endpoint Description
[GET] api/v1/taxon_concepts Lists taxon concepts
CITES logoPowered by logo
Send feedback

View File

@ -1,58 +0,0 @@
Got it, you need the essential information to use the IUCN Red List API V4, without the code. Here's a concise guide:
**1. Base URL:**
* The base URL for all API requests is: `https://apiv4.iucnredlist.org`
**2. Authentication:**
* The API uses Bearer token authentication.
* You'll need to include an `Authorization` header in your HTTP requests.
* The header value should be: `Bearer YOUR_API_TOKEN` (replace `YOUR_API_TOKEN` with your actual token).
**3. Common Endpoints and Parameters:**
Here are a few common endpoints with examples of parameters you can pass:
* **Get a list of biogeographical realms:**
* Endpoint: `GET /api/v4/biogeographical_realms/`
* No parameters needed.
* **Get assessments for a biogeographical realm:**
* Endpoint: `GET /api/v4/biogeographical_realms/{code}` (replace `{code}` with the realm code, e.g., `NT`)
* Parameters (query parameters):
* `page`: Page number (integer).
* `year_published`: Filter by year published (integer).
* `latest`: Filter by latest (boolean).
* `possibly_extinct`: Filter by possibly extinct (boolean).
* `possibly_extinct_in_the_wild`: Filter by possibly extinct in the wild (boolean).
* `scope_code`: Filter by scope code (integer).
* **Get taxa by scientific name:**
* Endpoint: `GET /api/v4/taxa/scientific_name`
* Parameters (query parameters):
* `genus_name`: The genus name (string, required).
* `species_name`: The species name (string, required).
* `infra_name`: The infra-name (string, optional).
* `subpopulation_name`: The subpopulation name (string, optional).
* **Get assessments by kingdom name:**
* Endpoint: `GET /api/v4/taxa/kingdom/{kingdom_name}` (replace `{kingdom_name}` with the kingdom name, e.g., `Animalia`)
* Parameters (query parameters):
* `page`: Page number (integer).
* `year_published`: Filter by year published (integer).
* `latest`: Filter by latest (boolean).
* `scope_code`: Filter by scope code (integer).
**4. Request Example (using cURL):**
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" "https://apiv4.iucnredlist.org/api/v4/biogeographical_realms/NT?page=1"
```
**5. Response Format:**
* The API returns JSON responses.
**Important Notes:**
* Replace `YOUR_API_TOKEN` with your actual token.
* Refer to the official API documentation for a complete list of endpoints and parameters.
* The API documentation will also provide example responses, which will help you understand the returned data.

View File

@ -1,58 +0,0 @@
Got it, you need the essential information to use the IUCN Red List API V4, without the code. Here's a concise guide:
**1. Base URL:**
* The base URL for all API requests is: `https://apiv4.iucnredlist.org`
**2. Authentication:**
* The API uses Bearer token authentication.
* You'll need to include an `Authorization` header in your HTTP requests.
* The header value should be: `Bearer YOUR_API_TOKEN` (replace `YOUR_API_TOKEN` with your actual token).
**3. Common Endpoints and Parameters:**
Here are a few common endpoints with examples of parameters you can pass:
* **Get a list of biogeographical realms:**
* Endpoint: `GET /api/v4/biogeographical_realms/`
* No parameters needed.
* **Get assessments for a biogeographical realm:**
* Endpoint: `GET /api/v4/biogeographical_realms/{code}` (replace `{code}` with the realm code, e.g., `NT`)
* Parameters (query parameters):
* `page`: Page number (integer).
* `year_published`: Filter by year published (integer).
* `latest`: Filter by latest (boolean).
* `possibly_extinct`: Filter by possibly extinct (boolean).
* `possibly_extinct_in_the_wild`: Filter by possibly extinct in the wild (boolean).
* `scope_code`: Filter by scope code (integer).
* **Get taxa by scientific name:**
* Endpoint: `GET /api/v4/taxa/scientific_name`
* Parameters (query parameters):
* `genus_name`: The genus name (string, required).
* `species_name`: The species name (string, required).
* `infra_name`: The infra-name (string, optional).
* `subpopulation_name`: The subpopulation name (string, optional).
* **Get assessments by kingdom name:**
* Endpoint: `GET /api/v4/taxa/kingdom/{kingdom_name}` (replace `{kingdom_name}` with the kingdom name, e.g., `Animalia`)
* Parameters (query parameters):
* `page`: Page number (integer).
* `year_published`: Filter by year published (integer).
* `latest`: Filter by latest (boolean).
* `scope_code`: Filter by scope code (integer).
**4. Request Example (using cURL):**
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" "https://apiv4.iucnredlist.org/api/v4/biogeographical_realms/NT?page=1"
```
**5. Response Format:**
* The API returns JSON responses.
**Important Notes:**
* Replace `YOUR_API_TOKEN` with your actual token.
* Refer to the official API documentation for a complete list of endpoints and parameters.
* The API documentation will also provide example responses, which will help you understand the returned data.

View File

@ -1,58 +0,0 @@
Got it, you need the essential information to use the IUCN Red List API V4, without the code. Here's a concise guide:
**1. Base URL:**
* The base URL for all API requests is: `https://apiv4.iucnredlist.org`
**2. Authentication:**
* The API uses Bearer token authentication.
* You'll need to include an `Authorization` header in your HTTP requests.
* The header value should be: `Bearer YOUR_API_TOKEN` (replace `YOUR_API_TOKEN` with your actual token).
**3. Common Endpoints and Parameters:**
Here are a few common endpoints with examples of parameters you can pass:
* **Get a list of biogeographical realms:**
* Endpoint: `GET /api/v4/biogeographical_realms/`
* No parameters needed.
* **Get assessments for a biogeographical realm:**
* Endpoint: `GET /api/v4/biogeographical_realms/{code}` (replace `{code}` with the realm code, e.g., `NT`)
* Parameters (query parameters):
* `page`: Page number (integer).
* `year_published`: Filter by year published (integer).
* `latest`: Filter by latest (boolean).
* `possibly_extinct`: Filter by possibly extinct (boolean).
* `possibly_extinct_in_the_wild`: Filter by possibly extinct in the wild (boolean).
* `scope_code`: Filter by scope code (integer).
* **Get taxa by scientific name:**
* Endpoint: `GET /api/v4/taxa/scientific_name`
* Parameters (query parameters):
* `genus_name`: The genus name (string, required).
* `species_name`: The species name (string, required).
* `infra_name`: The infra-name (string, optional).
* `subpopulation_name`: The subpopulation name (string, optional).
* **Get assessments by kingdom name:**
* Endpoint: `GET /api/v4/taxa/kingdom/{kingdom_name}` (replace `{kingdom_name}` with the kingdom name, e.g., `Animalia`)
* Parameters (query parameters):
* `page`: Page number (integer).
* `year_published`: Filter by year published (integer).
* `latest`: Filter by latest (boolean).
* `scope_code`: Filter by scope code (integer).
**4. Request Example (using cURL):**
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" "https://apiv4.iucnredlist.org/api/v4/biogeographical_realms/NT?page=1"
```
**5. Response Format:**
* The API returns JSON responses.
**Important Notes:**
* Replace `YOUR_API_TOKEN` with your actual token.
* Refer to the official API documentation for a complete list of endpoints and parameters.
* The API documentation will also provide example responses, which will help you understand the returned data.

View File

@ -1,165 +0,0 @@
To gather detailed information about species by connecting to multiple APIs, follow these steps:
## Step 1: Connect to GBIF API for Taxonomic and Occurrence Data
1. **Install pygbif**: Use Python to install the `pygbif` library, which simplifies interactions with the GBIF API.
```bash
pip install pygbif
```
2. **Fetch Species Data**: Use the `species.name_lookup()` function to retrieve taxonomic information.
```python
from pygbif import species
# Example: Fetch data for 'Helianthus annuus'
data = species.name_lookup(q='Helianthus annuus', rank="species")
print(data)
```
## Step 2: Connect to Wikidata Query Service for Biological Attributes
1. **Access Wikidata Query Service**: Use the SPARQL endpoint at `https://query.wikidata.org/sparql`.
2. **Query for Biological Attributes**: Use SPARQL to fetch attributes like lifespan or population.
```sparql
# Example query to get lifespan of a species
PREFIX wdt:
PREFIX wd:
SELECT ?item ?itemLabel ?lifespan
WHERE {
?item wdt:P31 wd:Q15978631; # Species
wdt:P2114 ?lifespan. # Lifespan
SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
```
## Step 3: Connect to IUCN Red List API for Conservation Status
1. **Obtain an API Key**: Register at the IUCN Red List to get an API token.
2. **Fetch Conservation Status**: Use the `iucnredlist` R package or the IUCN API directly.
```r
# R example using iucnredlist package
library(iucnredlist)
api <- init_api("your_api_key")
assessment_raw <- assessment_data(api, 266696959)
assessment <- parse_assessment_data(assessment_raw)
print(assessment)
```
## Step 4: Connect to Swedish Biodiversity Data Infrastructure (SBDI) Bioatlas
1. **Access SBDI Bioatlas**: Use the SBDI guides to access regional biodiversity data.
2. **Fetch Regional Data**: Follow the step-by-step guides for using the Bioatlas APIs.
## Step 5: Combine Data
1. **Integrate Datasets**: Use programming languages like Python or R to merge data from different APIs based on species identifiers.
2. **Analyze and Visualize**: Use libraries like Pandas, NumPy, and Matplotlib (Python) or dplyr and ggplot2 (R) to analyze and visualize the combined data.
### Example Integration in Python
```python
import pandas as pd
from pygbif import species
# Fetch GBIF data
gbif_data = species.name_lookup(q='Helianthus annuus', rank="species")
# Fetch Wikidata data using SPARQL (example query above)
# For simplicity, assume you have a function to execute SPARQL queries
wikidata_data = execute_sparql_query(query)
# Fetch IUCN data (example using a hypothetical IUCN API function)
iucn_data = fetch_iucn_data(species_id)
# Combine data into a single DataFrame
combined_data = pd.DataFrame({
'GBIF': gbif_data,
'Wikidata': wikidata_data,
'IUCN': iucn_data
})
# Print combined data
print(combined_data)
```
This approach allows you to gather comprehensive information about species by leveraging multiple APIs.
Citations:
[1] https://pygbif.readthedocs.io/en/latest/modules/species.html
[2] https://techdocs.gbif.org/en/openapi/v1/species
[3] https://en.wikibooks.org/wiki/SPARQL/Wikidata_Query_Service
[4] https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/A_gentle_introduction_to_the_Wikidata_Query_Service
[5] https://iucn-uk.github.io/iucnredlist/
[6] https://publicapi.dev/iucn-api
[7] https://docs.biodiversitydata.se/tutorials/step-by-step-help-guides/
[8] https://biodiversitydata.se/explore-analyze/use-your-own-tools/our-apis/
[9] https://data-blog.gbif.org/post/gbif-api-beginners-guide/
[10] https://rdrr.io/github/bioatlas/SBDI4R/src/R/search_guids.R
[11] https://github.com/ManonGros/Small-scripts-using-GBIF-API
[12] https://www.youtube.com/watch?v=2h9LNS8C764
[13] https://www.nies.go.jp/biowm/en/GBIFSpecies/
[14] https://gbif.github.io/dwc-api/apidocs/org/gbif/dwc/terms/GbifTerm.html
[15] https://phylonext.github.io/inputdata/
[16] https://github.com/gbif/gbif-api/issues/82
[17] https://peerj.com/preprints/3304v1.pdf
[18] https://www.youtube.com/watch?v=netnQb-6F0M
[19] https://github.com/NaturalHistoryMuseum/gbif-name-resolution
[20] https://fairsharing.org/FAIRsharing.zv11j3
[21] https://www.youtube.com/watch?v=TXdjxnjCvng
[22] https://stackoverflow.com/questions/39773812/how-to-query-for-people-using-wikidata-and-sparql
[23] https://query.wikidata.org
[24] https://www.youtube.com/watch?v=B59vEET-nEk
[25] https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/Wikidata_Query_Help
[26] https://librarycarpentry.github.io/lc-wikidata/05-intro_to_querying.html
[27] https://stackoverflow.com/questions/45316749/how-to-create-a-local-wikidata-query-service
[28] https://wikidataworkshop.github.io/2022/papers/Wikidata_Workshop_2022_paper_2349.pdf
[29] https://addshore.com/2019/10/your-own-wikidata-query-service-with-no-limits/
[30] https://apiv3.iucnredlist.org
[31] https://api.iucnredlist.org/api-docs/index.html
[32] https://github.com/ropensci/rredlist
[33] https://cran.r-project.org/web/packages/rredlist/vignettes/rredlist.html
[34] https://pypi.org/project/IUCN-API/
[35] https://apiv3.iucnredlist.org/api/v3/docs
[36] https://api.iucnredlist.org
[37] https://github.com/ropensci/rredlist/issues/52
[38] https://github.com/biodiversitydata-se/repo-overview
[39] https://www.biodiversa.eu/wp-content/uploads/2024/01/Biodiversa-Governance-Sub-pilot-Sweden.pdf
[40] https://community.atlassian.com/t5/Atlassian-Home-questions/Can-we-interact-with-Atlas-API/qaq-p/2427472
[41] https://biodiversitydata-se.github.io/r-tools-tutorial/
[42] https://github.com/biodiversitydata-se/documentation-overview
[43] https://docs.beyondtrust.com/entitle/docs/configuring-mongodb-atlas-api-key
[44] https://docs.biodiversitydata.se/analyse-data/
[45] https://www.frontiersin.org/journals/molecular-biosciences/articles/10.3389/fmolb.2022.926623/full
[46] https://github.com/bioatlas/r-functionality/
[47] https://chemrxiv.org/engage/api-gateway/chemrxiv/assets/orp/resource/item/6540eb2548dad23120c52242/original/the-hitchhiker-s-guide-to-statistical-analysis-of-feature-based-molecular-networks-from-non-targeted-metabolomics-data.pdf
[48] https://rdrr.io/github/bioatlas/SBDI4R/man/sbdi_lists.html
[49] https://royalsocietypublishing.org/doi/10.1098/rspb.2002.2218
[50] https://docs.ropensci.org/rgbif/articles/gbif_sql_downloads.html
[51] https://github.com/ManonGros/Small-scripts-using-GBIF-API/blob/master/query_species_list/functions_query_from_species_list.py
[52] https://techdocs.gbif.org/en/openapi/
[53] https://aubreymoore.github.io/blog/posts/using-the-species-api-to-mine-the-gbif-backbone-taxonomy/
[54] https://tutorials.inbo.be/tutorials/r_gbif_checklist/
[55] https://metacpan.org/dist/App-wdq/view/script/wdq
[56] https://www.mediawiki.org/wiki/Wikidata_Query_Service/User_Manual/ru
[57] https://www.mediawiki.org/wiki/Wikidata_Query_Service/User_Manual
[58] https://wikitech.wikimedia.org/wiki/Wikidata_Query_Service
[59] https://flograttarola.com/post/rbiodiversidata/
[60] https://publicapis.io/iucn-species-database-api
[61] https://cran.r-project.org/web/packages/rredlist/rredlist.pdf
[62] https://github.com/CerrenRichards/IUCN-Threats
[63] https://git-og.github.io/EasyOpenRedList/
[64] https://www.youtube.com/watch?v=4T6GXtptmj4
[65] https://academic.oup.com/nargab/article/5/1/lqad003/6997971
[66] https://docs.biodiversitydata.se/tutorials/step-by-step-help-guides/setting-up-and-using-your-sbdi-bioatlas-account/
[67] https://apidocs.tbauctions.com/getting-started
[68] https://biodiversitydata-se.github.io/mol-data/
[69] https://www.biorxiv.org/content/10.1101/2022.08.29.505468.full
---
Answer from Perplexity: pplx.ai/share

View File

@ -1,26 +0,0 @@
import { Switch, Route } from "wouter";
import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster";
import NotFound from "@/pages/not-found";
import Home from "@/pages/home";
function Router() {
return (
<Switch>
<Route path="/" component={Home} />
<Route component={NotFound} />
</Switch>
);
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
<Toaster />
</QueryClientProvider>
);
}
export default App;

View File

@ -1,117 +0,0 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { apiClient } from "@/lib/api";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
interface ApiStatusProps {
citesToken: string | null;
iucnToken?: string | null;
}
export default function ApiStatus({ citesToken, iucnToken }: ApiStatusProps) {
const [iucnStatus, setIucnStatus] = useState<'checking' | 'connected' | 'error'>('checking');
const [citesStatus, setCitesStatus] = useState<'checking' | 'connected' | 'error'>('checking');
// Check CITES API status
const {
data: citesApiData,
isLoading: isCitesLoading
} = useQuery({
queryKey: ['api-status-check-cites'],
queryFn: async () => {
return await apiClient.checkCitesApiStatus();
},
retry: 1,
staleTime: 60000, // 1 minute
});
// Check IUCN API status
const {
data: iucnApiData,
isLoading: isIucnLoading
} = useQuery({
queryKey: ['api-status-check-iucn'],
queryFn: async () => {
return await apiClient.checkIucnApiStatus();
},
retry: 1,
staleTime: 60000, // 1 minute
});
useEffect(() => {
if (isCitesLoading) {
setCitesStatus('checking');
} else if (citesApiData?.connected) {
setCitesStatus('connected');
} else {
setCitesStatus('error');
}
}, [isCitesLoading, citesApiData]);
useEffect(() => {
if (isIucnLoading) {
setIucnStatus('checking');
} else if (iucnApiData?.connected) {
setIucnStatus('connected');
} else {
setIucnStatus('error');
}
}, [isIucnLoading, iucnApiData]);
return (
<div className="flex items-center space-x-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Badge
variant={citesStatus === 'connected' ? 'default' : 'destructive'}
className="text-xs px-2 py-0.5"
>
CITES API: {citesStatus === 'checking' ? 'Checking...' : citesStatus === 'connected' ? 'Connected' : 'Not Connected'}
</Badge>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-md">
{citesStatus === 'connected'
? citesToken
? `CITES+ API is connected and working (Token: ${citesToken.substring(0, 6)}...)`
: 'CITES+ API is connected and working'
: citesStatus === 'checking'
? 'Checking CITES+ API connection...'
: citesApiData?.message || 'CITES+ API is not connected. Click "API Token" and add your CITES+ API token.'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Badge
variant={iucnStatus === 'connected' ? 'default' : 'destructive'}
className="text-xs px-2 py-0.5"
>
IUCN API v4: {iucnStatus === 'checking'
? 'Checking...'
: iucnStatus === 'connected'
? 'Connected'
: 'Not Connected'}
</Badge>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-md">
{iucnStatus === 'connected'
? iucnToken
? `IUCN Red List API v4 is connected and working (Token: ${iucnToken.substring(0, 6)}...)`
: 'IUCN Red List API v4 is connected and working'
: iucnStatus === 'checking'
? 'Checking IUCN Red List API v4 connection...'
: iucnApiData?.message || 'IUCN Red List API v4 connection issue. To use IUCN API features, click "API Token" and add your API token.'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}

View File

@ -1,187 +0,0 @@
import { useState, useEffect } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { queryClient } from "@/lib/queryClient";
import { apiClient } from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface AuthenticationPanelProps {
token: string;
iucnToken?: string;
onSave: () => void;
}
export default function AuthenticationPanel({ token, iucnToken: initialIucnToken, onSave }: AuthenticationPanelProps) {
const { toast } = useToast();
const [citesToken, setCitesToken] = useState(token || "");
const [iucnToken, setIucnToken] = useState(initialIucnToken || "");
const [activeTab, setActiveTab] = useState("cites");
// Fetch both tokens from the server
const { data: tokenData } = useQuery({
queryKey: ['/api/token'],
queryFn: async () => {
return await apiClient.getTokens();
},
enabled: true, // Always fetch the tokens
});
// Update local state when token data is fetched
useEffect(() => {
if (tokenData) {
setCitesToken(tokenData.token || "");
setIucnToken(tokenData.iucnToken || "");
}
}, [tokenData]);
const saveTokenMutation = useMutation({
mutationFn: async (params: { citesToken: string; iucnToken?: string }) => {
return await apiClient.saveToken(params.citesToken, params.iucnToken);
},
onSuccess: (response) => {
if (response.success) {
queryClient.invalidateQueries({ queryKey: ['/api/token'] });
toast({
title: "Success",
description: "API tokens have been saved successfully",
});
onSave();
} else {
toast({
title: "Error",
description: response.message || "Failed to save API tokens",
variant: "destructive",
});
}
},
onError: () => {
toast({
title: "Error",
description: "Failed to save API tokens",
variant: "destructive",
});
}
});
const handleSaveTokens = () => {
if (!citesToken.trim()) {
toast({
title: "Validation Error",
description: "Please enter a CITES API token",
variant: "destructive",
});
return;
}
saveTokenMutation.mutate({
citesToken,
iucnToken: iucnToken.trim() ? iucnToken : undefined
});
};
const handleGetCitesToken = () => {
window.open("https://api.speciesplus.net/", "_blank");
};
const handleGetIucnToken = () => {
window.open("https://apiv4.iucnredlist.org/api/v4/docs", "_blank");
};
return (
<Card>
<CardContent className="pt-4">
<h2 className="text-lg font-semibold mb-3 flex items-center">
<svg
className="h-5 w-5 text-primary mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
</svg>
API Authentication
</h2>
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-4">
<TabsList className="grid grid-cols-2">
<TabsTrigger value="cites">CITES+ API</TabsTrigger>
<TabsTrigger value="iucn">IUCN Red List API</TabsTrigger>
</TabsList>
<TabsContent value="cites" className="pt-4">
<div className="mb-4">
<Label htmlFor="citesToken" className="block text-sm font-medium text-gray-700 mb-1">
CITES+ API Token (Required)
</Label>
<Input
type="password"
id="citesToken"
className="w-full"
placeholder="Enter your CITES API token"
value={citesToken}
onChange={(e) => setCitesToken(e.target.value)}
/>
<div className="mt-2 text-xs text-gray-500">
The CITES+ API provides species listings, distributions, and legislation information.
</div>
<Button
variant="link"
className="text-primary text-sm px-0 py-1"
onClick={handleGetCitesToken}
>
Get a CITES+ API token
</Button>
</div>
</TabsContent>
<TabsContent value="iucn" className="pt-4">
<div className="mb-4">
<Label htmlFor="iucnToken" className="block text-sm font-medium text-gray-700 mb-1">
IUCN Red List API v4 Token (Optional)
</Label>
<Input
type="password"
id="iucnToken"
className="w-full"
placeholder="Enter your IUCN API token"
value={iucnToken}
onChange={(e) => setIucnToken(e.target.value)}
/>
<div className="mt-2 text-xs text-gray-500">
The IUCN Red List API provides conservation status, threats, habitats, and conservation measures.
</div>
<Button
variant="link"
className="text-primary text-sm px-0 py-1"
onClick={handleGetIucnToken}
>
Get an IUCN Red List API token
</Button>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-between items-center">
<Button
className="bg-primary text-white"
onClick={handleSaveTokens}
disabled={saveTokenMutation.isPending}
>
{saveTokenMutation.isPending ? "Saving..." : "Save Tokens"}
</Button>
<div className="text-xs text-gray-500">
Both tokens are stored securely.
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -1,63 +0,0 @@
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Search } from "@shared/schema";
interface RecentSearchesProps {
recentSearches: Search[];
onSelectSearch: (query: string) => void;
}
export default function RecentSearches({ recentSearches, onSelectSearch }: RecentSearchesProps) {
return (
<Card>
<CardContent className="pt-4">
<h2 className="text-lg font-semibold mb-3 flex items-center">
<svg
className="h-5 w-5 text-primary mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
Recent Searches
</h2>
<div className="space-y-2">
{recentSearches.length > 0 ? (
recentSearches.map((item) => (
<div key={item.id} className="mb-2 last:mb-0">
<Button
variant="outline"
className="w-full text-left justify-between hover:bg-slate-100 rounded text-sm"
onClick={() => onSelectSearch(item.query)}
>
<span className="font-medium truncate">{item.query}</span>
<svg
className="h-4 w-4 text-gray-400 ml-2 shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</Button>
</div>
))
) : (
<p className="text-sm text-gray-500 text-center py-2">No recent searches</p>
)}
</div>
</CardContent>
</Card>
);
}

View File

@ -1,275 +0,0 @@
import { useState, useEffect } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
apiClient,
CITES_API_ENDPOINTS,
IUCN_API_ENDPOINTS,
CitesLegislation,
Distribution,
IucnSpeciesResponse,
IucnThreatsResponse,
IucnHabitatsResponse,
IucnMeasuresResponse
} from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import SpeciesTabs from "@/components/species-tabs";
interface ResultsContainerProps {
currentSpecies: any;
onSpeciesSaved: () => void;
}
export default function ResultsContainer({
currentSpecies,
onSpeciesSaved
}: ResultsContainerProps) {
const { toast } = useToast();
const [apiResponse, setApiResponse] = useState<string>("");
// Load species details when a species is selected
const { data: citesLegislation, isLoading: isLoadingLegislation } = useQuery({
queryKey: ['/api/species/details', currentSpecies?.id, 'cites_legislation'],
queryFn: async () => {
if (!currentSpecies?.id) return null;
const response = await apiClient.getSpeciesDetails(
currentSpecies.id,
CITES_API_ENDPOINTS.CITES_LEGISLATION
);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.id,
});
const { data: distributions, isLoading: isLoadingDistributions } = useQuery({
queryKey: ['/api/species/details', currentSpecies?.id, 'distributions'],
queryFn: async () => {
if (!currentSpecies?.id) return null;
const response = await apiClient.getSpeciesDetails(
currentSpecies.id,
CITES_API_ENDPOINTS.DISTRIBUTIONS
);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.id,
});
const { data: references, isLoading: isLoadingReferences } = useQuery({
queryKey: ['/api/species/details', currentSpecies?.id, 'references'],
queryFn: async () => {
if (!currentSpecies?.id) return null;
const response = await apiClient.getSpeciesDetails(
currentSpecies.id,
CITES_API_ENDPOINTS.REFERENCES
);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.id,
});
// IUCN API Queries
const { data: iucnData, isLoading: isLoadingIucnSpecies } = useQuery({
queryKey: ['/api/iucn/species', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnSpeciesByName(currentSpecies.full_name);
// Debug log
console.log("IUCN Species API response:", response);
if (response.success && response.data) {
console.log("IUCN response data structure:", Object.keys(response.data));
if (response.data.result) {
console.log("IUCN result items:", response.data.result.length);
}
}
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const { data: iucnThreats, isLoading: isLoadingIucnThreats } = useQuery({
queryKey: ['/api/iucn/threats', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnThreats(currentSpecies.full_name);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const { data: iucnHabitats, isLoading: isLoadingIucnHabitats } = useQuery({
queryKey: ['/api/iucn/habitats', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnHabitats(currentSpecies.full_name);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const { data: iucnMeasures, isLoading: isLoadingIucnMeasures } = useQuery({
queryKey: ['/api/iucn/measures', currentSpecies?.full_name],
queryFn: async () => {
if (!currentSpecies?.full_name) return null;
const response = await apiClient.getIucnMeasures(currentSpecies.full_name);
return response.success ? response.data : null;
},
enabled: !!currentSpecies?.full_name,
});
const saveSpeciesMutation = useMutation({
mutationFn: async () => {
if (!currentSpecies) return null;
// Prepare species data for saving
const speciesData = {
scientificName: currentSpecies.full_name,
commonName: currentSpecies.common_names?.[0]?.name || '',
rank: currentSpecies.rank,
kingdom: currentSpecies.taxonomy?.kingdom || '',
phylum: currentSpecies.taxonomy?.phylum || '',
class: currentSpecies.taxonomy?.class || '',
order: currentSpecies.taxonomy?.order || '',
family: currentSpecies.taxonomy?.family || '',
genus: currentSpecies.taxonomy?.genus || '',
citesListings: currentSpecies.cites_listings || [],
citesId: currentSpecies.id,
apiData: currentSpecies,
};
return await apiClient.saveSpecies(speciesData);
},
onSuccess: (response) => {
if (response?.success) {
onSpeciesSaved();
} else {
toast({
title: "Error",
description: response?.message || "Failed to save species data",
variant: "destructive",
});
}
},
onError: () => {
toast({
title: "Error",
description: "Failed to save species data",
variant: "destructive",
});
}
});
// Update API response when currentSpecies changes
useEffect(() => {
if (currentSpecies) {
setApiResponse(JSON.stringify(currentSpecies, null, 2));
}
}, [currentSpecies]);
const handleSaveToDatabase = () => {
saveSpeciesMutation.mutate();
};
const isLoading = isLoadingLegislation || isLoadingDistributions || isLoadingReferences ||
isLoadingIucnSpecies || isLoadingIucnThreats || isLoadingIucnHabitats || isLoadingIucnMeasures ||
saveSpeciesMutation.isPending;
// No species selected yet
if (!currentSpecies) {
return (
<Card>
<CardContent className="py-16 text-center">
<svg
className="h-16 w-16 mx-auto text-gray-300 mb-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<h3 className="text-xl font-medium text-gray-500">No Species Data Yet</h3>
<p className="text-gray-400 mt-2">Search for a species to view CITES information</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardContent className="pt-4">
{/* Species Header */}
<div className="border-b pb-4 mb-4">
<div className="flex justify-between items-start">
<div>
<h2 className="text-2xl font-bold">{currentSpecies.full_name}</h2>
<p className="text-gray-500">
{currentSpecies.common_names?.[0]?.name || 'No common name available'}
</p>
</div>
<div className="text-right">
<span className="inline-block bg-secondary text-white text-sm px-2 py-1 rounded font-medium">
{currentSpecies.cites_listings?.[0]?.appendix
? `CITES Appendix ${currentSpecies.cites_listings[0].appendix}`
: 'No CITES listing'}
</span>
</div>
</div>
<div className="mt-2 text-sm text-gray-600">
<p>
{currentSpecies.taxonomy
? `Class: ${currentSpecies.taxonomy.class} | Order: ${currentSpecies.taxonomy.order} | Family: ${currentSpecies.taxonomy.family}`
: 'No taxonomy information available'}
</p>
</div>
</div>
{/* Tabs Content */}
<SpeciesTabs
species={currentSpecies}
citesLegislation={citesLegislation}
distributions={distributions}
references={references}
iucnData={iucnData}
iucnThreats={iucnThreats}
iucnHabitats={iucnHabitats}
iucnMeasures={iucnMeasures}
apiResponse={apiResponse}
isLoading={isLoading}
/>
{/* Save Button */}
<div className="text-right mt-4">
<Button
variant="link"
className="text-primary text-sm"
onClick={handleSaveToDatabase}
disabled={saveSpeciesMutation.isPending}
>
<svg
className="h-4 w-4 mr-1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
{saveSpeciesMutation.isPending ? "Saving..." : "Save to Database"}
</Button>
</div>
</CardContent>
</Card>
);
}

View File

@ -1,82 +0,0 @@
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Species } from "@shared/schema";
import { format } from "date-fns";
interface SavedSpeciesProps {
savedSpecies: Species[];
onSelectSpecies: (species: any) => void;
}
export default function SavedSpecies({ savedSpecies, onSelectSpecies }: SavedSpeciesProps) {
const handleViewDetails = (species: Species) => {
onSelectSpecies(species.apiData);
};
return (
<Card>
<CardContent className="pt-4">
<h2 className="text-lg font-semibold mb-3 flex items-center">
<svg
className="h-5 w-5 text-primary mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path>
<line x1="16" y1="8" x2="2" y2="22"></line>
<line x1="17.5" y1="15" x2="9" y2="15"></line>
</svg>
Saved Species
</h2>
<div>
{savedSpecies.length > 0 ? (
savedSpecies.map((species) => (
<div
key={species.id}
className="mb-3 last:mb-0 border rounded hover:shadow-sm transition-shadow"
>
<div className="p-3">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold">{species.scientificName}</h3>
<p className="text-sm text-gray-500">{species.commonName || 'No common name'}</p>
</div>
{species.citesListings && (
<span className="inline-block bg-secondary text-white text-xs px-2 py-1 rounded">
{Array.isArray(species.citesListings) && species.citesListings[0]?.appendix
? `Appendix ${species.citesListings[0].appendix}`
: 'No listing'}
</span>
)}
</div>
<div className="mt-2 flex justify-between items-center">
<span className="text-xs text-gray-500">
Saved on: {species.searchedAt ? format(new Date(species.searchedAt), 'yyyy-MM-dd') : 'Unknown'}
</span>
<Button
variant="link"
className="text-primary text-sm p-0 h-auto"
onClick={() => handleViewDetails(species)}
>
View Details
</Button>
</div>
</div>
</div>
))
) : (
<div className="text-center py-4">
<p className="text-gray-500">No species saved to database yet</p>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View File

@ -1,154 +0,0 @@
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { apiClient } from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
interface SearchFormProps {
token: string | null;
onSpeciesFound: (species: any) => void;
}
export default function SearchForm({ token, onSpeciesFound }: SearchFormProps) {
const { toast } = useToast();
const [searchQuery, setSearchQuery] = useState("");
const [responseFormat, setResponseFormat] = useState<"json" | "xml">("json");
const searchMutation = useMutation({
mutationFn: async ({ query, format }: { query: string, format: "json" | "xml" }) => {
return await apiClient.searchSpecies(query, format);
},
onSuccess: (response) => {
if (response.success && response.data?.taxon_concepts?.length) {
onSpeciesFound(response.data.taxon_concepts[0]);
toast({
title: "Success",
description: "Species information retrieved successfully",
});
} else {
toast({
title: "No results",
description: "No species found matching your search criteria",
variant: "destructive",
});
}
},
onError: (error: any) => {
toast({
title: "Error",
description: error.message || "Failed to search for species",
variant: "destructive",
});
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!searchQuery.trim()) {
toast({
title: "Validation Error",
description: "Please enter a species name to search",
variant: "destructive",
});
return;
}
if (!token) {
toast({
title: "API Token Required",
description: "Please set your CITES+ API token first",
variant: "destructive",
});
return;
}
searchMutation.mutate({ query: searchQuery, format: responseFormat });
};
return (
<Card>
<CardContent className="pt-4">
<h2 className="text-lg font-semibold mb-3 flex items-center">
<svg
className="h-5 w-5 text-primary mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
Species Search
</h2>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<Label htmlFor="speciesName" className="block text-sm font-medium text-gray-700 mb-1">
Species Name
</Label>
<Input
type="text"
id="speciesName"
className="w-full"
placeholder="Enter scientific or common name"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
required
/>
<p className="text-xs text-gray-500 mt-1">
Example: Panthera leo, Tiger, Elephas maximus
</p>
</div>
<div className="mb-4">
<Label className="block text-sm font-medium text-gray-700 mb-1">
Response Format
</Label>
<RadioGroup
defaultValue="json"
className="flex space-x-4"
value={responseFormat}
onValueChange={(value) => setResponseFormat(value as "json" | "xml")}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="json" id="format-json" />
<Label htmlFor="format-json">JSON</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="xml" id="format-xml" />
<Label htmlFor="format-xml">XML</Label>
</div>
</RadioGroup>
</div>
<Button
type="submit"
className="w-full bg-primary hover:bg-blue-700 flex items-center justify-center"
disabled={searchMutation.isPending}
>
<svg
className="h-4 w-4 mr-2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<span>{searchMutation.isPending ? "Searching..." : "Search Species"}</span>
</Button>
</form>
</CardContent>
</Card>
);
}

View File

@ -1,458 +0,0 @@
import { useState, useEffect } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/hooks/use-toast";
interface SpeciesTabsProps {
species: any;
citesLegislation: any;
distributions: any;
references: any;
iucnData: any;
iucnThreats: any;
iucnHabitats: any;
iucnMeasures: any;
apiResponse: string;
isLoading: boolean;
}
export default function SpeciesTabs({
species,
citesLegislation,
distributions,
references,
iucnData,
iucnThreats,
iucnHabitats,
iucnMeasures,
apiResponse,
isLoading
}: SpeciesTabsProps) {
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("overview");
// Debug logging for IUCN data
useEffect(() => {
console.log("SpeciesTabs received iucnData:", iucnData);
console.log("SpeciesTabs received iucnThreats:", iucnThreats);
console.log("SpeciesTabs received iucnHabitats:", iucnHabitats);
console.log("SpeciesTabs received iucnMeasures:", iucnMeasures);
}, [iucnData, iucnThreats, iucnHabitats, iucnMeasures]);
// Handle copy API response to clipboard
const handleCopyResponse = async () => {
try {
await navigator.clipboard.writeText(apiResponse);
toast({
title: "Copied",
description: "API response copied to clipboard",
});
} catch (err) {
toast({
title: "Error",
description: "Failed to copy API response",
variant: "destructive",
});
}
};
if (isLoading) {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-[200px] w-full" />
</div>
);
}
return (
<Tabs defaultValue="overview" value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full mb-4 overflow-x-auto border-b">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="legislation">CITES Legislation</TabsTrigger>
<TabsTrigger value="distribution">Distribution</TabsTrigger>
<TabsTrigger value="conservation">Conservation Status</TabsTrigger>
<TabsTrigger value="references">References</TabsTrigger>
<TabsTrigger value="api-response">API Response</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="border rounded p-3">
<h3 className="font-semibold text-gray-700 mb-2">CITES Status</h3>
{species.cites_listings && species.cites_listings.length > 0 ? (
<>
<p className="text-sm">
Listed in CITES Appendix {species.cites_listings[0].appendix}
since {new Date(species.cites_listings[0].effective_at).toLocaleDateString()}
</p>
<p className="text-sm mt-1">
{species.cites_listings[0].appendix === 'I'
? 'All commercial international trade is prohibited.'
: species.cites_listings[0].appendix === 'II'
? 'International trade is regulated and requires permits.'
: 'International trade is regulated within countries that have requested monitoring assistance.'}
</p>
</>
) : (
<p className="text-sm">No CITES listing information available.</p>
)}
</div>
<div className="border rounded p-3">
<h3 className="font-semibold text-gray-700 mb-2">Taxonomic Information</h3>
<p className="text-sm">Rank: {species.rank || 'Unknown'}</p>
<p className="text-sm mt-1">Author: {species.author_year || 'Unknown'}</p>
</div>
</div>
<div className="border rounded p-4 mb-4">
<h3 className="font-semibold text-gray-700 mb-2">Species Information</h3>
<p className="text-sm mb-3">
{species.full_name} {species.common_names?.[0] ? `(${species.common_names[0].name})` : ''} is
a species of {species.taxonomy?.family?.toLowerCase() || 'organism'}.
</p>
<div className="flex flex-wrap gap-2">
{species.taxonomy?.kingdom && (
<span className="inline-block bg-slate-100 px-2 py-1 rounded-full text-xs">
{species.taxonomy.kingdom}
</span>
)}
{species.taxonomy?.class && (
<span className="inline-block bg-slate-100 px-2 py-1 rounded-full text-xs">
{species.taxonomy.class}
</span>
)}
{species.taxonomy?.order && (
<span className="inline-block bg-slate-100 px-2 py-1 rounded-full text-xs">
{species.taxonomy.order}
</span>
)}
{species.taxonomy?.family && (
<span className="inline-block bg-slate-100 px-2 py-1 rounded-full text-xs">
{species.taxonomy.family}
</span>
)}
</div>
</div>
</TabsContent>
{/* CITES Legislation Tab */}
<TabsContent value="legislation" className="space-y-4">
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">Appendix Listings</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Appendix</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Effective From</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{citesLegislation?.cites_listings && citesLegislation.cites_listings.length > 0 ? (
citesLegislation.cites_listings.map((listing: any, index: number) => (
<tr key={index}>
<td className="px-3 py-2 whitespace-nowrap">{listing.appendix}</td>
<td className="px-3 py-2 whitespace-nowrap">
{new Date(listing.effective_at).toLocaleDateString()}
</td>
<td className="px-3 py-2">{listing.annotation || 'None'}</td>
</tr>
))
) : (
<tr>
<td className="px-3 py-2" colSpan={3}>No listing information available</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">CITES Quotas</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Year</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Quota</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Publication Date</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{citesLegislation?.cites_quotas && citesLegislation.cites_quotas.length > 0 ? (
citesLegislation.cites_quotas.map((quota: any, index: number) => (
<tr key={index}>
<td className="px-3 py-2">{quota.year}</td>
<td className="px-3 py-2">{quota.quota}</td>
<td className="px-3 py-2">
{quota.publication_date ? new Date(quota.publication_date).toLocaleDateString() : 'N/A'}
</td>
<td className="px-3 py-2">{quota.notes || 'None'}</td>
</tr>
))
) : (
<tr>
<td className="px-3 py-2" colSpan={4}>No quota information available</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">CITES Suspensions</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Start Date</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">End Date</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Applies to</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{citesLegislation?.cites_suspensions && citesLegislation.cites_suspensions.length > 0 ? (
citesLegislation.cites_suspensions.map((suspension: any, index: number) => (
<tr key={index}>
<td className="px-3 py-2">
{suspension.start_date ? new Date(suspension.start_date).toLocaleDateString() : 'N/A'}
</td>
<td className="px-3 py-2">
{suspension.end_date ? new Date(suspension.end_date).toLocaleDateString() : 'Ongoing'}
</td>
<td className="px-3 py-2">{suspension.applies_to || 'All'}</td>
<td className="px-3 py-2">{suspension.notes || 'None'}</td>
</tr>
))
) : (
<tr>
<td className="px-3 py-2" colSpan={4}>No suspension information available</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</TabsContent>
{/* Distribution Tab */}
<TabsContent value="distribution" className="space-y-4">
<div className="border rounded p-4 mb-4">
<h3 className="font-semibold text-gray-700 mb-2">Geographic Range</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country/Territory</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">References</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{distributions?.distributions && distributions.distributions.length > 0 ? (
distributions.distributions.map((distribution: any, index: number) => (
<tr key={index}>
<td className="px-3 py-2">{distribution.name}</td>
<td className="px-3 py-2">
{distribution.tags && distribution.tags.length > 0
? distribution.tags.join(', ')
: 'Native'}
</td>
<td className="px-3 py-2">
{distribution.references && distribution.references.length > 0
? distribution.references.map((ref: any) => ref.citation || 'Unnamed reference').join('; ')
: 'No references available'}
</td>
</tr>
))
) : (
<tr>
<td className="px-3 py-2" colSpan={3}>No distribution information available</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</TabsContent>
{/* Conservation Status Tab */}
<TabsContent value="conservation" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">IUCN Red List Status</h3>
{iucnData?.result && iucnData.result.length > 0 ? (
<div className="space-y-2">
<div className="flex items-center">
<div className={`h-4 w-4 rounded-full mr-2 ${
iucnData.result[0].category === 'EX' ? 'bg-black' :
iucnData.result[0].category === 'EW' ? 'bg-purple-800' :
iucnData.result[0].category === 'CR' ? 'bg-red-600' :
iucnData.result[0].category === 'EN' ? 'bg-orange-500' :
iucnData.result[0].category === 'VU' ? 'bg-yellow-400' :
iucnData.result[0].category === 'NT' ? 'bg-yellow-200' :
iucnData.result[0].category === 'LC' ? 'bg-green-500' :
'bg-gray-400'
}`}></div>
<span className="text-sm font-medium">
{iucnData.result[0].category === 'EX' ? 'Extinct' :
iucnData.result[0].category === 'EW' ? 'Extinct in the Wild' :
iucnData.result[0].category === 'CR' ? 'Critically Endangered' :
iucnData.result[0].category === 'EN' ? 'Endangered' :
iucnData.result[0].category === 'VU' ? 'Vulnerable' :
iucnData.result[0].category === 'NT' ? 'Near Threatened' :
iucnData.result[0].category === 'LC' ? 'Least Concern' :
iucnData.result[0].category === 'DD' ? 'Data Deficient' :
iucnData.result[0].category === 'NE' ? 'Not Evaluated' :
iucnData.result[0].category}
</span>
</div>
<p className="text-sm">Assessed: {iucnData.result[0].assessment_date}</p>
<p className="text-sm">Criteria: {iucnData.result[0].criteria || 'Not specified'}</p>
<p className="text-sm">
Population trend: {
iucnData.result[0].population_trend === 'decreasing' ? 'Decreasing ↓' :
iucnData.result[0].population_trend === 'increasing' ? 'Increasing ↑' :
iucnData.result[0].population_trend === 'stable' ? 'Stable →' :
'Unknown'
}
</p>
</div>
) : (
<p className="text-sm">No IUCN Red List data available for this species.</p>
)}
</div>
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">Habitat Types</h3>
{iucnHabitats?.result && iucnHabitats.result.length > 0 ? (
<ul className="space-y-1 text-sm">
{iucnHabitats.result.map((habitat: any, index: number) => (
<li key={index} className="flex items-start">
<span className="text-primary mr-2"></span>
<span>
{habitat.habitat}
{habitat.suitability && ` (${habitat.suitability})`}
{habitat.majorimportance === 'Yes' && ' - Major importance'}
</span>
</li>
))}
</ul>
) : (
<p className="text-sm">No habitat information available.</p>
)}
</div>
</div>
<div className="border rounded p-4 mb-4">
<h3 className="font-semibold text-gray-700 mb-2">Threats</h3>
{iucnThreats?.result && iucnThreats.result.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Threat</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Timing</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Scope</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Severity</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Score</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{iucnThreats.result.map((threat: any, index: number) => (
<tr key={index}>
<td className="px-3 py-2">{threat.title}</td>
<td className="px-3 py-2">{threat.timing || 'Unknown'}</td>
<td className="px-3 py-2">{threat.scope || 'Unknown'}</td>
<td className="px-3 py-2">{threat.severity || 'Unknown'}</td>
<td className="px-3 py-2">{threat.score || 'Unknown'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-sm">No threat information available.</p>
)}
</div>
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">Conservation Measures</h3>
{iucnMeasures?.result && iucnMeasures.result.length > 0 ? (
<ul className="space-y-1 text-sm">
{iucnMeasures.result.map((measure: any, index: number) => (
<li key={index} className="flex items-start">
<span className="text-primary mr-2"></span>
<span>
{measure.title}
{measure.year ? ` (${measure.year})` : ''}
</span>
</li>
))}
</ul>
) : (
<p className="text-sm">No conservation measures information available.</p>
)}
</div>
</TabsContent>
{/* References Tab */}
<TabsContent value="references" className="space-y-4">
<div className="border rounded p-4">
<h3 className="font-semibold text-gray-700 mb-2">Standard References</h3>
<ul className="list-disc list-inside text-sm space-y-2">
{references?.references && references.references.length > 0 ? (
references.references.map((ref: any, index: number) => (
<li key={index}>
{ref.citation || `${ref.author || 'Unknown'} (${ref.year || 'n.d.'}). ${ref.title || 'Untitled'}.`}
</li>
))
) : (
<li>No reference information available</li>
)}
</ul>
</div>
</TabsContent>
{/* API Response Tab */}
<TabsContent value="api-response">
<div className="border rounded bg-slate-50 p-4">
<div className="flex justify-between items-center mb-3">
<h3 className="font-semibold text-gray-700">Raw API Response</h3>
<div>
<Button
variant="link"
className="text-primary text-sm"
onClick={handleCopyResponse}
>
<svg
className="h-4 w-4 mr-1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
Copy
</Button>
</div>
</div>
<div className="overflow-auto bg-slate-900 text-white p-4 rounded font-mono text-sm">
<pre className="whitespace-pre-wrap">{apiResponse}</pre>
</div>
</div>
</TabsContent>
</Tabs>
);
}

View File

@ -1,56 +0,0 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -1,139 +0,0 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
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}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.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}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -1,59 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -1,5 +0,0 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View File

@ -1,48 +0,0 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -1,115 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -1,64 +0,0 @@
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -1,260 +0,0 @@
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@ -1,363 +0,0 @@
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@ -1,28 +0,0 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -1,9 +0,0 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -1,153 +0,0 @@
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -1,198 +0,0 @@
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@ -1,120 +0,0 @@
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,
}

View File

@ -1,116 +0,0 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -1,198 +0,0 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -1,176 +0,0 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -1,27 +0,0 @@
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@ -1,69 +0,0 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Dot } from "lucide-react"
import { cn } from "@/lib/utils"
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

View File

@ -1,234 +0,0 @@
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@ -1,128 +0,0 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@ -1,117 +0,0 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}

View File

@ -1,29 +0,0 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -1,26 +0,0 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -1,42 +0,0 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -1,43 +0,0 @@
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -1,46 +0,0 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -1,29 +0,0 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -1,138 +0,0 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
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}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.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-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -1,762 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
if (setOpenProp) {
return setOpenProp?.(
typeof value === "function" ? value(open) : value
)
}
_setOpen(value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full text-sidebar-foreground has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
className={cn(
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 flex-1 max-w-[--skeleton-width]"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@ -1,15 +0,0 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -1,26 +0,0 @@
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@ -1,27 +0,0 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -1,117 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -1,24 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -1,127 +0,0 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -1,33 +0,0 @@
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -1,59 +0,0 @@
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View File

@ -1,43 +0,0 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

View File

@ -1,28 +0,0 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -1,19 +0,0 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View File

@ -1,191 +0,0 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View File

@ -1,13 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-border;
}
body {
@apply font-sans antialiased bg-background text-foreground;
}
}

View File

@ -1,365 +0,0 @@
import axios from "axios";
import { Species, InsertSpecies } from "@shared/schema";
export const CITES_API_ENDPOINTS = {
CITES_LEGISLATION: "cites_legislation",
DISTRIBUTIONS: "distributions",
EU_LEGISLATION: "eu_legislation",
REFERENCES: "references"
};
export const IUCN_API_ENDPOINTS = {
// V4 API only
BASE_URL: "https://apiv4.iucnredlist.org/api/v4",
VERSION: "version",
TAXA: "taxa",
TAXA_BY_SCIENTIFIC_NAME: "taxa/scientific_name", // GET with params: genus_name and species_name
THREATS: "threats", // GET with params: taxonid
HABITATS: "habitats", // GET with params: taxonid
MEASURES: "measures" // GET with params: taxonid
};
// API types
export interface ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
status?: number;
}
export interface TokenResponse {
token: string | null;
iucnToken: string | null;
}
export interface ApiStatusResponse {
success: boolean;
connected: boolean;
message?: string;
}
export interface SpeciesSearchResponse {
taxon_concepts: TaxonConcept[];
pagination: {
current_page: number;
per_page: number;
total_entries: number;
total_pages: number;
};
}
export interface TaxonConcept {
id: number;
full_name: string;
author_year: string;
rank: string;
name_status: string;
taxonomy: {
kingdom: string;
phylum: string;
class: string;
order: string;
family: string;
genus: string;
};
cites_listing: string;
cites_listings: CitesListing[];
common_names: CommonName[];
}
export interface CitesListing {
appendix: string;
annotation: string | null;
effective_at: string;
}
export interface CommonName {
name: string;
language: string;
iso_code: string;
}
export interface CitesLegislation {
cites_listings: CitesListing[];
cites_quotas: any[];
cites_suspensions: any[];
}
export interface Distribution {
id: number;
name: string;
iso_code2: string;
geo_entity_type: string;
tags: string[];
references: Reference[];
}
export interface Reference {
id: number;
citation: string;
author: string;
title: string;
year: number;
}
export interface IucnSpeciesResult {
taxonid: number;
scientific_name: string;
kingdom: string;
phylum: string;
class: string;
order: string;
family: string;
genus: string;
main_common_name: string;
authority: string;
published_year: number;
category: string;
assessment_date: string;
criteria: string;
population_trend: string;
marine_system: boolean;
freshwater_system: boolean;
terrestrial_system: boolean;
}
export interface IucnSpeciesResponse {
result: IucnSpeciesResult[];
}
export interface IucnThreat {
code: string;
title: string;
timing: string;
scope: string;
severity: string;
score: string;
}
export interface IucnThreatsResponse {
result: IucnThreat[];
}
export interface IucnHabitat {
code: string;
habitat: string;
suitability: string;
season: string;
majorimportance: string;
}
export interface IucnHabitatsResponse {
result: IucnHabitat[];
}
export interface IucnMeasure {
code: string;
title: string;
year: number;
}
export interface IucnMeasuresResponse {
result: IucnMeasure[];
}
// API client for interacting with our backend
export const apiClient = {
// API Status methods
async checkIucnApiStatus(): Promise<ApiStatusResponse> {
try {
const response = await axios.get<ApiStatusResponse>("/api/iucn/status");
return response.data;
} catch (error: any) {
return {
success: false,
connected: false,
message: error.response?.data?.message || "Failed to check IUCN API status"
};
}
},
async checkCitesApiStatus(): Promise<ApiStatusResponse> {
try {
const response = await axios.get<ApiStatusResponse>("/api/cites/status");
return response.data;
} catch (error: any) {
return {
success: false,
connected: false,
message: error.response?.data?.message || "Failed to check CITES API status"
};
}
},
// Token management
async saveToken(token: string, iucnToken?: string): Promise<ApiResponse<any>> {
try {
const response = await axios.post<ApiResponse<any>>("/api/token", {
token,
iucnToken,
isActive: true
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to save token"
};
}
},
async getTokens(): Promise<TokenResponse> {
try {
const response = await axios.get<TokenResponse>("/api/token");
return response.data;
} catch (error) {
console.error("Failed to retrieve API tokens:", error);
return { token: null, iucnToken: null };
}
},
async getToken(): Promise<string | null> {
try {
const response = await axios.get<TokenResponse>("/api/token");
return response.data.token;
} catch (error) {
console.error("Failed to retrieve API token:", error);
return null;
}
},
// Species search
async searchSpecies(query: string, format: "json" | "xml" = "json"): Promise<ApiResponse<SpeciesSearchResponse>> {
try {
const response = await axios.get<ApiResponse<SpeciesSearchResponse>>("/api/species/search", {
params: { query, format }
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to search species"
};
}
},
// Get specific species details
async getSpeciesDetails(
id: number,
endpoint?: string,
format: "json" | "xml" = "json"
): Promise<ApiResponse<any>> {
try {
const response = await axios.get<ApiResponse<any>>(`/api/species/${id}`, {
params: { endpoint, format }
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to get species details"
};
}
},
// Save species to database
async saveSpecies(speciesData: InsertSpecies): Promise<ApiResponse<Species>> {
try {
const response = await axios.post<ApiResponse<Species>>("/api/species", speciesData);
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to save species data"
};
}
},
// Get all saved species
async getSavedSpecies(): Promise<ApiResponse<Species[]>> {
try {
const response = await axios.get<{success: boolean; species: Species[]}>("/api/species");
return {
success: true,
data: response.data.species
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to retrieve saved species"
};
}
},
// Get recent searches
async getRecentSearches(limit: number = 10): Promise<ApiResponse<any>> {
try {
const response = await axios.get<ApiResponse<any>>("/api/searches", {
params: { limit }
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to retrieve recent searches"
};
}
},
// IUCN Red List API methods
async getIucnSpeciesByName(scientificName: string): Promise<ApiResponse<IucnSpeciesResponse>> {
try {
const response = await axios.get<ApiResponse<IucnSpeciesResponse>>("/api/iucn/species", {
params: { name: scientificName }
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to retrieve IUCN species data"
};
}
},
async getIucnThreats(scientificName: string): Promise<ApiResponse<IucnThreatsResponse>> {
try {
const response = await axios.get<ApiResponse<IucnThreatsResponse>>("/api/iucn/threats", {
params: { name: scientificName }
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to retrieve IUCN threats data"
};
}
},
async getIucnHabitats(scientificName: string): Promise<ApiResponse<IucnHabitatsResponse>> {
try {
const response = await axios.get<ApiResponse<IucnHabitatsResponse>>("/api/iucn/habitats", {
params: { name: scientificName }
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to retrieve IUCN habitats data"
};
}
},
async getIucnMeasures(scientificName: string): Promise<ApiResponse<IucnMeasuresResponse>> {
try {
const response = await axios.get<ApiResponse<IucnMeasuresResponse>>("/api/iucn/measures", {
params: { name: scientificName }
});
return response.data;
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || "Failed to retrieve IUCN conservation measures data"
};
}
}
};

View File

@ -1,57 +0,0 @@
import { QueryClient, QueryFunction } from "@tanstack/react-query";
async function throwIfResNotOk(res: Response) {
if (!res.ok) {
const text = (await res.text()) || res.statusText;
throw new Error(`${res.status}: ${text}`);
}
}
export async function apiRequest(
method: string,
url: string,
data?: unknown | undefined,
): Promise<Response> {
const res = await fetch(url, {
method,
headers: data ? { "Content-Type": "application/json" } : {},
body: data ? JSON.stringify(data) : undefined,
credentials: "include",
});
await throwIfResNotOk(res);
return res;
}
type UnauthorizedBehavior = "returnNull" | "throw";
export const getQueryFn: <T>(options: {
on401: UnauthorizedBehavior;
}) => QueryFunction<T> =
({ on401: unauthorizedBehavior }) =>
async ({ queryKey }) => {
const res = await fetch(queryKey[0] as string, {
credentials: "include",
});
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
return null;
}
await throwIfResNotOk(res);
return await res.json();
};
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: getQueryFn({ on401: "throw" }),
refetchInterval: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
retry: false,
},
mutations: {
retry: false,
},
},
});

View File

@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -1,8 +0,0 @@
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
const root = document.getElementById("root");
if (!root) throw new Error("Root element not found");
createRoot(root).render(<App />);

View File

@ -1,216 +0,0 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { apiClient } from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
import AuthenticationPanel from "@/components/authentication-panel";
import SearchForm from "@/components/search-form";
import RecentSearches from "@/components/recent-searches";
import ResultsContainer from "@/components/results-container";
import SavedSpecies from "@/components/saved-species";
import ApiStatus from "@/components/api-status";
export default function Home() {
const { toast } = useToast();
const [showAuthPanel, setShowAuthPanel] = useState(false);
const [selectedSpecies, setSelectedSpecies] = useState<any>(null);
// Get the active tokens
const { data: tokensData } = useQuery({
queryKey: ['/api/token'],
queryFn: async () => await apiClient.getTokens(),
initialData: { token: null, iucnToken: null },
});
const { data: citesTokenData } = useQuery({
queryKey: ['/api/token/cites'],
queryFn: async () => await apiClient.getToken(),
initialData: null,
});
// Get recent searches
const { data: recentSearches } = useQuery({
queryKey: ['/api/searches'],
queryFn: async () => {
const response = await apiClient.getRecentSearches();
return response.success ? response.data.searches : [];
},
});
// Get saved species
const { data: savedSpecies, refetch: refetchSavedSpecies } = useQuery({
queryKey: ['/api/species'],
queryFn: async () => {
const response = await apiClient.getSavedSpecies();
return response.success && response.data ? response.data : [];
},
});
const handleSearchSelect = async (query: string) => {
try {
const response = await apiClient.searchSpecies(query);
if (response.success && response.data?.taxon_concepts?.length) {
setSelectedSpecies(response.data.taxon_concepts[0]);
} else {
toast({
title: "Species not found",
description: "No matching species found for your search",
variant: "destructive",
});
}
} catch (error) {
toast({
title: "Error",
description: "Failed to search for species",
variant: "destructive",
});
}
};
const handleSpeciesSaved = () => {
refetchSavedSpecies();
toast({
title: "Species saved",
description: "Species information has been saved to the database",
});
};
return (
<div className="flex flex-col min-h-screen">
{/* Header */}
<header className="bg-primary text-white shadow-md">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<div className="flex items-center space-x-2">
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 2L5 6.5M12 2v6.5M12 2l7 4.5M5 6.5V18l3.5 2M5 6.5l7 4.5M22 9l-7.5 4.5M22 9V16M22 9l-3.5-2M5 18l7 4m0 0 7-4M19 18l-7-4.5m0 0L8.5 9"/>
</svg>
<h1 className="text-xl font-bold">CITES+ Species Lookup</h1>
<div className="ml-6">
<ApiStatus
citesToken={tokensData.token || null}
iucnToken={tokensData.iucnToken || null}
/>
</div>
</div>
<div>
<button
className="bg-white text-primary px-3 py-1 rounded text-sm font-medium hover:bg-slate-100 transition-colors"
onClick={() => setShowAuthPanel(!showAuthPanel)}
>
<svg
className="h-4 w-4 inline-block mr-1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
</svg>
API Token
</button>
</div>
</div>
</header>
{/* Main Content */}
<main className="flex-grow container mx-auto px-4 py-6 flex flex-col md:flex-row gap-6">
{/* Sidebar - Search and Authentication */}
<div className="w-full md:w-1/3 space-y-4">
{showAuthPanel && (
<AuthenticationPanel
token={tokensData.token || ""}
iucnToken={tokensData.iucnToken || ""}
onSave={() => setShowAuthPanel(false)}
/>
)}
<SearchForm
token={tokensData.token || null}
onSpeciesFound={setSelectedSpecies}
/>
<RecentSearches
recentSearches={recentSearches || []}
onSelectSearch={handleSearchSelect}
/>
</div>
{/* Main Content Area - Results */}
<div className="w-full md:w-2/3 space-y-4">
<ResultsContainer
currentSpecies={selectedSpecies}
onSpeciesSaved={handleSpeciesSaved}
/>
<SavedSpecies
savedSpecies={savedSpecies || []}
onSelectSpecies={setSelectedSpecies}
/>
</div>
</main>
{/* Footer */}
<footer className="bg-slate-800 text-white py-4 mt-8">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<p className="text-sm">
CITES+ Species Lookup Tool
</p>
<p className="text-xs text-slate-400">
Using the CITES+ API for species conservation data
</p>
</div>
<div className="flex items-center">
<a href="https://api.speciesplus.net/api/v1/taxon_concepts" target="_blank" rel="noopener noreferrer" className="text-slate-300 hover:text-white text-sm">
<svg
className="h-4 w-4 inline-block mr-1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
API Documentation
</a>
<span className="mx-3 text-slate-500">|</span>
<a href="#" className="text-slate-300 hover:text-white text-sm">
<svg
className="h-4 w-4 inline-block mr-1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
Help
</a>
</div>
</div>
</div>
</footer>
</div>
);
}

View File

@ -1,21 +0,0 @@
import { Card, CardContent } from "@/components/ui/card";
import { AlertCircle } from "lucide-react";
export default function NotFound() {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-gray-50">
<Card className="w-full max-w-md mx-4">
<CardContent className="pt-6">
<div className="flex mb-4 gap-2">
<AlertCircle className="h-8 w-8 text-red-500" />
<h1 className="text-2xl font-bold text-gray-900">404 Page Not Found</h1>
</div>
<p className="mt-4 text-sm text-gray-600">
Did you forget to add the page to the router?
</p>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,14 +0,0 @@
import { defineConfig } from "drizzle-kit";
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL, ensure the database is provisioned");
}
export default defineConfig({
out: "./migrations",
schema: "./shared/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL,
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

View File

@ -2,7 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arctic Species Tracker</title>
<meta name="description" content="Track conservation status, CITES listings, and trade data for Arctic species" />
</head>
<body>
<div id="root"></div>

7394
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,104 +1,48 @@
{
"name": "rest-express",
"version": "1.0.0",
"name": "arctic-species-tracker",
"private": true,
"version": "0.1.0",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "tsx server/index.ts",
"build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
"start": "NODE_ENV=production node dist/index.js",
"check": "tsc",
"db:push": "drizzle-kit push"
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@jridgewell/trace-mapping": "^0.3.25",
"@neondatabase/serverless": "^0.10.4",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@replit/vite-plugin-shadcn-theme-json": "^0.0.4",
"@tanstack/react-query": "^5.60.5",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.0",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@supabase/supabase-js": "^2.39.7",
"@tanstack/react-query": "^5.24.1",
"@tanstack/react-query-devtools": "^5.24.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"connect-pg-simple": "^10.0.0",
"date-fns": "^3.6.0",
"drizzle-orm": "^0.39.1",
"drizzle-zod": "^0.7.0",
"embla-carousel-react": "^8.3.0",
"express": "^4.21.2",
"express-session": "^1.18.1",
"framer-motion": "^11.13.1",
"input-otp": "^1.2.4",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.1",
"react-icons": "^5.4.0",
"react-resizable-panels": "^2.1.4",
"recharts": "^2.13.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.0",
"wouter": "^3.3.5",
"ws": "^8.18.0",
"zod": "^3.23.8",
"zod-validation-error": "^3.4.0"
"date-fns": "^3.3.1",
"jotai": "^2.6.4",
"lucide-react": "^0.338.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.1",
"recharts": "^2.12.2",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@replit/vite-plugin-cartographer": "^0.0.11",
"@replit/vite-plugin-runtime-error-modal": "^0.0.3",
"@tailwindcss/typography": "^0.5.15",
"@types/connect-pg-simple": "^7.0.3",
"@types/express": "4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "20.16.11",
"@types/passport": "^1.0.16",
"@types/passport-local": "^1.0.38",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/ws": "^8.5.13",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.30.4",
"esbuild": "^0.25.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"tsx": "^4.19.1",
"typescript": "5.6.3",
"vite": "^5.4.14"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
"@types/node": "^20.11.20",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.1.4"
}
}

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
}

View File

@ -1,70 +0,0 @@
import express, { type Request, Response, NextFunction } from "express";
import { registerRoutes } from "./routes";
import { setupVite, serveStatic, log } from "./vite";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: Record<string, any> | undefined = undefined;
const originalResJson = res.json;
res.json = function (bodyJson, ...args) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
(async () => {
const server = await registerRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// importantly only setup vite in development and after
// setting up all the other routes so the catch-all route
// doesn't interfere with the other routes
if (app.get("env") === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
// ALWAYS serve the app on port 5000
// this serves both the API and the client.
// It is the only port that is not firewalled.
const port = 5000;
server.listen({
port,
host: "0.0.0.0",
reusePort: true,
}, () => {
log(`serving on port ${port}`);
});
})();

View File

@ -1,736 +0,0 @@
import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import axios from "axios";
import { insertSpeciesSchema, insertSearchSchema, insertApiTokenSchema } from "@shared/schema";
import { ZodError } from "zod";
import { fromZodError } from "zod-validation-error";
const CITES_BASE_URL = "https://api.speciesplus.net/api/v1";
export async function registerRoutes(app: Express): Promise<Server> {
// API Token routes
app.post("/api/token", async (req: Request, res: Response) => {
try {
const tokenData = insertApiTokenSchema.parse(req.body);
let citesValid = false;
let iucnValid = false;
let warnings = [];
// Validate the CITES token by making a test request to CITES API
try {
await axios.get(`${CITES_BASE_URL}/taxon_concepts`, {
headers: {
"X-Authentication-Token": tokenData.token
}
});
citesValid = true;
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid CITES API token. Please check and try again."
});
}
// If IUCN token is provided, try to validate it
if (tokenData.iucnToken) {
try {
await axios.get("https://api.iucnredlist.org/api/v4/information/api_version", {
params: {
token: tokenData.iucnToken
}
});
iucnValid = true;
} catch (iucnError) {
warnings.push("The IUCN v4 token could not be validated. Please check your token and try again.");
// Don't store the invalid token
tokenData.iucnToken = null;
}
}
// Save the validated token
const savedToken = await storage.saveApiToken(tokenData);
// Return appropriate response based on validation results
if (warnings.length > 0) {
return res.json({
success: true,
token: savedToken,
warnings,
message: "CITES API token validated and saved. The IUCN v4 token validation failed."
});
}
res.json({
success: true,
token: savedToken,
message: tokenData.iucnToken
? "Both CITES and IUCN API tokens validated and saved successfully!"
: "CITES API token validated and saved successfully!"
});
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
success: false,
message: fromZodError(error).message
});
}
console.error("Error saving API token:", error);
res.status(500).json({
success: false,
message: "Failed to save API token"
});
}
});
app.get("/api/token", async (req: Request, res: Response) => {
try {
const token = await storage.getActiveToken();
res.json({
token: token?.token || null,
iucnToken: token?.iucnToken || null
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve API token"
});
}
});
// Species search routes
app.get("/api/species/search", async (req: Request, res: Response) => {
try {
const { query, format = "json" } = req.query;
if (!query) {
return res.status(400).json({
success: false,
message: "Search query is required"
});
}
// Get the API token
const tokenData = await storage.getActiveToken();
if (!tokenData) {
return res.status(401).json({
success: false,
message: "API token is not configured. Please set your CITES+ API token."
});
}
// Record the search query
await storage.addSearch({ query: query.toString() });
// Make request to CITES API
try {
const response = await axios.get(`${CITES_BASE_URL}/taxon_concepts.${format}`, {
params: { name: query },
headers: {
"X-Authentication-Token": tokenData.token
}
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from CITES+ API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to search species"
});
}
});
app.get("/api/species/:id", async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { endpoint, format = "json" } = req.query;
const tokenData = await storage.getActiveToken();
if (!tokenData) {
return res.status(401).json({
success: false,
message: "API token is not configured"
});
}
// Make request to CITES API for specific data about the taxon
try {
let apiUrl = `${CITES_BASE_URL}/taxon_concepts/${id}`;
if (endpoint) {
apiUrl += `/${endpoint}`;
}
apiUrl += `.${format}`;
const response = await axios.get(apiUrl, {
headers: {
"X-Authentication-Token": tokenData.token
}
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from CITES+ API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to get species details"
});
}
});
// Save species to database
app.post("/api/species", async (req: Request, res: Response) => {
try {
const speciesData = insertSpeciesSchema.parse(req.body);
const savedSpecies = await storage.saveSpecies(speciesData);
res.json({ success: true, species: savedSpecies });
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
success: false,
message: fromZodError(error).message
});
}
res.status(500).json({
success: false,
message: "Failed to save species data"
});
}
});
// Get all saved species
app.get("/api/species", async (req: Request, res: Response) => {
try {
const allSpecies = await storage.getAllSpecies();
res.json({ success: true, species: allSpecies });
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve saved species"
});
}
});
// Get recent searches
app.get("/api/searches", async (req: Request, res: Response) => {
try {
const { limit = 10 } = req.query;
const recentSearches = await storage.getRecentSearches(Number(limit));
res.json({ success: true, searches: recentSearches });
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve recent searches"
});
}
});
// CITES API Status check endpoint
app.get("/api/cites/status", async (req: Request, res: Response) => {
try {
const activeToken = await storage.getActiveToken();
if (!activeToken || !activeToken.token) {
return res.status(401).json({
success: false,
connected: false,
message: "CITES API token is not configured"
});
}
try {
// Make a request to the CITES API to verify the token
const response = await axios.get(`${CITES_BASE_URL}/taxon_concepts/search`, {
params: {
page: 1,
per_page: 1
},
headers: {
'X-Authentication-Token': activeToken.token
}
});
res.json({
success: true,
connected: true,
message: "CITES API is connected and responding"
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
connected: false,
message: "Failed to connect to CITES API or invalid token",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
connected: false,
message: "Error checking CITES API status"
});
}
});
// IUCN API Status check endpoint - v4 only
app.get("/api/iucn/status", async (req: Request, res: Response) => {
try {
// First try to use environment variable for API key
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
connected: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// Check if the API is working by hitting the version endpoint
const response = await axios.get("https://api.iucnredlist.org/api/v4/information/api_version", {
params: {
token: iucnToken
}
});
console.log("IUCN API v4 version check response:", response.data);
return res.json({
success: true,
connected: true,
apiVersion: "v4",
message: "IUCN API v4 is connected and responding"
});
} catch (error: any) {
console.log("IUCN API check failed:", error.message);
// Return useful error messages
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
connected: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
// For network errors or other issues
return res.status(error.response?.status || 500).json({
success: false,
connected: false,
message: "Failed to connect to IUCN API: " + (error.response?.data?.message || error.message)
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
connected: false,
message: "Error checking IUCN API status: " + errorMessage
});
}
});
// IUCN Red List API routes
app.get("/api/iucn/species", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Try to get IUCN token from environment variable first
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// Use the v4 API with scientific name endpoint
const response = await axios.get("https://api.iucnredlist.org/api/v4/taxa/scientific_name", {
params: {
token: iucnToken,
genus_name: genusName,
species_name: speciesName || ""
}
});
// Debug the response structure
console.log("IUCN API response structure:",
Object.keys(response.data),
response.data.result ? `Result has ${response.data.result.length} items` : "No result property");
return res.json({
success: true,
data: response.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API species lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN species data: " + errorMessage
});
}
});
app.get("/api/iucn/threats", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API query
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Try to get IUCN token from environment variable first
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// First, we need to find the species taxon ID using scientific name lookup
const taxaResponse = await axios.get("https://apiv4.iucnredlist.org/api/v4/taxa/scientific_name", {
headers: {
"Authorization": `Bearer ${iucnToken}`
},
params: {
genus_name: genusName,
species_name: speciesName || ""
}
});
// Check if we found the species
if (!taxaResponse.data?.result || taxaResponse.data.result.length === 0) {
return res.status(404).json({
success: false,
message: `No species found with the name "${name}" in the IUCN Red List database.`
});
}
const taxonId = taxaResponse.data.result[0].taxonid;
// Now retrieve the threats using the taxon ID
const threatsResponse = await axios.get(`https://api.iucnredlist.org/api/v4/threats`, {
params: {
token: iucnToken,
taxonid: taxonId
}
});
// Debug the response structure
console.log("IUCN Threats API response structure:",
Object.keys(threatsResponse.data),
threatsResponse.data.result ? `Result has ${threatsResponse.data.result.length} items` : "No result property");
return res.json({
success: true,
data: threatsResponse.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API threats lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN threats data: " + errorMessage
});
}
});
app.get("/api/iucn/habitats", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API query
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Try to get IUCN token from environment variable first
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// First, we need to find the species taxon ID using scientific name lookup
const taxaResponse = await axios.get("https://apiv4.iucnredlist.org/api/v4/taxa/scientific_name", {
headers: {
"Authorization": `Bearer ${iucnToken}`
},
params: {
genus_name: genusName,
species_name: speciesName || ""
}
});
// Check if we found the species
if (!taxaResponse.data?.result || taxaResponse.data.result.length === 0) {
return res.status(404).json({
success: false,
message: `No species found with the name "${name}" in the IUCN Red List database.`
});
}
const taxonId = taxaResponse.data.result[0].taxonid;
// Now retrieve the habitats using the taxon ID
const habitatsResponse = await axios.get(`https://api.iucnredlist.org/api/v4/habitats`, {
params: {
token: iucnToken,
taxonid: taxonId
}
});
// Debug the response structure
console.log("IUCN Habitats API response structure:",
Object.keys(habitatsResponse.data),
habitatsResponse.data.result ? `Result has ${habitatsResponse.data.result.length} items` : "No result property");
return res.json({
success: true,
data: habitatsResponse.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API habitats lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN habitats data: " + errorMessage
});
}
});
app.get("/api/iucn/measures", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API query
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Try to get IUCN token from environment variable first
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// First, we need to find the species taxon ID using scientific name lookup
const taxaResponse = await axios.get("https://apiv4.iucnredlist.org/api/v4/taxa/scientific_name", {
headers: {
"Authorization": `Bearer ${iucnToken}`
},
params: {
genus_name: genusName,
species_name: speciesName || ""
}
});
// Check if we found the species
if (!taxaResponse.data?.result || taxaResponse.data.result.length === 0) {
return res.status(404).json({
success: false,
message: `No species found with the name "${name}" in the IUCN Red List database.`
});
}
const taxonId = taxaResponse.data.result[0].taxonid;
// Now retrieve the conservation measures using the taxon ID
const measuresResponse = await axios.get(`https://api.iucnredlist.org/api/v4/measures`, {
params: {
token: iucnToken,
taxonid: taxonId
}
});
// Debug the response structure
console.log("IUCN Measures API response structure:",
Object.keys(measuresResponse.data),
measuresResponse.data.result ? `Result has ${measuresResponse.data.result.length} items` : "No result property");
return res.json({
success: true,
data: measuresResponse.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API conservation measures lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN conservation measures data: " + errorMessage
});
}
});
const httpServer = createServer(app);
return httpServer;
}

View File

@ -1,144 +0,0 @@
import {
species, type Species, type InsertSpecies,
searches, type Search, type InsertSearch,
apiTokens, type ApiToken, type InsertApiToken
} from "@shared/schema";
export interface IStorage {
// Species methods
getSpecies(id: number): Promise<Species | undefined>;
getSpeciesByName(scientificName: string): Promise<Species | undefined>;
getAllSpecies(): Promise<Species[]>;
saveSpecies(speciesData: InsertSpecies): Promise<Species>;
// Search methods
addSearch(search: InsertSearch): Promise<Search>;
getRecentSearches(limit?: number): Promise<Search[]>;
// API Token methods
saveApiToken(token: InsertApiToken): Promise<ApiToken>;
getActiveToken(): Promise<ApiToken | undefined>;
updateToken(id: number, token: string, iucnToken?: string): Promise<ApiToken | undefined>;
}
export class MemStorage implements IStorage {
private speciesStore: Map<number, Species>;
private searchesStore: Map<number, Search>;
private tokensStore: Map<number, ApiToken>;
private speciesId: number;
private searchId: number;
private tokenId: number;
constructor() {
this.speciesStore = new Map();
this.searchesStore = new Map();
this.tokensStore = new Map();
this.speciesId = 1;
this.searchId = 1;
this.tokenId = 1;
}
// Species methods
async getSpecies(id: number): Promise<Species | undefined> {
return this.speciesStore.get(id);
}
async getSpeciesByName(scientificName: string): Promise<Species | undefined> {
return Array.from(this.speciesStore.values()).find(
(species) => species.scientificName.toLowerCase() === scientificName.toLowerCase()
);
}
async getAllSpecies(): Promise<Species[]> {
return Array.from(this.speciesStore.values()).sort(
(a, b) => new Date(b.searchedAt!).getTime() - new Date(a.searchedAt!).getTime()
);
}
async saveSpecies(speciesData: InsertSpecies): Promise<Species> {
const existing = await this.getSpeciesByName(speciesData.scientificName);
if (existing) {
// Update existing species with new data and timestamp
const updated: Species = {
...existing,
...speciesData,
searchedAt: new Date(),
};
this.speciesStore.set(existing.id, updated);
return updated;
}
const id = this.speciesId++;
const timestamp = new Date();
const species: Species = {
...(speciesData as any), // Cast to any to avoid TypeScript errors
id,
searchedAt: timestamp
};
this.speciesStore.set(id, species);
return species;
}
// Search methods
async addSearch(searchData: InsertSearch): Promise<Search> {
const id = this.searchId++;
const search: Search = {
...searchData,
id,
timestamp: new Date()
};
this.searchesStore.set(id, search);
return search;
}
async getRecentSearches(limit: number = 10): Promise<Search[]> {
return Array.from(this.searchesStore.values())
.sort((a, b) => {
const dateA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const dateB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return dateB - dateA;
})
.slice(0, limit);
}
// API Token methods
async saveApiToken(tokenData: InsertApiToken): Promise<ApiToken> {
// Deactivate all existing tokens
this.tokensStore.forEach((token) => {
token.isActive = false;
});
const id = this.tokenId++;
// Ensure all required fields are present
const token: ApiToken = {
id,
token: tokenData.token,
iucnToken: tokenData.iucnToken || null,
isActive: tokenData.isActive || true,
createdAt: new Date()
};
this.tokensStore.set(id, token);
return token;
}
async getActiveToken(): Promise<ApiToken | undefined> {
return Array.from(this.tokensStore.values()).find(token => token.isActive);
}
async updateToken(id: number, token: string, iucnToken?: string): Promise<ApiToken | undefined> {
const existingToken = this.tokensStore.get(id);
if (!existingToken) return undefined;
const updatedToken = {
...existingToken,
token,
...(iucnToken !== undefined ? { iucnToken } : {})
};
this.tokensStore.set(id, updatedToken);
return updatedToken;
}
}
export const storage = new MemStorage();

View File

@ -1,88 +0,0 @@
import express, { type Express } from "express";
import fs from "fs";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import { createServer as createViteServer, createLogger } from "vite";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import { type Server } from "http";
import viteConfig from "../vite.config";
import { nanoid } from "nanoid";
const viteLogger = createLogger();
export function log(message: string, source = "express") {
const formattedTime = new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
console.log(`${formattedTime} [${source}] ${message}`);
}
export async function setupVite(app: Express, server: Server) {
const serverOptions = {
middlewareMode: true,
hmr: { server },
allowedHosts: true,
};
const vite = await createViteServer({
...viteConfig,
configFile: false,
customLogger: {
...viteLogger,
error: (msg, options) => {
viteLogger.error(msg, options);
process.exit(1);
},
},
server: serverOptions,
appType: "custom",
});
app.use(vite.middlewares);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
try {
const clientTemplate = path.resolve(
__dirname,
"..",
"client",
"index.html",
);
// always reload the index.html file from disk incase it changes
let template = await fs.promises.readFile(clientTemplate, "utf-8");
template = template.replace(
`src="/src/main.tsx"`,
`src="/src/main.tsx?v=${nanoid()}"`,
);
const page = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(page);
} catch (e) {
vite.ssrFixStacktrace(e as Error);
next(e);
}
});
}
export function serveStatic(app: Express) {
const distPath = path.resolve(__dirname, "public");
if (!fs.existsSync(distPath)) {
throw new Error(
`Could not find the build directory: ${distPath}, make sure to build the client first`,
);
}
app.use(express.static(distPath));
// fall through to index.html if the file doesn't exist
app.use("*", (_req, res) => {
res.sendFile(path.resolve(distPath, "index.html"));
});
}

View File

@ -1,74 +0,0 @@
import { pgTable, text, serial, integer, boolean, timestamp, jsonb } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
export const species = pgTable("species", {
id: serial("id").primaryKey(),
scientificName: text("scientific_name").notNull(),
commonName: text("common_name"),
rank: text("rank"),
kingdom: text("kingdom"),
phylum: text("phylum"),
class: text("class"),
order: text("order"),
family: text("family"),
genus: text("genus"),
citesListings: jsonb("cites_listings"),
citesId: integer("cites_id"),
iucnStatus: text("iucn_status"),
iucnId: text("iucn_id"),
iucnData: jsonb("iucn_data"),
apiData: jsonb("api_data"),
searchedAt: timestamp("searched_at").defaultNow(),
});
export const insertSpeciesSchema = createInsertSchema(species).pick({
scientificName: true,
commonName: true,
rank: true,
kingdom: true,
phylum: true,
class: true,
order: true,
family: true,
genus: true,
citesListings: true,
citesId: true,
iucnStatus: true,
iucnId: true,
iucnData: true,
apiData: true,
});
export type InsertSpecies = z.infer<typeof insertSpeciesSchema>;
export type Species = typeof species.$inferSelect;
export const searches = pgTable("searches", {
id: serial("id").primaryKey(),
query: text("query").notNull(),
timestamp: timestamp("timestamp").defaultNow(),
});
export const insertSearchSchema = createInsertSchema(searches).pick({
query: true,
});
export type InsertSearch = z.infer<typeof insertSearchSchema>;
export type Search = typeof searches.$inferSelect;
export const apiTokens = pgTable("api_tokens", {
id: serial("id").primaryKey(),
token: text("token").notNull(), // CITES API token
iucnToken: text("iucn_token"), // IUCN API v4 token
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
});
export const insertApiTokenSchema = createInsertSchema(apiTokens).pick({
token: true,
iucnToken: true,
isActive: true,
});
export type InsertApiToken = z.infer<typeof insertApiTokenSchema>;
export type ApiToken = typeof apiTokens.$inferSelect;

15
src/App.tsx Normal file
View File

@ -0,0 +1,15 @@
import { Routes, Route } from 'react-router-dom';
import { HomePage } from '@/pages/home';
function App() {
return (
<div className="min-h-screen bg-background">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="*" element={<div className="container p-8 text-center">Page not found</div>} />
</Routes>
</div>
);
}
export default App;

View File

@ -0,0 +1,45 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
type DebugPanelProps = {
data: any;
title?: string;
};
export function DebugPanel({ data, title = 'Debug Data' }: DebugPanelProps) {
const [isVisible, setIsVisible] = useState(false);
// Extract CITES listings if they exist
const citesListings = data?.cites_listings || [];
return (
<Card className="border-dashed border-gray-300">
<CardHeader className="py-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-normal text-muted-foreground">
{title} {citesListings.length > 0 && `(${citesListings.length} CITES listings)`}
</CardTitle>
<Button variant="outline" size="sm" onClick={() => setIsVisible(!isVisible)}>
{isVisible ? 'Hide' : 'Show'}
</Button>
</div>
</CardHeader>
{isVisible && (
<CardContent className="pb-3 pt-0">
{citesListings.length > 0 && (
<div className="mb-4 rounded border border-yellow-200 bg-yellow-50 p-2">
<h3 className="mb-2 text-sm font-semibold text-yellow-800">CITES Listings:</h3>
<pre className="overflow-auto text-xs text-yellow-900">
{JSON.stringify(citesListings, null, 2)}
</pre>
</div>
)}
<pre className="max-h-96 overflow-auto rounded bg-slate-50 p-2 text-xs">
{JSON.stringify(data, null, 2)}
</pre>
</CardContent>
)}
</Card>
);
}

View File

@ -0,0 +1,113 @@
import { useQuery } from '@tanstack/react-query';
import { getSpeciesById } from '@/lib/api';
import { SpeciesTabs } from './species-tabs';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { IUCN_STATUS_COLORS, CITES_APPENDIX_COLORS } from '@/lib/utils';
import { Loader2, ChevronLeft } from 'lucide-react';
import { DebugPanel } from './debug-panel';
import { useEffect } from 'react';
type ResultsContainerProps = {
speciesId: string | null;
onBack: () => void;
};
export function ResultsContainer({ speciesId, onBack }: ResultsContainerProps) {
const { data: species, isLoading, error } = useQuery({
queryKey: ['species', speciesId],
queryFn: () => getSpeciesById(speciesId as string),
enabled: !!speciesId,
});
// Add this useEffect to see the full species data in console when it changes
useEffect(() => {
if (species) {
console.log('ResultsContainer species data loaded for ID:', species.id);
console.log('CITES listings in ResultsContainer:', species.cites_listings?.length);
if (species.cites_listings?.length > 0) {
console.log('First CITES listing:', species.cites_listings[0]);
if (species.cites_listings.length > 1) {
console.log('Second CITES listing:', species.cites_listings[1]);
}
}
}
}, [species]);
if (!speciesId) {
return null;
}
if (isLoading) {
return (
<div className="flex h-64 w-full items-center justify-center">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
if (error || !species) {
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="text-destructive">Error Loading Species</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4">There was an error loading the species information. Please try again.</p>
<Button onClick={onBack} variant="outline" size="sm">
<ChevronLeft className="mr-2 h-4 w-4" />
Back to Search
</Button>
</CardContent>
</Card>
);
}
return (
<div className="w-full space-y-6">
<div className="flex items-start justify-between">
<Button onClick={onBack} variant="outline" size="sm">
<ChevronLeft className="mr-2 h-4 w-4" />
Back to Search
</Button>
<div className="flex flex-wrap items-center gap-2">
{species.latest_assessment?.status && (
<Badge className={IUCN_STATUS_COLORS[species.latest_assessment.status]}>
IUCN: {species.latest_assessment.status}
</Badge>
)}
{species.current_cites_listing && (
<Badge className={CITES_APPENDIX_COLORS[species.current_cites_listing.appendix]}>
CITES: Appendix {species.current_cites_listing.appendix}
</Badge>
)}
</div>
</div>
<Card>
<CardHeader>
<CardTitle className="flex flex-wrap items-center gap-2">
<span className="italic">{species.scientific_name}</span>
</CardTitle>
<CardDescription>
{species.common_name}
{species.common_names?.length > 0 && (
<span className="ml-2 text-xs">
(Also known as: {species.common_names.map((cn) => cn.name).join(', ')})
</span>
)}
</CardDescription>
</CardHeader>
<CardContent>
<SpeciesTabs species={species} />
</CardContent>
</Card>
{/* Debug panel to see data */}
<DebugPanel data={species} title="Species Data from Supabase" />
</div>
);
}

View File

@ -0,0 +1,91 @@
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 { Search, Loader2 } from 'lucide-react';
import { IUCN_STATUS_COLORS } from '@/lib/utils';
export function SearchForm({ onSelectSpecies }: { onSelectSpecies: (id: string) => void }) {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState('');
// Debounce search term
const handleSearch = (value: string) => {
setSearchTerm(value);
if (value.length >= 3) {
const timeoutId = setTimeout(() => {
setDebouncedTerm(value);
}, 500);
return () => clearTimeout(timeoutId);
} else if (value.length === 0) {
setDebouncedTerm('');
}
};
// Fetch search results
const { data: searchResults, isLoading } = useQuery({
queryKey: ['searchSpecies', debouncedTerm],
queryFn: () => searchSpecies(debouncedTerm),
enabled: debouncedTerm.length >= 3,
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (searchTerm.length >= 3) {
setDebouncedTerm(searchTerm);
}
};
return (
<div className="w-full max-w-2xl">
<form onSubmit={handleSubmit} className="relative">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Input
type="text"
placeholder="Search for a species (scientific or common name)..."
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
className="pr-10"
/>
{isLoading ? (
<Loader2 className="absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 animate-spin text-muted-foreground" />
) : (
<Search className="absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
)}
</div>
<Button type="submit" disabled={searchTerm.length < 3}>
Search
</Button>
</div>
</form>
{/* Search Results */}
{searchResults && searchResults.length > 0 && (
<Card className="mt-2">
<CardContent className="p-2">
<ul className="divide-y divide-border">
{searchResults.map((species) => (
<li
key={species.id}
className="cursor-pointer p-2 hover:bg-muted"
onClick={() => onSelectSpecies(species.id)}
>
<div className="font-medium italic">{species.scientific_name}</div>
<div className="text-sm text-muted-foreground">{species.common_name}</div>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{searchResults && searchResults.length === 0 && debouncedTerm.length >= 3 && (
<p className="mt-2 text-sm text-muted-foreground">No species found matching your search.</p>
)}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -33,4 +33,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
)
}
export { Badge, badgeVariants }
export { Badge, badgeVariants }

View File

@ -1,11 +1,10 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
@ -31,26 +30,26 @@ const buttonVariants = cva(
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
);
}
)
Button.displayName = "Button"
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@ -76,4 +76,4 @@ const CardFooter = React.forwardRef<
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
@ -22,4 +22,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
)
Input.displayName = "Input"
export { Input }
export { Input }

View File

@ -21,4 +21,4 @@ const Label = React.forwardRef<
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
export { Label }

View File

@ -155,4 +155,4 @@ export {
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
}

View File

@ -1,9 +1,8 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
@ -17,8 +16,8 @@ const TabsList = React.forwardRef<
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@ -32,8 +31,8 @@ const TabsTrigger = React.forwardRef<
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@ -47,7 +46,7 @@ const TabsContent = React.forwardRef<
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };

10
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_SUPABASE_URL: string
readonly VITE_SUPABASE_ANON_KEY: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

76
src/index.css Normal file
View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

382
src/lib/api.ts Normal file
View File

@ -0,0 +1,382 @@
import { supabase } from './supabase';
import { Database } from '../types/supabase';
export type Species = Database['public']['Tables']['species']['Row'] & {
common_names?: CommonName[];
primary_common_name?: string;
};
export type CommonName = Database['public']['Tables']['common_names']['Row'];
export type Subpopulation = Database['public']['Tables']['subpopulations']['Row'];
export type IucnAssessment = Database['public']['Tables']['iucn_assessments']['Row'];
// Update the type definition to match the actual database structure
export type CitesListing = Omit<Database['public']['Tables']['cites_listings']['Row'], 'listing_date'> & {
listing_date: string;
};
export type CitesTradeRecord = Database['public']['Tables']['cites_trade_records']['Row'];
export type TimelineEvent = Database['public']['Tables']['timeline_events']['Row'];
export type SpeciesDetails = Species & {
common_names: CommonName[];
subpopulations: Subpopulation[];
iucn_assessments: IucnAssessment[];
cites_listings: CitesListing[];
latest_assessment?: IucnAssessment;
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...");
// First get all species
const { data: allSpecies, error: speciesError } = await supabase
.from('species')
.select('*')
.order('scientific_name');
if (speciesError) {
console.error("Error fetching species:", speciesError);
throw speciesError;
}
if (!allSpecies || allSpecies.length === 0) {
console.warn("No species found in database!");
return [];
}
console.log(`Fetched ${allSpecies.length} total species records`);
// Manually filter out duplicates by scientific_name
const uniqueSpeciesMap = new Map();
allSpecies.forEach(species => {
if (!uniqueSpeciesMap.has(species.scientific_name)) {
uniqueSpeciesMap.set(species.scientific_name, species);
}
});
const distinctSpecies = Array.from(uniqueSpeciesMap.values());
console.log(`Filtered to ${distinctSpecies.length} distinct species`);
// Then get common names for all species
const { data: commonNames, error: commonNamesError } = await supabase
.from('common_names')
.select('*')
.in('species_id', distinctSpecies.map(s => s.id));
if (commonNamesError) {
console.error("Error fetching common names:", commonNamesError);
throw commonNamesError;
}
console.log(`Fetched ${commonNames?.length || 0} common names`);
// Group common names by species_id
const commonNamesBySpecies = new Map();
commonNames?.forEach(cn => {
if (!commonNamesBySpecies.has(cn.species_id)) {
commonNamesBySpecies.set(cn.species_id, []);
}
commonNamesBySpecies.get(cn.species_id).push(cn);
});
// Transform the data to include primary_common_name
const transformedSpecies = distinctSpecies.map(species => ({
...species,
common_names: commonNamesBySpecies.get(species.id) || [],
primary_common_name: commonNamesBySpecies.get(species.id)?.[0]?.name || species.common_name || species.scientific_name
}));
return transformedSpecies;
} catch (error) {
console.error("Error in getAllSpecies:", error);
return []; // Return empty array instead of throwing to avoid breaking the UI
}
}
export async function getSpeciesById(id: string): Promise<SpeciesDetails | null> {
try {
// Get species basic info
const { data: species, error: speciesError } = await supabase
.from('species')
.select('*')
.eq('id', id)
.single();
if (speciesError) throw speciesError;
if (!species) return null;
// Get common names
const { data: commonNames, error: commonNamesError } = await supabase
.from('common_names')
.select('*')
.eq('species_id', id);
if (commonNamesError) throw commonNamesError;
// Get subpopulations
const { data: subpopulations, error: subpopulationsError } = await supabase
.from('subpopulations')
.select('*')
.eq('species_id', id);
if (subpopulationsError) throw subpopulationsError;
// Get IUCN assessments
const { data: iucnAssessments, error: iucnError } = await supabase
.from('iucn_assessments')
.select('*')
.eq('species_id', id)
.order('year_published', { ascending: false });
if (iucnError) throw iucnError;
// Approach 1: Just get all CITES listings without any filtering
console.log('Getting all CITES listings');
const { data: allListings, error: allListingsError } = await supabase
.from('cites_listings')
.select('*');
if (allListingsError) {
console.error('Error getting ALL listings:', allListingsError);
throw allListingsError;
}
console.log('All listings in database:', allListings);
// Now manually filter to this species
const citesListings = allListings.filter(listing =>
listing.species_id === id
);
console.log(`Found ${citesListings.length} listings for species ID ${id}:`, citesListings);
// Find latest assessment
const latestAssessment = iucnAssessments?.find(a => a.is_latest) ||
(iucnAssessments && iucnAssessments.length > 0 ? iucnAssessments[0] : undefined);
// Set current CITES listing
const currentCitesListing = citesListings.find(l => l.is_current) ||
(citesListings.length > 0 ? citesListings[0] : undefined);
// Construct the response
return {
...species,
common_names: commonNames || [],
subpopulations: subpopulations || [],
iucn_assessments: iucnAssessments || [],
cites_listings: citesListings,
latest_assessment: latestAssessment,
current_cites_listing: currentCitesListing,
};
} catch (error) {
console.error('Error in getSpeciesById:', error);
throw error;
}
}
export async function getTimelineEvents(speciesId: string) {
const { data, error } = await supabase
.from('timeline_events')
.select('*')
.eq('species_id', speciesId)
.not('event_type', 'eq', 'cites_trade')
.order('event_date', { ascending: false });
if (error) throw error;
return data as TimelineEvent[];
}
export async function getCitesTradeRecords(speciesId: string) {
try {
console.log('getCitesTradeRecords called with species ID:', speciesId);
// First get the species details to have the scientific name as fallback
const { data: speciesData, error: speciesError } = await supabase
.from('species')
.select('scientific_name, common_name, family, genus, species_name')
.eq('id', speciesId)
.single();
if (speciesError) {
console.error('Error fetching species info:', speciesError);
} else {
console.log('Found species for trade lookup:', speciesData);
}
console.log('Fetching all CITES trade records...');
// Initialize array to store all records
let allRecords: CitesTradeRecord[] = [];
let page = 0;
const pageSize = 1000;
let hasMore = true;
// Fetch records in batches until we have all of them
while (hasMore) {
const { data: records, error: directError } = await supabase
.from('cites_trade_records')
.select('*')
.eq('species_id', speciesId)
.order('year', { ascending: false })
.range(page * pageSize, (page + 1) * pageSize - 1);
if (directError) {
console.error('Error with direct species_id query:', directError);
break;
}
if (!records || records.length === 0) {
hasMore = false;
break;
}
allRecords = [...allRecords, ...records];
console.log(`Fetched batch ${page + 1}, total records so far: ${allRecords.length}`);
// If we got less than the page size, we've reached the end
if (records.length < pageSize) {
hasMore = false;
} else {
page++;
}
}
if (allRecords.length > 0) {
console.log(`Found total of ${allRecords.length} records with direct species_id query`);
console.log('First few records:', allRecords.slice(0, 3));
console.log('Year range:', Math.min(...allRecords.map(r => r.year)),
'to', Math.max(...allRecords.map(r => r.year)));
return allRecords;
}
// If no direct matches found, try the fallback approach
console.log('No records found with direct query, trying fallback matching...');
// Reset pagination for fallback approach
allRecords = [];
page = 0;
hasMore = true;
while (hasMore) {
const { data: records, error: fallbackError } = await supabase
.from('cites_trade_records')
.select('*')
.order('year', { ascending: false })
.range(page * pageSize, (page + 1) * pageSize - 1);
if (fallbackError) {
console.error('Error with fallback query:', fallbackError);
break;
}
if (!records || records.length === 0) {
hasMore = false;
break;
}
// Filter records for this batch
const filteredRecords = records.filter(record => {
// Try scientific name match with the taxon field
if (record.taxon &&
speciesData?.scientific_name &&
record.taxon.toLowerCase() === speciesData.scientific_name.toLowerCase()) {
return true;
}
// Try family match
if (record.family &&
speciesData?.family &&
record.family.toLowerCase() === speciesData.family.toLowerCase()) {
// For family matches, also check genus if available
if (record.genus &&
speciesData.genus &&
record.genus.toLowerCase() === speciesData.genus.toLowerCase()) {
return true;
}
}
return false;
});
allRecords = [...allRecords, ...filteredRecords];
console.log(`Fetched batch ${page + 1}, total filtered records so far: ${allRecords.length}`);
if (records.length < pageSize) {
hasMore = false;
} else {
page++;
}
}
if (allRecords.length > 0) {
console.log(`Found total of ${allRecords.length} trade records after filtering`);
console.log('Year range:', Math.min(...allRecords.map(r => r.year)),
'to', Math.max(...allRecords.map(r => r.year)));
}
// Sort by year descending
allRecords.sort((a, b) => b.year - a.year);
return allRecords;
} catch (error) {
console.error('Error in getCitesTradeRecords:', error);
return [];
}
}
// Function to search species by scientific or common name
export async function searchSpecies(query: string) {
if (!query || query.length < 3) return [];
// First search in scientific_name and common_name fields
const { data: speciesResults, error: speciesError } = await supabase
.from('species')
.select('*')
.or(`scientific_name.ilike.%${query}%,common_name.ilike.%${query}%`)
.limit(20);
if (speciesError) throw speciesError;
// Also search in common_names table
const { data: commonNamesResults, error: commonNamesError } = await supabase
.from('common_names')
.select('*, species!inner(*)')
.ilike('name', `%${query}%`)
.limit(20);
if (commonNamesError) throw commonNamesError;
// Combine the results, removing duplicates
const speciesMap = new Map<string, Species>();
if (speciesResults) {
for (const species of speciesResults) {
speciesMap.set(species.id, species);
}
}
if (commonNamesResults) {
for (const result of commonNamesResults) {
if (result.species) {
speciesMap.set(result.species.id, result.species);
}
}
}
return Array.from(speciesMap.values());
}

10
src/lib/query-client.ts Normal file
View File

@ -0,0 +1,10 @@
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false,
},
},
});

40
src/lib/supabase.ts Normal file
View File

@ -0,0 +1,40 @@
import { createClient } from '@supabase/supabase-js';
import { Database } from '../types/supabase';
// Work around TypeScript errors with Vite environment variables
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string;
console.log('Initializing Supabase client...');
console.log('URL available:', !!supabaseUrl);
console.log('Key available:', !!supabaseAnonKey);
if (!supabaseUrl || !supabaseAnonKey) {
console.error('Missing Supabase environment variables!');
throw new Error('Missing Supabase environment variables');
}
// Create the Supabase client
const client = createClient<Database>(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
}
});
// Test the connection
(async () => {
try {
const { data, error } = await client.from('species').select('id').limit(1);
if (error) {
console.error('Failed to connect to Supabase:', error.message);
} else {
console.log('Successfully connected to Supabase');
}
} catch (e) {
console.error('Failed to test Supabase connection:', e);
}
})();
// Export the client
export const supabase = client;

49
src/lib/utils.ts Normal file
View File

@ -0,0 +1,49 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(date: string | Date): string {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
export const IUCN_STATUS_COLORS: Record<string, string> = {
'EX': 'bg-black text-white', // Extinct
'EW': 'bg-gray-800 text-white', // Extinct in the Wild
'CR': 'bg-red-600 text-white', // Critically Endangered
'EN': 'bg-orange-600 text-white', // Endangered
'VU': 'bg-yellow-500 text-black', // Vulnerable
'NT': 'bg-yellow-300 text-black', // Near Threatened
'LC': 'bg-green-500 text-black', // Least Concern
'DD': 'bg-gray-500 text-white', // Data Deficient
'NE': 'bg-gray-300 text-black', // Not Evaluated
};
export const IUCN_STATUS_FULL_NAMES: Record<string, string> = {
'EX': 'Extinct',
'EW': 'Extinct in the Wild',
'CR': 'Critically Endangered',
'EN': 'Endangered',
'VU': 'Vulnerable',
'NT': 'Near Threatened',
'LC': 'Least Concern',
'DD': 'Data Deficient',
'NE': 'Not Evaluated',
};
export const CITES_APPENDIX_COLORS: Record<string, string> = {
'I': 'bg-red-600 text-white', // Appendix I
'II': 'bg-blue-600 text-white', // Appendix II
'III': 'bg-green-600 text-white', // Appendix III
};
export function truncateString(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + '...';
}

19
src/main.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
import './index.css'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/query-client.ts'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>,
)

110
src/pages/home.tsx Normal file
View File

@ -0,0 +1,110 @@
import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getAllSpecies } from '@/lib/api';
import { SearchForm } from '@/components/search-form';
import { ResultsContainer } from '@/components/results-container';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Search, Globe } from 'lucide-react';
export function HomePage() {
const [selectedSpeciesId, setSelectedSpeciesId] = useState<string | null>(null);
const [showSearch, setShowSearch] = useState<boolean>(false);
const { data: allSpecies, isLoading } = useQuery({
queryKey: ['allSpecies'],
queryFn: getAllSpecies,
});
// Create a filtered list of unique species by scientific name
const uniqueSpecies = useMemo(() => {
if (!allSpecies) return [];
const uniqueMap = new Map();
allSpecies.forEach(species => {
if (!uniqueMap.has(species.scientific_name)) {
uniqueMap.set(species.scientific_name, species);
}
});
return Array.from(uniqueMap.values());
}, [allSpecies]);
const handleSelectSpecies = (id: string) => {
setSelectedSpeciesId(id);
setShowSearch(false);
};
const handleBackToSearch = () => {
setSelectedSpeciesId(null);
};
const handleToggleSearch = () => {
setShowSearch(!showSearch);
if (selectedSpeciesId) {
setSelectedSpeciesId(null);
}
};
console.log('All species length:', allSpecies?.length);
console.log('Unique species length:', uniqueSpecies?.length);
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">
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" />
Search Species
</Button>
<Button onClick={() => setShowSearch(false)} variant={!showSearch && !selectedSpeciesId ? "default" : "outline"}>
<Globe className="mr-2 h-4 w-4" />
Browse All Species
</Button>
</div>
</div>
{showSearch && !selectedSpeciesId ? (
<div className="mx-auto flex max-w-2xl flex-col items-center space-y-6">
<SearchForm onSelectSpecies={handleSelectSpecies} />
</div>
) : selectedSpeciesId ? (
<ResultsContainer speciesId={selectedSpeciesId} onBack={handleBackToSearch} />
) : (
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{isLoading ? (
<p className="col-span-full text-center">Loading species data...</p>
) : uniqueSpecies.length > 0 ? (
uniqueSpecies.map(species => (
<Card
key={species.id}
className="cursor-pointer transition-all hover:shadow-md"
onClick={() => handleSelectSpecies(species.id)}
>
<CardHeader className="p-4 pb-2">
<CardTitle className="text-lg">
<span className="italic">{species.scientific_name}</span>
</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">
{species.family}
</span>
</div>
</CardContent>
</Card>
))
) : (
<p className="col-span-full text-center">No species found</p>
)}
</div>
)}
</div>
);
}

301
src/types/supabase.ts Normal file
View File

@ -0,0 +1,301 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
public: {
Tables: {
species: {
Row: {
id: string
scientific_name: string
common_name: string
kingdom: string
phylum: string
class: string
order_name: string
family: string
genus: string
species_name: string
authority: string | null
sis_id: number | null
created_at: string
}
Insert: {
id?: string
scientific_name: string
common_name: string
kingdom: string
phylum: string
class: string
order_name: string
family: string
genus: string
species_name: string
authority?: string | null
sis_id?: number | null
created_at?: string
}
Update: {
id?: string
scientific_name?: string
common_name?: string
kingdom?: string
phylum?: string
class?: string
order_name?: string
family?: string
genus?: string
species_name?: string
authority?: string | null
sis_id?: number | null
created_at?: string
}
}
common_names: {
Row: {
id: string
species_id: string
name: string
language: string
is_main: boolean
}
Insert: {
id?: string
species_id: string
name: string
language: string
is_main?: boolean
}
Update: {
id?: string
species_id?: string
name?: string
language?: string
is_main?: boolean
}
}
subpopulations: {
Row: {
id: string
species_id: string
scientific_name: string
subpopulation_name: string
sis_id: number | null
authority: string | null
}
Insert: {
id?: string
species_id: string
scientific_name: string
subpopulation_name: string
sis_id?: number | null
authority?: string | null
}
Update: {
id?: string
species_id?: string
scientific_name?: string
subpopulation_name?: string
sis_id?: number | null
authority?: string | null
}
}
iucn_assessments: {
Row: {
id: string
species_id: string
year_published: number
is_latest: boolean
possibly_extinct: boolean
possibly_extinct_in_wild: boolean
status: string | null
url: string | null
assessment_id: number | null
scope_code: string | null
scope_description: string | null
}
Insert: {
id?: string
species_id: string
year_published: number
is_latest?: boolean
possibly_extinct?: boolean
possibly_extinct_in_wild?: boolean
status?: string | null
url?: string | null
assessment_id?: number | null
scope_code?: string | null
scope_description?: string | null
}
Update: {
id?: string
species_id?: string
year_published?: number
is_latest?: boolean
possibly_extinct?: boolean
possibly_extinct_in_wild?: boolean
status?: string | null
url?: string | null
assessment_id?: number | null
scope_code?: string | null
scope_description?: string | null
}
}
cites_listings: {
Row: {
id: string
species_id: string
appendix: string
listing_date: string
notes: string | null
is_current: boolean
}
Insert: {
id?: string
species_id: string
appendix: string
listing_date: string
notes?: string | null
is_current?: boolean
}
Update: {
id?: string
species_id?: string
appendix?: string
listing_date?: string
notes?: string | null
is_current?: boolean
}
}
cites_trade_records: {
Row: {
id: string
species_id: string
record_id: string | null
year: number
appendix: string
taxon: string
class: string | null
order_name: string | null
family: string | null
genus: string | null
term: string
quantity: number | null
unit: string | null
importer: string | null
exporter: string | null
origin: string | null
purpose: string | null
source: string | null
reporter_type: string | null
import_permit: string | null
export_permit: string | null
origin_permit: string | null
}
Insert: {
id?: string
species_id: string
record_id?: string | null
year: number
appendix: string
taxon: string
class?: string | null
order_name?: string | null
family?: string | null
genus?: string | null
term: string
quantity?: number | null
unit?: string | null
importer?: string | null
exporter?: string | null
origin?: string | null
purpose?: string | null
source?: string | null
reporter_type?: string | null
import_permit?: string | null
export_permit?: string | null
origin_permit?: string | null
}
Update: {
id?: string
species_id?: string
record_id?: string | null
year?: number
appendix?: string
taxon?: string
class?: string | null
order_name?: string | null
family?: string | null
genus?: string | null
term?: string
quantity?: number | null
unit?: string | null
importer?: string | null
exporter?: string | null
origin?: string | null
purpose?: string | null
source?: string | null
reporter_type?: string | null
import_permit?: string | null
export_permit?: string | null
origin_permit?: string | null
}
}
timeline_events: {
Row: {
id: string
species_id: string
event_date: string
year: number
event_type: string
title: string
description: string | null
status: string | null
source_type: string
source_id: string | null
}
Insert: {
id?: string
species_id: string
event_date: string
year: number
event_type: string
title: string
description?: string | null
status?: string | null
source_type: string
source_id?: string | null
}
Update: {
id?: string
species_id?: string
event_date?: string
year?: number
event_type?: string
title?: string
description?: string | null
status?: string | null
source_type?: string
source_id?: string | null
}
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}

Some files were not shown because too many files have changed in this diff Show More