Initial commit: Arctic Species 2025 Frontend
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_SUPABASE_URL=your_supabase_url
|
||||
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
|
36
.gitignore
vendored
36
.gitignore
vendored
@ -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
42
.replit
@ -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
263
README.md
@ -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
|
@ -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…
|
@ -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
|
@ -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
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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 }
|
@ -1,5 +0,0 @@
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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 }
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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 }
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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 }
|
@ -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 }
|
@ -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 }
|
@ -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 }
|
@ -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 }
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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 }
|
@ -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 }
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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 }
|
@ -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,
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 }
|
@ -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 }
|
@ -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 }
|
@ -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
|
||||
}
|
@ -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 }
|
@ -1,13 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply font-sans antialiased bg-background text-foreground;
|
||||
}
|
||||
}
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
@ -1,6 +0,0 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
@ -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 />);
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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 |
@ -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
7394
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
132
package.json
132
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,4 @@ export default {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
});
|
||||
})();
|
736
server/routes.ts
736
server/routes.ts
@ -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;
|
||||
}
|
@ -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();
|
@ -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"));
|
||||
});
|
||||
}
|
@ -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
15
src/App.tsx
Normal 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;
|
45
src/components/debug-panel.tsx
Normal file
45
src/components/debug-panel.tsx
Normal 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>
|
||||
);
|
||||
}
|
113
src/components/results-container.tsx
Normal file
113
src/components/results-container.tsx
Normal 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>
|
||||
);
|
||||
}
|
91
src/components/search-form.tsx
Normal file
91
src/components/search-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
1130
src/components/species-tabs.tsx
Normal file
1130
src/components/species-tabs.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -33,4 +33,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants }
|
@ -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 };
|
@ -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 }
|
@ -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 }
|
@ -21,4 +21,4 @@ const Label = React.forwardRef<
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
export { Label }
|
@ -155,4 +155,4 @@ export {
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
}
|
@ -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
10
src/env.d.ts
vendored
Normal 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
76
src/index.css
Normal 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
382
src/lib/api.ts
Normal 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
10
src/lib/query-client.ts
Normal 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
40
src/lib/supabase.ts
Normal 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
49
src/lib/utils.ts
Normal 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
19
src/main.tsx
Normal 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
110
src/pages/home.tsx
Normal 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
301
src/types/supabase.ts
Normal 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
Reference in New Issue
Block a user