adding freight web and holding

This commit is contained in:
JB
2025-12-24 00:38:53 -05:00
parent 0c8630b8ba
commit f39e9132ad
164 changed files with 5559 additions and 156655 deletions

12
.gitignore vendored
View File

@@ -1,6 +1,6 @@
dist
node_modules
.env.keys
data
db
coverage
dist
node_modules
.env.keys
data
db
coverage

View File

@@ -1,179 +1,179 @@
# Memory Bank
I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization.
## Memory Bank Structure
The Memory Bank consists of core files and optional context files, all in Markdown format.
### Core Files (Required)
1. `brief.md`
This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved.
- Foundation document that shapes all other files
- Created at project start if it doesn't exist
- Defines core requirements and goals
- Source of truth for project scope
2. `product.md`
- Why this project exists
- Problems it solves
- How it should work
- User experience goals
3. `context.md`
This file should be short and factual, not creative or speculative.
- Current work focus
- Recent changes
- Next steps
4. `architecture.md`
- System architecture
- Source Code paths
- Key technical decisions
- Design patterns in use
- Component relationships
- Critical implementation paths
5. `tech.md`
- Technologies used
- Development setup
- Technical constraints
- Dependencies
- Tool usage patterns
### Additional Files
Create additional files/folders within memory-bank/ when they help organize:
- `tasks.md` - Documentation of repetitive tasks and their workflows
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures
## Core workflows
### Memory Bank Initialization
The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built.
When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including:
- All source code files and their relationships
- Configuration files and build system setup
- Project structure and organization patterns
- Documentation and comments
- Dependencies and external integrations
- Testing frameworks and patterns
I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness.
After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions.
### Memory Bank Update
Memory Bank updates occur when:
1. Discovering new project patterns
2. After implementing significant changes
3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files)
4. When context needs clarification
If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?"
To execute Memory Bank update, I will:
1. Review ALL project files
2. Document current state
3. Document Insights & Patterns
4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state.
### Add Task
When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**.
This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include:
- Adding support for new AI model versions
- Implementing new API endpoints following established patterns
- Adding new features that follow existing architecture
Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks.
To execute Add Task workflow:
1. Create or update `tasks.md` in the memory bank folder
2. Document the task with:
- Task name and description
- Files that need to be modified
- Step-by-step workflow followed
- Important considerations or gotchas
- Example of the completed implementation
3. Include any context that was discovered during task execution but wasn't previously documented
Example task entry:
```markdown
## Add New Model Support
**Last performed:** [date]
**Files to modify:**
- `/providers/gemini.md` - Add model to documentation
- `/src/providers/gemini-config.ts` - Add model configuration
- `/src/constants/models.ts` - Add to model list
- `/tests/providers/gemini.test.ts` - Add test cases
**Steps:**
1. Add model configuration with proper token limits
2. Update documentation with model capabilities
3. Add to constants file for UI display
4. Write tests for new model configuration
**Important notes:**
- Check Google's documentation for exact token limits
- Ensure backward compatibility with existing configurations
- Test with actual API calls before committing
```
### Regular Task Execution
In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional.
The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like:
"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API."
When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed.
If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?"
In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes.
## Context Window Management
When the context window fills up during an extended session:
1. I should suggest updating the memory bank to preserve the current state
2. Recommend starting a fresh conversation/task
3. In the new conversation, I will automatically load the memory bank files to maintain continuity
## Technical Implementation
Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access.
## Important Notes
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user.
IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
# Memory Bank
I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization.
## Memory Bank Structure
The Memory Bank consists of core files and optional context files, all in Markdown format.
### Core Files (Required)
1. `brief.md`
This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved.
- Foundation document that shapes all other files
- Created at project start if it doesn't exist
- Defines core requirements and goals
- Source of truth for project scope
2. `product.md`
- Why this project exists
- Problems it solves
- How it should work
- User experience goals
3. `context.md`
This file should be short and factual, not creative or speculative.
- Current work focus
- Recent changes
- Next steps
4. `architecture.md`
- System architecture
- Source Code paths
- Key technical decisions
- Design patterns in use
- Component relationships
- Critical implementation paths
5. `tech.md`
- Technologies used
- Development setup
- Technical constraints
- Dependencies
- Tool usage patterns
### Additional Files
Create additional files/folders within memory-bank/ when they help organize:
- `tasks.md` - Documentation of repetitive tasks and their workflows
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures
## Core workflows
### Memory Bank Initialization
The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built.
When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including:
- All source code files and their relationships
- Configuration files and build system setup
- Project structure and organization patterns
- Documentation and comments
- Dependencies and external integrations
- Testing frameworks and patterns
I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness.
After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions.
### Memory Bank Update
Memory Bank updates occur when:
1. Discovering new project patterns
2. After implementing significant changes
3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files)
4. When context needs clarification
If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?"
To execute Memory Bank update, I will:
1. Review ALL project files
2. Document current state
3. Document Insights & Patterns
4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state.
### Add Task
When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**.
This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include:
- Adding support for new AI model versions
- Implementing new API endpoints following established patterns
- Adding new features that follow existing architecture
Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks.
To execute Add Task workflow:
1. Create or update `tasks.md` in the memory bank folder
2. Document the task with:
- Task name and description
- Files that need to be modified
- Step-by-step workflow followed
- Important considerations or gotchas
- Example of the completed implementation
3. Include any context that was discovered during task execution but wasn't previously documented
Example task entry:
```markdown
## Add New Model Support
**Last performed:** [date]
**Files to modify:**
- `/providers/gemini.md` - Add model to documentation
- `/src/providers/gemini-config.ts` - Add model configuration
- `/src/constants/models.ts` - Add to model list
- `/tests/providers/gemini.test.ts` - Add test cases
**Steps:**
1. Add model configuration with proper token limits
2. Update documentation with model capabilities
3. Add to constants file for UI display
4. Write tests for new model configuration
**Important notes:**
- Check Google's documentation for exact token limits
- Ensure backward compatibility with existing configurations
- Test with actual API calls before committing
```
### Regular Task Execution
In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional.
The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like:
"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API."
When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed.
If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?"
In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes.
## Context Window Management
When the context window fills up during an extended session:
1. I should suggest updating the memory bank to preserve the current state
2. Recommend starting a fresh conversation/task
3. In the new conversation, I will automatically load the memory bank files to maintain continuity
## Technical Implementation
Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access.
## Important Notes
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user.
IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.

View File

@@ -1,213 +1,213 @@
# Architecture Overview - Star Kitten
## System Architecture
Star Kitten follows a modular monorepo architecture with four main packages and two applications:
```mermaid
graph TB
subgraph "Applications"
EB[eve-bot<br/>Discord Bot]
EW[eve-web<br/>Brisa Web App]
end
subgraph "Core Packages"
D[@star-kitten/discord<br/>Discord Framework]
E[@star-kitten/eve<br/>EVE Online Integration]
U[@star-kitten/util<br/>Shared Utilities]
end
subgraph "External Services"
DS[Discord API]
ESI[EVE ESI API]
JA[Janice API]
ET[EveTycoon API]
end
subgraph "Data Layer"
DB[(SQLite Database)]
RD[(Reference Data<br/>JSON Files)]
end
EB --> D
EB --> E
EW --> E
EW --> U
D --> DS
E --> ESI
E --> JA
E --> ET
E --> DB
E --> RD
U --> DB
```
## Source Code Paths
### Core Packages
#### [@star-kitten/discord](packages/discord/)
- **Entry Point**: [`packages/discord/src/index.ts`](packages/discord/src/index.ts)
- **Command System**: [`packages/discord/src/commands/`](packages/discord/src/commands/)
- [`import-commands.ts`](packages/discord/src/commands/import-commands.ts) - Auto-discovery of command files
- [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) - Command execution handler
- **Localization**: [`packages/discord/src/locales.ts`](packages/discord/src/locales.ts)
#### [@star-kitten/eve](packages/eve/)
- **Entry Point**: [`packages/eve/src/index.ts`](packages/eve/src/index.ts)
- **Database Layer**: [`packages/eve/src/db/`](packages/eve/src/db/)
- [`schema.ts`](packages/eve/src/db/schema.ts) - Drizzle ORM schema definitions
- [`index.ts`](packages/eve/src/db/index.ts) - Database connection and exports
- [`models/`](packages/eve/src/db/models/) - Character and user model helpers
- **ESI Integration**: [`packages/eve/src/esi/`](packages/eve/src/esi/)
- [`auth.ts`](packages/eve/src/esi/auth.ts) - EVE SSO authentication flow
- [`scopes.ts`](packages/eve/src/esi/scopes.ts) - ESI scope definitions and token validation
- [`character.ts`](packages/eve/src/esi/character.ts) - Character API endpoints
- [`fetch.ts`](packages/eve/src/esi/fetch.ts) - ESI API client wrapper
- **Data Models**: [`packages/eve/src/models/`](packages/eve/src/models/)
- [`type.ts`](packages/eve/src/models/type.ts) - EVE item type definitions and utilities
- [`skill.ts`](packages/eve/src/models/skill.ts) - Skill-related models
- **Third-party APIs**: [`packages/eve/src/third-party/`](packages/eve/src/third-party/)
- [`janice.ts`](packages/eve/src/third-party/janice.ts) - Market appraisal integration
- [`evetycoon.ts`](packages/eve/src/third-party/evetycoon.ts) - Market data integration
- **Utilities**: [`packages/eve/src/utils/`](packages/eve/src/utils/)
- [`markdown.ts`](packages/eve/src/utils/markdown.ts) - EVE markup to Discord formatting
#### [@star-kitten/util](packages/util/)
- **Entry Point**: [`packages/util/src/`](packages/util/src/) (no index.ts, exports individual modules)
- **Core Utilities**:
- [`text.ts`](packages/util/src/text.ts) - Text processing and formatting
- [`jsonQuery.ts`](packages/util/src/jsonQuery.ts) - Streaming JSON querying for large files
- [`time.ts`](packages/util/src/time.ts) - Time utilities
- [`logger.ts`](packages/util/src/logger.ts) - Logging utilities
- **Scheduler System**: [`packages/util/src/scheduler/`](packages/util/src/scheduler/)
- [`scheduler.service.ts`](packages/util/src/scheduler/scheduler.service.ts) - Job scheduling service
- [`queue.ts`](packages/util/src/scheduler/queue.ts) - Job queue implementation
- [`workers/`](packages/util/src/scheduler/workers/) - Background worker implementations
### Applications
#### [eve-bot](packages/eve-bot/)
- **Entry Point**: [`packages/eve-bot/src/main.ts`](packages/eve-bot/src/main.ts)
- **Commands**: [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/)
- [`appraise.command.ts`](packages/eve-bot/src/commands/appraise.command.ts) - Market appraisal command
- **Types**: [`packages/eve-bot/src/types/global.d.ts`](packages/eve-bot/src/types/global.d.ts)
#### [eve-web](packages/eve-web/)
- **Entry Point**: [`packages/eve-web/src/pages/index.tsx`](packages/eve-web/src/pages/index.tsx)
- **Middleware**: [`packages/eve-web/src/middleware.ts`](packages/eve-web/src/middleware.ts) - EVE SSO integration
- **Components**: [`packages/eve-web/src/components/stats/`](packages/eve-web/src/components/stats/)
- [`skill-queue.tsx`](packages/eve-web/src/components/stats/skill-queue.tsx) - Real-time skill training display
- [`wallet.tsx`](packages/eve-web/src/components/stats/wallet.tsx) - Wallet balance and changes
- **API Routes**: [`packages/eve-web/src/api/auth/`](packages/eve-web/src/api/auth/) - Authentication endpoints
- **Utilities**: [`packages/eve-web/src/utils/cookies.ts`](packages/eve-web/src/utils/cookies.ts)
## Key Technical Decisions
### Command System Architecture
- **Auto-discovery**: Commands are automatically discovered using glob patterns (`**/*.command.{js,ts}`)
- **Convention-based**: Each command exports a default Discord command definition
- **Event-driven**: Commands handle their own interaction events using global event listeners
- **Localized**: Full internationalization support with description translations
### Database Architecture
- **ORM**: Drizzle ORM with SQLite for local data storage
- **Schema**: [`packages/eve/src/db/schema.ts`](packages/eve/src/db/schema.ts) defines all tables
- **Models**: Active Record pattern with helper classes for complex operations
- **Migrations**: Drizzle Kit handles schema migrations
### EVE Online Integration
- **OAuth Flow**: Complete EVE SSO implementation with token management
- **Scope Management**: Comprehensive ESI scope definitions and validation
- **Token Refresh**: Automatic token refresh with scope preservation
- **Data Caching**: Smart caching of EVE reference data and API responses
### Web Interface Architecture
- **Framework**: Brisa for server-side rendering with progressive enhancement
- **Authentication**: EVE SSO integrated at middleware level
- **Components**: Server-side components with suspense for async data loading
- **Styling**: Tailwind CSS with DaisyUI components
## Design Patterns
### Package Exports
Each package uses structured exports for different concerns:
```typescript
// @star-kitten/eve exports
export * from "./esi"; // ESI API integration
export * as CharacterAPI from "./esi/character";
export * as models from "./models"; // Data models
export * as db from "./db"; // Database access
export * from "./third-party"; // External APIs
```
### Error Handling
- **API Errors**: Consistent error wrapping for external API calls
- **Token Validation**: Graceful handling of expired/invalid tokens
- **Database Errors**: Transaction rollback and error logging
### Data Processing
- **Streaming**: Large JSON files processed via streaming for memory efficiency
- **Caching**: Multi-level caching (in-memory, database, file system)
- **Type Safety**: Full TypeScript coverage with strict type checking
## Component Relationships
### Discord Bot Flow
1. [`main.ts`](packages/eve-bot/src/main.ts) initializes Dysnomia client
2. [`importCommands()`](packages/discord/src/commands/import-commands.ts) discovers command files
3. Commands register interaction handlers on global client
4. User interactions trigger command-specific handlers
### Web Authentication Flow
1. [`middleware.ts`](packages/eve-web/src/middleware.ts) intercepts requests
2. EVE SSO redirect initiated for unauthenticated users
3. OAuth callback validates tokens and creates/updates user records
4. Character data synchronized from ESI API
5. Dashboard components display real-time character statistics
### Data Synchronization
1. Reference data downloaded from external sources
2. JSON files processed using streaming query utilities
3. Character data fetched from ESI API
4. Local database updated with character and user information
5. Web components display cached data with real-time updates
## Critical Implementation Paths
### Adding New Commands
1. Create `*.command.ts` file in [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/)
2. Export Discord command definition as default
3. Add interaction handlers to global client event listeners
4. Commands automatically discovered and registered
### EVE API Integration
1. Define required scopes in [`scopes.ts`](packages/eve/src/esi/scopes.ts)
2. Implement API client in appropriate ESI module
3. Add character model methods for data access
4. Create web components for data display
### Database Schema Changes
1. Update [`schema.ts`](packages/eve/src/db/schema.ts) with new tables/columns
2. Generate migration using Drizzle Kit
3. Update model classes with new methods
4. Add database exports to [`db/index.ts`](packages/eve/src/db/index.ts)
# Architecture Overview - Star Kitten
## System Architecture
Star Kitten follows a modular monorepo architecture with four main packages and two applications:
```mermaid
graph TB
subgraph "Applications"
EB[eve-bot<br/>Discord Bot]
EW[eve-web<br/>Brisa Web App]
end
subgraph "Core Packages"
D[@star-kitten/discord<br/>Discord Framework]
E[@star-kitten/eve<br/>EVE Online Integration]
U[@star-kitten/util<br/>Shared Utilities]
end
subgraph "External Services"
DS[Discord API]
ESI[EVE ESI API]
JA[Janice API]
ET[EveTycoon API]
end
subgraph "Data Layer"
DB[(SQLite Database)]
RD[(Reference Data<br/>JSON Files)]
end
EB --> D
EB --> E
EW --> E
EW --> U
D --> DS
E --> ESI
E --> JA
E --> ET
E --> DB
E --> RD
U --> DB
```
## Source Code Paths
### Core Packages
#### [@star-kitten/discord](packages/discord/)
- **Entry Point**: [`packages/discord/src/index.ts`](packages/discord/src/index.ts)
- **Command System**: [`packages/discord/src/commands/`](packages/discord/src/commands/)
- [`import-commands.ts`](packages/discord/src/commands/import-commands.ts) - Auto-discovery of command files
- [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) - Command execution handler
- **Localization**: [`packages/discord/src/locales.ts`](packages/discord/src/locales.ts)
#### [@star-kitten/eve](packages/eve/)
- **Entry Point**: [`packages/eve/src/index.ts`](packages/eve/src/index.ts)
- **Database Layer**: [`packages/eve/src/db/`](packages/eve/src/db/)
- [`schema.ts`](packages/eve/src/db/schema.ts) - Drizzle ORM schema definitions
- [`index.ts`](packages/eve/src/db/index.ts) - Database connection and exports
- [`models/`](packages/eve/src/db/models/) - Character and user model helpers
- **ESI Integration**: [`packages/eve/src/esi/`](packages/eve/src/esi/)
- [`auth.ts`](packages/eve/src/esi/auth.ts) - EVE SSO authentication flow
- [`scopes.ts`](packages/eve/src/esi/scopes.ts) - ESI scope definitions and token validation
- [`character.ts`](packages/eve/src/esi/character.ts) - Character API endpoints
- [`fetch.ts`](packages/eve/src/esi/fetch.ts) - ESI API client wrapper
- **Data Models**: [`packages/eve/src/models/`](packages/eve/src/models/)
- [`type.ts`](packages/eve/src/models/type.ts) - EVE item type definitions and utilities
- [`skill.ts`](packages/eve/src/models/skill.ts) - Skill-related models
- **Third-party APIs**: [`packages/eve/src/third-party/`](packages/eve/src/third-party/)
- [`janice.ts`](packages/eve/src/third-party/janice.ts) - Market appraisal integration
- [`evetycoon.ts`](packages/eve/src/third-party/evetycoon.ts) - Market data integration
- **Utilities**: [`packages/eve/src/utils/`](packages/eve/src/utils/)
- [`markdown.ts`](packages/eve/src/utils/markdown.ts) - EVE markup to Discord formatting
#### [@star-kitten/util](packages/util/)
- **Entry Point**: [`packages/util/src/`](packages/util/src/) (no index.ts, exports individual modules)
- **Core Utilities**:
- [`text.ts`](packages/util/src/text.ts) - Text processing and formatting
- [`jsonQuery.ts`](packages/util/src/jsonQuery.ts) - Streaming JSON querying for large files
- [`time.ts`](packages/util/src/time.ts) - Time utilities
- [`logger.ts`](packages/util/src/logger.ts) - Logging utilities
- **Scheduler System**: [`packages/util/src/scheduler/`](packages/util/src/scheduler/)
- [`scheduler.service.ts`](packages/util/src/scheduler/scheduler.service.ts) - Job scheduling service
- [`queue.ts`](packages/util/src/scheduler/queue.ts) - Job queue implementation
- [`workers/`](packages/util/src/scheduler/workers/) - Background worker implementations
### Applications
#### [eve-bot](packages/eve-bot/)
- **Entry Point**: [`packages/eve-bot/src/main.ts`](packages/eve-bot/src/main.ts)
- **Commands**: [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/)
- [`appraise.command.ts`](packages/eve-bot/src/commands/appraise.command.ts) - Market appraisal command
- **Types**: [`packages/eve-bot/src/types/global.d.ts`](packages/eve-bot/src/types/global.d.ts)
#### [eve-web](packages/eve-web/)
- **Entry Point**: [`packages/eve-web/src/pages/index.tsx`](packages/eve-web/src/pages/index.tsx)
- **Middleware**: [`packages/eve-web/src/middleware.ts`](packages/eve-web/src/middleware.ts) - EVE SSO integration
- **Components**: [`packages/eve-web/src/components/stats/`](packages/eve-web/src/components/stats/)
- [`skill-queue.tsx`](packages/eve-web/src/components/stats/skill-queue.tsx) - Real-time skill training display
- [`wallet.tsx`](packages/eve-web/src/components/stats/wallet.tsx) - Wallet balance and changes
- **API Routes**: [`packages/eve-web/src/api/auth/`](packages/eve-web/src/api/auth/) - Authentication endpoints
- **Utilities**: [`packages/eve-web/src/utils/cookies.ts`](packages/eve-web/src/utils/cookies.ts)
## Key Technical Decisions
### Command System Architecture
- **Auto-discovery**: Commands are automatically discovered using glob patterns (`**/*.command.{js,ts}`)
- **Convention-based**: Each command exports a default Discord command definition
- **Event-driven**: Commands handle their own interaction events using global event listeners
- **Localized**: Full internationalization support with description translations
### Database Architecture
- **ORM**: Drizzle ORM with SQLite for local data storage
- **Schema**: [`packages/eve/src/db/schema.ts`](packages/eve/src/db/schema.ts) defines all tables
- **Models**: Active Record pattern with helper classes for complex operations
- **Migrations**: Drizzle Kit handles schema migrations
### EVE Online Integration
- **OAuth Flow**: Complete EVE SSO implementation with token management
- **Scope Management**: Comprehensive ESI scope definitions and validation
- **Token Refresh**: Automatic token refresh with scope preservation
- **Data Caching**: Smart caching of EVE reference data and API responses
### Web Interface Architecture
- **Framework**: Brisa for server-side rendering with progressive enhancement
- **Authentication**: EVE SSO integrated at middleware level
- **Components**: Server-side components with suspense for async data loading
- **Styling**: Tailwind CSS with DaisyUI components
## Design Patterns
### Package Exports
Each package uses structured exports for different concerns:
```typescript
// @star-kitten/eve exports
export * from "./esi"; // ESI API integration
export * as CharacterAPI from "./esi/character";
export * as models from "./models"; // Data models
export * as db from "./db"; // Database access
export * from "./third-party"; // External APIs
```
### Error Handling
- **API Errors**: Consistent error wrapping for external API calls
- **Token Validation**: Graceful handling of expired/invalid tokens
- **Database Errors**: Transaction rollback and error logging
### Data Processing
- **Streaming**: Large JSON files processed via streaming for memory efficiency
- **Caching**: Multi-level caching (in-memory, database, file system)
- **Type Safety**: Full TypeScript coverage with strict type checking
## Component Relationships
### Discord Bot Flow
1. [`main.ts`](packages/eve-bot/src/main.ts) initializes Dysnomia client
2. [`importCommands()`](packages/discord/src/commands/import-commands.ts) discovers command files
3. Commands register interaction handlers on global client
4. User interactions trigger command-specific handlers
### Web Authentication Flow
1. [`middleware.ts`](packages/eve-web/src/middleware.ts) intercepts requests
2. EVE SSO redirect initiated for unauthenticated users
3. OAuth callback validates tokens and creates/updates user records
4. Character data synchronized from ESI API
5. Dashboard components display real-time character statistics
### Data Synchronization
1. Reference data downloaded from external sources
2. JSON files processed using streaming query utilities
3. Character data fetched from ESI API
4. Local database updated with character and user information
5. Web components display cached data with real-time updates
## Critical Implementation Paths
### Adding New Commands
1. Create `*.command.ts` file in [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/)
2. Export Discord command definition as default
3. Add interaction handlers to global client event listeners
4. Commands automatically discovered and registered
### EVE API Integration
1. Define required scopes in [`scopes.ts`](packages/eve/src/esi/scopes.ts)
2. Implement API client in appropriate ESI module
3. Add character model methods for data access
4. Create web components for data display
### Database Schema Changes
1. Update [`schema.ts`](packages/eve/src/db/schema.ts) with new tables/columns
2. Generate migration using Drizzle Kit
3. Update model classes with new methods
4. Add database exports to [`db/index.ts`](packages/eve/src/db/index.ts)

View File

@@ -1,7 +1,7 @@
This project, Star Kitten, is a personal project of mine which will be a set of packges for building discord bots and websites focused on the games I play. I am starting with an eve online bot and webpage and will be building a star citizen discord bot later.
This project used bun, Dysnomia js, and currently Brisa, but I plan to change that to an Elysia server with Ripple SPA.
I use TypeScript and focus on re-usable functional components as much as I can.
I would like to do my best to maintain at least 80% coverage with all major functionality tested using bun:test.
This project, Star Kitten, is a personal project of mine which will be a set of packges for building discord bots and websites focused on the games I play. I am starting with an eve online bot and webpage and will be building a star citizen discord bot later.
This project used bun, Dysnomia js, and currently Brisa, but I plan to change that to an Elysia server with Ripple SPA.
I use TypeScript and focus on re-usable functional components as much as I can.
I would like to do my best to maintain at least 80% coverage with all major functionality tested using bun:test.

View File

@@ -1,167 +1,167 @@
# Project Context - Star Kitten
## Current Development State
### Project Maturity
- **Phase**: Active Development
- **Version**: 0.0.0 (pre-release)
- **Status**: Core framework established, basic functionality implemented
- **Last Major Update**: Memory bank initialization (January 2025)
### Current Work Focus
- **Primary**: EVE Online Discord bot and web interface development
- **Secondary**: Framework stabilization and documentation
- **Future**: Star Citizen integration planning
### Recent Changes
- Memory bank system initialized with comprehensive documentation
- Core architecture established across four packages
- Basic EVE Online integration functional
- Discord command system with auto-discovery implemented
- Web interface with EVE SSO authentication working
## Active Features
### Discord Bot (`eve-bot`)
- **Status**: Basic functionality working
- **Commands**:
- Market appraisal command (partial implementation)
- **Authentication**: Global client setup with Dysnomia
- **Deployment**: Environment-based configuration ready
### Web Interface (`eve-web`)
- **Status**: Basic dashboard working
- **Authentication**: EVE SSO OAuth flow implemented
- **Components**:
- Skill queue display with progress tracking
- Wallet balance with daily change calculation
- **Framework**: Brisa SSR with Tailwind CSS + DaisyUI
### Core Packages
- **@star-kitten/discord**: Command auto-discovery and handling
- **@star-kitten/eve**: ESI API integration, database models, third-party APIs
- **@star-kitten/util**: Streaming JSON queries, text processing, job scheduling
## Technical Debt & Known Issues
### Architecture Decisions Pending
- **Web Framework Migration**: Planned switch from Brisa to Elysia + Ripple SPA
- **Package Versioning**: All packages at 0.0.0, versioning strategy needed
- **Testing Coverage**: Current coverage unknown, target is 80%
### Implementation Gaps
- **Command System**: [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) references global commands object not used in current pattern
- **Appraisal Command**: Modal submission handling incomplete
- **Mining Fleet Module**: Database schema exists but no implementation
- **API Error Handling**: Needs standardization across packages
### Database Schema
- **Current Tables**: users, characters, resumeCommands, miningFleets, miningFleetParticipants
- **Migration Status**: Drizzle Kit configured but migration history unclear
- **Data Location**: SQLite database at configurable path
## Development Environment
### Setup Requirements
- **Runtime**: Bun (JavaScript runtime and package manager)
- **Language**: TypeScript with strict type checking
- **Database**: SQLite with Drizzle ORM
- **Development**: VS Code with format-on-save configuration
### Build System
- **Package Manager**: Bun workspaces
- **Build Tool**: tsdown for TypeScript compilation
- **Scripts**: Unified commands across packages (build, dev, test)
### External Dependencies
- **EVE Online**: ESI API access requires client credentials
- **Janice API**: Market appraisal service requires API key
- **Discord**: Bot token required for Discord API access
## Next Steps Priority
### Immediate (Current Sprint)
1. Complete appraisal command implementation
2. Add comprehensive error handling to ESI integration
3. Implement basic test coverage for core functionality
4. Document API authentication setup process
### Short Term (Next Month)
1. Implement mining fleet management functionality
2. Add more Discord commands (character lookup, skill planning)
3. Expand web interface with more character statistics
4. Establish proper versioning and release process
### Medium Term (Next Quarter)
1. Migrate web framework to Elysia + Ripple SPA
2. Add support for corporation and alliance management
3. Implement data synchronization and caching strategies
4. Create documentation for third-party developers
### Long Term (Next Year)
1. Star Citizen integration architecture
2. Multi-game framework abstraction
3. Plugin system for community extensions
4. Performance optimization and scaling
## Key Stakeholders
### Internal Development
- **Primary Developer**: j-b-3 (project owner and main developer)
- **Target Users**: EVE Online corporations and alliances
- **Future Contributors**: Open source community (planned)
### External Dependencies
- **CCP Games**: EVE Online ESI API provider
- **Discord**: Platform and API provider
- **Community**: EVE Online player community feedback
## Risk Assessment
### Technical Risks
- **Framework Migration**: Brisa to Elysia migration complexity
- **API Rate Limits**: EVE ESI and third-party API limitations
- **Database Performance**: SQLite scalability for large datasets
- **Token Management**: OAuth token refresh reliability
### Project Risks
- **Single Developer**: No backup maintainer currently
- **Community Adoption**: Uncertain uptake by EVE Online communities
- **Game API Changes**: Dependency on external game APIs
- **Competition**: Existing EVE Online bot solutions
## Success Metrics
### Current Measurements
- **Code Quality**: Type safety across all packages
- **Architecture**: Modular design with clear separation
- **Documentation**: Comprehensive memory bank established
- **Functionality**: Basic bot and web interface working
### Target Metrics
- **Test Coverage**: 80% across major functionality
- **Performance**: <2 second Discord command response times
- **Reliability**: 99%+ uptime for deployed services
- **Adoption**: Usage by multiple EVE Online communities
# Project Context - Star Kitten
## Current Development State
### Project Maturity
- **Phase**: Active Development
- **Version**: 0.0.0 (pre-release)
- **Status**: Core framework established, basic functionality implemented
- **Last Major Update**: Memory bank initialization (January 2025)
### Current Work Focus
- **Primary**: EVE Online Discord bot and web interface development
- **Secondary**: Framework stabilization and documentation
- **Future**: Star Citizen integration planning
### Recent Changes
- Memory bank system initialized with comprehensive documentation
- Core architecture established across four packages
- Basic EVE Online integration functional
- Discord command system with auto-discovery implemented
- Web interface with EVE SSO authentication working
## Active Features
### Discord Bot (`eve-bot`)
- **Status**: Basic functionality working
- **Commands**:
- Market appraisal command (partial implementation)
- **Authentication**: Global client setup with Dysnomia
- **Deployment**: Environment-based configuration ready
### Web Interface (`eve-web`)
- **Status**: Basic dashboard working
- **Authentication**: EVE SSO OAuth flow implemented
- **Components**:
- Skill queue display with progress tracking
- Wallet balance with daily change calculation
- **Framework**: Brisa SSR with Tailwind CSS + DaisyUI
### Core Packages
- **@star-kitten/discord**: Command auto-discovery and handling
- **@star-kitten/eve**: ESI API integration, database models, third-party APIs
- **@star-kitten/util**: Streaming JSON queries, text processing, job scheduling
## Technical Debt & Known Issues
### Architecture Decisions Pending
- **Web Framework Migration**: Planned switch from Brisa to Elysia + Ripple SPA
- **Package Versioning**: All packages at 0.0.0, versioning strategy needed
- **Testing Coverage**: Current coverage unknown, target is 80%
### Implementation Gaps
- **Command System**: [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) references global commands object not used in current pattern
- **Appraisal Command**: Modal submission handling incomplete
- **Mining Fleet Module**: Database schema exists but no implementation
- **API Error Handling**: Needs standardization across packages
### Database Schema
- **Current Tables**: users, characters, resumeCommands, miningFleets, miningFleetParticipants
- **Migration Status**: Drizzle Kit configured but migration history unclear
- **Data Location**: SQLite database at configurable path
## Development Environment
### Setup Requirements
- **Runtime**: Bun (JavaScript runtime and package manager)
- **Language**: TypeScript with strict type checking
- **Database**: SQLite with Drizzle ORM
- **Development**: VS Code with format-on-save configuration
### Build System
- **Package Manager**: Bun workspaces
- **Build Tool**: tsdown for TypeScript compilation
- **Scripts**: Unified commands across packages (build, dev, test)
### External Dependencies
- **EVE Online**: ESI API access requires client credentials
- **Janice API**: Market appraisal service requires API key
- **Discord**: Bot token required for Discord API access
## Next Steps Priority
### Immediate (Current Sprint)
1. Complete appraisal command implementation
2. Add comprehensive error handling to ESI integration
3. Implement basic test coverage for core functionality
4. Document API authentication setup process
### Short Term (Next Month)
1. Implement mining fleet management functionality
2. Add more Discord commands (character lookup, skill planning)
3. Expand web interface with more character statistics
4. Establish proper versioning and release process
### Medium Term (Next Quarter)
1. Migrate web framework to Elysia + Ripple SPA
2. Add support for corporation and alliance management
3. Implement data synchronization and caching strategies
4. Create documentation for third-party developers
### Long Term (Next Year)
1. Star Citizen integration architecture
2. Multi-game framework abstraction
3. Plugin system for community extensions
4. Performance optimization and scaling
## Key Stakeholders
### Internal Development
- **Primary Developer**: j-b-3 (project owner and main developer)
- **Target Users**: EVE Online corporations and alliances
- **Future Contributors**: Open source community (planned)
### External Dependencies
- **CCP Games**: EVE Online ESI API provider
- **Discord**: Platform and API provider
- **Community**: EVE Online player community feedback
## Risk Assessment
### Technical Risks
- **Framework Migration**: Brisa to Elysia migration complexity
- **API Rate Limits**: EVE ESI and third-party API limitations
- **Database Performance**: SQLite scalability for large datasets
- **Token Management**: OAuth token refresh reliability
### Project Risks
- **Single Developer**: No backup maintainer currently
- **Community Adoption**: Uncertain uptake by EVE Online communities
- **Game API Changes**: Dependency on external game APIs
- **Competition**: Existing EVE Online bot solutions
## Success Metrics
### Current Measurements
- **Code Quality**: Type safety across all packages
- **Architecture**: Modular design with clear separation
- **Documentation**: Comprehensive memory bank established
- **Functionality**: Basic bot and web interface working
### Target Metrics
- **Test Coverage**: 80% across major functionality
- **Performance**: <2 second Discord command response times
- **Reliability**: 99%+ uptime for deployed services
- **Adoption**: Usage by multiple EVE Online communities

View File

@@ -1,100 +1,100 @@
# Product Overview - Star Kitten
## What Star Kitten Is
Star Kitten is a comprehensive framework for building Discord bots and web applications focused on gaming communities, specifically MMO games like EVE Online and Star Citizen. It provides reusable packages and components that handle common gaming bot functionality including authentication, API integrations, data management, and user interfaces.
## Problems It Solves
### For Gaming Community Managers
- **Fragmented Tools**: Gaming communities often use multiple disconnected tools for different functions (Discord bots, web dashboards, data analysis)
- **Complex Authentication**: Managing OAuth flows for game APIs (EVE ESI, future Star Citizen APIs) is complex and error-prone
- **Data Integration**: Combining game data with Discord interactions and web interfaces requires significant development effort
- **Repetitive Development**: Building similar functionality across different gaming communities involves recreating the same patterns
### For Developers
- **Discord Bot Boilerplate**: Eliminates the need to write command handling, interaction management, and Discord client setup from scratch
- **Game API Complexity**: Abstracts complex game API authentication, token management, and data fetching
- **Database Management**: Provides pre-built schemas and models for common gaming bot use cases
- **Type Safety**: Offers comprehensive TypeScript definitions for all game data types and API responses
## How It Works
### Architecture Philosophy
Star Kitten follows a modular, package-based architecture where each package serves a specific purpose:
1. **[@star-kitten/discord](packages/discord/)** - Core Discord bot functionality with command handling and interaction management
2. **[@star-kitten/eve](packages/eve/)** - EVE Online specific integrations including ESI API, authentication, and data models
3. **[@star-kitten/util](packages/util/)** - Shared utilities including schedulers, text processing, and JSON querying
4. **[eve-bot](packages/eve-bot/)** - Complete EVE Online Discord bot implementation
5. **[eve-web](packages/eve-web/)** - Web interface for EVE Online bot management and statistics
### Key Workflows
#### Discord Bot Creation
1. Import command handling from `@star-kitten/discord`
2. Create command files with `.command.ts` extension
3. Bot automatically discovers and registers commands
4. Handle interactions through standardized command structure
#### EVE Online Integration
1. User authenticates via EVE SSO through web interface
2. Tokens stored securely with automatic refresh handling
3. ESI API calls abstracted through helper functions
4. Character data synchronized and cached in local database
#### Web Dashboard
1. Users authenticate via EVE SSO
2. Character data displayed through reactive components
3. Real-time statistics from ESI API
4. Admin functions for bot management
## User Experience Goals
### Discord Users
- **Intuitive Commands**: Slash commands with clear descriptions and localization support
- **Rich Interactions**: Modal forms, select menus, and interactive components for complex workflows
- **Immediate Feedback**: Fast response times with proper loading states and error handling
- **Contextual Help**: Commands provide guidance and examples for proper usage
### Web Users
- **Seamless Authentication**: Single sign-on through EVE Online credentials
- **Responsive Design**: Works well on desktop and mobile devices
- **Real-time Data**: Live updates of character stats, wallet balances, and skill queues
- **Progressive Enhancement**: Basic functionality works without JavaScript
### Bot Administrators
- **Easy Deployment**: Simple configuration through environment variables
- **Monitoring**: Built-in logging and error tracking
- **Scalability**: Modular architecture supports adding new features without breaking existing functionality
- **Data Control**: Local database with full control over user data and privacy
## Success Metrics
### Technical Success
- **Reliability**: 99%+ uptime for bot and web services
- **Performance**: < 2 second response times for Discord commands
- **Code Quality**: 80%+ test coverage maintained across all packages
- **Developer Experience**: New features can be added without modifying core packages
### User Adoption
- **Community Growth**: Framework adoption by multiple EVE Online corporations/alliances
- **Feature Usage**: High engagement with key features like appraisals, fleet management, and character tracking
- **Feedback Integration**: Regular updates based on user feedback and pain points
### Long-term Vision
- **Multi-Game Support**: Expand to Star Citizen and other MMO games
- **Ecosystem Growth**: Third-party packages extending Star Kitten functionality
- **Open Source Community**: Active contributor base improving and extending the framework
# Product Overview - Star Kitten
## What Star Kitten Is
Star Kitten is a comprehensive framework for building Discord bots and web applications focused on gaming communities, specifically MMO games like EVE Online and Star Citizen. It provides reusable packages and components that handle common gaming bot functionality including authentication, API integrations, data management, and user interfaces.
## Problems It Solves
### For Gaming Community Managers
- **Fragmented Tools**: Gaming communities often use multiple disconnected tools for different functions (Discord bots, web dashboards, data analysis)
- **Complex Authentication**: Managing OAuth flows for game APIs (EVE ESI, future Star Citizen APIs) is complex and error-prone
- **Data Integration**: Combining game data with Discord interactions and web interfaces requires significant development effort
- **Repetitive Development**: Building similar functionality across different gaming communities involves recreating the same patterns
### For Developers
- **Discord Bot Boilerplate**: Eliminates the need to write command handling, interaction management, and Discord client setup from scratch
- **Game API Complexity**: Abstracts complex game API authentication, token management, and data fetching
- **Database Management**: Provides pre-built schemas and models for common gaming bot use cases
- **Type Safety**: Offers comprehensive TypeScript definitions for all game data types and API responses
## How It Works
### Architecture Philosophy
Star Kitten follows a modular, package-based architecture where each package serves a specific purpose:
1. **[@star-kitten/discord](packages/discord/)** - Core Discord bot functionality with command handling and interaction management
2. **[@star-kitten/eve](packages/eve/)** - EVE Online specific integrations including ESI API, authentication, and data models
3. **[@star-kitten/util](packages/util/)** - Shared utilities including schedulers, text processing, and JSON querying
4. **[eve-bot](packages/eve-bot/)** - Complete EVE Online Discord bot implementation
5. **[eve-web](packages/eve-web/)** - Web interface for EVE Online bot management and statistics
### Key Workflows
#### Discord Bot Creation
1. Import command handling from `@star-kitten/discord`
2. Create command files with `.command.ts` extension
3. Bot automatically discovers and registers commands
4. Handle interactions through standardized command structure
#### EVE Online Integration
1. User authenticates via EVE SSO through web interface
2. Tokens stored securely with automatic refresh handling
3. ESI API calls abstracted through helper functions
4. Character data synchronized and cached in local database
#### Web Dashboard
1. Users authenticate via EVE SSO
2. Character data displayed through reactive components
3. Real-time statistics from ESI API
4. Admin functions for bot management
## User Experience Goals
### Discord Users
- **Intuitive Commands**: Slash commands with clear descriptions and localization support
- **Rich Interactions**: Modal forms, select menus, and interactive components for complex workflows
- **Immediate Feedback**: Fast response times with proper loading states and error handling
- **Contextual Help**: Commands provide guidance and examples for proper usage
### Web Users
- **Seamless Authentication**: Single sign-on through EVE Online credentials
- **Responsive Design**: Works well on desktop and mobile devices
- **Real-time Data**: Live updates of character stats, wallet balances, and skill queues
- **Progressive Enhancement**: Basic functionality works without JavaScript
### Bot Administrators
- **Easy Deployment**: Simple configuration through environment variables
- **Monitoring**: Built-in logging and error tracking
- **Scalability**: Modular architecture supports adding new features without breaking existing functionality
- **Data Control**: Local database with full control over user data and privacy
## Success Metrics
### Technical Success
- **Reliability**: 99%+ uptime for bot and web services
- **Performance**: < 2 second response times for Discord commands
- **Code Quality**: 80%+ test coverage maintained across all packages
- **Developer Experience**: New features can be added without modifying core packages
### User Adoption
- **Community Growth**: Framework adoption by multiple EVE Online corporations/alliances
- **Feature Usage**: High engagement with key features like appraisals, fleet management, and character tracking
- **Feedback Integration**: Regular updates based on user feedback and pain points
### Long-term Vision
- **Multi-Game Support**: Expand to Star Citizen and other MMO games
- **Ecosystem Growth**: Third-party packages extending Star Kitten functionality
- **Open Source Community**: Active contributor base improving and extending the framework

View File

@@ -1,151 +1,151 @@
# Tasks - Star Kitten
## Add New Discord Command
**Last performed:** Initial setup
**Files to modify:**
- `packages/eve-bot/src/commands/[command-name].command.ts` - New command file
- Global Discord client automatically registers new commands
**Steps:**
1. Create new command file following naming convention `[name].command.ts`
2. Export Discord command definition as default export
3. Add interaction handlers using global client event listeners
4. Include localization for command names and descriptions
5. Test command registration and interaction handling
**Important notes:**
- Commands are auto-discovered using glob patterns
- Each command handles its own interactions via global client
- Follow existing pattern from `appraise.command.ts`
- Include comprehensive error handling for all interactions
## Add New EVE ESI Integration
**Last performed:** Character and wallet integration
**Files to modify:**
- `packages/eve/src/esi/[module].ts` - New ESI module
- `packages/eve/src/esi/index.ts` - Export new module
- `packages/eve/src/esi/scopes.ts` - Add required scopes if needed
- `packages/eve/src/db/models/character.model.ts` - Add helper methods if needed
**Steps:**
1. Define required ESI scopes in scopes.ts
2. Create new ESI module with fetch functions
3. Add proper TypeScript interfaces for API responses
4. Implement caching where appropriate
5. Add character model helper methods for data access
6. Update exports in index.ts
**Important notes:**
- Always handle token refresh automatically
- Include comprehensive error handling for API failures
- Follow existing patterns from character.ts and wallet integrations
- Cache responses appropriately to respect rate limits
## Database Schema Migration
**Last performed:** Initial schema setup
**Files to modify:**
- `packages/eve/src/db/schema.ts` - Schema definitions
- `packages/eve/src/db/models/` - Model helpers if needed
- `packages/eve/src/db/index.ts` - Export new tables
**Steps:**
1. Update schema.ts with new tables/columns
2. Generate migration using `bun run generate-migrations`
3. Test migration with `bun run migrate`
4. Update model classes with new methods
5. Add exports to db/index.ts
6. Update database initialization in main applications
**Important notes:**
- Always backup database before migrations
- Test migrations on development database first
- Update all references to schema changes
- Consider data migration scripts for existing data
## Add New Web Component
**Last performed:** Skill queue and wallet components
**Files to modify:**
- `packages/eve-web/src/components/[category]/[component].tsx` - New component
- `packages/eve-web/src/pages/index.tsx` - Import and use component
- Related ESI integration if data source needed
**Steps:**
1. Create new component following existing patterns
2. Implement async data loading with suspense fallback
3. Add proper TypeScript interfaces for props
4. Style using Tailwind CSS and DaisyUI components
5. Import and integrate into appropriate page
6. Test with real data and loading states
**Important notes:**
- Always provide suspense fallback for loading states
- Follow existing component patterns from skill-queue.tsx and wallet.tsx
- Use server-side rendering capabilities of Brisa
- Ensure responsive design for mobile devices
## Add Third-Party API Integration
**Last performed:** Janice API integration
**Files to modify:**
- `packages/eve/src/third-party/[service].ts` - New API integration
- `packages/eve/src/third-party/index.ts` - Export new service
- Environment configuration for API keys
**Steps:**
1. Create new service module with TypeScript interfaces
2. Implement API client with proper error handling
3. Add caching layer for performance
4. Include comprehensive validation for inputs/outputs
5. Add tests for all API functions
6. Update exports and environment configuration
**Important notes:**
- Always implement proper rate limiting and caching
- Include comprehensive error handling and validation
- Follow existing patterns from janice.ts
- Add API key management through environment variables
- Test with real API to ensure compatibility
## Update Reference Data
**Last performed:** Initial data setup
**Files to modify:**
- `data/reference-data/` - Static reference files
- `data/hoboleaks/` - SDE data files
- `packages/eve/src/models/` - Data model updates if needed
**Steps:**
1. Run `bun get-data` to download latest reference data
2. Verify data integrity and format consistency
3. Update data models if schema changes detected
4. Test applications with new reference data
5. Commit updated data files to repository
**Important notes:**
- Reference data updates can be large (hundreds of MB)
- Always verify data integrity before committing
- Test critical functionality after data updates
- Consider data migration scripts for breaking changes
- Monitor for EVE Online patch changes that affect data structure
# Tasks - Star Kitten
## Add New Discord Command
**Last performed:** Initial setup
**Files to modify:**
- `packages/eve-bot/src/commands/[command-name].command.ts` - New command file
- Global Discord client automatically registers new commands
**Steps:**
1. Create new command file following naming convention `[name].command.ts`
2. Export Discord command definition as default export
3. Add interaction handlers using global client event listeners
4. Include localization for command names and descriptions
5. Test command registration and interaction handling
**Important notes:**
- Commands are auto-discovered using glob patterns
- Each command handles its own interactions via global client
- Follow existing pattern from `appraise.command.ts`
- Include comprehensive error handling for all interactions
## Add New EVE ESI Integration
**Last performed:** Character and wallet integration
**Files to modify:**
- `packages/eve/src/esi/[module].ts` - New ESI module
- `packages/eve/src/esi/index.ts` - Export new module
- `packages/eve/src/esi/scopes.ts` - Add required scopes if needed
- `packages/eve/src/db/models/character.model.ts` - Add helper methods if needed
**Steps:**
1. Define required ESI scopes in scopes.ts
2. Create new ESI module with fetch functions
3. Add proper TypeScript interfaces for API responses
4. Implement caching where appropriate
5. Add character model helper methods for data access
6. Update exports in index.ts
**Important notes:**
- Always handle token refresh automatically
- Include comprehensive error handling for API failures
- Follow existing patterns from character.ts and wallet integrations
- Cache responses appropriately to respect rate limits
## Database Schema Migration
**Last performed:** Initial schema setup
**Files to modify:**
- `packages/eve/src/db/schema.ts` - Schema definitions
- `packages/eve/src/db/models/` - Model helpers if needed
- `packages/eve/src/db/index.ts` - Export new tables
**Steps:**
1. Update schema.ts with new tables/columns
2. Generate migration using `bun run generate-migrations`
3. Test migration with `bun run migrate`
4. Update model classes with new methods
5. Add exports to db/index.ts
6. Update database initialization in main applications
**Important notes:**
- Always backup database before migrations
- Test migrations on development database first
- Update all references to schema changes
- Consider data migration scripts for existing data
## Add New Web Component
**Last performed:** Skill queue and wallet components
**Files to modify:**
- `packages/eve-web/src/components/[category]/[component].tsx` - New component
- `packages/eve-web/src/pages/index.tsx` - Import and use component
- Related ESI integration if data source needed
**Steps:**
1. Create new component following existing patterns
2. Implement async data loading with suspense fallback
3. Add proper TypeScript interfaces for props
4. Style using Tailwind CSS and DaisyUI components
5. Import and integrate into appropriate page
6. Test with real data and loading states
**Important notes:**
- Always provide suspense fallback for loading states
- Follow existing component patterns from skill-queue.tsx and wallet.tsx
- Use server-side rendering capabilities of Brisa
- Ensure responsive design for mobile devices
## Add Third-Party API Integration
**Last performed:** Janice API integration
**Files to modify:**
- `packages/eve/src/third-party/[service].ts` - New API integration
- `packages/eve/src/third-party/index.ts` - Export new service
- Environment configuration for API keys
**Steps:**
1. Create new service module with TypeScript interfaces
2. Implement API client with proper error handling
3. Add caching layer for performance
4. Include comprehensive validation for inputs/outputs
5. Add tests for all API functions
6. Update exports and environment configuration
**Important notes:**
- Always implement proper rate limiting and caching
- Include comprehensive error handling and validation
- Follow existing patterns from janice.ts
- Add API key management through environment variables
- Test with real API to ensure compatibility
## Update Reference Data
**Last performed:** Initial data setup
**Files to modify:**
- `data/reference-data/` - Static reference files
- `data/hoboleaks/` - SDE data files
- `packages/eve/src/models/` - Data model updates if needed
**Steps:**
1. Run `bun get-data` to download latest reference data
2. Verify data integrity and format consistency
3. Update data models if schema changes detected
4. Test applications with new reference data
5. Commit updated data files to repository
**Important notes:**
- Reference data updates can be large (hundreds of MB)
- Always verify data integrity before committing
- Test critical functionality after data updates
- Consider data migration scripts for breaking changes
- Monitor for EVE Online patch changes that affect data structure

View File

@@ -1,249 +1,249 @@
# Technology Stack - Star Kitten
## Core Technologies
### Runtime & Package Management
- **Bun** - JavaScript runtime and package manager
- Fast package installation and script execution
- Native TypeScript support
- Built-in test runner (`bun test`)
- WebAPI compatibility for modern web standards
### Language & Type System
- **TypeScript** - Primary development language
- Strict type checking enabled across all packages
- Comprehensive type definitions for EVE Online data structures
- Interface-driven development for API integrations
- Global type declarations for shared client instances
### Build System
- **Workspace Architecture** - Bun workspaces for monorepo management
- Four core packages: `@star-kitten/discord`, `@star-kitten/eve`, `@star-kitten/util`, plus applications
- Cross-package dependencies managed through workspace references
- Unified build commands across all packages (`bun build`, `bun dev`, `bun test`)
- **tsdown** - TypeScript bundler for package builds
- Zero-config TypeScript compilation
- Watch mode support for development
- Declaration file generation
## Discord Integration
### Discord Framework
- **Dysnomia** - Discord API library
- Modern JavaScript Discord library with full TypeScript support
- Gateway intents configuration for minimal resource usage
- Built-in interaction handling (slash commands, modals, components)
- Event-driven architecture for command processing
### Command Architecture
- **Auto-discovery Pattern** - Commands automatically registered via file system scanning
- Glob patterns (`**/*.command.{js,ts}`) for command file detection
- Convention-based naming and structure
- Global client registration with event listeners
- **Internationalization** - Full i18n support
- Command names and descriptions localized across multiple languages
- Structured translation files for consistent localization
## Database & Data Management
### Database Layer
- **SQLite** - Local database storage
- Single-file database for simple deployment
- ACID compliance for data integrity
- Excellent performance for read-heavy workloads
- **Drizzle ORM** - Type-safe database access
- Schema-first approach with TypeScript definitions
- Automatic migration generation via Drizzle Kit
- Relationship mapping and query builder
- Active Record pattern implementation in model helpers
### Data Processing
- **Streaming JSON Processing** - Memory-efficient handling of large datasets
- `stream-json` library for parsing large EVE reference data files
- Query-based filtering during stream processing
- In-memory caching with TTL for frequently accessed data
- **Reference Data Management**
- External data sources (everef.net, hoboleaks)
- Automated download and extraction scripts
- Static export processes for optimized data formats
## Web Framework
### Frontend Framework
- **Brisa** - Server-side rendering framework
- Modern SSR with progressive enhancement
- Component-based architecture
- Built-in suspense support for async data loading
- TypeScript-first development experience
### Styling & UI
- **Tailwind CSS** - Utility-first CSS framework
- Responsive design system
- Component-level styling
- **DaisyUI** - Tailwind CSS component library
- Pre-built UI components (stats, progress bars, modals)
- Consistent design language across web interface
### Authentication & Middleware
- **EVE SSO Integration** - OAuth2 flow implementation
- Complete authentication middleware stack
- Token management with automatic refresh
- Scope validation and permission handling
- **Cookie Management** - Custom utilities for session handling
- Secure cookie implementation
- State management for OAuth flows
## External API Integrations
### EVE Online APIs
- **ESI (EVE Swagger Interface)** - Official EVE Online API
- Complete OAuth2 flow with scope management
- Character, corporation, and alliance data access
- Market data, skill queues, wallet information
- Automatic token refresh with scope preservation
- **JWT Token Validation** - Secure token handling
- Public key verification against EVE's JWKS endpoint
- Token scope extraction and validation
- Character ID resolution from JWT payloads
### Third-Party APIs
- **Janice API** - Market appraisal service
- Price checking and market analysis
- Bulk item appraisal functionality
- Caching layer for improved performance
- **EveTycoon API** - Market data integration
- Historical price data
- Market trend analysis
## Development Tools & Practices
### Code Quality
- **Prettier** - Code formatting
- Consistent formatting across all packages
- Integration with VS Code for format-on-save
- Shared configuration files (`.prettierrc.yaml`)
- **ESLint** - Code linting (implied by VS Code settings)
- TypeScript-aware linting rules
- Automatic fixing on save
### Testing Framework
- **Bun Test** - Native test runner
- Fast test execution with TypeScript support
- Coverage reporting capabilities
- Target: 80%+ test coverage across major functionality
### Development Environment
- **VS Code Configuration**
- Consistent editor settings across team
- Format on save enabled
- Trailing whitespace removal
- Final newline insertion
- **Environment Management**
- dotenvx for environment variable management
- Separate configurations for development and production
- Secure key management for API credentials
## Utility Libraries
### Scheduling & Background Jobs
- **Cron Parser** - Schedule management
- Cron expression parsing and validation
- Job scheduling with repeat patterns
- **Custom Queue System** - Background job processing
- SQLite-based job queue
- Worker thread implementation
- Email notifications and other background tasks
### Text & Data Processing
- **Date Utilities** - Time manipulation
- date-fns for date calculations
- EVE time conversion utilities
- **Logging** - Structured application logs
- Winston logging framework
- Configurable log levels and outputs
- **Text Processing** - Formatting and conversion
- Number formatting (K, M, B suffixes)
- EVE markup to Discord markdown conversion
- Text truncation and cleaning utilities
## Deployment & Configuration
### Environment Configuration
- **Multi-Environment Support**
- Development, production environment files
- Secure API key management
- Database path configuration
- **Docker Ready** - Containerization support
- Bun-based container builds
- SQLite database volume mounting
### Database Management
- **Migration System** - Schema version control
- Drizzle Kit migration generation
- Automated migration execution
- Schema evolution tracking
## Architecture Patterns
### Package Organization
- **Modular Monorepo** - Clear separation of concerns
- Core packages for reusable functionality
- Application packages for specific implementations
- Shared utilities across packages
- **Export Patterns** - Structured package APIs
- Namespace exports for logical grouping
- Direct exports for commonly used functions
- Type-only exports where appropriate
### Error Handling
- **Consistent Error Patterns** - Standardized error handling
- API error wrapping and propagation
- Graceful degradation for external service failures
- Comprehensive error logging
### Performance Optimizations
- **Caching Strategy** - Multi-level caching
- In-memory caching for frequently accessed data
- Database-level caching for computed results
- HTTP response caching for external APIs
- **Streaming Processing** - Memory-efficient data handling
- Large file processing without memory overflow
- Real-time data updates through streaming APIs
## Future Technology Considerations
### Migration Plans
- **Web Framework Migration** - Planned transition from Brisa to Elysia + Ripple SPA
- Backend: Elysia for high-performance HTTP server
- Frontend: Ripple SPA for modern client-side architecture
- Gradual migration strategy maintaining backward compatibility
### Expansion Capabilities
- **Multi-Game Support** - Architecture designed for game expansion
- Pluggable game integration modules
- Shared utilities across different game APIs
- Common authentication and data patterns
# Technology Stack - Star Kitten
## Core Technologies
### Runtime & Package Management
- **Bun** - JavaScript runtime and package manager
- Fast package installation and script execution
- Native TypeScript support
- Built-in test runner (`bun test`)
- WebAPI compatibility for modern web standards
### Language & Type System
- **TypeScript** - Primary development language
- Strict type checking enabled across all packages
- Comprehensive type definitions for EVE Online data structures
- Interface-driven development for API integrations
- Global type declarations for shared client instances
### Build System
- **Workspace Architecture** - Bun workspaces for monorepo management
- Four core packages: `@star-kitten/discord`, `@star-kitten/eve`, `@star-kitten/util`, plus applications
- Cross-package dependencies managed through workspace references
- Unified build commands across all packages (`bun build`, `bun dev`, `bun test`)
- **tsdown** - TypeScript bundler for package builds
- Zero-config TypeScript compilation
- Watch mode support for development
- Declaration file generation
## Discord Integration
### Discord Framework
- **Dysnomia** - Discord API library
- Modern JavaScript Discord library with full TypeScript support
- Gateway intents configuration for minimal resource usage
- Built-in interaction handling (slash commands, modals, components)
- Event-driven architecture for command processing
### Command Architecture
- **Auto-discovery Pattern** - Commands automatically registered via file system scanning
- Glob patterns (`**/*.command.{js,ts}`) for command file detection
- Convention-based naming and structure
- Global client registration with event listeners
- **Internationalization** - Full i18n support
- Command names and descriptions localized across multiple languages
- Structured translation files for consistent localization
## Database & Data Management
### Database Layer
- **SQLite** - Local database storage
- Single-file database for simple deployment
- ACID compliance for data integrity
- Excellent performance for read-heavy workloads
- **Drizzle ORM** - Type-safe database access
- Schema-first approach with TypeScript definitions
- Automatic migration generation via Drizzle Kit
- Relationship mapping and query builder
- Active Record pattern implementation in model helpers
### Data Processing
- **Streaming JSON Processing** - Memory-efficient handling of large datasets
- `stream-json` library for parsing large EVE reference data files
- Query-based filtering during stream processing
- In-memory caching with TTL for frequently accessed data
- **Reference Data Management**
- External data sources (everef.net, hoboleaks)
- Automated download and extraction scripts
- Static export processes for optimized data formats
## Web Framework
### Frontend Framework
- **Brisa** - Server-side rendering framework
- Modern SSR with progressive enhancement
- Component-based architecture
- Built-in suspense support for async data loading
- TypeScript-first development experience
### Styling & UI
- **Tailwind CSS** - Utility-first CSS framework
- Responsive design system
- Component-level styling
- **DaisyUI** - Tailwind CSS component library
- Pre-built UI components (stats, progress bars, modals)
- Consistent design language across web interface
### Authentication & Middleware
- **EVE SSO Integration** - OAuth2 flow implementation
- Complete authentication middleware stack
- Token management with automatic refresh
- Scope validation and permission handling
- **Cookie Management** - Custom utilities for session handling
- Secure cookie implementation
- State management for OAuth flows
## External API Integrations
### EVE Online APIs
- **ESI (EVE Swagger Interface)** - Official EVE Online API
- Complete OAuth2 flow with scope management
- Character, corporation, and alliance data access
- Market data, skill queues, wallet information
- Automatic token refresh with scope preservation
- **JWT Token Validation** - Secure token handling
- Public key verification against EVE's JWKS endpoint
- Token scope extraction and validation
- Character ID resolution from JWT payloads
### Third-Party APIs
- **Janice API** - Market appraisal service
- Price checking and market analysis
- Bulk item appraisal functionality
- Caching layer for improved performance
- **EveTycoon API** - Market data integration
- Historical price data
- Market trend analysis
## Development Tools & Practices
### Code Quality
- **Prettier** - Code formatting
- Consistent formatting across all packages
- Integration with VS Code for format-on-save
- Shared configuration files (`.prettierrc.yaml`)
- **ESLint** - Code linting (implied by VS Code settings)
- TypeScript-aware linting rules
- Automatic fixing on save
### Testing Framework
- **Bun Test** - Native test runner
- Fast test execution with TypeScript support
- Coverage reporting capabilities
- Target: 80%+ test coverage across major functionality
### Development Environment
- **VS Code Configuration**
- Consistent editor settings across team
- Format on save enabled
- Trailing whitespace removal
- Final newline insertion
- **Environment Management**
- dotenvx for environment variable management
- Separate configurations for development and production
- Secure key management for API credentials
## Utility Libraries
### Scheduling & Background Jobs
- **Cron Parser** - Schedule management
- Cron expression parsing and validation
- Job scheduling with repeat patterns
- **Custom Queue System** - Background job processing
- SQLite-based job queue
- Worker thread implementation
- Email notifications and other background tasks
### Text & Data Processing
- **Date Utilities** - Time manipulation
- date-fns for date calculations
- EVE time conversion utilities
- **Logging** - Structured application logs
- Winston logging framework
- Configurable log levels and outputs
- **Text Processing** - Formatting and conversion
- Number formatting (K, M, B suffixes)
- EVE markup to Discord markdown conversion
- Text truncation and cleaning utilities
## Deployment & Configuration
### Environment Configuration
- **Multi-Environment Support**
- Development, production environment files
- Secure API key management
- Database path configuration
- **Docker Ready** - Containerization support
- Bun-based container builds
- SQLite database volume mounting
### Database Management
- **Migration System** - Schema version control
- Drizzle Kit migration generation
- Automated migration execution
- Schema evolution tracking
## Architecture Patterns
### Package Organization
- **Modular Monorepo** - Clear separation of concerns
- Core packages for reusable functionality
- Application packages for specific implementations
- Shared utilities across packages
- **Export Patterns** - Structured package APIs
- Namespace exports for logical grouping
- Direct exports for commonly used functions
- Type-only exports where appropriate
### Error Handling
- **Consistent Error Patterns** - Standardized error handling
- API error wrapping and propagation
- Graceful degradation for external service failures
- Comprehensive error logging
### Performance Optimizations
- **Caching Strategy** - Multi-level caching
- In-memory caching for frequently accessed data
- Database-level caching for computed results
- HTTP response caching for external APIs
- **Streaming Processing** - Memory-efficient data handling
- Large file processing without memory overflow
- Real-time data updates through streaming APIs
## Future Technology Considerations
### Migration Plans
- **Web Framework Migration** - Planned transition from Brisa to Elysia + Ripple SPA
- Backend: Elysia for high-performance HTTP server
- Frontend: Ripple SPA for modern client-side architecture
- Gradual migration strategy maintaining backward compatibility
### Expansion Capabilities
- **Multi-Game Support** - Architecture designed for game expansion
- Pluggable game integration modules
- Shared utilities across different game APIs
- Common authentication and data patterns

View File

@@ -1,3 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode"]
}
{
"recommendations": ["esbenp.prettier-vscode"]
}

24
.vscode/settings.json vendored
View File

@@ -1,12 +1,12 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules\\typescript\\lib"
}
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules\\typescript\\lib"
}

801
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,21 @@
import * as StarKitten from '@star-kitten/discord';
import type { ExecutableInteraction } from '@star-kitten/discord';
import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/discord/components';
import type { PageContext } from '@star-kitten/discord/pages';
import { type Appraisal } from '@star-kitten/eve/third-party/janice.js';
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
export function renderAppraisal(
appraisal: Appraisal,
pageCtx: PageContext<any>,
interaction: ExecutableInteraction,
) {
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
});
const world = 'world';
return (
StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Container", {"color":"0x1da57a"}, StarKitten.createElement("TextDisplay", {}, ""+ `Hello ${world}` +""), pageCtx.state.currentPage !== "share" ? StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Button", {"key":"share","disabled":"{!unknown}"}, "Share in Channel")) : undefined))
)
}
import * as StarKitten from '@star-kitten/discord';
import type { ExecutableInteraction } from '@star-kitten/discord';
import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/discord/components';
import type { PageContext } from '@star-kitten/discord/pages';
import { type Appraisal } from '@star-kitten/eve/third-party/janice.js';
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
export function renderAppraisal(
appraisal: Appraisal,
pageCtx: PageContext<any>,
interaction: ExecutableInteraction,
) {
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
});
const world = 'world';
return (
StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Container", {"color":"0x1da57a"}, StarKitten.createElement("TextDisplay", {}, ""+ `Hello ${world}` +""), pageCtx.state.currentPage !== "share" ? StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Button", {"key":"share","disabled":"{!unknown}"}, "Share in Channel")) : undefined))
)
}

View File

@@ -1,29 +0,0 @@
import type {} from '@star-kitten/discord/jsx';
import { ActionRow, Container, Button, TextDisplay } from '@star-kitten/discord';
export function renderAppraisal() {
const formatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
});
const world = 'world';
const rand = Math.random() * 1000;
const pageCtx = { state: { currentPage: 'home' } };
let jsx = (
<ActionRow>
<Container color="0x1da57a">
<TextDisplay content={`Hello ${world}`} />
{pageCtx.state.currentPage !== 'share' ?
<ActionRow>
<Button customId="share" label="Share in Channel" disabled={rand < 500} />
</ActionRow>
: undefined}
</Container>
</ActionRow>
);
console.log(jsx);
}
renderAppraisal();

View File

@@ -1,69 +1,69 @@
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type GuildChannelTypes,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
declare namespace JSX {
type Component =
| ActionRow
| Button
| StringSelectMenu
| UserSelectMenu
| RoleSelectMenu
| MentionableSelectMenu
| ChannelSelectMenu
| TextInput
| LabelComponent
| ContainerComponent
| {
type: 10;
content: string;
}
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent
| InteractionButton
| URLButton
| PremiumButton
| ThumbnailComponent;
type Element = Component | Promise<Component>;
interface ElementClass {
render: any;
}
interface ElementAttributesProperty {
props: {};
}
interface IntrinsicElements {
// Allow any element, but prefer known elements
[elemName: string]: any;
// Known elements
ActionRow: { children: any | any[] };
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
TextDisplay: { content: string };
}
}
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type GuildChannelTypes,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
declare namespace JSX {
type Component =
| ActionRow
| Button
| StringSelectMenu
| UserSelectMenu
| RoleSelectMenu
| MentionableSelectMenu
| ChannelSelectMenu
| TextInput
| LabelComponent
| ContainerComponent
| {
type: 10;
content: string;
}
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent
| InteractionButton
| URLButton
| PremiumButton
| ThumbnailComponent;
type Element = Component | Promise<Component>;
interface ElementClass {
render: any;
}
interface ElementAttributesProperty {
props: {};
}
interface IntrinsicElements {
// Allow any element, but prefer known elements
[elemName: string]: any;
// Known elements
ActionRow: { children: any | any[] };
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
TextDisplay: { content: string };
}
}

View File

@@ -14,7 +14,8 @@
},
"author": "Author Name <author.name@mail.com>",
"files": [
"dist"
"dist",
"src"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -26,9 +27,15 @@
"./pages": "./dist/pages/index.js",
"./common": "./dist/common/index.js",
"./package.json": "./package.json",
"./jsx": "./src/jsx/jsx.ts",
"./jsx-runtime": "./dist/jsx/index.js",
"./jsx-dev-runtime": "./dist/jsx/index.js"
"./jsx": "./dist/jsx/index.js",
"./jsx-runtime": {
"types": "./src/jsx/types.d.ts",
"default": "./dist/jsx/jsx-runtime.js"
},
"./jsx-dev-runtime": {
"types": "./src/jsx/types.d.ts",
"default": "./dist/jsx/jsx-dev-runtime.js"
}
},
"publishConfig": {
"access": "public"

View File

@@ -1,14 +1,14 @@
import type { Cache } from '@core/cache.type';
import type { KVStore } from '@core/kv-store.type.ts';
import type { Client } from '@projectdysnomia/dysnomia';
import type { CommandState } from './command-state';
export interface PartialContext<T = any> {
client: Client;
cache: Cache;
kv: KVStore;
id?: string; // unique id for this command instance
state?: CommandState<T>; // state associated with this command instance
}
export type CommandContext<T = any> = Required<PartialContext<T>>;
import type { Cache } from '@core/cache.type';
import type { KVStore } from '@core/kv-store.type.ts';
import type { Client } from '@projectdysnomia/dysnomia';
import type { CommandState } from './command-state';
export interface PartialContext<T = any> {
client: Client;
cache: Cache;
kv: KVStore;
id?: string; // unique id for this command instance
state?: CommandState<T>; // state associated with this command instance
}
export type CommandContext<T = any> = Required<PartialContext<T>>;

View File

@@ -1,32 +1,32 @@
import {
AutocompleteInteraction,
CommandInteraction,
ComponentInteraction,
Constants,
ModalSubmitInteraction,
type ApplicationCommandOptionAutocomplete,
type ApplicationCommandOptions,
type ApplicationCommandStructure,
type ChatInputApplicationCommandStructure,
} from '@projectdysnomia/dysnomia';
import type { CommandContext, PartialContext } from './command-context.type';
export interface CommandHandler<T extends ApplicationCommandStructure> {
definition: T;
execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>;
}
export type ExecutableInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction;
export type ChatCommandDefinition = Omit<ChatInputApplicationCommandStructure, 'type'>;
export function createChatCommand(
definition: ChatCommandDefinition,
execute: (interaction: CommandInteraction, ctx: CommandContext) => Promise<void>,
): CommandHandler<ChatInputApplicationCommandStructure> {
const def = definition as ChatInputApplicationCommandStructure;
def.type = 1; // CHAT_INPUT
return {
definition: def,
execute,
};
}
import {
AutocompleteInteraction,
CommandInteraction,
ComponentInteraction,
Constants,
ModalSubmitInteraction,
type ApplicationCommandOptionAutocomplete,
type ApplicationCommandOptions,
type ApplicationCommandStructure,
type ChatInputApplicationCommandStructure,
} from '@projectdysnomia/dysnomia';
import type { CommandContext, PartialContext } from './command-context.type';
export interface CommandHandler<T extends ApplicationCommandStructure> {
definition: T;
execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>;
}
export type ExecutableInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction;
export type ChatCommandDefinition = Omit<ChatInputApplicationCommandStructure, 'type'>;
export function createChatCommand(
definition: ChatCommandDefinition,
execute: (interaction: CommandInteraction, ctx: CommandContext) => Promise<void>,
): CommandHandler<ChatInputApplicationCommandStructure> {
const def = definition as ChatInputApplicationCommandStructure;
def.type = 1; // CHAT_INPUT
return {
definition: def,
execute,
};
}

View File

@@ -1,45 +1,45 @@
import {
Interaction,
CommandInteraction,
Constants,
ModalSubmitInteraction,
ComponentInteraction,
AutocompleteInteraction,
PingInteraction,
} from '@projectdysnomia/dysnomia';
import type { ExecutableInteraction } from './command-handler';
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND;
}
export function isModalSubmit(interaction: Interaction): interaction is ModalSubmitInteraction {
return interaction.type === Constants.InteractionTypes.MODAL_SUBMIT;
}
export function isMessageComponent(interaction: Interaction): interaction is ComponentInteraction {
return interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT;
}
export function isAutocomplete(interaction: Interaction): interaction is AutocompleteInteraction {
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE;
}
export function isPing(interaction: Interaction): interaction is PingInteraction {
return interaction.type === Constants.InteractionTypes.PING;
}
export function commandHasName(interaction: Interaction, name: string): boolean {
return isApplicationCommand(interaction) && interaction.data.name === name;
}
export function commandHasIdPrefix(interaction: Interaction, prefix: string): boolean {
return (isModalSubmit(interaction) || isMessageComponent(interaction)) && interaction.data.custom_id.startsWith(prefix);
}
export function getCommandName(interaction: ExecutableInteraction): string | undefined {
if (isApplicationCommand(interaction) || isAutocomplete(interaction)) {
return interaction.data.name;
}
return undefined;
}
import {
Interaction,
CommandInteraction,
Constants,
ModalSubmitInteraction,
ComponentInteraction,
AutocompleteInteraction,
PingInteraction,
} from '@projectdysnomia/dysnomia';
import type { ExecutableInteraction } from './command-handler';
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND;
}
export function isModalSubmit(interaction: Interaction): interaction is ModalSubmitInteraction {
return interaction.type === Constants.InteractionTypes.MODAL_SUBMIT;
}
export function isMessageComponent(interaction: Interaction): interaction is ComponentInteraction {
return interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT;
}
export function isAutocomplete(interaction: Interaction): interaction is AutocompleteInteraction {
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE;
}
export function isPing(interaction: Interaction): interaction is PingInteraction {
return interaction.type === Constants.InteractionTypes.PING;
}
export function commandHasName(interaction: Interaction, name: string): boolean {
return isApplicationCommand(interaction) && interaction.data.name === name;
}
export function commandHasIdPrefix(interaction: Interaction, prefix: string): boolean {
return (isModalSubmit(interaction) || isMessageComponent(interaction)) && interaction.data.custom_id.startsWith(prefix);
}
export function getCommandName(interaction: ExecutableInteraction): string | undefined {
if (isApplicationCommand(interaction) || isAutocomplete(interaction)) {
return interaction.data.name;
}
return undefined;
}

View File

@@ -1,63 +1,63 @@
import { type InteractionModalContent, type Component } from '@projectdysnomia/dysnomia';
import type { CommandContext, PartialContext } from './command-context.type';
import { isApplicationCommand, isMessageComponent } from './command-helpers';
import type { ExecutableInteraction } from './command-handler';
export function injectInteraction(interaction: ExecutableInteraction, ctx: PartialContext): [ExecutableInteraction, CommandContext] {
// Wrap the interaction methods to inject command tracking ids into all custom_ids for modals and components.
if (ctx.state.name && (isApplicationCommand(interaction) || isMessageComponent(interaction))) {
const _originalCreateModal = interaction.createModal.bind(interaction);
interaction.createModal = (content: InteractionModalContent) => {
validateCustomIdLength(content.custom_id);
content.custom_id = `${content.custom_id}_${ctx.state.id}`;
return _originalCreateModal(content);
};
const _originalCreateMessage = interaction.createMessage.bind(interaction);
interaction.createMessage = (content) => {
if (typeof content === 'string') return _originalCreateMessage(content);
if (content.components) {
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
}
return _originalCreateMessage(content);
};
const _originalEditMessage = interaction.editMessage.bind(interaction);
interaction.editMessage = (messageID, content) => {
if (typeof content === 'string') return _originalEditMessage(messageID, content);
if (content.components) {
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
}
return _originalEditMessage(messageID, content);
};
const _originalCreateFollowup = interaction.createFollowup.bind(interaction);
interaction.createFollowup = (content) => {
if (typeof content === 'string') return _originalCreateFollowup(content);
if (content.components) {
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
}
return _originalCreateFollowup(content);
};
}
return [interaction, ctx as CommandContext];
}
function validateCustomIdLength(customId: string) {
if (customId.length > 80) {
throw new Error(`Custom ID too long: ${customId.length} characters (max 80) with this framework. Consider using shorter IDs.`);
}
}
function addCommandIdToComponentCustomIds(components: Component[], commandId: string) {
components.forEach((component) => {
if (!component) return;
if ('custom_id' in component) {
validateCustomIdLength(component.custom_id as string);
component.custom_id = `${component.custom_id}_${commandId}`;
}
if ('components' in component && Array.isArray(component.components)) {
addCommandIdToComponentCustomIds(component.components, commandId);
}
});
}
import { type InteractionModalContent, type Component } from '@projectdysnomia/dysnomia';
import type { CommandContext, PartialContext } from './command-context.type';
import { isApplicationCommand, isMessageComponent } from './command-helpers';
import type { ExecutableInteraction } from './command-handler';
export function injectInteraction(interaction: ExecutableInteraction, ctx: PartialContext): [ExecutableInteraction, CommandContext] {
// Wrap the interaction methods to inject command tracking ids into all custom_ids for modals and components.
if (ctx.state.name && (isApplicationCommand(interaction) || isMessageComponent(interaction))) {
const _originalCreateModal = interaction.createModal.bind(interaction);
interaction.createModal = (content: InteractionModalContent) => {
validateCustomIdLength(content.custom_id);
content.custom_id = `${content.custom_id}_${ctx.state.id}`;
return _originalCreateModal(content);
};
const _originalCreateMessage = interaction.createMessage.bind(interaction);
interaction.createMessage = (content) => {
if (typeof content === 'string') return _originalCreateMessage(content);
if (content.components) {
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
}
return _originalCreateMessage(content);
};
const _originalEditMessage = interaction.editMessage.bind(interaction);
interaction.editMessage = (messageID, content) => {
if (typeof content === 'string') return _originalEditMessage(messageID, content);
if (content.components) {
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
}
return _originalEditMessage(messageID, content);
};
const _originalCreateFollowup = interaction.createFollowup.bind(interaction);
interaction.createFollowup = (content) => {
if (typeof content === 'string') return _originalCreateFollowup(content);
if (content.components) {
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
}
return _originalCreateFollowup(content);
};
}
return [interaction, ctx as CommandContext];
}
function validateCustomIdLength(customId: string) {
if (customId.length > 80) {
throw new Error(`Custom ID too long: ${customId.length} characters (max 80) with this framework. Consider using shorter IDs.`);
}
}
function addCommandIdToComponentCustomIds(components: Component[], commandId: string) {
components.forEach((component) => {
if (!component) return;
if ('custom_id' in component) {
validateCustomIdLength(component.custom_id as string);
component.custom_id = `${component.custom_id}_${commandId}`;
}
if ('components' in component && Array.isArray(component.components)) {
addCommandIdToComponentCustomIds(component.components, commandId);
}
});
}

View File

@@ -1,56 +1,56 @@
import { createReactiveState } from '@star-kitten/util/reactive-state.js';
import type { PartialContext } from './command-context.type';
import { isApplicationCommand, isAutocomplete } from './command-helpers';
import type { ExecutableInteraction } from './command-handler';
export interface CommandState<T = any> {
id: string; // unique id for this command instance
name: string; // command name
data: T; // internal data storage
}
export async function getCommandState<T>(interaction: ExecutableInteraction, ctx: PartialContext): Promise<CommandState<T>> {
const id = instanceIdFromInteraction(interaction);
let state: CommandState<T>;
// get state from kv store if possible
if (ctx.kv.has(`command-state:${id}`)) {
state = await ctx.kv.get<CommandState<T>>(`command-state:${id}`);
}
if (!state) {
state = { id: id, name: '', data: {} as T };
}
const [reactiveState, subscribe] = createReactiveState(state);
subscribe(async (newState) => {
if (ctx.kv) {
await ctx.kv.set(`command-state:${id}`, newState);
}
});
ctx.state = reactiveState;
return reactiveState;
}
function instanceIdFromInteraction(interaction: ExecutableInteraction) {
if (isAutocomplete(interaction)) {
// autocomplete should not be stateful, they get no id
return '';
}
if (isApplicationCommand(interaction)) {
// for application commands, we create a new instance id
const instance_id = crypto.randomUUID();
return instance_id;
}
const interact = interaction;
const customId: string = interact.data.custom_id;
const commandId = customId.split('_').pop();
interaction;
// command id should be a uuid
if (commandId && /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(commandId)) {
return commandId;
}
console.error(`Invalid command id extracted from interaction: ${customId}`);
return '';
}
import { createReactiveState } from '@star-kitten/util/reactive-state.js';
import type { PartialContext } from './command-context.type';
import { isApplicationCommand, isAutocomplete } from './command-helpers';
import type { ExecutableInteraction } from './command-handler';
export interface CommandState<T = any> {
id: string; // unique id for this command instance
name: string; // command name
data: T; // internal data storage
}
export async function getCommandState<T>(interaction: ExecutableInteraction, ctx: PartialContext): Promise<CommandState<T>> {
const id = instanceIdFromInteraction(interaction);
let state: CommandState<T>;
// get state from kv store if possible
if (ctx.kv.has(`command-state:${id}`)) {
state = await ctx.kv.get<CommandState<T>>(`command-state:${id}`);
}
if (!state) {
state = { id: id, name: '', data: {} as T };
}
const [reactiveState, subscribe] = createReactiveState(state);
subscribe(async (newState) => {
if (ctx.kv) {
await ctx.kv.set(`command-state:${id}`, newState);
}
});
ctx.state = reactiveState;
return reactiveState;
}
function instanceIdFromInteraction(interaction: ExecutableInteraction) {
if (isAutocomplete(interaction)) {
// autocomplete should not be stateful, they get no id
return '';
}
if (isApplicationCommand(interaction)) {
// for application commands, we create a new instance id
const instance_id = crypto.randomUUID();
return instance_id;
}
const interact = interaction;
const customId: string = interact.data.custom_id;
const commandId = customId.split('_').pop();
interaction;
// command id should be a uuid
if (commandId && /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(commandId)) {
return commandId;
}
console.error(`Invalid command id extracted from interaction: ${customId}`);
return '';
}

View File

@@ -1,73 +1,73 @@
import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers';
import type { PartialContext } from './command-context.type';
import type { CommandHandler, ExecutableInteraction } from './command-handler';
import { injectInteraction } from './command-injection';
import { getCommandState } from './command-state';
export async function handleCommands(
interaction: ExecutableInteraction,
commands: Record<string, CommandHandler<ApplicationCommandStructure>>,
ctx: PartialContext,
) {
ctx.state = await getCommandState(interaction, ctx);
if (!ctx.state.name) {
ctx.state.name = getCommandName(interaction);
}
if (isAutocomplete(interaction) && ctx.state.name) {
const acCommand = commands[ctx.state.name];
return acCommand.execute(interaction, ctx as any);
}
if (!ctx.state.id) {
console.error(`No command ID found for interaction ${interaction.id}`);
return;
}
const command = commands[ctx.state.name || ''];
if (!command) {
console.warn(`No command found for interaction: ${JSON.stringify(interaction, undefined, 2)}`);
return;
}
cleanInteractionCustomIds(interaction, ctx.state.id);
const [injectedInteraction, fullContext] = await injectInteraction(interaction, ctx);
return command.execute(injectedInteraction, fullContext);
}
export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) {
ctx.client.on('interactionCreate', async (interaction) => {
if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) {
handleCommands(interaction, commands, ctx);
}
});
}
function cleanInteractionCustomIds(interaction: ExecutableInteraction, id: string) {
if ('components' in interaction && Array.isArray(interaction.components) && id) {
removeCommandIdFromComponentCustomIds(interaction.components, id);
}
if ('data' in interaction && id) {
if ('custom_id' in interaction.data && typeof interaction.data.custom_id === 'string') {
interaction.data.custom_id = interaction.data.custom_id.replace(`_${id}`, '');
}
if ('components' in interaction.data && Array.isArray(interaction.data.components)) {
removeCommandIdFromComponentCustomIds(interaction.data.components as any, id);
}
}
}
function removeCommandIdFromComponentCustomIds(components: { custom_id?: string; components?: any[] }[], commandId: string) {
components.forEach((component) => {
if ('custom_id' in component) {
component.custom_id = component.custom_id.replace(`_${commandId}`, '');
}
if ('components' in component && Array.isArray(component.components)) {
removeCommandIdFromComponentCustomIds(component.components, commandId);
}
if ('component' in component && 'custom_id' in (component as any).component && Array.isArray(component.components)) {
(component.component as any).custom_id = (component.component as any).custom_id.replace(`_${commandId}`, '');
}
});
}
import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers';
import type { PartialContext } from './command-context.type';
import type { CommandHandler, ExecutableInteraction } from './command-handler';
import { injectInteraction } from './command-injection';
import { getCommandState } from './command-state';
export async function handleCommands(
interaction: ExecutableInteraction,
commands: Record<string, CommandHandler<ApplicationCommandStructure>>,
ctx: PartialContext,
) {
ctx.state = await getCommandState(interaction, ctx);
if (!ctx.state.name) {
ctx.state.name = getCommandName(interaction);
}
if (isAutocomplete(interaction) && ctx.state.name) {
const acCommand = commands[ctx.state.name];
return acCommand.execute(interaction, ctx as any);
}
if (!ctx.state.id) {
console.error(`No command ID found for interaction ${interaction.id}`);
return;
}
const command = commands[ctx.state.name || ''];
if (!command) {
console.warn(`No command found for interaction: ${JSON.stringify(interaction, undefined, 2)}`);
return;
}
cleanInteractionCustomIds(interaction, ctx.state.id);
const [injectedInteraction, fullContext] = await injectInteraction(interaction, ctx);
return command.execute(injectedInteraction, fullContext);
}
export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) {
ctx.client.on('interactionCreate', async (interaction) => {
if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) {
handleCommands(interaction, commands, ctx);
}
});
}
function cleanInteractionCustomIds(interaction: ExecutableInteraction, id: string) {
if ('components' in interaction && Array.isArray(interaction.components) && id) {
removeCommandIdFromComponentCustomIds(interaction.components, id);
}
if ('data' in interaction && id) {
if ('custom_id' in interaction.data && typeof interaction.data.custom_id === 'string') {
interaction.data.custom_id = interaction.data.custom_id.replace(`_${id}`, '');
}
if ('components' in interaction.data && Array.isArray(interaction.data.components)) {
removeCommandIdFromComponentCustomIds(interaction.data.components as any, id);
}
}
}
function removeCommandIdFromComponentCustomIds(components: { custom_id?: string; components?: any[] }[], commandId: string) {
components.forEach((component) => {
if ('custom_id' in component) {
component.custom_id = component.custom_id.replace(`_${commandId}`, '');
}
if ('components' in component && Array.isArray(component.components)) {
removeCommandIdFromComponentCustomIds(component.components, commandId);
}
if ('component' in component && 'custom_id' in (component as any).component && Array.isArray(component.components)) {
(component.component as any).custom_id = (component.component as any).custom_id.replace(`_${commandId}`, '');
}
});
}

View File

@@ -1,19 +1,19 @@
import { Glob } from 'bun';
import { join } from 'node:path';
import type { CommandHandler } from './command-handler';
import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
export async function importCommands(
pattern: string = '**/*.command.{js,ts}',
baseDir: string = join(process.cwd(), 'src'),
commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {},
): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> {
const glob = new Glob(pattern);
for await (const file of glob.scan({ cwd: baseDir, absolute: true })) {
const command = (await import(file)).default as CommandHandler<ApplicationCommandStructure>;
commandRegistry[command.definition.name] = command;
}
return commandRegistry;
}
import { Glob } from 'bun';
import { join } from 'node:path';
import type { CommandHandler } from './command-handler';
import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
export async function importCommands(
pattern: string = '**/*.command.{js,ts}',
baseDir: string = join(process.cwd(), 'src'),
commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {},
): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> {
const glob = new Glob(pattern);
for await (const file of glob.scan({ cwd: baseDir, absolute: true })) {
const command = (await import(file)).default as CommandHandler<ApplicationCommandStructure>;
commandRegistry[command.definition.name] = command;
}
return commandRegistry;
}

View File

@@ -1,80 +1,80 @@
import {
Constants,
type ApplicationCommandOptions,
type ApplicationCommandOptionsBoolean,
type ApplicationCommandOptionsInteger,
type ApplicationCommandOptionsMentionable,
type ApplicationCommandOptionsNumber,
type ApplicationCommandOptionsRole,
type ApplicationCommandOptionsString,
type ApplicationCommandOptionsSubCommand,
type ApplicationCommandOptionsSubCommandGroup,
type ApplicationCommandOptionsUser,
} from '@projectdysnomia/dysnomia';
export type StringOptionDefinition = Omit<ApplicationCommandOptionsString, 'type'> & { autocomplete?: boolean };
export function stringOption(options: StringOptionDefinition): ApplicationCommandOptionsString {
const def = options as ApplicationCommandOptionsString;
def.type = Constants.ApplicationCommandOptionTypes.STRING;
return def;
}
export type IntegerOptionDefinition = Omit<ApplicationCommandOptionsInteger, 'type'> & { autocomplete?: boolean };
export function integerOption(options: IntegerOptionDefinition): ApplicationCommandOptionsInteger {
const def = options as ApplicationCommandOptionsInteger;
def.type = Constants.ApplicationCommandOptionTypes.INTEGER;
return def;
}
export type BooleanOptionDefinition = Omit<ApplicationCommandOptionsBoolean, 'type'>;
export function booleanOption(options: BooleanOptionDefinition): ApplicationCommandOptionsBoolean {
const def = options as ApplicationCommandOptionsBoolean;
def.type = Constants.ApplicationCommandOptionTypes.BOOLEAN;
return def;
}
export type UserOptionDefinition = Omit<ApplicationCommandOptionsUser, 'type'> & { autocomplete?: boolean };
export function userOption(options: UserOptionDefinition): ApplicationCommandOptionsUser {
const def = options as ApplicationCommandOptionsUser;
def.type = Constants.ApplicationCommandOptionTypes.USER;
return def;
}
export type ChannelOptionDefinition = Omit<ApplicationCommandOptions, 'type'> & { autocomplete?: boolean };
export function channelOption(options: ChannelOptionDefinition): ApplicationCommandOptions {
const def = options as ApplicationCommandOptions;
def.type = Constants.ApplicationCommandOptionTypes.CHANNEL;
return def;
}
export type RoleOptionDefinition = Omit<ApplicationCommandOptionsRole, 'type'> & { autocomplete?: boolean };
export function roleOption(options: RoleOptionDefinition): ApplicationCommandOptionsRole {
const def = options as ApplicationCommandOptionsRole;
def.type = Constants.ApplicationCommandOptionTypes.ROLE;
return def;
}
export type MentionableOptionDefinition = Omit<ApplicationCommandOptionsMentionable, 'type'>;
export function mentionableOption(options: MentionableOptionDefinition): ApplicationCommandOptionsMentionable {
const def = options as ApplicationCommandOptionsMentionable;
def.type = Constants.ApplicationCommandOptionTypes.MENTIONABLE;
return def;
}
export type NumberOptionDefinition = Omit<ApplicationCommandOptionsNumber, 'type'> & { autocomplete?: boolean };
export function numberOption(options: NumberOptionDefinition): ApplicationCommandOptionsNumber {
const def = options as ApplicationCommandOptionsNumber;
def.type = Constants.ApplicationCommandOptionTypes.NUMBER;
return def;
}
export type AttachmentOptionDefinition = Omit<ApplicationCommandOptions, 'type'>;
export function attachmentOption(options: AttachmentOptionDefinition): ApplicationCommandOptions {
const def = options as ApplicationCommandOptions;
def.type = Constants.ApplicationCommandOptionTypes.ATTACHMENT;
return def;
}
export type SubCommandOptionDefinition = Omit<ApplicationCommandOptionsSubCommand, 'type'>;
export function subCommandOption(options: SubCommandOptionDefinition): ApplicationCommandOptionsSubCommand {
const def = options as ApplicationCommandOptionsSubCommand;
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND;
return def;
}
export type SubCommandGroupOptionDefinition = Omit<ApplicationCommandOptionsSubCommandGroup, 'type'>;
export function subCommandGroupOption(options: SubCommandGroupOptionDefinition): ApplicationCommandOptionsSubCommandGroup {
const def = options as ApplicationCommandOptionsSubCommandGroup;
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND_GROUP;
return def;
}
import {
Constants,
type ApplicationCommandOptions,
type ApplicationCommandOptionsBoolean,
type ApplicationCommandOptionsInteger,
type ApplicationCommandOptionsMentionable,
type ApplicationCommandOptionsNumber,
type ApplicationCommandOptionsRole,
type ApplicationCommandOptionsString,
type ApplicationCommandOptionsSubCommand,
type ApplicationCommandOptionsSubCommandGroup,
type ApplicationCommandOptionsUser,
} from '@projectdysnomia/dysnomia';
export type StringOptionDefinition = Omit<ApplicationCommandOptionsString, 'type'> & { autocomplete?: boolean };
export function stringOption(options: StringOptionDefinition): ApplicationCommandOptionsString {
const def = options as ApplicationCommandOptionsString;
def.type = Constants.ApplicationCommandOptionTypes.STRING;
return def;
}
export type IntegerOptionDefinition = Omit<ApplicationCommandOptionsInteger, 'type'> & { autocomplete?: boolean };
export function integerOption(options: IntegerOptionDefinition): ApplicationCommandOptionsInteger {
const def = options as ApplicationCommandOptionsInteger;
def.type = Constants.ApplicationCommandOptionTypes.INTEGER;
return def;
}
export type BooleanOptionDefinition = Omit<ApplicationCommandOptionsBoolean, 'type'>;
export function booleanOption(options: BooleanOptionDefinition): ApplicationCommandOptionsBoolean {
const def = options as ApplicationCommandOptionsBoolean;
def.type = Constants.ApplicationCommandOptionTypes.BOOLEAN;
return def;
}
export type UserOptionDefinition = Omit<ApplicationCommandOptionsUser, 'type'> & { autocomplete?: boolean };
export function userOption(options: UserOptionDefinition): ApplicationCommandOptionsUser {
const def = options as ApplicationCommandOptionsUser;
def.type = Constants.ApplicationCommandOptionTypes.USER;
return def;
}
export type ChannelOptionDefinition = Omit<ApplicationCommandOptions, 'type'> & { autocomplete?: boolean };
export function channelOption(options: ChannelOptionDefinition): ApplicationCommandOptions {
const def = options as ApplicationCommandOptions;
def.type = Constants.ApplicationCommandOptionTypes.CHANNEL;
return def;
}
export type RoleOptionDefinition = Omit<ApplicationCommandOptionsRole, 'type'> & { autocomplete?: boolean };
export function roleOption(options: RoleOptionDefinition): ApplicationCommandOptionsRole {
const def = options as ApplicationCommandOptionsRole;
def.type = Constants.ApplicationCommandOptionTypes.ROLE;
return def;
}
export type MentionableOptionDefinition = Omit<ApplicationCommandOptionsMentionable, 'type'>;
export function mentionableOption(options: MentionableOptionDefinition): ApplicationCommandOptionsMentionable {
const def = options as ApplicationCommandOptionsMentionable;
def.type = Constants.ApplicationCommandOptionTypes.MENTIONABLE;
return def;
}
export type NumberOptionDefinition = Omit<ApplicationCommandOptionsNumber, 'type'> & { autocomplete?: boolean };
export function numberOption(options: NumberOptionDefinition): ApplicationCommandOptionsNumber {
const def = options as ApplicationCommandOptionsNumber;
def.type = Constants.ApplicationCommandOptionTypes.NUMBER;
return def;
}
export type AttachmentOptionDefinition = Omit<ApplicationCommandOptions, 'type'>;
export function attachmentOption(options: AttachmentOptionDefinition): ApplicationCommandOptions {
const def = options as ApplicationCommandOptions;
def.type = Constants.ApplicationCommandOptionTypes.ATTACHMENT;
return def;
}
export type SubCommandOptionDefinition = Omit<ApplicationCommandOptionsSubCommand, 'type'>;
export function subCommandOption(options: SubCommandOptionDefinition): ApplicationCommandOptionsSubCommand {
const def = options as ApplicationCommandOptionsSubCommand;
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND;
return def;
}
export type SubCommandGroupOptionDefinition = Omit<ApplicationCommandOptionsSubCommandGroup, 'type'>;
export function subCommandGroupOption(options: SubCommandGroupOptionDefinition): ApplicationCommandOptionsSubCommandGroup {
const def = options as ApplicationCommandOptionsSubCommandGroup;
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND_GROUP;
return def;
}

View File

@@ -1,11 +1,11 @@
import type { ApplicationCommandStructure, Client } from '@projectdysnomia/dysnomia';
export async function registerCommands(client: Client, commands: ApplicationCommandStructure[]) {
if (!client) throw new Error('Client not initialized');
if (!(await client.getCommands()).length || process.env.RESET_COMMANDS === 'true' || process.env.NODE_ENV === 'development') {
console.debug('Registering commands...');
const response = await client.bulkEditCommands(commands);
console.debug(`Registered ${response.length} commands.`);
}
return commands;
}
import type { ApplicationCommandStructure, Client } from '@projectdysnomia/dysnomia';
export async function registerCommands(client: Client, commands: ApplicationCommandStructure[]) {
if (!client) throw new Error('Client not initialized');
if (!(await client.getCommands()).length || process.env.RESET_COMMANDS === 'true' || process.env.NODE_ENV === 'development') {
console.debug('Registering commands...');
const response = await client.bulkEditCommands(commands);
console.debug(`Registered ${response.length} commands.`);
}
return commands;
}

View File

@@ -1 +1 @@
export * from './text';
export * from './text';

View File

@@ -1,2 +1,2 @@
export const WHITE_SPACE = ' '; // non-breaking space
export const BREAKING_WHITE_SPACE = '\u200B';
export const WHITE_SPACE = ' '; // non-breaking space
export const BREAKING_WHITE_SPACE = '\u200B';

View File

@@ -1,314 +1,314 @@
import {
Constants,
type ActionRow,
type Button,
type ChannelSelectMenu,
type GuildChannelTypes,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
export type ActionRowItem = Button | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu | ChannelSelectMenu;
export const createActionRow = (...components: ActionRowItem[]): ActionRow => ({
type: Constants.ComponentTypes.ACTION_ROW,
components,
});
export enum ButtonStyle {
PRIMARY = 1,
SECONDARY = 2,
SUCCESS = 3,
DANGER = 4,
}
export interface ButtonOptions {
style?: ButtonStyle;
emoji?: PartialEmoji;
disabled?: boolean;
}
export const createButton = (label: string, custom_id: string, options?: ButtonOptions): InteractionButton => ({
type: Constants.ComponentTypes.BUTTON,
style: options?.style ?? Constants.ButtonStyles.PRIMARY,
label,
custom_id,
...options,
});
export interface URLButtonOptions {
emoji?: PartialEmoji;
disabled?: boolean;
}
export const createURLButton = (label: string, url: string, options?: URLButtonOptions): URLButton => ({
type: Constants.ComponentTypes.BUTTON,
style: Constants.ButtonStyles.LINK,
label,
url,
...options,
});
export interface PremiumButtonOptions {
emoji?: PartialEmoji;
disabled?: boolean;
}
export const createPremiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({
type: Constants.ComponentTypes.BUTTON,
style: Constants.ButtonStyles.PREMIUM,
sku_id,
...options,
});
export interface StringSelectOpts {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
required?: boolean; // Note: not actually a property of StringSelectMenu, but useful for modals
}
export interface StringSelectOption {
label: string;
value: string;
description?: string;
emoji?: {
name?: string;
id?: string;
animated?: boolean;
};
default?: boolean;
}
export const createStringSelect = (
custom_id: string,
selectOpts: StringSelectOpts,
...options: StringSelectOption[]
): StringSelectMenu => ({
type: Constants.ComponentTypes.STRING_SELECT,
custom_id,
options,
placeholder: selectOpts.placeholder ?? '',
min_values: selectOpts.min_values ?? 1,
max_values: selectOpts.max_values ?? 1,
disabled: selectOpts.disabled ?? false,
required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals
});
export interface TextInputOptions {
isParagraph?: boolean;
label?: string;
min_length?: number;
max_length?: number;
required?: boolean;
value?: string;
placeholder?: string;
}
export const createTextInput = (custom_id: string, options?: TextInputOptions): TextInput => ({
type: Constants.ComponentTypes.TEXT_INPUT,
custom_id,
style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT,
label: options?.label ?? '',
min_length: options?.min_length ?? 0,
max_length: options?.max_length ?? 4000,
required: options?.required ?? false,
value: options?.value ?? '',
placeholder: options?.placeholder ?? '',
});
export interface UserSelectOptions {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'user' }>;
}
export const createUserSelect = (custom_id: string, options?: UserSelectOptions): UserSelectMenu => ({
type: Constants.ComponentTypes.USER_SELECT,
custom_id,
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface RoleSelectOptions {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'role' }>;
}
export const createRoleSelect = (custom_id: string, options?: RoleSelectOptions): RoleSelectMenu => ({
type: Constants.ComponentTypes.ROLE_SELECT,
custom_id,
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface MentionableSelectOptions {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'user' | 'role' }>;
}
export const createMentionableSelect = (custom_id: string, options?: MentionableSelectOptions): MentionableSelectMenu => ({
type: Constants.ComponentTypes.MENTIONABLE_SELECT,
custom_id,
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface ChannelSelectOptions {
channel_types?: GuildChannelTypes[];
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'channel' }>;
}
export const createChannelSelect = (custom_id: string, options?: ChannelSelectOptions): ChannelSelectMenu => ({
type: Constants.ComponentTypes.CHANNEL_SELECT,
custom_id,
channel_types: options?.channel_types ?? [],
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface SectionOptions {
components: Array<TextDisplayComponent>;
accessory: Button | ThumbnailComponent;
}
export const createSection = (accessory: Button | ThumbnailComponent, ...components: Array<TextDisplayComponent>): SectionComponent => ({
type: Constants.ComponentTypes.SECTION,
accessory,
components,
});
/**
* Creates a text display component where the text will be displayed similar to a message: supports markdown
* @param content The text content to display.
* @returns The created text display component.
*/
export const createTextDisplay = (content: string) => ({
type: Constants.ComponentTypes.TEXT_DISPLAY,
content,
});
export interface ThumbnailOptions {
media: {
url: string; // Supports arbitrary urls and attachment://<filename> references
};
description?: string;
spoiler?: boolean;
}
export const createThumbnail = (url: string, description?: string, spoiler?: boolean): ThumbnailComponent => ({
type: Constants.ComponentTypes.THUMBNAIL,
media: {
url,
},
description,
spoiler,
});
export interface MediaItem {
url: string; // Supports arbitrary urls and attachment://<filename> references
description?: string;
spoiler?: boolean;
}
export const createMediaGallery = (...items: MediaItem[]): MediaGalleryComponent => ({
type: Constants.ComponentTypes.MEDIA_GALLERY,
items: items.map((item) => ({
type: Constants.ComponentTypes.FILE,
media: { url: item.url },
description: item.description,
spoiler: item.spoiler,
})),
});
export interface FileOptions {
url: string; // Supports only attachment://<filename> references
spoiler?: boolean;
}
export const createFile = (url: string, spoiler?: boolean): FileComponent => ({
type: Constants.ComponentTypes.FILE,
file: {
url,
},
spoiler,
});
export enum Padding {
SMALL = 1,
LARGE = 2,
}
export interface SeparatorOptions {
divider?: boolean;
spacing?: Padding;
}
export const createSeparator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({
type: Constants.ComponentTypes.SEPARATOR,
divider,
spacing: spacing ?? Padding.SMALL,
});
export interface ContainerOptions {
accent_color?: number;
spoiler?: boolean;
}
export type ContainerItems =
| ActionRow
| TextDisplayComponent
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent;
export const createContainer = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({
type: Constants.ComponentTypes.CONTAINER,
...options,
components,
});
export const createModalLabel = (label: string, component: TextInput | StringSelectMenu): LabelComponent => ({
type: Constants.ComponentTypes.LABEL,
label,
component,
});
import {
Constants,
type ActionRow,
type Button,
type ChannelSelectMenu,
type GuildChannelTypes,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
export type ActionRowItem = Button | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu | ChannelSelectMenu;
export const createActionRow = (...components: ActionRowItem[]): ActionRow => ({
type: Constants.ComponentTypes.ACTION_ROW,
components,
});
export enum ButtonStyle {
PRIMARY = 1,
SECONDARY = 2,
SUCCESS = 3,
DANGER = 4,
}
export interface ButtonOptions {
style?: ButtonStyle;
emoji?: PartialEmoji;
disabled?: boolean;
}
export const createButton = (label: string, custom_id: string, options?: ButtonOptions): InteractionButton => ({
type: Constants.ComponentTypes.BUTTON,
style: options?.style ?? Constants.ButtonStyles.PRIMARY,
label,
custom_id,
...options,
});
export interface URLButtonOptions {
emoji?: PartialEmoji;
disabled?: boolean;
}
export const createURLButton = (label: string, url: string, options?: URLButtonOptions): URLButton => ({
type: Constants.ComponentTypes.BUTTON,
style: Constants.ButtonStyles.LINK,
label,
url,
...options,
});
export interface PremiumButtonOptions {
emoji?: PartialEmoji;
disabled?: boolean;
}
export const createPremiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({
type: Constants.ComponentTypes.BUTTON,
style: Constants.ButtonStyles.PREMIUM,
sku_id,
...options,
});
export interface StringSelectOpts {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
required?: boolean; // Note: not actually a property of StringSelectMenu, but useful for modals
}
export interface StringSelectOption {
label: string;
value: string;
description?: string;
emoji?: {
name?: string;
id?: string;
animated?: boolean;
};
default?: boolean;
}
export const createStringSelect = (
custom_id: string,
selectOpts: StringSelectOpts,
...options: StringSelectOption[]
): StringSelectMenu => ({
type: Constants.ComponentTypes.STRING_SELECT,
custom_id,
options,
placeholder: selectOpts.placeholder ?? '',
min_values: selectOpts.min_values ?? 1,
max_values: selectOpts.max_values ?? 1,
disabled: selectOpts.disabled ?? false,
required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals
});
export interface TextInputOptions {
isParagraph?: boolean;
label?: string;
min_length?: number;
max_length?: number;
required?: boolean;
value?: string;
placeholder?: string;
}
export const createTextInput = (custom_id: string, options?: TextInputOptions): TextInput => ({
type: Constants.ComponentTypes.TEXT_INPUT,
custom_id,
style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT,
label: options?.label ?? '',
min_length: options?.min_length ?? 0,
max_length: options?.max_length ?? 4000,
required: options?.required ?? false,
value: options?.value ?? '',
placeholder: options?.placeholder ?? '',
});
export interface UserSelectOptions {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'user' }>;
}
export const createUserSelect = (custom_id: string, options?: UserSelectOptions): UserSelectMenu => ({
type: Constants.ComponentTypes.USER_SELECT,
custom_id,
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface RoleSelectOptions {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'role' }>;
}
export const createRoleSelect = (custom_id: string, options?: RoleSelectOptions): RoleSelectMenu => ({
type: Constants.ComponentTypes.ROLE_SELECT,
custom_id,
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface MentionableSelectOptions {
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'user' | 'role' }>;
}
export const createMentionableSelect = (custom_id: string, options?: MentionableSelectOptions): MentionableSelectMenu => ({
type: Constants.ComponentTypes.MENTIONABLE_SELECT,
custom_id,
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface ChannelSelectOptions {
channel_types?: GuildChannelTypes[];
placeholder?: string;
min_values?: number;
max_values?: number;
disabled?: boolean;
default_values?: Array<{ id: string; type: 'channel' }>;
}
export const createChannelSelect = (custom_id: string, options?: ChannelSelectOptions): ChannelSelectMenu => ({
type: Constants.ComponentTypes.CHANNEL_SELECT,
custom_id,
channel_types: options?.channel_types ?? [],
placeholder: options?.placeholder ?? '',
min_values: options?.min_values ?? 1,
max_values: options?.max_values ?? 1,
disabled: options?.disabled ?? false,
default_values: options?.default_values ?? [],
});
export interface SectionOptions {
components: Array<TextDisplayComponent>;
accessory: Button | ThumbnailComponent;
}
export const createSection = (accessory: Button | ThumbnailComponent, ...components: Array<TextDisplayComponent>): SectionComponent => ({
type: Constants.ComponentTypes.SECTION,
accessory,
components,
});
/**
* Creates a text display component where the text will be displayed similar to a message: supports markdown
* @param content The text content to display.
* @returns The created text display component.
*/
export const createTextDisplay = (content: string) => ({
type: Constants.ComponentTypes.TEXT_DISPLAY,
content,
});
export interface ThumbnailOptions {
media: {
url: string; // Supports arbitrary urls and attachment://<filename> references
};
description?: string;
spoiler?: boolean;
}
export const createThumbnail = (url: string, description?: string, spoiler?: boolean): ThumbnailComponent => ({
type: Constants.ComponentTypes.THUMBNAIL,
media: {
url,
},
description,
spoiler,
});
export interface MediaItem {
url: string; // Supports arbitrary urls and attachment://<filename> references
description?: string;
spoiler?: boolean;
}
export const createMediaGallery = (...items: MediaItem[]): MediaGalleryComponent => ({
type: Constants.ComponentTypes.MEDIA_GALLERY,
items: items.map((item) => ({
type: Constants.ComponentTypes.FILE,
media: { url: item.url },
description: item.description,
spoiler: item.spoiler,
})),
});
export interface FileOptions {
url: string; // Supports only attachment://<filename> references
spoiler?: boolean;
}
export const createFile = (url: string, spoiler?: boolean): FileComponent => ({
type: Constants.ComponentTypes.FILE,
file: {
url,
},
spoiler,
});
export enum Padding {
SMALL = 1,
LARGE = 2,
}
export interface SeparatorOptions {
divider?: boolean;
spacing?: Padding;
}
export const createSeparator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({
type: Constants.ComponentTypes.SEPARATOR,
divider,
spacing: spacing ?? Padding.SMALL,
});
export interface ContainerOptions {
accent_color?: number;
spoiler?: boolean;
}
export type ContainerItems =
| ActionRow
| TextDisplayComponent
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent;
export const createContainer = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({
type: Constants.ComponentTypes.CONTAINER,
...options,
components,
});
export const createModalLabel = (label: string, component: TextInput | StringSelectMenu): LabelComponent => ({
type: Constants.ComponentTypes.LABEL,
label,
component,
});

View File

@@ -1,23 +1,23 @@
import {
Constants,
type ComponentBase,
type ModalSubmitInteractionDataLabelComponent,
type ModalSubmitInteractionDataStringSelectComponent,
type ModalSubmitInteractionDataTextInputComponent,
} from '@projectdysnomia/dysnomia';
export function isModalLabel(component: ComponentBase): component is ModalSubmitInteractionDataLabelComponent {
return component.type === Constants.ComponentTypes.LABEL;
}
export function isModalTextInput(component: ComponentBase): component is ModalSubmitInteractionDataTextInputComponent {
return component.type === Constants.ComponentTypes.TEXT_INPUT;
}
export function isModalSelect(component: ComponentBase): component is ModalSubmitInteractionDataStringSelectComponent {
return component.type === Constants.ComponentTypes.STRING_SELECT;
}
export function componentHasIdPrefix(component: ComponentBase, prefix: string): boolean {
return (isModalTextInput(component) || isModalSelect(component)) && component.custom_id.startsWith(prefix);
}
import {
Constants,
type ComponentBase,
type ModalSubmitInteractionDataLabelComponent,
type ModalSubmitInteractionDataStringSelectComponent,
type ModalSubmitInteractionDataTextInputComponent,
} from '@projectdysnomia/dysnomia';
export function isModalLabel(component: ComponentBase): component is ModalSubmitInteractionDataLabelComponent {
return component.type === Constants.ComponentTypes.LABEL;
}
export function isModalTextInput(component: ComponentBase): component is ModalSubmitInteractionDataTextInputComponent {
return component.type === Constants.ComponentTypes.TEXT_INPUT;
}
export function isModalSelect(component: ComponentBase): component is ModalSubmitInteractionDataStringSelectComponent {
return component.type === Constants.ComponentTypes.STRING_SELECT;
}
export function componentHasIdPrefix(component: ComponentBase, prefix: string): boolean {
return (isModalTextInput(component) || isModalSelect(component)) && component.custom_id.startsWith(prefix);
}

View File

@@ -1,2 +1,2 @@
export * from './helpers';
export * from './builders';
export * from './helpers';
export * from './builders';

View File

@@ -1,54 +1,54 @@
import { importCommands, initializeCommandHandling, registerCommands } from '@commands';
import { Client } from '@projectdysnomia/dysnomia';
import kv, { asyncKV } from '@star-kitten/util/kv.js';
import type { KVStore } from './kv-store.type.ts';
import type { Cache } from './cache.type.ts';
export interface DiscordBotOptions {
token?: string;
intents?: number[];
commandPattern?: string;
commandBaseDir?: string;
keyStore?: KVStore;
cache?: Cache;
onError?: (error: Error) => void;
onReady?: () => void;
}
export function startDiscordBot({
token = process.env.DISCORD_BOT_TOKEN || '',
intents = [],
commandPattern = '**/*.command.{js,ts}',
commandBaseDir = 'src',
keyStore = asyncKV,
cache = kv,
onError,
onReady,
}: DiscordBotOptions = {}): Client {
const client = new Client(`Bot ${token}`, {
gateway: {
intents,
},
});
client.on('ready', async () => {
console.debug(`Logged in as ${client.user?.username}#${client.user?.discriminator}`);
onReady?.();
const commands = await importCommands(commandPattern, commandBaseDir);
await registerCommands(
client,
Object.values(commands).map((cmd) => cmd.definition),
);
initializeCommandHandling(commands, { client, cache, kv: keyStore });
console.debug('Bot is ready and command handling is initialized.');
});
client.on('error', (error) => {
console.error('An error occurred:', error);
onError?.(error);
});
client.connect().catch(console.error);
return client;
}
import { importCommands, initializeCommandHandling, registerCommands } from '@commands';
import { Client } from '@projectdysnomia/dysnomia';
import kv, { asyncKV } from '@star-kitten/util/kv.js';
import type { KVStore } from './kv-store.type.ts';
import type { Cache } from './cache.type.ts';
export interface DiscordBotOptions {
token?: string;
intents?: number[];
commandPattern?: string;
commandBaseDir?: string;
keyStore?: KVStore;
cache?: Cache;
onError?: (error: Error) => void;
onReady?: () => void;
}
export function startDiscordBot({
token = process.env.DISCORD_BOT_TOKEN || '',
intents = [],
commandPattern = '**/*.command.{js,ts}',
commandBaseDir = 'src',
keyStore = asyncKV,
cache = kv,
onError,
onReady,
}: DiscordBotOptions = {}): Client {
const client = new Client(`Bot ${token}`, {
gateway: {
intents,
},
});
client.on('ready', async () => {
console.debug(`Logged in as ${client.user?.username}#${client.user?.discriminator}`);
onReady?.();
const commands = await importCommands(commandPattern, commandBaseDir);
await registerCommands(
client,
Object.values(commands).map((cmd) => cmd.definition),
);
initializeCommandHandling(commands, { client, cache, kv: keyStore });
console.debug('Bot is ready and command handling is initialized.');
});
client.on('error', (error) => {
console.error('An error occurred:', error);
onError?.(error);
});
client.connect().catch(console.error);
return client;
}

View File

@@ -1,6 +1,6 @@
export interface Cache {
get: <T>(key: string) => T | undefined;
set: <T>(key: string, value: T, ttl?: number | string) => boolean;
del: (key: string | string[]) => number;
has: (key: string) => boolean;
}
export interface Cache {
get: <T>(key: string) => T | undefined;
set: <T>(key: string, value: T, ttl?: number | string) => boolean;
del: (key: string | string[]) => number;
has: (key: string) => boolean;
}

View File

@@ -1,3 +1,3 @@
export * from './bot';
export * from './cache.type';
export * from './kv-store.type.ts';
export * from './bot';
export * from './cache.type';
export * from './kv-store.type.ts';

View File

@@ -1,7 +1,7 @@
export interface KVStore {
get: <T>(key: string) => Promise<T | undefined>;
set: (key: string, value: any) => Promise<boolean>;
delete: (key: string) => Promise<number>;
has: (key: string) => Promise<boolean>;
clear: () => Promise<void>;
}
export interface KVStore {
get: <T>(key: string) => Promise<T | undefined>;
set: (key: string, value: any) => Promise<boolean>;
delete: (key: string) => Promise<number>;
has: (key: string) => Promise<boolean>;
clear: () => Promise<void>;
}

View File

@@ -2,3 +2,5 @@ export * from './locales';
export * from './commands';
export * from './core';
export * from './jsx';
export * from './components';
export * from './pages';

View File

@@ -1,7 +1,7 @@
export function createElement(tag: string, attrs: Record<string, any> = {}, ...children: any[]) {
return {
tag,
attrs,
children,
};
}
export function createElement(tag: string, attrs: Record<string, any> = {}, ...children: any[]) {
return {
tag,
attrs,
children,
};
}

View File

@@ -1,2 +1,2 @@
export * from './parser';
export * from './createElement';
export * from './parser';
export * from './createElement';

View File

@@ -1,10 +1,10 @@
import { describe, it, expect } from 'bun:test';
import { parseJSDFile } from './parser_new';
import path from 'node:path';
describe('parseJSDFile', () => {
it('should parse a JSD file', async () => {
const result = await parseJSDFile(path.join(__dirname, '../../fixtures/jsd/test.tsd'));
expect(result).toEqual(true);
});
});
import { describe, it, expect } from 'bun:test';
import { parseJSDFile } from './parser_new';
import path from 'node:path';
describe('parseJSDFile', () => {
it('should parse a JSD file', async () => {
const result = await parseJSDFile(path.join(__dirname, '../../fixtures/jsd/test.tsd'));
expect(result).toEqual(true);
});
});

View File

@@ -1,97 +1,97 @@
import fs from 'node:fs/promises';
import parse, { type DOMNode } from 'html-dom-parser';
import type { ChildNode } from 'domhandler';
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
export async function parseJSDFile(filename: string) {
const content = (await fs.readFile(filename)).toString();
const matches = JSD_STRING.exec(content);
if (matches) {
let html = matches[1] + '>';
const root = parse(html);
const translated = translate(root[0]);
const str = content.replace(matches[1] + '>', translated);
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
}
return true;
}
interface state {
inInterpolation?: boolean;
children?: string[][];
parent?: Text[];
}
function translate(root: DOMNode | ChildNode | null, state: state = {}): string | null {
if (!root || typeof root !== 'object') return null;
let children = [];
if ('children' in root && Array.isArray(root.children) && root.children.length > 0) {
for (const child of root.children) {
const translated = translate(child, state);
if (translated) {
if (state.inInterpolation && state.parent[state.children.length - 1] === child) {
state.children[state.children.length - 1].push(translated);
} else {
children.push(translated);
}
}
}
}
if ('nodeType' in root && root.nodeType === 3) {
if (root.data.trim() === '') return null;
return parseText(root.data.trim(), state, root);
}
if ('name' in root && root.name) {
let tagName = root.name || 'unknown';
let attrs = 'attribs' in root ? root.attribs : {};
return `StarKitten.createElement("${tagName}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
}
}
const JSD_INTERPOLATION = /\{(.+)\}/gs;
const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs;
const JSD_END_EXP_INTERPOLATION = /\)(.+)\}/gs;
function parseText(text: string, state: state = {}, parent: Text = {}): string {
let interpolations = text.match(JSD_INTERPOLATION);
if (!interpolations) {
if (text.match(JSD_START_EXP_INTERPOLATION)) {
state.inInterpolation = true;
state.children = state.children || [[]];
state.parent = state.parent || [];
state.parent.push(parent);
return text.substring(1, text.length - 1);
} else if (text.match(JSD_END_EXP_INTERPOLATION)) {
const combined = state.children?.[state.children.length - 1].join(' ');
state.children?.[state.children.length - 1].splice(0);
state.children?.pop();
state.parent?.pop();
if (state.children.length === 0) {
state.inInterpolation = false;
return combined + ' ' + text.substring(1, text.length - 1);
}
}
return `"${text}"`;
} else {
text = replaceInterpolations(text);
return `"${text}"`;
}
}
function replaceInterpolations(text: string, isOnJSON: boolean = false) {
let interpolations = null;
while ((interpolations = JSD_INTERPOLATION.exec(text))) {
if (isOnJSON) {
text = text.replace(`"{${interpolations[1]}}"`, interpolations[1]);
} else {
text = text.replace(`{${interpolations[1]}}`, `"+ ${interpolations[1]} +"`);
}
}
return text;
}
import fs from 'node:fs/promises';
import parse, { type DOMNode } from 'html-dom-parser';
import type { ChildNode } from 'domhandler';
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
export async function parseJSDFile(filename: string) {
const content = (await fs.readFile(filename)).toString();
const matches = JSD_STRING.exec(content);
if (matches) {
let html = matches[1] + '>';
const root = parse(html);
const translated = translate(root[0]);
const str = content.replace(matches[1] + '>', translated);
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
}
return true;
}
interface state {
inInterpolation?: boolean;
children?: string[][];
parent?: Text[];
}
function translate(root: DOMNode | ChildNode | null, state: state = {}): string | null {
if (!root || typeof root !== 'object') return null;
let children = [];
if ('children' in root && Array.isArray(root.children) && root.children.length > 0) {
for (const child of root.children) {
const translated = translate(child, state);
if (translated) {
if (state.inInterpolation && state.parent[state.children.length - 1] === child) {
state.children[state.children.length - 1].push(translated);
} else {
children.push(translated);
}
}
}
}
if ('nodeType' in root && root.nodeType === 3) {
if (root.data.trim() === '') return null;
return parseText(root.data.trim(), state, root);
}
if ('name' in root && root.name) {
let tagName = root.name || 'unknown';
let attrs = 'attribs' in root ? root.attribs : {};
return `StarKitten.createElement("${tagName}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
}
}
const JSD_INTERPOLATION = /\{(.+)\}/gs;
const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs;
const JSD_END_EXP_INTERPOLATION = /\)(.+)\}/gs;
function parseText(text: string, state: state = {}, parent: Text = {}): string {
let interpolations = text.match(JSD_INTERPOLATION);
if (!interpolations) {
if (text.match(JSD_START_EXP_INTERPOLATION)) {
state.inInterpolation = true;
state.children = state.children || [[]];
state.parent = state.parent || [];
state.parent.push(parent);
return text.substring(1, text.length - 1);
} else if (text.match(JSD_END_EXP_INTERPOLATION)) {
const combined = state.children?.[state.children.length - 1].join(' ');
state.children?.[state.children.length - 1].splice(0);
state.children?.pop();
state.parent?.pop();
if (state.children.length === 0) {
state.inInterpolation = false;
return combined + ' ' + text.substring(1, text.length - 1);
}
}
return `"${text}"`;
} else {
text = replaceInterpolations(text);
return `"${text}"`;
}
}
function replaceInterpolations(text: string, isOnJSON: boolean = false) {
let interpolations = null;
while ((interpolations = JSD_INTERPOLATION.exec(text))) {
if (isOnJSON) {
text = text.replace(`"{${interpolations[1]}}"`, interpolations[1]);
} else {
text = text.replace(`{${interpolations[1]}}`, `"+ ${interpolations[1]} +"`);
}
}
return text;
}

View File

@@ -1,101 +1,101 @@
import fs from 'node:fs/promises';
import * as acorn from 'acorn';
import jsx from 'acorn-jsx';
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
const parser = acorn.Parser.extend(jsx());
export async function parseJSDFile(filename: string) {
const content = (await fs.readFile(filename)).toString();
const matches = JSD_STRING.exec(content);
if (matches) {
const jsxc = matches[1] + '>';
const ast = parser.parse(jsxc, { ecmaVersion: 2020, sourceType: 'module' });
const translated = traverseJSX((ast.body[0] as any).expression);
const str = content.replace(matches[1] + '>', translated);
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
}
return true;
}
function traverseJSX(node: any): string {
if (node.type === 'JSXElement') {
const tag = node.openingElement.name.name;
const attrs: Record<string, any> = {};
for (const attr of node.openingElement.attributes) {
if (attr.type === 'JSXAttribute') {
const name = attr.name.name;
const value = attr.value;
if (value.type === 'Literal') {
attrs[name] = value.value;
} else if (value.type === 'JSXExpressionContainer') {
attrs[name] = `{${generateCode(value.expression)}}`;
} else if (value) {
attrs[name] = value.raw;
}
}
}
const children = [];
for (const child of node.children) {
const translated = traverseJSX(child);
if (translated) {
children.push(translated);
}
}
return `StarKitten.createElement("${tag}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
} else if (node.type === 'JSXExpressionContainer') {
const expr = generateCode(node.expression);
if (node.expression.type === 'TemplateLiteral' || (node.expression.type === 'Literal' && typeof node.expression.value === 'string')) {
return `""+ ${expr} +""`;
} else {
return expr;
}
} else if (node.type === 'JSXText') {
const text = node.value.trim();
if (text) {
return `"${text}"`;
}
}
return '';
}
function generateCode(node: any): string {
if (node.type === 'JSXElement') {
return traverseJSX(node);
} else if (node.type === 'Identifier') {
return node.name;
} else if (node.type === 'Literal') {
return JSON.stringify(node.value);
} else if (node.type === 'TemplateLiteral') {
const quasis = node.quasis.map((q: any) => q.value.raw);
const expressions = node.expressions.map((e: any) => generateCode(e));
let result = quasis[0];
for (let i = 0; i < expressions.length; i++) {
result += '${' + expressions[i] + '}' + quasis[i + 1];
}
return '`' + result + '`';
} else if (node.type === 'MemberExpression') {
const op = node.optional ? '?.' : '.';
return generateCode(node.object) + op + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
} else if (node.type === 'OptionalMemberExpression') {
return generateCode(node.object) + '?.' + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
} else if (node.type === 'CallExpression') {
return generateCode(node.callee) + '(' + node.arguments.map((a: any) => generateCode(a)).join(', ') + ')';
} else if (node.type === 'BinaryExpression') {
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
} else if (node.type === 'ConditionalExpression') {
return generateCode(node.test) + ' ? ' + generateCode(node.consequent) + ' : ' + generateCode(node.alternate);
} else if (node.type === 'LogicalExpression') {
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
} else if (node.type === 'UnaryExpression') {
return node.operator + generateCode(node.argument);
} else if (node.type === 'ObjectExpression') {
return '{' + node.properties.map((p: any) => generateCode(p.key) + ': ' + generateCode(p.value)).join(', ') + '}';
} else if (node.type === 'ArrayExpression') {
return '[' + node.elements.map((e: any) => generateCode(e)).join(', ') + ']';
} else {
return node.raw || node.name || 'unknown';
}
}
import fs from 'node:fs/promises';
import * as acorn from 'acorn';
import jsx from 'acorn-jsx';
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
const parser = acorn.Parser.extend(jsx());
export async function parseJSDFile(filename: string) {
const content = (await fs.readFile(filename)).toString();
const matches = JSD_STRING.exec(content);
if (matches) {
const jsxc = matches[1] + '>';
const ast = parser.parse(jsxc, { ecmaVersion: 2020, sourceType: 'module' });
const translated = traverseJSX((ast.body[0] as any).expression);
const str = content.replace(matches[1] + '>', translated);
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
}
return true;
}
function traverseJSX(node: any): string {
if (node.type === 'JSXElement') {
const tag = node.openingElement.name.name;
const attrs: Record<string, any> = {};
for (const attr of node.openingElement.attributes) {
if (attr.type === 'JSXAttribute') {
const name = attr.name.name;
const value = attr.value;
if (value.type === 'Literal') {
attrs[name] = value.value;
} else if (value.type === 'JSXExpressionContainer') {
attrs[name] = `{${generateCode(value.expression)}}`;
} else if (value) {
attrs[name] = value.raw;
}
}
}
const children = [];
for (const child of node.children) {
const translated = traverseJSX(child);
if (translated) {
children.push(translated);
}
}
return `StarKitten.createElement("${tag}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
} else if (node.type === 'JSXExpressionContainer') {
const expr = generateCode(node.expression);
if (node.expression.type === 'TemplateLiteral' || (node.expression.type === 'Literal' && typeof node.expression.value === 'string')) {
return `""+ ${expr} +""`;
} else {
return expr;
}
} else if (node.type === 'JSXText') {
const text = node.value.trim();
if (text) {
return `"${text}"`;
}
}
return '';
}
function generateCode(node: any): string {
if (node.type === 'JSXElement') {
return traverseJSX(node);
} else if (node.type === 'Identifier') {
return node.name;
} else if (node.type === 'Literal') {
return JSON.stringify(node.value);
} else if (node.type === 'TemplateLiteral') {
const quasis = node.quasis.map((q: any) => q.value.raw);
const expressions = node.expressions.map((e: any) => generateCode(e));
let result = quasis[0];
for (let i = 0; i < expressions.length; i++) {
result += '${' + expressions[i] + '}' + quasis[i + 1];
}
return '`' + result + '`';
} else if (node.type === 'MemberExpression') {
const op = node.optional ? '?.' : '.';
return generateCode(node.object) + op + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
} else if (node.type === 'OptionalMemberExpression') {
return generateCode(node.object) + '?.' + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
} else if (node.type === 'CallExpression') {
return generateCode(node.callee) + '(' + node.arguments.map((a: any) => generateCode(a)).join(', ') + ')';
} else if (node.type === 'BinaryExpression') {
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
} else if (node.type === 'ConditionalExpression') {
return generateCode(node.test) + ' ? ' + generateCode(node.consequent) + ' : ' + generateCode(node.alternate);
} else if (node.type === 'LogicalExpression') {
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
} else if (node.type === 'UnaryExpression') {
return node.operator + generateCode(node.argument);
} else if (node.type === 'ObjectExpression') {
return '{' + node.properties.map((p: any) => generateCode(p.key) + ': ' + generateCode(p.value)).join(', ') + '}';
} else if (node.type === 'ArrayExpression') {
return '[' + node.elements.map((e: any) => generateCode(e)).join(', ') + ']';
} else {
return node.raw || node.name || 'unknown';
}
}

View File

@@ -1,5 +1,5 @@
import { createActionRow } from '@components';
export function ActionRow(props: { children: any | any[] }) {
return createActionRow(...(Array.isArray(props.children) ? props.children : [props.children]));
}
import { createActionRow } from '@components';
export function ActionRow(props: { children: any | any[] }) {
return createActionRow(...(Array.isArray(props.children) ? props.children : [props.children]));
}

View File

@@ -1,6 +1,6 @@
import { createButton, type ButtonStyle } from '@components';
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
export function Button(props: { label: string; customId: string; style?: ButtonStyle; emoji?: PartialEmoji; disabled?: boolean }) {
return createButton(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled });
}
import { createButton, type ButtonStyle } from '@components';
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
export function Button(props: { label: string; customId: string; style: ButtonStyle; emoji?: PartialEmoji; disabled?: boolean }) {
return createButton(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled });
}

View File

@@ -1,8 +1,8 @@
import { createContainer } from '@components';
export function Container(props: { accent?: number; spoiler?: boolean; children: any | any[] }) {
return createContainer(
{ accent_color: props.accent, spoiler: props.spoiler },
...(Array.isArray(props.children) ? props.children : [props.children]),
);
}
import { createContainer } from '@components';
export function Container(props: { accent?: number; spoiler?: boolean; children: any | any[] }) {
return createContainer(
{ accent_color: props.accent, spoiler: props.spoiler },
...(Array.isArray(props.children) ? props.children : [props.children]),
);
}

View File

@@ -1,4 +1,4 @@
export * from './action-row';
export * from './button';
export * from './container';
export * from './text-display';
export * from './action-row';
export * from './button';
export * from './container';
export * from './text-display';

View File

@@ -1,5 +1,5 @@
import { createTextDisplay } from '@components/builders';
export function TextDisplay(props: { content: string }) {
return createTextDisplay(props.content);
}
import { createTextDisplay } from '@components/builders';
export function TextDisplay(props: { content: string }) {
return createTextDisplay(props.content);
}

View File

@@ -1,3 +1,3 @@
export * from './runtime';
export * from './components';
export * as JSX from './jsx';
export * from './components';
export * from './jsx';
export * from './runtime';

View File

@@ -0,0 +1 @@
export { jsxDEV } from './runtime';

View File

@@ -0,0 +1 @@
export { jsx } from './runtime';

View File

@@ -1,69 +1,69 @@
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
export type Component =
| ActionRow
| Button
| StringSelectMenu
| UserSelectMenu
| RoleSelectMenu
| MentionableSelectMenu
| ChannelSelectMenu
| TextInput
| LabelComponent
| ContainerComponent
| TextDisplayComponent
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent
| InteractionButton
| URLButton
| PremiumButton
| ThumbnailComponent;
export type Element = Component | Promise<Component>;
export interface ElementClass {
render: any;
}
export interface ElementAttributesProperty {
props: {};
}
export interface IntrinsicElements {
// Allow any element, but prefer known elements
[elemName: string]: any;
// Known elements
ActionRow: { children: any | any[] };
Button: {
label: string;
customId: string;
style?: number;
emoji?: PartialEmoji;
disabled?: boolean;
};
Container: { color?: string; accent?: number; spoiler?: boolean; children: any | any[] };
TextDisplay: { content: string };
}
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
export type Component =
| ActionRow
| Button
| StringSelectMenu
| UserSelectMenu
| RoleSelectMenu
| MentionableSelectMenu
| ChannelSelectMenu
| TextInput
| LabelComponent
| ContainerComponent
| TextDisplayComponent
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent
| InteractionButton
| URLButton
| PremiumButton
| ThumbnailComponent;
export type Element = Component | Promise<Component>;
export interface ElementClass {
render: any;
}
export interface ElementAttributesProperty {
props: {};
}
export interface IntrinsicElements {
// Allow any element, but prefer known elements
// [elemName: string]: any;
// Known elements (forcing re-parse)
actionRow: { children: any | any[] };
button: {
label: string;
customId: string;
style: number;
emoji?: PartialEmoji;
disabled?: boolean;
};
container: { color?: string; accent?: number; spoiler?: boolean; children: any | any[] };
textDisplay: { content: string };
}

View File

@@ -1,30 +1,52 @@
export function jsx(type: any, props: Record<string, any>) {
console.log('JSX', type, props);
if (typeof type === 'function') {
return type(props);
}
return {
type,
props,
};
}
export function jsxDEV(
type: any,
props: Record<string, any>,
key: string | number | symbol,
isStaticChildren: boolean,
source: any,
self: any,
) {
console.log('JSX DEV', type, props);
if (typeof type === 'function') {
return type(props);
}
return {
type,
props: { ...props, key },
_source: source,
_self: self,
};
}
import { ActionRow } from './components/action-row';
import { Button } from './components/button';
import { Container } from './components/container';
import { TextDisplay } from './components/text-display';
const intrinsicComponentMap: Record<string, (props: any) => any> = {
actionRow: ActionRow,
button: Button,
container: Container,
textDisplay: TextDisplay,
};
export function jsx(type: any, props: Record<string, any>) {
console.log('JSX', type, props);
if (typeof type === 'function') {
return type(props);
}
if (typeof type === 'string' && intrinsicComponentMap[type]) {
return intrinsicComponentMap[type](props);
}
return {
type,
props,
};
}
export function jsxDEV(
type: any,
props: Record<string, any>,
key: string | number | symbol,
isStaticChildren: boolean,
source: any,
self: any,
) {
console.log('JSX DEV', type, props);
if (typeof type === 'function') {
return type(props);
}
if (typeof type === 'string' && intrinsicComponentMap[type]) {
return intrinsicComponentMap[type](props);
}
return {
type,
props: { ...props, key },
_source: source,
_self: self,
};
}

View File

@@ -1,8 +1,8 @@
import type { Component, IntrinsicElements as StarKittenIntrinsicElements } from './jsx';
declare global {
namespace JSX {
type Element = Component;
interface IntrinsicElements extends StarKittenIntrinsicElements {}
}
}
import type { Component, IntrinsicElements as StarKittenIntrinsicElements } from './jsx';
declare global {
namespace JSX {
type Element = Component;
interface IntrinsicElements extends StarKittenIntrinsicElements {}
}
}

View File

@@ -1,26 +1,26 @@
export type Locales = 'en' | 'ru' | 'de' | 'fr' | 'ja' | 'es' | 'zh' | 'ko';
export const ALL_LOCALES: Locales[] = ['en', 'ru', 'de', 'fr', 'ja', 'es', 'zh', 'ko'];
export const DEFAULT_LOCALE: Locales = 'en';
export const LOCALE_NAMES: { [key in Locales]: string } = {
en: 'English',
ru: 'Русский',
de: 'Deutsch',
fr: 'Français',
ja: '日本語',
es: 'Español',
zh: '中文',
ko: '한국어',
};
export function toDiscordLocale(locale: Locales): string {
switch (locale) {
case 'en': return 'en-US';
case 'ru': return 'ru';
case 'de': return 'de';
case 'fr': return 'fr';
case 'ja': return 'ja';
case 'es': return 'es-ES';
case 'zh': return 'zh-CN';
case 'ko': return 'ko';
default: return 'en-US';
}
export type Locales = 'en' | 'ru' | 'de' | 'fr' | 'ja' | 'es' | 'zh' | 'ko';
export const ALL_LOCALES: Locales[] = ['en', 'ru', 'de', 'fr', 'ja', 'es', 'zh', 'ko'];
export const DEFAULT_LOCALE: Locales = 'en';
export const LOCALE_NAMES: { [key in Locales]: string } = {
en: 'English',
ru: 'Русский',
de: 'Deutsch',
fr: 'Français',
ja: '日本語',
es: 'Español',
zh: '中文',
ko: '한국어',
};
export function toDiscordLocale(locale: Locales): string {
switch (locale) {
case 'en': return 'en-US';
case 'ru': return 'ru';
case 'de': return 'de';
case 'fr': return 'fr';
case 'ja': return 'ja';
case 'es': return 'es-ES';
case 'zh': return 'zh-CN';
case 'ko': return 'ko';
default: return 'en-US';
}
}

View File

@@ -1,2 +1,2 @@
export * from './pages';
export * from './subroutes';
export * from './pages';
export * from './subroutes';

View File

@@ -1,166 +1,166 @@
import { isAutocomplete, isMessageComponent, isModalSubmit, isPing, type CommandContext } from '@commands';
import {
Constants,
type InteractionContentEdit,
type InteractionModalContent,
type CommandInteraction,
type ComponentInteraction,
type ModalSubmitInteraction,
Interaction,
} from '@projectdysnomia/dysnomia';
export type PagesInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction;
export enum PageType {
MODAL = 'modal',
MESSAGE = 'message',
FOLLOWUP = 'followup',
}
export interface Page<T> {
key: string;
type?: PageType; // defaults to MESSAGE
followUpFlags?: number;
render: (
ctx: PageContext<T>,
) => (InteractionModalContent | InteractionContentEdit) | Promise<InteractionModalContent | InteractionContentEdit>;
}
export interface PagesOptions<T> {
pages: Record<string, Page<T>>;
initialPage?: string;
timeout?: number; // in seconds
ephemeral?: boolean; // whether the initial message should be ephemeral
useEmbeds?: boolean; // will not enable components v2
initialStateData?: T; // initial state to merge with default state
router?: (ctx: PageContext<T>) => string; // function to determine the next page key
}
export interface PageState<T> {
currentPage: string;
timeoutAt: number; // timestamp in ms
lastInteractionAt?: number; // timestamp in ms
messageId?: string;
channelId?: string;
data: T;
}
export interface PageContext<T> {
state: PageState<T>;
custom_id: string; // current interaction custom_id
interaction: PagesInteraction;
goToPage: (pageKey: string) => Promise<InteractionContentEdit>;
}
function createPageContext<T>(interaction: PagesInteraction, options: PagesOptions<T>, state: PageState<T>): PageContext<T> {
return {
state,
interaction,
custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : (options.initialPage ?? 'root'),
goToPage: (pageKey: string) => {
const page = options.pages[pageKey];
this.state.currentPage = pageKey;
if (!page) {
throw new Error(`Page with key "${pageKey}" not found`);
}
return page.render(createPageContext(interaction, options, { ...state, currentPage: pageKey })) as Promise<InteractionContentEdit>;
},
};
}
function defaultPageState<T>(options: PagesOptions<T>): PageState<T> {
const timeoutAt = options.timeout ? Date.now() + options.timeout * 1000 : Infinity;
return {
currentPage: options.initialPage ?? options.pages[0].key,
timeoutAt,
lastInteractionAt: Date.now(),
data: options.initialStateData ?? ({} as T),
};
}
function getPageState<T>(options: PagesOptions<T>, cmdCtx: CommandContext & { state: { __pageState?: PageState<T> } }) {
const cmdState = cmdCtx.state;
if ('__pageState' in cmdState && cmdState.__pageState) {
return cmdState.__pageState as PageState<T>;
}
cmdState.__pageState = defaultPageState(options);
return cmdState.__pageState as PageState<T>;
}
function validateOptions<T>(options: PagesOptions<T>) {
const keys = Object.keys(options.pages);
const uniqueKeys = new Set(keys);
if (uniqueKeys.size !== keys.length) {
throw new Error('Duplicate page keys found');
}
}
function getFlags(options: PagesOptions<any>) {
let flags = 0;
if (options.ephemeral) {
flags |= Constants.MessageFlags.EPHEMERAL;
}
if (!options.useEmbeds) {
flags |= Constants.MessageFlags.IS_COMPONENTS_V2;
}
return flags;
}
export async function usePages<T>(options: PagesOptions<T>, interaction: Interaction, cmdCtx: CommandContext) {
if (isAutocomplete(interaction) || isPing(interaction)) {
throw new Error('usePages cannot be used with autocomplete or ping interactions');
}
const pagesInteraction = interaction as PagesInteraction;
validateOptions(options);
const pageState = getPageState(options, cmdCtx);
const pageContext = createPageContext(pagesInteraction, options, pageState);
const pageKey =
options.router ? options.router(pageContext) : (pageContext.custom_id ?? options.initialPage ?? Object.keys(options.pages)[0]);
// if we have subroutes, we only want the main route from the page key
const page = options.pages[pageKey.split(':')[0]] ?? options.pages[0];
pageContext.state.currentPage = page.key;
if (page.type === PageType.MODAL && !isModalSubmit(pagesInteraction)) {
// we don't defer modals and can't respond to a modal with a modal.
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
return await pagesInteraction.createModal(content as InteractionModalContent);
}
if (page.type === PageType.FOLLOWUP) {
if (!pageState.messageId) {
throw new Error('Cannot send a followup message before an initial message has been sent');
}
const flags = page.type === PageType.FOLLOWUP ? (page.followUpFlags ?? getFlags(options)) : getFlags(options);
await pagesInteraction.defer(flags);
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
return await pagesInteraction.createFollowup({
flags,
...(content as InteractionContentEdit),
});
}
if (pageState.messageId && isMessageComponent(pagesInteraction)) {
await pagesInteraction.deferUpdate();
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
return await pagesInteraction.editMessage(pageState.messageId, content as InteractionContentEdit);
}
{
await pagesInteraction.defer(getFlags(options));
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
const message = await pagesInteraction.createFollowup({
flags: getFlags(options),
...(content as InteractionContentEdit),
});
pageState.messageId = message.id;
pageState.channelId = message.channel?.id;
return message;
}
}
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
return typeof (value as Promise<T>)?.then === 'function';
}
import { isAutocomplete, isMessageComponent, isModalSubmit, isPing, type CommandContext } from '@commands';
import {
Constants,
type InteractionContentEdit,
type InteractionModalContent,
type CommandInteraction,
type ComponentInteraction,
type ModalSubmitInteraction,
Interaction,
} from '@projectdysnomia/dysnomia';
export type PagesInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction;
export enum PageType {
MODAL = 'modal',
MESSAGE = 'message',
FOLLOWUP = 'followup',
}
export interface Page<T> {
key: string;
type?: PageType; // defaults to MESSAGE
followUpFlags?: number;
render: (
ctx: PageContext<T>,
) => (InteractionModalContent | InteractionContentEdit) | Promise<InteractionModalContent | InteractionContentEdit>;
}
export interface PagesOptions<T> {
pages: Record<string, Page<T>>;
initialPage?: string;
timeout?: number; // in seconds
ephemeral?: boolean; // whether the initial message should be ephemeral
useEmbeds?: boolean; // will not enable components v2
initialStateData?: T; // initial state to merge with default state
router?: (ctx: PageContext<T>) => string; // function to determine the next page key
}
export interface PageState<T> {
currentPage: string;
timeoutAt: number; // timestamp in ms
lastInteractionAt?: number; // timestamp in ms
messageId?: string;
channelId?: string;
data: T;
}
export interface PageContext<T> {
state: PageState<T>;
custom_id: string; // current interaction custom_id
interaction: PagesInteraction;
goToPage: (pageKey: string) => Promise<InteractionContentEdit>;
}
function createPageContext<T>(interaction: PagesInteraction, options: PagesOptions<T>, state: PageState<T>): PageContext<T> {
return {
state,
interaction,
custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : (options.initialPage ?? 'root'),
goToPage: (pageKey: string) => {
const page = options.pages[pageKey];
this.state.currentPage = pageKey;
if (!page) {
throw new Error(`Page with key "${pageKey}" not found`);
}
return page.render(createPageContext(interaction, options, { ...state, currentPage: pageKey })) as Promise<InteractionContentEdit>;
},
};
}
function defaultPageState<T>(options: PagesOptions<T>): PageState<T> {
const timeoutAt = options.timeout ? Date.now() + options.timeout * 1000 : Infinity;
return {
currentPage: options.initialPage ?? options.pages[0].key,
timeoutAt,
lastInteractionAt: Date.now(),
data: options.initialStateData ?? ({} as T),
};
}
function getPageState<T>(options: PagesOptions<T>, cmdCtx: CommandContext & { state: { __pageState?: PageState<T> } }) {
const cmdState = cmdCtx.state;
if ('__pageState' in cmdState && cmdState.__pageState) {
return cmdState.__pageState as PageState<T>;
}
cmdState.__pageState = defaultPageState(options);
return cmdState.__pageState as PageState<T>;
}
function validateOptions<T>(options: PagesOptions<T>) {
const keys = Object.keys(options.pages);
const uniqueKeys = new Set(keys);
if (uniqueKeys.size !== keys.length) {
throw new Error('Duplicate page keys found');
}
}
function getFlags(options: PagesOptions<any>) {
let flags = 0;
if (options.ephemeral) {
flags |= Constants.MessageFlags.EPHEMERAL;
}
if (!options.useEmbeds) {
flags |= Constants.MessageFlags.IS_COMPONENTS_V2;
}
return flags;
}
export async function usePages<T>(options: PagesOptions<T>, interaction: Interaction, cmdCtx: CommandContext) {
if (isAutocomplete(interaction) || isPing(interaction)) {
throw new Error('usePages cannot be used with autocomplete or ping interactions');
}
const pagesInteraction = interaction as PagesInteraction;
validateOptions(options);
const pageState = getPageState(options, cmdCtx);
const pageContext = createPageContext(pagesInteraction, options, pageState);
const pageKey =
options.router ? options.router(pageContext) : (pageContext.custom_id ?? options.initialPage ?? Object.keys(options.pages)[0]);
// if we have subroutes, we only want the main route from the page key
const page = options.pages[pageKey.split(':')[0]] ?? options.pages[0];
pageContext.state.currentPage = page.key;
if (page.type === PageType.MODAL && !isModalSubmit(pagesInteraction)) {
// we don't defer modals and can't respond to a modal with a modal.
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
return await pagesInteraction.createModal(content as InteractionModalContent);
}
if (page.type === PageType.FOLLOWUP) {
if (!pageState.messageId) {
throw new Error('Cannot send a followup message before an initial message has been sent');
}
const flags = page.type === PageType.FOLLOWUP ? (page.followUpFlags ?? getFlags(options)) : getFlags(options);
await pagesInteraction.defer(flags);
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
return await pagesInteraction.createFollowup({
flags,
...(content as InteractionContentEdit),
});
}
if (pageState.messageId && isMessageComponent(pagesInteraction)) {
await pagesInteraction.deferUpdate();
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
return await pagesInteraction.editMessage(pageState.messageId, content as InteractionContentEdit);
}
{
await pagesInteraction.defer(getFlags(options));
const cnt = page.render(pageContext);
const content = isPromise(cnt) ? await cnt : cnt;
const message = await pagesInteraction.createFollowup({
flags: getFlags(options),
...(content as InteractionContentEdit),
});
pageState.messageId = message.id;
pageState.channelId = message.channel?.id;
return message;
}
}
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
return typeof (value as Promise<T>)?.then === 'function';
}

View File

@@ -1,99 +1,99 @@
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
import { createActionRow, createButton, createMediaGallery, type ButtonOptions, type ContainerItems } from '@components';
import type { PageContext } from './pages';
export function getSubrouteKey(prefix: string, subroutes: string[]) {
return `${prefix}:${subroutes.join(':')}`;
}
export function parseSubrouteKey(key: string, expectedPrefix: string, expectedLength: number, defaults: string[] = []) {
const parts = key.split(':');
if (parts[0] !== expectedPrefix) {
throw new Error(`Unexpected prefix: ${parts[0]}`);
}
if (parts.length - 1 < expectedLength && defaults.length) {
// fill in defaults
parts.push(...defaults.slice(parts.length - 1));
}
if (parts.length !== expectedLength + 1) {
throw new Error(`Expected ${expectedLength} subroutes, but got ${parts.length - 1}`);
}
return parts.slice(1);
}
export function renderSubrouteButtons(
currentSubroute: string,
subRoutes: string[],
subrouteIndex: number,
prefix: string,
subroutes: { label: string; value: string; emoji?: PartialEmoji }[],
options?: Partial<ButtonOptions>,
) {
return subroutes
.filter((sr) => sr !== undefined)
.map(({ label, value, emoji }) => {
const routes = [...subRoutes];
routes[subrouteIndex] = currentSubroute == value ? '_' : value;
return createButton(label, getSubrouteKey(prefix, routes), {
...options,
disabled: value === currentSubroute,
emoji,
});
});
}
export interface SubrouteOptions {
label: string;
value: string;
emoji?: PartialEmoji;
}
export function renderSubroutes<T, CType = ContainerItems>(
context: PageContext<T>,
prefix: string,
subroutes: (SubrouteOptions & {
banner?: string;
actionRowPosition?: 'top' | 'bottom';
})[][],
render: (currentSubroute: string, ctx: PageContext<T>) => CType,
btnOptions?: Partial<ButtonOptions>,
defaultSubroutes?: string[], // if not provided, will use the first option of each subroute
): CType[] {
const currentSubroutes = parseSubrouteKey(
context.custom_id,
prefix,
subroutes.length,
defaultSubroutes || subroutes.map((s) => s[0].value),
);
const components = subroutes
.filter((sr) => sr.length > 0)
.map((srOpts, index) => {
const opts = srOpts.filter((sr) => sr !== undefined);
if (opts.length === 0) return undefined;
// find the current subroute, or default to the first
const sri = opts.findIndex((s) => s.value === currentSubroutes[index]);
const current = opts[sri] || opts[0];
const components = [];
const actionRow = createActionRow(...renderSubrouteButtons(current.value, currentSubroutes, index, prefix, opts, btnOptions));
if (current.banner) {
components.push(createMediaGallery({ url: current.banner }));
}
if (!current.actionRowPosition || current.actionRowPosition === 'top') {
components.push(actionRow);
}
components.push(render(current.value, context));
if (current.actionRowPosition === 'bottom') {
components.push(actionRow);
}
return components;
})
.flat()
.filter((c) => c !== undefined);
return components;
}
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
import { createActionRow, createButton, createMediaGallery, type ButtonOptions, type ContainerItems } from '@components';
import type { PageContext } from './pages';
export function getSubrouteKey(prefix: string, subroutes: string[]) {
return `${prefix}:${subroutes.join(':')}`;
}
export function parseSubrouteKey(key: string, expectedPrefix: string, expectedLength: number, defaults: string[] = []) {
const parts = key.split(':');
if (parts[0] !== expectedPrefix) {
throw new Error(`Unexpected prefix: ${parts[0]}`);
}
if (parts.length - 1 < expectedLength && defaults.length) {
// fill in defaults
parts.push(...defaults.slice(parts.length - 1));
}
if (parts.length !== expectedLength + 1) {
throw new Error(`Expected ${expectedLength} subroutes, but got ${parts.length - 1}`);
}
return parts.slice(1);
}
export function renderSubrouteButtons(
currentSubroute: string,
subRoutes: string[],
subrouteIndex: number,
prefix: string,
subroutes: { label: string; value: string; emoji?: PartialEmoji }[],
options?: Partial<ButtonOptions>,
) {
return subroutes
.filter((sr) => sr !== undefined)
.map(({ label, value, emoji }) => {
const routes = [...subRoutes];
routes[subrouteIndex] = currentSubroute == value ? '_' : value;
return createButton(label, getSubrouteKey(prefix, routes), {
...options,
disabled: value === currentSubroute,
emoji,
});
});
}
export interface SubrouteOptions {
label: string;
value: string;
emoji?: PartialEmoji;
}
export function renderSubroutes<T, CType = ContainerItems>(
context: PageContext<T>,
prefix: string,
subroutes: (SubrouteOptions & {
banner?: string;
actionRowPosition?: 'top' | 'bottom';
})[][],
render: (currentSubroute: string, ctx: PageContext<T>) => CType,
btnOptions?: Partial<ButtonOptions>,
defaultSubroutes?: string[], // if not provided, will use the first option of each subroute
): CType[] {
const currentSubroutes = parseSubrouteKey(
context.custom_id,
prefix,
subroutes.length,
defaultSubroutes || subroutes.map((s) => s[0].value),
);
const components = subroutes
.filter((sr) => sr.length > 0)
.map((srOpts, index) => {
const opts = srOpts.filter((sr) => sr !== undefined);
if (opts.length === 0) return undefined;
// find the current subroute, or default to the first
const sri = opts.findIndex((s) => s.value === currentSubroutes[index]);
const current = opts[sri] || opts[0];
const components = [];
const actionRow = createActionRow(...renderSubrouteButtons(current.value, currentSubroutes, index, prefix, opts, btnOptions));
if (current.banner) {
components.push(createMediaGallery({ url: current.banner }));
}
if (!current.actionRowPosition || current.actionRowPosition === 'top') {
components.push(actionRow);
}
components.push(render(current.value, context));
if (current.actionRowPosition === 'bottom') {
components.push(actionRow);
}
return components;
})
.flat()
.filter((c) => c !== undefined);
return components;
}

View File

@@ -1,40 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
// Enable latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"composite": true,
"jsx": "react-jsx",
"jsxImportSource": "@star-kitten/discord",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": false,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
// Paths
"paths": {
"@*": ["./src/*"],
"@types": ["./types/*"]
},
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"typeRoots": ["src/types", "./node_modules/@types"]
"typeRoots": ["src/types", "./node_modules/@types"],
"types": []
},
"include": ["src", "types", "src/jsx/types.d.ts"],
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"]

View File

@@ -9,6 +9,8 @@ export default defineConfig([
'./src/pages/index.ts',
'./src/common/index.ts',
'./src/jsx/index.ts',
'./src/jsx/jsx-runtime.ts',
'./src/jsx/jsx-dev-runtime.ts',
],
platform: 'node',
dts: true,

View File

@@ -1,65 +1,65 @@
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type GuildChannelTypes,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
declare namespace JSX {
type Component =
| Button
| StringSelectMenu
| UserSelectMenu
| RoleSelectMenu
| MentionableSelectMenu
| ChannelSelectMenu
| TextInput
| LabelComponent
| ContainerComponent
| TextDisplayComponent
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent
| InteractionButton
| URLButton
| PremiumButton
| ThumbnailComponent;
type Element = Component | Promise<Component>;
interface ElementClass {
render: any;
}
interface ElementAttributesProperty {
props: {};
}
interface IntrinsicElements {
// Allow any element, but prefer known elements
[elemName: string]: any;
// Known elements
ActionRow: { children: any | any[] };
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
TextDisplay: { content: string };
}
}
import {
type ActionRow,
type Button,
type ChannelSelectMenu,
type GuildChannelTypes,
type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu,
type StringSelectMenu,
type TextInput,
type UserSelectMenu,
type LabelComponent,
type ContainerComponent,
type TextDisplayComponent,
type SectionComponent,
type MediaGalleryComponent,
type SeparatorComponent,
type FileComponent,
type InteractionButton,
type URLButton,
type PremiumButton,
type ThumbnailComponent,
} from '@projectdysnomia/dysnomia';
declare namespace JSX {
type Component =
| Button
| StringSelectMenu
| UserSelectMenu
| RoleSelectMenu
| MentionableSelectMenu
| ChannelSelectMenu
| TextInput
| LabelComponent
| ContainerComponent
| TextDisplayComponent
| SectionComponent
| MediaGalleryComponent
| SeparatorComponent
| FileComponent
| InteractionButton
| URLButton
| PremiumButton
| ThumbnailComponent;
type Element = Component | Promise<Component>;
interface ElementClass {
render: any;
}
interface ElementAttributesProperty {
props: {};
}
interface IntrinsicElements {
// Allow any element, but prefer known elements
[elemName: string]: any;
// Known elements
ActionRow: { children: any | any[] };
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
TextDisplay: { content: string };
}
}

View File

@@ -16,7 +16,8 @@
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
"@star-kitten/discord": "workspace:^0.0.0",
"@star-kitten/eve": "workspace:^0.0.0",
"@star-kitten/util": "workspace:^0.0.0"
"@star-kitten/util": "workspace:^0.0.0",
"mkdirp": "^3.0.1"
},
"scripts": {
"dev": "bunx dotenvx run -f .env.development -- bun run --watch src/main.ts",

View File

@@ -1,90 +1,90 @@
import { Constants, type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import { appraiseItems, type Appraisal } from '@star-kitten/eve/third-party/janice.js';
import { isModalSubmit } from '@star-kitten/discord/commands';
import { componentHasIdPrefix, isModalLabel, isModalSelect, isModalTextInput } from '@star-kitten/discord/components';
import type { CommandContext, ExecutableInteraction } from '@star-kitten/discord/commands';
import { PageType, usePages } from '@star-kitten/discord/pages';
import { renderAppraisal } from './renderAppraisal';
import { renderAppraisalModal } from './renderAppraisalModal';
const definition: ChatInputApplicationCommandStructure = {
type: Constants.ApplicationCommandTypes.CHAT_INPUT,
name: 'appraise',
nameLocalizations: {
de: 'bewerten',
'es-ES': 'tasar',
fr: 'estimer',
ja: '査定',
ko: '감정',
ru: 'оценить',
'zh-CN': '评估',
},
description: 'Evaluate the worth of your space junk',
descriptionLocalizations: {
de: 'Bewerten Sie den Wert Ihres Weltraumschrotts',
'es-ES': 'Evalúa el valor de tu chatarra espacial',
fr: 'Évaluez la valeur de vos déchets spatiaux',
ja: 'あなたの宇宙のガラクタの価値を評価します',
ko: '우주 쓰레기의 가치를 평가하십시오',
ru: 'Оцените стоимость вашего космического мусора',
'zh-CN': '评估您宇宙垃圾的价值',
},
};
export interface AppraisalState {
appraisal?: Appraisal;
}
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
return await usePages<AppraisalState>(
{
pages: {
appraiseModal: {
key: 'appraiseModal',
type: PageType.MODAL,
render: async () => renderAppraisalModal(interaction),
},
appraisalResult: {
key: 'appraisalResult',
render: async (pageCtx) => {
if (!isModalSubmit(interaction)) {
throw new Error('Expected a modal submit interaction for appraisalResult page');
}
let marketId = 2; // Default to Jita
let items = '';
interaction.data.components.forEach((comp) => {
if (isModalLabel(comp)) {
if (isModalSelect(comp.component) && componentHasIdPrefix(comp.component, `market`)) {
marketId = Number.parseInt(comp.component.values[0]) || marketId;
} else if (isModalTextInput(comp.component) && componentHasIdPrefix(comp.component, `input`)) {
items = comp.component.value || items;
}
}
});
const appraisal = await appraiseItems(items, marketId);
pageCtx.state.data.appraisal = appraisal;
return renderAppraisal(appraisal, pageCtx, interaction);
},
},
share: {
key: 'share',
type: PageType.FOLLOWUP,
followUpFlags: Constants.MessageFlags.IS_COMPONENTS_V2,
render: async (pageCtx) => renderAppraisal(pageCtx.state.data.appraisal!, pageCtx, interaction),
},
},
initialPage: 'appraiseModal',
timeout: 300, // 5 minutes
ephemeral: true,
},
interaction,
ctx,
);
}
export default {
definition,
execute,
};
import { Constants, type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import { appraiseItems, type Appraisal } from '@star-kitten/eve/third-party/janice.js';
import { isModalSubmit } from '@star-kitten/discord/commands';
import { componentHasIdPrefix, isModalLabel, isModalSelect, isModalTextInput } from '@star-kitten/discord/components';
import type { CommandContext, ExecutableInteraction } from '@star-kitten/discord/commands';
import { PageType, usePages } from '@star-kitten/discord/pages';
import { renderAppraisal } from './renderAppraisal';
import { renderAppraisalModal } from './renderAppraisalModal';
const definition: ChatInputApplicationCommandStructure = {
type: Constants.ApplicationCommandTypes.CHAT_INPUT,
name: 'appraise',
nameLocalizations: {
de: 'bewerten',
'es-ES': 'tasar',
fr: 'estimer',
ja: '査定',
ko: '감정',
ru: 'оценить',
'zh-CN': '评估',
},
description: 'Evaluate the worth of your space junk',
descriptionLocalizations: {
de: 'Bewerten Sie den Wert Ihres Weltraumschrotts',
'es-ES': 'Evalúa el valor de tu chatarra espacial',
fr: 'Évaluez la valeur de vos déchets spatiaux',
ja: 'あなたの宇宙のガラクタの価値を評価します',
ko: '우주 쓰레기의 가치를 평가하십시오',
ru: 'Оцените стоимость вашего космического мусора',
'zh-CN': '评估您宇宙垃圾的价值',
},
};
export interface AppraisalState {
appraisal?: Appraisal;
}
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
return await usePages<AppraisalState>(
{
pages: {
appraiseModal: {
key: 'appraiseModal',
type: PageType.MODAL,
render: async () => renderAppraisalModal(interaction),
},
appraisalResult: {
key: 'appraisalResult',
render: async (pageCtx) => {
if (!isModalSubmit(interaction)) {
throw new Error('Expected a modal submit interaction for appraisalResult page');
}
let marketId = 2; // Default to Jita
let items = '';
interaction.data.components.forEach((comp) => {
if (isModalLabel(comp)) {
if (isModalSelect(comp.component) && componentHasIdPrefix(comp.component, `market`)) {
marketId = Number.parseInt(comp.component.values[0]) || marketId;
} else if (isModalTextInput(comp.component) && componentHasIdPrefix(comp.component, `input`)) {
items = comp.component.value || items;
}
}
});
const appraisal = await appraiseItems(items, marketId);
pageCtx.state.data.appraisal = appraisal;
return renderAppraisal(appraisal, pageCtx, interaction);
},
},
share: {
key: 'share',
type: PageType.FOLLOWUP,
followUpFlags: Constants.MessageFlags.IS_COMPONENTS_V2,
render: async (pageCtx) => renderAppraisal(pageCtx.state.data.appraisal!, pageCtx, interaction),
},
},
initialPage: 'appraiseModal',
timeout: 300, // 5 minutes
ephemeral: true,
},
interaction,
ctx,
);
}
export default {
definition,
execute,
};

View File

@@ -1,50 +0,0 @@
import type { ExecutableInteraction } from '@star-kitten/discord';
import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/discord/components';
import type { PageContext } from '@star-kitten/discord/pages';
import { type Appraisal } from '@star-kitten/eve/third-party/janice.js';
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
import type { AppraisalState } from './appraise.command';
export function renderAppraisal(
appraisal: Appraisal,
pageCtx: PageContext<AppraisalState>,
interaction: ExecutableInteraction,
) {
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
});
const container = createContainer(
{
accent_color: 0x1da57a,
},
createTextDisplay(`
# [Appraisal ${appraisal.id} @ ${appraisal.market.name}](https://janice.e-351.com/a/${appraisal.id})
### Buy: \`${formatter.format(appraisal.effectivePrices.totalBuyPrice)}\` ISK
### Split: \`${formatter.format(appraisal.effectivePrices.totalSplitPrice)}\` ISK
### Sell: \`${formatter.format(appraisal.effectivePrices.totalSellPrice)}\` ISK
-# Volume: ${formatter.format(appraisal.totalPackagedVolume)}
\`\`\`
Buy: Sell: Qty: Item:
${appraisal.items.map((i) => `${formatNumberToShortForm(i.effectivePrices.buyPrice).padEnd(10)}${formatNumberToShortForm(i.effectivePrices.sellPrice).padEnd(10)}${formatNumberToShortForm(i.amount).padEnd(10)}${i.itemType.name}`).join('\n')}
\`\`\`
-# https://janice.e-351.com/a/${appraisal.id}\n\n
`),
);
if (pageCtx.state.currentPage !== 'share') {
container.components.push(
createActionRow(
createButton('Share in Channel', 'share', {
disabled: !interaction.channel?.id,
}),
),
);
}
return {
type: 1,
components: [container],
};
}

View File

@@ -0,0 +1,79 @@
import type { ExecutableInteraction } from '@star-kitten/discord';
import * as StarKitten from '@star-kitten/discord';
import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/discord/components';
import type { PageContext } from '@star-kitten/discord/pages';
import { type Appraisal } from '@star-kitten/eve/third-party/janice.js';
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
import type { AppraisalState } from './appraise.command';
export function renderAppraisal(
appraisal: Appraisal,
pageCtx: PageContext<AppraisalState>,
interaction: ExecutableInteraction,
): StarKitten.Component {
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
});
// const container = createContainer(
// {
// accent_color: 0x1da57a,
// },
// createTextDisplay(`
// # [Appraisal ${appraisal.id} @ ${appraisal.market.name}](https://janice.e-351.com/a/${appraisal.id})
// ### Buy: \`${formatter.format(appraisal.effectivePrices.totalBuyPrice)}\` ISK
// ### Split: \`${formatter.format(appraisal.effectivePrices.totalSplitPrice)}\` ISK
// ### Sell: \`${formatter.format(appraisal.effectivePrices.totalSellPrice)}\` ISK
// -# Volume: ${formatter.format(appraisal.totalPackagedVolume)} m³
// \`\`\`
// Buy: Sell: Qty: Item:
// ${appraisal.items.map((i) => `${formatNumberToShortForm(i.effectivePrices.buyPrice).padEnd(10)}${formatNumberToShortForm(i.effectivePrices.sellPrice).padEnd(10)}${formatNumberToShortForm(i.amount).padEnd(10)}${i.itemType.name}`).join('\n')}
// \`\`\`
// -# https://janice.e-351.com/a/${appraisal.id}\n\n
// `),
// );
// if (pageCtx.state.currentPage !== 'share') {
// container.components.push(
// createActionRow(
// createButton('Share in Channel', 'share', {
// disabled: !interaction.channel?.id,
// }),
// ),
// );
// }
// return {
// type: 1,
// components: [container],
// };
return (
<container accent={0x1da57a}>
<textDisplay
content={`
# [Appraisal ${appraisal.id} @ ${appraisal.market.name}](https://janice.e-351.com/a/${appraisal.id})
### Buy: \`${formatter.format(appraisal.effectivePrices.totalBuyPrice)}\` ISK
### Split: \`${formatter.format(appraisal.effectivePrices.totalSplitPrice)}\` ISK
### Sell: \`${formatter.format(appraisal.effectivePrices.totalSellPrice)}\` ISK
-# Volume: ${formatter.format(appraisal.totalPackagedVolume)}
\`\`\`
Buy: Sell: Qty: Item:
${appraisal.items.map((i) => `${formatNumberToShortForm(i.effectivePrices.buyPrice).padEnd(10)}${formatNumberToShortForm(i.effectivePrices.sellPrice).padEnd(10)}${formatNumberToShortForm(i.amount).padEnd(10)}${i.itemType.name}`).join('\n')}
\`\`\`
-# https://janice.e-351.com/a/${appraisal.id}\n\n
`}
/>
{pageCtx.state.currentPage !== 'share' ? (
<actionRow>
<button
customId="share"
label="Share in Channel"
disabled={!interaction.channel?.id}
style={StarKitten.ButtonStyle.PRIMARY}
/>
</actionRow>
) : undefined}
</container>
);
}

View File

@@ -1,37 +1,37 @@
import type { Interaction } from '@projectdysnomia/dysnomia';
import { createModalLabel, createStringSelect, createTextInput } from '@star-kitten/discord/components';
import { markets } from '@star-kitten/eve/third-party/janice.js';
export function renderAppraisalModal(interaction: Interaction) {
return {
// next page to render will be appraisalResult
custom_id: `appraisalResult`,
title: 'Appraise Items',
components: [
createModalLabel(
'Select your market (default: Jita)',
createStringSelect(
'market',
{
placeholder: 'Select a market',
},
...markets.map((m) => ({
label: m.name,
value: m.id.toString(),
default: m.id === 2, // Jita
})),
),
),
createModalLabel(
'Enter items to appraise',
createTextInput('input', {
isParagraph: true,
placeholder: `Enter list of items to be appraised.
Tritanium 22222
Pyerite 8000
Mexallon 2444`,
}),
),
],
};
}
import type { Interaction } from '@projectdysnomia/dysnomia';
import { createModalLabel, createStringSelect, createTextInput } from '@star-kitten/discord/components';
import { markets } from '@star-kitten/eve/third-party/janice.js';
export function renderAppraisalModal(interaction: Interaction) {
return {
// next page to render will be appraisalResult
custom_id: `appraisalResult`,
title: 'Appraise Items',
components: [
createModalLabel(
'Select your market (default: Jita)',
createStringSelect(
'market',
{
placeholder: 'Select a market',
},
...markets.map((m) => ({
label: m.name,
value: m.id.toString(),
default: m.id === 2, // Jita
})),
),
),
createModalLabel(
'Enter items to appraise',
createTextInput('input', {
isParagraph: true,
placeholder: `Enter list of items to be appraised.
Tritanium 22222
Pyerite 8000
Mexallon 2444`,
}),
),
],
};
}

View File

@@ -1,140 +1,140 @@
import { renderSubroutes, type Page } from '@star-kitten/discord/pages';
import type { SearchState } from '../search.command';
import {
ButtonStyle,
createContainer,
createSection,
createSeparator,
createTextDisplay,
createThumbnail,
Padding,
} from '@star-kitten/discord/components';
import {
getGroup,
getType,
getUnit,
renderUnit,
typeGetAttribute,
typeHasAnyAttribute,
type Type,
} from '@star-kitten/eve/models';
import { attributeOrdering } from '@star-kitten/eve';
import { searchActionRow } from './helpers';
import { toTitleCase } from '@star-kitten/util/text.js';
enum Images {
ATTRIBUTES = 'https://iili.io/KTbaMR2.md.webp',
DEFENSES = 'https://iili.io/KTbSVoX.md.webp',
FITTING = 'https://iili.io/KufiFYG.md.webp',
FACILITIES = 'https://iili.io/KufikGt.md.webp',
}
const attributeCategoryMap = {
structure: 'UI/Fitting/Structure',
armor: 'UI/Common/Armor',
shield: 'UI/Common/Shield',
ewar: 'UI/Common/EWarResistances',
capacitor: 'UI/Fitting/FittingWindow/Capacitor',
targeting: 'UI/Fitting/FittingWindow/Targeting',
facilities: 'UI/InfoWindow/SharedFacilities',
fighters: 'UI/InfoWindow/FighterFacilities',
on_death: 'UI/InfoWindow/OnDeath',
jump_drive: 'UI/InfoWindow/JumpDriveSystems',
propulsion: 'UI/Compare/Propulsion',
};
const groupedCategories = [
// defenses
['shield', 'armor', 'structure', 'ewar'],
// fittings
['capacitor', 'targeting', 'propulsion'],
// facilities
['facilities', 'fighters', 'on_death', 'jump_drive'],
];
function getAttributeOrdering(type: Type) {
const group = getGroup(type.group_id);
switch (group.category_id) {
case 11:
return attributeOrdering['11'];
case 87:
return attributeOrdering['87'];
default:
return attributeOrdering.default;
}
}
const bannerMap = {
shield: Images.DEFENSES,
armor: Images.DEFENSES,
structure: Images.DEFENSES,
ewar: Images.DEFENSES,
capacitor: Images.FITTING,
targeting: Images.FITTING,
propulsion: Images.FITTING,
facilities: Images.FACILITIES,
fighters: Images.FACILITIES,
on_death: Images.FACILITIES,
jump_drive: Images.FACILITIES,
};
const page: Page<SearchState> = {
key: 'attributes',
render: (context) => {
const type = getType(context.state.data.type_id);
const ordering = getAttributeOrdering(type);
return {
components: [
createContainer(
{},
createSection(
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
createTextDisplay(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`),
),
...renderSubroutes(
context,
'attributes',
groupedCategories.map((group) =>
group.map((cat) => {
const attrCat = ordering[attributeCategoryMap[cat]];
const attrs = attrCat.groupedCategories
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
: attrCat.normalAttributes;
if (!typeHasAnyAttribute(type, attrs)) {
return undefined;
}
return {
label: toTitleCase(cat.replace('_', ' ')),
value: cat,
banner: bannerMap[cat],
};
}),
),
(currentRoute) => {
const lines: string[] = [];
const attrCat = ordering[attributeCategoryMap[currentRoute]];
const attrs = attrCat.groupedCategories
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
: attrCat.normalAttributes;
attrs.map((attrId) => {
const attr = typeGetAttribute(type, attrId);
if (!attr) return;
const unit = attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : '';
lines.push(`${attr.attribute.display_name.en.padEnd(24)} ${unit}`);
});
return createTextDisplay('```\n' + lines.join('\n') + '\n```');
},
{ style: ButtonStyle.SECONDARY },
),
createSeparator(Padding.LARGE),
searchActionRow('attributes'),
),
],
};
},
};
export default page;
import { renderSubroutes, type Page } from '@star-kitten/discord/pages';
import type { SearchState } from '../search.command';
import {
ButtonStyle,
createContainer,
createSection,
createSeparator,
createTextDisplay,
createThumbnail,
Padding,
} from '@star-kitten/discord/components';
import {
getGroup,
getType,
getUnit,
renderUnit,
typeGetAttribute,
typeHasAnyAttribute,
type Type,
} from '@star-kitten/eve/models';
import { attributeOrdering } from '@star-kitten/eve';
import { searchActionRow } from './helpers';
import { toTitleCase } from '@star-kitten/util/text.js';
enum Images {
ATTRIBUTES = 'https://iili.io/KTbaMR2.md.webp',
DEFENSES = 'https://iili.io/KTbSVoX.md.webp',
FITTING = 'https://iili.io/KufiFYG.md.webp',
FACILITIES = 'https://iili.io/KufikGt.md.webp',
}
const attributeCategoryMap = {
structure: 'UI/Fitting/Structure',
armor: 'UI/Common/Armor',
shield: 'UI/Common/Shield',
ewar: 'UI/Common/EWarResistances',
capacitor: 'UI/Fitting/FittingWindow/Capacitor',
targeting: 'UI/Fitting/FittingWindow/Targeting',
facilities: 'UI/InfoWindow/SharedFacilities',
fighters: 'UI/InfoWindow/FighterFacilities',
on_death: 'UI/InfoWindow/OnDeath',
jump_drive: 'UI/InfoWindow/JumpDriveSystems',
propulsion: 'UI/Compare/Propulsion',
};
const groupedCategories = [
// defenses
['shield', 'armor', 'structure', 'ewar'],
// fittings
['capacitor', 'targeting', 'propulsion'],
// facilities
['facilities', 'fighters', 'on_death', 'jump_drive'],
];
function getAttributeOrdering(type: Type) {
const group = getGroup(type.group_id);
switch (group.category_id) {
case 11:
return attributeOrdering['11'];
case 87:
return attributeOrdering['87'];
default:
return attributeOrdering.default;
}
}
const bannerMap = {
shield: Images.DEFENSES,
armor: Images.DEFENSES,
structure: Images.DEFENSES,
ewar: Images.DEFENSES,
capacitor: Images.FITTING,
targeting: Images.FITTING,
propulsion: Images.FITTING,
facilities: Images.FACILITIES,
fighters: Images.FACILITIES,
on_death: Images.FACILITIES,
jump_drive: Images.FACILITIES,
};
const page: Page<SearchState> = {
key: 'attributes',
render: (context) => {
const type = getType(context.state.data.type_id);
const ordering = getAttributeOrdering(type);
return {
components: [
createContainer(
{},
createSection(
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
createTextDisplay(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`),
),
...renderSubroutes(
context,
'attributes',
groupedCategories.map((group) =>
group.map((cat) => {
const attrCat = ordering[attributeCategoryMap[cat]];
const attrs = attrCat.groupedCategories
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
: attrCat.normalAttributes;
if (!typeHasAnyAttribute(type, attrs)) {
return undefined;
}
return {
label: toTitleCase(cat.replace('_', ' ')),
value: cat,
banner: bannerMap[cat],
};
}),
),
(currentRoute) => {
const lines: string[] = [];
const attrCat = ordering[attributeCategoryMap[currentRoute]];
const attrs = attrCat.groupedCategories
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
: attrCat.normalAttributes;
attrs.map((attrId) => {
const attr = typeGetAttribute(type, attrId);
if (!attr) return;
const unit = attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : '';
lines.push(`${attr.attribute.display_name.en.padEnd(24)} ${unit}`);
});
return createTextDisplay('```\n' + lines.join('\n') + '\n```');
},
{ style: ButtonStyle.SECONDARY },
),
createSeparator(Padding.LARGE),
searchActionRow('attributes'),
),
],
};
},
};
export default page;

View File

@@ -1,11 +1,11 @@
import { createActionRow, createButton } from '@star-kitten/discord/components';
export function searchActionRow(pageKey: string) {
return createActionRow(
createButton('Main', 'main', { disabled: pageKey === 'main' }),
createButton('Attributes', 'attributes', { disabled: pageKey === 'attributes' }),
createButton('Fittings', 'fittings', { disabled: pageKey === 'fittings' }),
createButton('Skills', 'skills', { disabled: pageKey === 'skills' }),
createButton('Industry', 'industry', { disabled: pageKey === 'industry' }),
);
}
import { createActionRow, createButton } from '@star-kitten/discord/components';
export function searchActionRow(pageKey: string) {
return createActionRow(
createButton('Main', 'main', { disabled: pageKey === 'main' }),
createButton('Attributes', 'attributes', { disabled: pageKey === 'attributes' }),
createButton('Fittings', 'fittings', { disabled: pageKey === 'fittings' }),
createButton('Skills', 'skills', { disabled: pageKey === 'skills' }),
createButton('Industry', 'industry', { disabled: pageKey === 'industry' }),
);
}

View File

@@ -1,89 +1,89 @@
import type { Page } from '@star-kitten/discord/pages';
import type { SearchState } from '../search.command';
import {
createContainer,
createMediaGallery,
createSection,
createTextDisplay,
createThumbnail,
createURLButton,
} from '@star-kitten/discord/components';
import { getRoleBonuses, getSkillBonuses, getType } from '@star-kitten/eve/models/type.js';
import { cleanText } from '@star-kitten/eve/utils/markdown.js';
import { typeSearch } from '@star-kitten/eve/utils/typeSearch.js';
import { isApplicationCommand } from '@star-kitten/discord';
import { fetchPrice } from '@star-kitten/eve/third-party/evetycoon.js';
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
import { searchActionRow } from './helpers';
const page: Page<SearchState> = {
key: 'main',
render: async (context) => {
if (!context.state.data.type_id && isApplicationCommand(context.interaction)) {
const typeName = context.interaction.data.options?.find((opt) => opt.name === 'name')?.value;
const found = await typeSearch(typeName as string);
if (!found) {
return {
components: [createTextDisplay(`No item found for: ${typeName}`)],
};
}
context.state.data.type_id = found.type_id;
}
const type = getType(context.state.data.type_id);
const skillBonuses = getSkillBonuses(type);
const roleBonuses = getRoleBonuses(type);
const price = await fetchPrice(type.type_id);
return {
components: [
createContainer(
{},
createSection(
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
createTextDisplay(`
# [${type.name.en}](https://everef.net/types/${type.type_id})
${skillBonuses
.map((bonus) => {
return `## Bonus per level of ${bonus.skill.name.en}
${bonus.bonuses
.sort((a, b) => a.importance - b.importance)
.map((b) => `${b.bonus}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
.join('\n')}`;
})
.join('\n')}
${
roleBonuses.length > 0
? `\n## Role Bonuses
${roleBonuses
.sort((a, b) => a.importance - b.importance)
.map((b) => `${b.bonus ?? ''}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
.join('\n')}`
: ''
}
`),
),
createMediaGallery({
url: 'https://iili.io/KTPCFRt.md.webp',
}),
// createSeparator(Padding.LARGE),
createSection(
createURLButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`),
createTextDisplay(
`## Buy: ${price ? formatNumberToShortForm(price.buyAvgFivePercent) : '--'} ISK
## Sell: ${price ? formatNumberToShortForm(price.sellAvgFivePercent) : '--'} ISK`,
),
),
createTextDisplay(`-# Type Id: ${type.type_id}`),
searchActionRow('main'),
),
],
};
},
};
export default page;
import type { Page } from '@star-kitten/discord/pages';
import type { SearchState } from '../search.command';
import {
createContainer,
createMediaGallery,
createSection,
createTextDisplay,
createThumbnail,
createURLButton,
} from '@star-kitten/discord/components';
import { getRoleBonuses, getSkillBonuses, getType } from '@star-kitten/eve/models/type.js';
import { cleanText } from '@star-kitten/eve/utils/markdown.js';
import { typeSearch } from '@star-kitten/eve/utils/typeSearch.js';
import { isApplicationCommand } from '@star-kitten/discord';
import { fetchPrice } from '@star-kitten/eve/third-party/evetycoon.js';
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
import { searchActionRow } from './helpers';
const page: Page<SearchState> = {
key: 'main',
render: async (context) => {
if (!context.state.data.type_id && isApplicationCommand(context.interaction)) {
const typeName = context.interaction.data.options?.find((opt) => opt.name === 'name')?.value;
const found = await typeSearch(typeName as string);
if (!found) {
return {
components: [createTextDisplay(`No item found for: ${typeName}`)],
};
}
context.state.data.type_id = found.type_id;
}
const type = getType(context.state.data.type_id);
const skillBonuses = getSkillBonuses(type);
const roleBonuses = getRoleBonuses(type);
const price = await fetchPrice(type.type_id);
return {
components: [
createContainer(
{},
createSection(
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
createTextDisplay(`
# [${type.name.en}](https://everef.net/types/${type.type_id})
${skillBonuses
.map((bonus) => {
return `## Bonus per level of ${bonus.skill.name.en}
${bonus.bonuses
.sort((a, b) => a.importance - b.importance)
.map((b) => `${b.bonus}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
.join('\n')}`;
})
.join('\n')}
${
roleBonuses.length > 0
? `\n## Role Bonuses
${roleBonuses
.sort((a, b) => a.importance - b.importance)
.map((b) => `${b.bonus ?? ''}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
.join('\n')}`
: ''
}
`),
),
createMediaGallery({
url: 'https://iili.io/KTPCFRt.md.webp',
}),
// createSeparator(Padding.LARGE),
createSection(
createURLButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`),
createTextDisplay(
`## Buy: ${price ? formatNumberToShortForm(price.buyAvgFivePercent) : '--'} ISK
## Sell: ${price ? formatNumberToShortForm(price.sellAvgFivePercent) : '--'} ISK`,
),
),
createTextDisplay(`-# Type Id: ${type.type_id}`),
searchActionRow('main'),
),
],
};
},
};
export default page;

View File

@@ -1,69 +1,69 @@
import {
createChatCommand,
isAutocomplete,
stringOption,
type CommandContext,
type ExecutableInteraction,
} from '@star-kitten/discord';
import { usePages } from '@star-kitten/discord/pages';
import { initializeTypeSearch, typeSearchAutoComplete } from '@star-kitten/eve/utils/typeSearch.js';
import main from './pages/main';
import attributes from './pages/attributes';
let now = Date.now();
console.debug('Initializing type search...');
await initializeTypeSearch().catch((e) => {
console.error('Failed to initialize type search', e);
process.exit(1);
});
console.debug(`Type search initialized. Took ${Date.now() - now}ms`);
export interface SearchState {
type_id: number;
}
export default createChatCommand(
{
name: 'search',
description: 'Search for a type',
options: [
stringOption({
name: 'name',
description: 'The type name to search for',
autocomplete: true,
required: true,
}),
],
},
execute,
);
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
if (isAutocomplete(interaction)) {
const focusedOption = interaction.data.options?.find((opt) => opt.focused);
if (focusedOption?.name === 'name') {
const value = focusedOption.value as string;
const results = await typeSearchAutoComplete(value);
if (results) {
await interaction.result(results);
} else {
await interaction.result([]);
}
}
return;
}
usePages<SearchState>(
{
pages: {
main,
attributes,
},
initialPage: 'main',
ephemeral: false,
},
interaction,
ctx,
);
}
import {
createChatCommand,
isAutocomplete,
stringOption,
type CommandContext,
type ExecutableInteraction,
} from '@star-kitten/discord';
import { usePages } from '@star-kitten/discord/pages';
import { initializeTypeSearch, typeSearchAutoComplete } from '@star-kitten/eve/utils/typeSearch.js';
import main from './pages/main';
import attributes from './pages/attributes';
let now = Date.now();
console.debug('Initializing type search...');
await initializeTypeSearch().catch((e) => {
console.error('Failed to initialize type search', e);
process.exit(1);
});
console.debug(`Type search initialized. Took ${Date.now() - now}ms`);
export interface SearchState {
type_id: number;
}
export default createChatCommand(
{
name: 'search',
description: 'Search for a type',
options: [
stringOption({
name: 'name',
description: 'The type name to search for',
autocomplete: true,
required: true,
}),
],
},
execute,
);
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
if (isAutocomplete(interaction)) {
const focusedOption = interaction.data.options?.find((opt) => opt.focused);
if (focusedOption?.name === 'name') {
const value = focusedOption.value as string;
const results = await typeSearchAutoComplete(value);
if (results) {
await interaction.result(results);
} else {
await interaction.result([]);
}
}
return;
}
usePages<SearchState>(
{
pages: {
main,
attributes,
},
initialPage: 'main',
ephemeral: false,
},
interaction,
ctx,
);
}

View File

@@ -1,3 +1,3 @@
import { startDiscordBot } from '@star-kitten/discord';
startDiscordBot();
import { startDiscordBot } from '@star-kitten/discord';
startDiscordBot();

View File

@@ -0,0 +1,26 @@
export function renderAppraisal() {
const formatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
});
const world = 'world';
const rand = Math.random() * 1000;
const pageCtx = { state: { currentPage: 'home' } };
let jsx = (
<actionRow>
<container accent={0x1da57a}>
<textDisplay content={`Hello ${world}`} />
{pageCtx.state.currentPage !== 'share' ? (
<actionRow>
<button customId="share" label="Share in Channel" disabled={rand < 500} />
</actionRow>
) : undefined}
</container>
</actionRow>
);
console.log(jsx);
}
renderAppraisal();

View File

@@ -1,38 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
// Enable latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"composite": true,
"jsx": "react-jsx",
"jsxImportSource": "@star-kitten/discord",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": false,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
// Paths
"paths": {
"@*": ["./src/*"]
},
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"typeRoots": ["src/types", "./node_modules/@types"]
},
"include": ["src", "types"],

View File

@@ -1,30 +0,0 @@
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY_DEVELOPMENT="02572da3d4f3a844588a944214c0e142a5a01deaa6551456af146d34b574024416"
# .env.development
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="02292a330aa041b5f7efc51504e0c208accba67a6877a217ab43cbb59c3c0c3e66"
# .env
DEBUG="encrypted:BC7p62nrs3NV7XdxnBbO1WsHGm8IgDEbOS1RmgORHSqh05vGIv+hmqwau61FamrU/puT4btAsG+iLcSeypCQV5e7bBpr0qu0HQoVyMzunBvrN5ivzNY0Af800lNynsBXzq0cXTY="
PORT="encrypted:BJpY7J2J+0z4LUNnKRr7HzcpETcdnWFuRAOC3hVl2cZyiCBl706vJqv+iY3BgA0mus73t9fwYjGRrPSXSQbcSEBzr+Jquj8Gkvy7loXKkKp4Gz1tqX554txfY0XjrgMO3oHATO8="
NODE_ENV="encrypted:BJun6Kdf/kBSrIYUgw7pfnMwlrTjvUUq/w2yjqn+X5UgaxUxzLsI0JabYlxQCxoDMSEagQYWI5HkRaZvYuXHzyN2aXm6drC2bahg9aWZTVyWYu00FFwIah7l/tuMA/caeO7s5dwkuOgCvlOQ"
LOG_LEVEL="encrypted:BKBvZDS7xkzgg5IqiTc9izmt4om4CjX7t4LA8gMY+0ru0NtVBpkSkchil4PvaRNhktcNjtIzfE6sduRoFw5T1tt88PjtKWIhORyACZk2ZtR3vuO/xecq2q1rIIp5kD1gcp4ujltK"
BASE_URL="encrypted:BMueek9QzKR3k+Foe0xUruZUxwwIXBcrL7B6ksdBNFaF92nHHy2HLLOhOpKNebGfSwb9mBR5kOXb095+hsDEcqAiv5nc5BjWGfk/wkFpRfFcmyEGYlmrxLclbFdQFXIqYrzmN7ae/VV1VlRzNLBipO0smJj4LZY649oV2A=="
EVE_CLIENT_ID="encrypted:BPOAohc2MmP7VvvLybwvcl7XgzYRqWC6IzBkOf+T73kN8YlHYKt611uYNjU27G6hkVRK3DSfgCuxTPzrQBPxvAY6pDbqFG8pVU9cKDmoeHYtiKh7KkHuBN/cEzll6x8hpIwwrY32nzQjxYMvyVO5UgG7OHK1T/jkeya2TW2DSgGn"
EVE_CLIENT_SECRET="encrypted:BE0VYniO3JEFNt1R4iUrGA7W7cSp7gQtG7Y86VQeWnte+idjnqSFmv2lmz83rc7Idvi/VU+ipuY6RL2+49jAb/oUaXGUiwguBAnlFU+ypOVy2Ed29o7yggqiB2+dUuu4xDAsLAfSXErnw4gsDsEPAMqaKhCYz0LHEvJX5ZdwfAcfrpWoLeI/Vm0="
EVE_CALLBACK_URL="encrypted:BIjACAnGtL06X0vkvmydydup9HZDcPA+DAYUAhlH3lsq8GJPD5XxlRwVx02VzZQuATfqm1JwGDyYbw8ceaWD2RLlcjSXPF7MWDpYzG1FExE7FZbFRBBO8XqGH9X8kfxYsuca/Td8KuIPjyS5BNkyM4GQcTlojKa15Hk4GXmpNP6Gyb45XNPCegRJL5aARQ=="
ESI_USER_AGENT="encrypted:BEUaqMbwPNvJeF/d5q5LJ5Owd5wcQ8Jg/BTGn6qns4cwlX/e6QqLLmfp8E8SxQ8Z6h+qDLpZj0HROJIOK9Y2Xb6qjB+hCnjceRMTx1QWpNS6jXQ85TQiZfYzee57QFleau621B77KIuM5DjPUZZ02efAL+2Yk83amrh3vnzvwrnvM5mMGTzc2TbeQwB6MEdjAdvAz65VGX9DnCwbtP3bMKE7og4+sKMUrTYZpCsILug="
DISCORD_APP_ID="encrypted:BM3yHCf9kTxcIQzzmNseT0/xol6ZLYTjZ3m3NKybW1oD2joZ/gTUIg2+mgfaeCqY1CPaSGppxguDPFgthMbWihAdeGuxiITiwLDulCTLcgjBsyT6IlsKUSsE5ZiEZl1A+ikNG1/8rxrF0MsIjVqfw+U+ev8="
DISCORD_APP_SECRET="encrypted:BNVLmKb2ZJq1+iIYKwGaCtcM+hak5NPXLNgJnPzTlpd/5zUTKc8OtYZLhg2oqtOYv8rxf8sbjpXNWB6lZL5J5NUuPcQSfVOMr3U1BN6b9WsfWZ/2Pr4cm8kqqqjJVTF49/DasRQU5VlIXodvCI2XfWrcPFV20NvE8HzsJQz/g5cF"
DISCORD_PUBLIC_KEY="encrypted:BEmxCcAqODfgukOza3EGzsXdrnXv3qVbzlLFYsD4Iba9mQFCaOwChIQNkN/+Ve5NV5sZeefU5sEXZRYbb/hRjJjMRX4NrhEZn2Pg3mI5/FG/+uCD0cWWs4JGzTRwUcBcG8FZ2Mw/kP1ymUqMRkJCYC+XdyYqcP8zNrQ6/aca3HLcqPma7j2/1lsbX4UJ3QcKDg3bsY+107MLLJJ4+TYiwcE="
DISCORD_BOT_TOKEN="encrypted:BCLvoICUVOYz7pkV3f+dufxEZiWcKxQ7E56cCUXtLQjjorW5WsCftgoCdP5NXKSbhHYZBqrTCAHqPp/kLgaVjDXxXu2+61DiCNjB3t/kNIudkfsuzJ0vusKcVzgnQOmTmYnOZ+SYo7hKPrjLi2/Vk8r6K26TATp9t+iuSBMsEs7t9nnnySSYIXyHfXRXbRBg4NUFOmeDRUqBhhix9mgr+MI5SPmQgsXCvA=="
DISCORD_TEST_GUILD_ID="encrypted:BKRJRRvQw2aoBrVoVZpPPYSzCy+2VXLLzBb1zNzUR5510qobPNDbUoIlJdm41moQS0ALG94l8miVLtkOKW6MGZyepq+gE1zSEu7eIJWHB8/eSUnGuNeqSghI4Kxr8kn04Bl4eDb1UgP1Fu+8XSXjVel85Q=="
JANICE_KEY="encrypted:BPq+CJycBbmmoDHNmHHYiQ00PeISDQTqp2IdlLZD52V2wWPOmXnRnqgvoXff64ebUayySaW1sQtvoaMJE3Gt9E/FUquYiRTPUMT5+oW7Xo60IRgAhRW9n2m/YUDIORxR0J0qxKc9cdE75VWJNqLELKKwsriBdpqj0+4yM/Mn6IaU"
PERPLEXITY_API_KEY="encrypted:BHxscbb6WwhFgt5Cp/WzrYGJ6nJvIFeoS3JrTaibPbQ7kiu3C/Zx8klgRhlBF25+HAA8chZHT5MeE99FSD59RqIQTcnKAyfUnxNq+ovoXMV7Nk2Fb/rrdLnvUf8VoeH7L4ZyOVF1wv0r4xm/7Qqc1JN50fME6Qa47KnnQekq/n6o2e5HVI153yJnlCQu/SvcyXCY6VW2"

View File

@@ -1,23 +0,0 @@
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY_PRODUCTION="02f0469506f6722d8fcc179c199ff159ca32f082000c8e7a1465891adb50a4c031"
# .env.production
DEBUG="encrypted:BKnKyDjoQANl7bGi3568JTnA/7sUBdUVlc8nNznTwxs5Lu/4iMDu1cY0x0iE7b0z+RXEuA/w6bERB5uTmWkCdglEI5S6LhnXZzcV55iLBY8rHO/MIDBF39vn/PBsMiA86gBmtaIn"
PORT="encrypted:BKaGvooBBBWB13491yjfYwYT2zG1MZiYi3+Y6wW8ZhuvMOEsfPuW94rv3cE2LoguALsBFXH9rn3lQJysZLJcYd9AxJoLqxmWHkEpbQ35PYDSvkJ0GsEGlrd74hVbnh57A7Dmqqc="
NODE_ENV="encrypted:BIiY7XZ9vy9stbiNC2u+o4ibruGTtMuRXUJPs3lMxkHKlK6gksg0ddTPbia/qZkZudGjnEhmqYDUPfSWQDdmf8gFsuBgSgYGhR2GCNw7mCCmsDO/wE3ojNnlvuetnVfLeJa7ugvOGjX5QTs="
LOG_LEVEL="encrypted:BEAJviH6nTAR4AdFEoiud4ZHV+dwvURoZys4M9KsYAn5MD+nlNEnzS+9vtE3NPwqzfkpK0Z/46xB+SUIXwhwaJ9Yzgz2WqLK1UXEB6fhQqHXdIqvW2ug/+hUxW9k0ueMu5I9btE="
BASE_URL="encrypted:BJ6YYd2cX31HuTvGnNxLK33KQgzWxU9yRtlwAc79hhbuioHP5lMu7LxCu8NnXfcaEvevWsEt3cG4I7PNqzlBmU1WhTwsdJ4Kqi2cvs6hKwMeVWvtgdI+ymojG/GoglkbHjSc9737dsth2+erI4qbjkafqYuOC9S9"
DISCORD_APP_ID="encrypted:BMqY+6wep/q7bWO9Yc/tJukPHH5H6vvItZpOFK5zaLP92Fx4S+qyZvH/LZKfUBSxZ1d1vbAqo4V+HPxNPyvwXaZyu/qlRf5ZTP/hkt1k9RoNb5UNOMSoD6GSZFS24/JEKuDPzQnvEOb0+prPHEJknvTMXmI="
DISCORD_APP_SECRET="encrypted:BBBWkXA3zi8rCWEiMH66v1hFb6Jqw0Bn4H/6b5qWbVakm5UDckOmgQSjXosJLA1VX0vY+38s5fT1ICPIBv2b0rzpaQ09GpRIwryyTT+VLvMcZfrt1CP4ISC0uzlE2p9qceK9EG+6I7ge4pRkzpxotwnWAg9SCqVcNb04kRwPhUvj"
DISCORD_PUBLIC_KEY="encrypted:BPqsvWPJ7/IBYynqFMHmEcpMBS+T5CXyfnzZU3flouPwcCaKkIc3xkLhtVco7nWdv2v/hw9ulysmVJhT6CiW9r1k0XRqRnv4q1PDDSTLrP0c1cXPg6pFLcOEv7e2CU+Gkj7UEFZz3xgrb6aWkkLRO0yATcA20xbdnOv3rbigOrJCfJwbbAWPQk7yun8oF6O/mGvWW9Blv8if54fp8vBOeUs="
DISCORD_BOT_TOKEN="encrypted:BMfVvYAKvIsMW+dS1X5SZcXfUIxrsunb3q4iXM2ifTlSP8ZlFpA2jxvgYj16jKcexiyVUBhWMSyUC4eS2AuIY/6fKCuA/5JtvqMltzcxPYAi3VEYIz3AgURiMRNy2QsNnqEXxNiakNaPq5Tv4dqVB4z7YeQ16QdvpDWxD4XUQFtG8Q5jZQK/ISj4xJT9cmaxLUJB9XQcbAa8Oseghpk/A1i5JXO05Rx4vQ=="
EVE_CLIENT_ID="encrypted:BCBtASu/2DN+EvLPM//WQjBdfwRscwSC5zKBHLTTTcXMfVur8GtDB6ZcBWstmC4YdiimjPobi1RJ+qdYndu1SM300g6UwOmmO2sNhOpG5nyP0mT2HNgwcJzl+Z7Ad1Vr/iByzYyqkc+uYr9NwhvJDPud+HP11dTjKvw+9Ht9/abA"
EVE_CLIENT_SECRET="encrypted:BMccZ43R9rT33amzo0zfIgLM8hKDCMXLrj+5h0TNLH1RhwrsUxcKgl17MAVqV+8uPBbB171kRRnjKLaQDjjJM27Jv1SV5bn316qrIx35Tkl2Ocd5wjEs7TSAjf8HwzUhiH9F68+IrQ36Vm8w27+RmsaRtvTtiWWVmYvBXw4PFMprTE7SG0bFq3M="
EVE_CALLBACK_URL="encrypted:BDsEZqRGXFzRigkBq50UYj14UvNjRM4Ao3PLSxVTeyc+2Fad1DQa9mfFE9yBnp3l1H5KMQcPJdWf/MxyAa9J0RvXm9l01lbmkgXu+C+HJXWHKJ7/b91NrQqngm2l76jp80WjtmdJ2D5WOUGrIxZatQaqgh8TexQAjVwjkTeQO97PJnbF0FyNQOlu"
ESI_USER_AGENT="encrypted:BJ9Pib5a8/qxfROzBfjlAKr/fEvgepN8o6NCI0l3aiYvFuk5hczaA57TKPMP6P2Ct/Juj47YuU8bqF147y8C556NMiY2HDPbrnenXKdAh4xCerjXhkFqowPvEMVxoeuiyhRM7mPmUSAw7AbYi7AxDtCTw80/6S2/b9/32XBk4eCnSdJmM9kFxwHVFQNK83V0Sr5XEymT4S1kntvqlFsBel/5KxMMfNieqTiT+b5mVyM="
AUTH_DB_PATH="encrypted:BAqssA/4tJHhxv+pQuSXln5reiqtIdaJzIakctW9fs3omlsZr8j7pXHvZEPQAyYnH2u396tXQxZLXSfdj68q5odUEXUDt6kxN70h3ikL/4gbkfpPkW24wd4NlVPA21GZR+rBpvfpZN1u57Lvp8Lm/QvUfTlka4H5"
JANICE_KEY="encrypted:BFEsjnnZNfYFIXvGXKVtko5c8zh5sZze7hjFORfAb4QsHqHh/SqXVKClMCyEa8OMCjNtd8Zmz8LOckaOUYAh09Xi57KM6Eh33CirHipys0rdeURcwSkI9RSXPZOvmOfKZ9yDmhd3iov3AF6b+wwDQ9/rhYZrqh/NWETeHV98Xgv1"
PERPLEXITY_API_KEY="encrypted:BIRn8UX4BgL/4QOCaz2cNZVfiJY3zR/Qclr2UI8FnDsUR8mu+hWi8SVWeaauzLSRwiWU1Ihc3/sWUxi8Jz/Ma1dGcDdPwPO7kiZuN2a1Tl3NKiSMmDlNYszLekhpESoUzVOa/605lcKsTemqC8SvVfq7rOPuC2QP7/7bIGSPGDShIfBjU7dUpvfpDX0/Vf++kLHkNiV1"

View File

@@ -1,8 +0,0 @@
trailingComma: all
tabWidth: 2
useTabs: false
semi: true
singleQuote: true
printWidth: 140
experimentalTernaries: true
quoteProps: consistent

View File

@@ -1,83 +0,0 @@
# Star Kitten Web
Project created with [Brisa](https://github.com/brisa-build/brisa).
## Getting Started
### Installation
```bash
bun install
```
### Link the Library
`star-kitten-lib` has not been published, so link to it locally before running this web project.
```bash
cd star-kitten-lib
bun link
cd ../web
bun link star-kitten-lib
```
### Download static eve reference data & Hoboleaks archive from [EVE Ref](https://everef.net/).
```bash
cd star-kitten-lib
bun get-data
```
### Initialize the sqlite database
```bash
cd star-kitten-lib
bun generate-migrations
bun migrate
```
Drizzle's migrations seems to fail on the first try sometimes, so just grab the .sql from the generation and run those against the kitten.db file to create the tables & indexes.
## Environment Variables
Create a .env file in the root directory with the following values:
```yaml
#General
BASE_URL=http://localhost:3000
DEBUG=true
PORT=3000
NODE_ENV=development
LOG_LEVEL=debug
# EVE - https://developers.eveonline.com/applications
EVE_CLIENT_ID=YOUR_EVE_CLIENT_ID
EVE_CLIENT_SECRET=YOUR_EVE_SECRET
EVE_CALLBACK_URL=http://localhost:3000/auth/callback
ESI_USER_AGENT=ADD_YOUR_USER_AGENT_INFO_HERE
# For using Janice's Appraisal API
JANICE_KEY=XXX
# For using Perplexities AI API
PERPLEXITY_API_KEY=XXX
```
### Development
```bash
bun dev
```
### Build
```bash
bun build
```
### Start
```bash
bun start
```

View File

@@ -1,7 +0,0 @@
import type { Configuration } from "brisa";
import tailwindcss from 'brisa-tailwindcss';
export default {
integrations: [tailwindcss()],
} as Configuration;

View File

@@ -1,4 +0,0 @@
export interface IntrinsicCustomElements {
'counter-client': JSX.WebComponentAttributes<typeof import("D:\dev\@star-kitten\packages\eve-web\src\web-components\counter-client.tsx").default>;
}
export type PageRoute = "/" | "/about" | "/auth/error" | "/auth/success";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,55 +0,0 @@
// @bun
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
enumerable: true
});
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __require = import.meta.require;
// src/utils/cookies.ts
function getCookies(headers) {
if (!headers)
return {};
const cookieHeader = headers.get("Cookie");
const cookies = {};
if (cookieHeader === null)
return {};
for (const kv of cookieHeader.split(";")) {
const [cookieKey, ...cookieVal] = kv.split("=");
const key = cookieKey.trim();
cookies[key] = cookieVal.join("=");
}
return cookies;
}
function setCookie(response, key, value, maxAge) {
response.headers.append("Set-Cookie", `${key}=${value}${maxAge ? "; Path=/; Max-Age=" + maxAge : ""}`);
}
function removeCookie(response, key) {
response.headers.append("Set-Cookie", `${key}=""; Path=/; Max-Age=-1;`);
}
// src/api/auth/discordID/[discordID]/characterID/[characterID]/scopes/[scopes].ts
async function GET({ store, route: { params } }) {
const eveauth = store.get("eveauth");
const response = await eveauth.redirect(params["scopes"]);
setCookie(response, "discordID", params["discordID"], 60 * 10);
setCookie(response, "characterID", params["characterID"], 60 * 10);
return response;
}
export {
GET
};
//# debugId=AEE66F1C816CC1AD64756E2164756E21
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi5cXHNyY1xcdXRpbHNcXGNvb2tpZXMudHMiLCAiLi5cXHNyY1xcYXBpXFxhdXRoXFxkaXNjb3JkSURcXFtkaXNjb3JkSURdXFxjaGFyYWN0ZXJJRFxcW2NoYXJhY3RlcklEXVxcc2NvcGVzXFxbc2NvcGVzXS50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsKICAgICJcbmV4cG9ydCBmdW5jdGlvbiBnZXRDb29raWVzKGhlYWRlcnM6IEhlYWRlcnMpIHtcbiAgaWYgKCFoZWFkZXJzKSByZXR1cm4ge307XG4gIGNvbnN0IGNvb2tpZUhlYWRlciA9IGhlYWRlcnMuZ2V0KFwiQ29va2llXCIpO1xuICBjb25zdCBjb29raWVzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge307XG5cbiAgaWYgKGNvb2tpZUhlYWRlciA9PT0gbnVsbCkgcmV0dXJuIHt9O1xuXG4gIGZvciAoY29uc3Qga3Ygb2YgY29va2llSGVhZGVyLnNwbGl0KFwiO1wiKSkge1xuICAgIGNvbnN0IFtjb29raWVLZXksIC4uLmNvb2tpZVZhbF0gPSBrdi5zcGxpdChcIj1cIik7XG4gICAgY29uc3Qga2V5ID0gY29va2llS2V5LnRyaW0oKTtcbiAgICBjb29raWVzW2tleV0gPSBjb29raWVWYWwuam9pbihcIj1cIik7XG4gIH1cblxuICByZXR1cm4gY29va2llcztcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNldENvb2tpZShyZXNwb25zZTogUmVzcG9uc2UsIGtleTogc3RyaW5nLCB2YWx1ZTogc3RyaW5nLCBtYXhBZ2U/OiBudW1iZXIpIHtcbiAgcmVzcG9uc2UuaGVhZGVycy5hcHBlbmQoJ1NldC1Db29raWUnLCBgJHtrZXl9PSR7dmFsdWV9JHttYXhBZ2UgPyAnOyBQYXRoPS87IE1heC1BZ2U9JyArIG1heEFnZSA6ICcnfWApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVtb3ZlQ29va2llKHJlc3BvbnNlOiBSZXNwb25zZSwga2V5OiBzdHJpbmcpIHtcbiAgcmVzcG9uc2UuaGVhZGVycy5hcHBlbmQoJ1NldC1Db29raWUnLCBgJHtrZXl9PVwiXCI7IFBhdGg9LzsgTWF4LUFnZT0tMTtgKTtcbn0iLAogICAgImltcG9ydCB0eXBlIHsgRVZFQXV0aCB9IGZyb20gJ0AvbWlkZGxld2FyZSc7XG5pbXBvcnQgeyBzZXRDb29raWUgfSBmcm9tICdAL3V0aWxzJztcbmltcG9ydCB0eXBlIHsgUmVxdWVzdENvbnRleHQgfSBmcm9tICdicmlzYSc7XG5cbi8vIEdFVCAvYXBpL2F1dGgvZGlzY29yZElELzpkaXNjb3JkSUQvY2hhcmFjdGVySUQvOmNoYXJhY3RlcklEL3Njb3Blcy86c2NvcGVzXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gR0VUKHsgc3RvcmUsIHJvdXRlOiB7IHBhcmFtcyB9IH06IFJlcXVlc3RDb250ZXh0KSB7XG4gIC8vIHRoaXMgaXMgdXNlZCB0byBzZXQgdGhlIHNjb3BlcyB0aGF0IHdlcmUgc2VudCwgc28ganVzdCBwYXNzIHRoZW0gYWxvbmcgdG8gYXV0aCBkaXJlY3RseSBcbiAgLy8gd2l0aCB0aGUgcHJvdmlkZWQgc2NvcGVzXG4gIGNvbnN0IGV2ZWF1dGg6IEVWRUF1dGggPSBzdG9yZS5nZXQoJ2V2ZWF1dGgnKTtcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBldmVhdXRoLnJlZGlyZWN0KHBhcmFtcyFbJ3Njb3BlcyddIGFzIHN0cmluZyk7XG4gIHNldENvb2tpZShyZXNwb25zZSwgJ2Rpc2NvcmRJRCcsIHBhcmFtcyFbJ2Rpc2NvcmRJRCddIGFzIHN0cmluZywgNjAgKiAxMCAvKiAxMCBtaW4gKi8pO1xuICBzZXRDb29raWUocmVzcG9uc2UsICdjaGFyYWN0ZXJJRCcsIHBhcmFtcyFbJ2NoYXJhY3RlcklEJ10gYXMgc3RyaW5nLCA2MCAqIDEwIC8qIDEwIG1pbiAqLyk7XG4gIHJldHVybiByZXNwb25zZTtcbn0iCiAgXSwKICAibWFwcGluZ3MiOiAiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFDTyxTQUFTLFVBQVUsQ0FBQyxTQUFrQjtBQUFBLEVBQzNDLEtBQUs7QUFBQSxJQUFTLE9BQU8sQ0FBQztBQUFBLEVBQ3RCLE1BQU0sZUFBZSxRQUFRLElBQUksUUFBUTtBQUFBLEVBQ3pDLE1BQU0sVUFBa0MsQ0FBQztBQUFBLEVBRXpDLElBQUksaUJBQWlCO0FBQUEsSUFBTSxPQUFPLENBQUM7QUFBQSxFQUVuQyxXQUFXLE1BQU0sYUFBYSxNQUFNLEdBQUcsR0FBRztBQUFBLElBQ3hDLE9BQU8sY0FBYyxhQUFhLEdBQUcsTUFBTSxHQUFHO0FBQUEsSUFDOUMsTUFBTSxNQUFNLFVBQVUsS0FBSztBQUFBLElBQzNCLFFBQVEsT0FBTyxVQUFVLEtBQUssR0FBRztBQUFBLEVBQ25DO0FBQUEsRUFFQSxPQUFPO0FBQUE7QUFHRixTQUFTLFNBQVMsQ0FBQyxVQUFvQixLQUFhLE9BQWUsUUFBaUI7QUFBQSxFQUN6RixTQUFTLFFBQVEsT0FBTyxjQUFjLEdBQUcsT0FBTyxRQUFRLFNBQVMsdUJBQXVCLFNBQVMsSUFBSTtBQUFBO0FBR2hHLFNBQVMsWUFBWSxDQUFDLFVBQW9CLEtBQWE7QUFBQSxFQUM1RCxTQUFTLFFBQVEsT0FBTyxjQUFjLEdBQUcsNkJBQTZCO0FBQUE7O0FDakJ4RSxlQUFzQixHQUFHLEdBQUcsT0FBTyxTQUFTLFlBQTRCO0FBQUEsRUFHdEUsTUFBTSxVQUFtQixNQUFNLElBQUksU0FBUztBQUFBLEVBQzVDLE1BQU0sV0FBVyxNQUFNLFFBQVEsU0FBUyxPQUFRLFNBQW1CO0FBQUEsRUFDbkUsVUFBVSxVQUFVLGFBQWEsT0FBUSxjQUF3QixLQUFLLEVBQWU7QUFBQSxFQUNyRixVQUFVLFVBQVUsZUFBZSxPQUFRLGdCQUEwQixLQUFLLEVBQWU7QUFBQSxFQUN6RixPQUFPO0FBQUE7IiwKICAiZGVidWdJZCI6ICJBRUU2NkYxQzgxNkNDMUFENjQ3NTZFMjE2NDc1NkUyMSIsCiAgIm5hbWVzIjogW10KfQ==

View File

@@ -1,54 +0,0 @@
// @bun
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
enumerable: true
});
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __require = import.meta.require;
// src/utils/cookies.ts
function getCookies(headers) {
if (!headers)
return {};
const cookieHeader = headers.get("Cookie");
const cookies = {};
if (cookieHeader === null)
return {};
for (const kv of cookieHeader.split(";")) {
const [cookieKey, ...cookieVal] = kv.split("=");
const key = cookieKey.trim();
cookies[key] = cookieVal.join("=");
}
return cookies;
}
function setCookie(response, key, value, maxAge) {
response.headers.append("Set-Cookie", `${key}=${value}${maxAge ? "; Path=/; Max-Age=" + maxAge : ""}`);
}
function removeCookie(response, key) {
response.headers.append("Set-Cookie", `${key}=""; Path=/; Max-Age=-1;`);
}
// src/api/auth/discordID/[discordID]/index.ts
async function GET({ store, route: { params } }) {
const eveauth = store.get("eveauth");
const response = await eveauth.redirect("publicData");
setCookie(response, "discordID", params["discordID"], 60 * 10);
return response;
}
export {
GET
};
//# debugId=7AF70B8658B4D3D064756E2164756E21
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi5cXHNyY1xcdXRpbHNcXGNvb2tpZXMudHMiLCAiLi5cXHNyY1xcYXBpXFxhdXRoXFxkaXNjb3JkSURcXFtkaXNjb3JkSURdXFxpbmRleC50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsKICAgICJcbmV4cG9ydCBmdW5jdGlvbiBnZXRDb29raWVzKGhlYWRlcnM6IEhlYWRlcnMpIHtcbiAgaWYgKCFoZWFkZXJzKSByZXR1cm4ge307XG4gIGNvbnN0IGNvb2tpZUhlYWRlciA9IGhlYWRlcnMuZ2V0KFwiQ29va2llXCIpO1xuICBjb25zdCBjb29raWVzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge307XG5cbiAgaWYgKGNvb2tpZUhlYWRlciA9PT0gbnVsbCkgcmV0dXJuIHt9O1xuXG4gIGZvciAoY29uc3Qga3Ygb2YgY29va2llSGVhZGVyLnNwbGl0KFwiO1wiKSkge1xuICAgIGNvbnN0IFtjb29raWVLZXksIC4uLmNvb2tpZVZhbF0gPSBrdi5zcGxpdChcIj1cIik7XG4gICAgY29uc3Qga2V5ID0gY29va2llS2V5LnRyaW0oKTtcbiAgICBjb29raWVzW2tleV0gPSBjb29raWVWYWwuam9pbihcIj1cIik7XG4gIH1cblxuICByZXR1cm4gY29va2llcztcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNldENvb2tpZShyZXNwb25zZTogUmVzcG9uc2UsIGtleTogc3RyaW5nLCB2YWx1ZTogc3RyaW5nLCBtYXhBZ2U/OiBudW1iZXIpIHtcbiAgcmVzcG9uc2UuaGVhZGVycy5hcHBlbmQoJ1NldC1Db29raWUnLCBgJHtrZXl9PSR7dmFsdWV9JHttYXhBZ2UgPyAnOyBQYXRoPS87IE1heC1BZ2U9JyArIG1heEFnZSA6ICcnfWApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVtb3ZlQ29va2llKHJlc3BvbnNlOiBSZXNwb25zZSwga2V5OiBzdHJpbmcpIHtcbiAgcmVzcG9uc2UuaGVhZGVycy5hcHBlbmQoJ1NldC1Db29raWUnLCBgJHtrZXl9PVwiXCI7IFBhdGg9LzsgTWF4LUFnZT0tMTtgKTtcbn0iLAogICAgImltcG9ydCB0eXBlIHsgRVZFQXV0aCB9IGZyb20gJ0AvbWlkZGxld2FyZSc7XG5pbXBvcnQgeyBzZXRDb29raWUgfSBmcm9tICdAL3V0aWxzJztcbmltcG9ydCB0eXBlIHsgUmVxdWVzdENvbnRleHQgfSBmcm9tICdicmlzYSc7XG5cbi8vIEdFVCAvYXBpL2F1dGgvZGlzY29yZElELzpkaXNjb3JkSURcbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBHRVQoeyBzdG9yZSwgcm91dGU6IHsgcGFyYW1zIH19OiBSZXF1ZXN0Q29udGV4dCkge1xuICAvLyBjYWxsZWQgd2hlbiBhZGRpbmcgYSBuZXcgY2hhcmFjdGVyLCBzbyBqdXN0IHJlZGlyZWN0IHRvIGF1dGggYW5kIHNldCBjb29raWVzXG4gIGNvbnN0IGV2ZWF1dGg6IEVWRUF1dGggPSAgc3RvcmUuZ2V0KCdldmVhdXRoJyk7XG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZXZlYXV0aC5yZWRpcmVjdCgncHVibGljRGF0YScpO1xuICBzZXRDb29raWUocmVzcG9uc2UsICdkaXNjb3JkSUQnLCBwYXJhbXMhWydkaXNjb3JkSUQnXSBhcyBzdHJpbmcsIDYwICogMTAgLyogMTAgbWluICovKTtcbiAgcmV0dXJuIHJlc3BvbnNlO1xufSIKICBdLAogICJtYXBwaW5ncyI6ICI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUNPLFNBQVMsVUFBVSxDQUFDLFNBQWtCO0FBQUEsRUFDM0MsS0FBSztBQUFBLElBQVMsT0FBTyxDQUFDO0FBQUEsRUFDdEIsTUFBTSxlQUFlLFFBQVEsSUFBSSxRQUFRO0FBQUEsRUFDekMsTUFBTSxVQUFrQyxDQUFDO0FBQUEsRUFFekMsSUFBSSxpQkFBaUI7QUFBQSxJQUFNLE9BQU8sQ0FBQztBQUFBLEVBRW5DLFdBQVcsTUFBTSxhQUFhLE1BQU0sR0FBRyxHQUFHO0FBQUEsSUFDeEMsT0FBTyxjQUFjLGFBQWEsR0FBRyxNQUFNLEdBQUc7QUFBQSxJQUM5QyxNQUFNLE1BQU0sVUFBVSxLQUFLO0FBQUEsSUFDM0IsUUFBUSxPQUFPLFVBQVUsS0FBSyxHQUFHO0FBQUEsRUFDbkM7QUFBQSxFQUVBLE9BQU87QUFBQTtBQUdGLFNBQVMsU0FBUyxDQUFDLFVBQW9CLEtBQWEsT0FBZSxRQUFpQjtBQUFBLEVBQ3pGLFNBQVMsUUFBUSxPQUFPLGNBQWMsR0FBRyxPQUFPLFFBQVEsU0FBUyx1QkFBdUIsU0FBUyxJQUFJO0FBQUE7QUFHaEcsU0FBUyxZQUFZLENBQUMsVUFBb0IsS0FBYTtBQUFBLEVBQzVELFNBQVMsUUFBUSxPQUFPLGNBQWMsR0FBRyw2QkFBNkI7QUFBQTs7QUNqQnhFLGVBQXNCLEdBQUcsR0FBRyxPQUFPLFNBQVMsWUFBMkI7QUFBQSxFQUVyRSxNQUFNLFVBQW9CLE1BQU0sSUFBSSxTQUFTO0FBQUEsRUFDN0MsTUFBTSxXQUFXLE1BQU0sUUFBUSxTQUFTLFlBQVk7QUFBQSxFQUNwRCxVQUFVLFVBQVUsYUFBYSxPQUFRLGNBQXdCLEtBQUssRUFBZTtBQUFBLEVBQ3JGLE9BQU87QUFBQTsiLAogICJkZWJ1Z0lkIjogIjdBRjcwQjg2NThCNEQzRDA2NDc1NkUyMTY0NzU2RTIxIiwKICAibmFtZXMiOiBbXQp9

View File

@@ -1,31 +0,0 @@
// @bun
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
enumerable: true
});
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __require = import.meta.require;
// src/api/auth/index.ts
function GET(request) {
const eveauth = request.store.get("eveauth");
return eveauth.redirect();
}
export {
GET
};
//# debugId=2B9DFE71D16A520B64756E2164756E21
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi5cXHNyY1xcYXBpXFxhdXRoXFxpbmRleC50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsKICAgICJpbXBvcnQgdHlwZSB7IFJlcXVlc3RDb250ZXh0IH0gZnJvbSAnYnJpc2EnO1xuaW1wb3J0IHR5cGUgeyBFVkVBdXRoIH0gZnJvbSAnQC9taWRkbGV3YXJlJztcblxuLy8gR0VUIC9hcGkvYXV0aC9cbmV4cG9ydCBmdW5jdGlvbiBHRVQocmVxdWVzdDogUmVxdWVzdENvbnRleHQpIHtcbiAgY29uc3QgZXZlYXV0aDogRVZFQXV0aCA9ICByZXF1ZXN0LnN0b3JlLmdldCgnZXZlYXV0aCcpO1xuICByZXR1cm4gZXZlYXV0aC5yZWRpcmVjdCgpO1xufSIKICBdLAogICJtYXBwaW5ncyI6ICI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUlPLFNBQVMsR0FBRyxDQUFDLFNBQXlCO0FBQUEsRUFDM0MsTUFBTSxVQUFvQixRQUFRLE1BQU0sSUFBSSxTQUFTO0FBQUEsRUFDckQsT0FBTyxRQUFRLFNBQVM7QUFBQTsiLAogICJkZWJ1Z0lkIjogIjJCOURGRTcxRDE2QTUyMEI2NDc1NkUyMTY0NzU2RTIxIiwKICAibmFtZXMiOiBbXQp9

View File

@@ -1 +0,0 @@
export default ["style-4253422825010316650.css","style-4799912372787954844.css","style-16814944961881043919.css"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
l$=new Set;u$=(k)=>{let g=(f)=>document.getElementById(f);l$.add(k);for(let f of l$){let j=g(`S:${f}`),h=g(`U:${f}`);if(j&&h)l$.delete(f),j.replaceWith(h.content.cloneNode(!0)),h.remove(),g(`R:${f}`)?.remove()}};

View File

@@ -1 +0,0 @@
\pages\index.js

File diff suppressed because one or more lines are too long

View File

@@ -1,56 +0,0 @@
// @bun
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
enumerable: true
});
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __require = import.meta.require;
// node_modules/brisa/jsx-runtime/index.js
var n = Symbol.for("isJSX");
function S(r) {
return i(null, r);
}
function i(r, { children: e, ...o }, s) {
let a = e;
if (Array.isArray(e) && !l(e))
a = e.map((t) => t?.[n] ? t : S({ children: t }));
return Object.assign([r, { ...o, key: s }, a], { [n]: true });
}
function l(r) {
return Array.isArray(r) && ((n in r) || m(r));
}
function m(r) {
return r?.[0] === "HTML" && typeof r[1]?.html === "string";
}
// src/pages/auth/error.tsx
function Error() {
return i(S, {
children: i("div", {
children: [i("h1", {
children: "EVE auth failed, close this tab and try again."
}, undefined, false, undefined, this), i("p", {
children: "If this issue persists reach out to an Administrator."
}, undefined, false, undefined, this)]
}, undefined, true, undefined, this)
}, undefined, false, undefined, this);
}
export {
Error as default
};
//# debugId=F0E3E3449C64DA0864756E2164756E21
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi5cXG5vZGVfbW9kdWxlc1xcYnJpc2FcXGpzeC1ydW50aW1lXFxpbmRleC5qcyIsICIuLlxcc3JjXFxwYWdlc1xcYXV0aFxcZXJyb3IudHN4Il0sCiAgInNvdXJjZXNDb250ZW50IjogWwogICAgIi8vIEBidW5cbnZhciBuPVN5bWJvbC5mb3IoXCJpc0pTWFwiKTtmdW5jdGlvbiBTKHIpe3JldHVybiBpKG51bGwscil9ZnVuY3Rpb24gaShyLHtjaGlsZHJlbjplLC4uLm99LHMpe2xldCBhPWU7aWYoQXJyYXkuaXNBcnJheShlKSYmIWwoZSkpYT1lLm1hcCgodCk9PnQ/LltuXT90OlMoe2NoaWxkcmVuOnR9KSk7cmV0dXJuIE9iamVjdC5hc3NpZ24oW3Isey4uLm8sa2V5OnN9LGFdLHtbbl06ITB9KX1mdW5jdGlvbiBsKHIpe3JldHVybiBBcnJheS5pc0FycmF5KHIpJiYoKG4gaW4gcil8fG0ocikpfWZ1bmN0aW9uIG0ocil7cmV0dXJuIHI/LlswXT09PVwiSFRNTFwiJiZ0eXBlb2YgclsxXT8uaHRtbD09PVwic3RyaW5nXCJ9ZXhwb3J0e2kgYXMganN4cyxpIGFzIGpzeERFVixpIGFzIGpzeCxtIGFzIGlzRGFuZ2VySFRNTCxsIGFzIGlzQXJyYXdPZkpTWENvbnRlbnQsUyBhcyBGcmFnbWVudH07XG4iLAogICAgImV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEVycm9yKCkge1xuICByZXR1cm4ganN4REVWXzd4ODFoMGtuKEZyYWdtZW50Xzh2Zzl4M3NxLCB7XG4gICAgY2hpbGRyZW46IGpzeERFVl83eDgxaDBrbihcImRpdlwiLCB7XG4gICAgICBjaGlsZHJlbjogW2pzeERFVl83eDgxaDBrbihcImgxXCIsIHtcbiAgICAgICAgY2hpbGRyZW46IFwiRVZFIGF1dGggZmFpbGVkLCBjbG9zZSB0aGlzIHRhYiBhbmQgdHJ5IGFnYWluLlwiXG4gICAgICB9LCB1bmRlZmluZWQsIGZhbHNlLCB1bmRlZmluZWQsIHRoaXMpLCBqc3hERVZfN3g4MWgwa24oXCJwXCIsIHtcbiAgICAgICAgY2hpbGRyZW46IFwiSWYgdGhpcyBpc3N1ZSBwZXJzaXN0cyByZWFjaCBvdXQgdG8gYW4gQWRtaW5pc3RyYXRvci5cIlxuICAgICAgfSwgdW5kZWZpbmVkLCBmYWxzZSwgdW5kZWZpbmVkLCB0aGlzKV1cbiAgICB9LCB1bmRlZmluZWQsIHRydWUsIHVuZGVmaW5lZCwgdGhpcylcbiAgfSwgdW5kZWZpbmVkLCBmYWxzZSwgdW5kZWZpbmVkLCB0aGlzKTtcbn1cbmltcG9ydCB7IGpzeCBhcyBqc3hfdzc3eWFmczQsIGpzeHMgYXMganN4c19laDZjNzhuaiwganN4REVWIGFzIGpzeERFVl83eDgxaDBrbiwgRnJhZ21lbnQgYXMgRnJhZ21lbnRfOHZnOXgzc3EgfSBmcm9tICdicmlzYS9qc3gtcnVudGltZSc7XG4iCiAgXSwKICAibWFwcGluZ3MiOiAiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFDQSxJQUFJLElBQUUsT0FBTyxJQUFJLE9BQU87QUFBRSxTQUFTLENBQUMsQ0FBQyxHQUFFO0FBQUEsRUFBQyxPQUFPLEVBQUUsTUFBSyxDQUFDO0FBQUE7QUFBRSxTQUFTLENBQUMsQ0FBQyxLQUFHLFVBQVMsTUFBSyxLQUFHLEdBQUU7QUFBQSxFQUFDLElBQUksSUFBRTtBQUFBLEVBQUUsSUFBRyxNQUFNLFFBQVEsQ0FBQyxNQUFJLEVBQUUsQ0FBQztBQUFBLElBQUUsSUFBRSxFQUFFLElBQUksQ0FBQyxNQUFJLElBQUksS0FBRyxJQUFFLEVBQUUsRUFBQyxVQUFTLEVBQUMsQ0FBQyxDQUFDO0FBQUEsRUFBRSxPQUFPLE9BQU8sT0FBTyxDQUFDLEdBQUUsS0FBSSxHQUFFLEtBQUksRUFBQyxHQUFFLENBQUMsR0FBRSxHQUFFLElBQUcsS0FBRSxDQUFDO0FBQUE7QUFBRSxTQUFTLENBQUMsQ0FBQyxHQUFFO0FBQUEsRUFBQyxPQUFPLE1BQU0sUUFBUSxDQUFDLE9BQUssS0FBSyxNQUFJLEVBQUUsQ0FBQztBQUFBO0FBQUcsU0FBUyxDQUFDLENBQUMsR0FBRTtBQUFBLEVBQUMsT0FBTyxJQUFJLE9BQUssVUFBUSxPQUFPLEVBQUUsSUFBSSxTQUFPO0FBQUE7OztBQ0R6VSxTQUF3QixLQUFLLEdBQUc7QUFBQSxFQUM5QixPQUFPLEVBQWdCLEdBQW1CO0FBQUEsSUFDeEMsVUFBVSxFQUFnQixPQUFPO0FBQUEsTUFDL0IsVUFBVSxDQUFDLEVBQWdCLE1BQU07QUFBQSxRQUMvQixVQUFVO0FBQUEsTUFDWixHQUFHLFdBQVcsT0FBTyxXQUFXLElBQUksR0FBRyxFQUFnQixLQUFLO0FBQUEsUUFDMUQsVUFBVTtBQUFBLE1BQ1osR0FBRyxXQUFXLE9BQU8sV0FBVyxJQUFJLENBQUM7QUFBQSxJQUN2QyxHQUFHLFdBQVcsTUFBTSxXQUFXLElBQUk7QUFBQSxFQUNyQyxHQUFHLFdBQVcsT0FBTyxXQUFXLElBQUk7QUFBQTsiLAogICJkZWJ1Z0lkIjogIkYwRTNFMzQ0OUM2NERBMDg2NDc1NkUyMTY0NzU2RTIxIiwKICAibmFtZXMiOiBbXQp9

View File

@@ -1,55 +0,0 @@
// @bun
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
enumerable: true
});
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __require = import.meta.require;
// node_modules/brisa/jsx-runtime/index.js
var n = Symbol.for("isJSX");
function S(r) {
return i(null, r);
}
function i(r, { children: e, ...o }, s) {
let a = e;
if (Array.isArray(e) && !l(e))
a = e.map((t) => t?.[n] ? t : S({ children: t }));
return Object.assign([r, { ...o, key: s }, a], { [n]: true });
}
function l(r) {
return Array.isArray(r) && ((n in r) || m(r));
}
function m(r) {
return r?.[0] === "HTML" && typeof r[1]?.html === "string";
}
// src/pages/auth/success.tsx
function Success() {
return i(S, {
children: i("div", {
class: "hero",
children: i("h1", {
children: "EVE auth success, you may close this browser tab."
}, undefined, false, undefined, this)
}, undefined, false, undefined, this)
}, undefined, false, undefined, this);
}
export {
Success as default
};
//# debugId=DE19EB2F6E2E7A6A64756E2164756E21
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi5cXG5vZGVfbW9kdWxlc1xcYnJpc2FcXGpzeC1ydW50aW1lXFxpbmRleC5qcyIsICIuLlxcc3JjXFxwYWdlc1xcYXV0aFxcc3VjY2Vzcy50c3giXSwKICAic291cmNlc0NvbnRlbnQiOiBbCiAgICAiLy8gQGJ1blxudmFyIG49U3ltYm9sLmZvcihcImlzSlNYXCIpO2Z1bmN0aW9uIFMocil7cmV0dXJuIGkobnVsbCxyKX1mdW5jdGlvbiBpKHIse2NoaWxkcmVuOmUsLi4ub30scyl7bGV0IGE9ZTtpZihBcnJheS5pc0FycmF5KGUpJiYhbChlKSlhPWUubWFwKCh0KT0+dD8uW25dP3Q6Uyh7Y2hpbGRyZW46dH0pKTtyZXR1cm4gT2JqZWN0LmFzc2lnbihbcix7Li4ubyxrZXk6c30sYV0se1tuXTohMH0pfWZ1bmN0aW9uIGwocil7cmV0dXJuIEFycmF5LmlzQXJyYXkocikmJigobiBpbiByKXx8bShyKSl9ZnVuY3Rpb24gbShyKXtyZXR1cm4gcj8uWzBdPT09XCJIVE1MXCImJnR5cGVvZiByWzFdPy5odG1sPT09XCJzdHJpbmdcIn1leHBvcnR7aSBhcyBqc3hzLGkgYXMganN4REVWLGkgYXMganN4LG0gYXMgaXNEYW5nZXJIVE1MLGwgYXMgaXNBcnJhd09mSlNYQ29udGVudCxTIGFzIEZyYWdtZW50fTtcbiIsCiAgICAiZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gU3VjY2VzcygpIHtcbiAgcmV0dXJuIGpzeERFVl83eDgxaDBrbihGcmFnbWVudF84dmc5eDNzcSwge1xuICAgIGNoaWxkcmVuOiBqc3hERVZfN3g4MWgwa24oXCJkaXZcIiwge1xuICAgICAgY2xhc3M6IFwiaGVyb1wiLFxuICAgICAgY2hpbGRyZW46IGpzeERFVl83eDgxaDBrbihcImgxXCIsIHtcbiAgICAgICAgY2hpbGRyZW46IFwiRVZFIGF1dGggc3VjY2VzcywgeW91IG1heSBjbG9zZSB0aGlzIGJyb3dzZXIgdGFiLlwiXG4gICAgICB9LCB1bmRlZmluZWQsIGZhbHNlLCB1bmRlZmluZWQsIHRoaXMpXG4gICAgfSwgdW5kZWZpbmVkLCBmYWxzZSwgdW5kZWZpbmVkLCB0aGlzKVxuICB9LCB1bmRlZmluZWQsIGZhbHNlLCB1bmRlZmluZWQsIHRoaXMpO1xufVxuaW1wb3J0IHsganN4IGFzIGpzeF93Nzd5YWZzNCwganN4cyBhcyBqc3hzX2VoNmM3OG5qLCBqc3hERVYgYXMganN4REVWXzd4ODFoMGtuLCBGcmFnbWVudCBhcyBGcmFnbWVudF84dmc5eDNzcSB9IGZyb20gJ2JyaXNhL2pzeC1ydW50aW1lJztcbiIKICBdLAogICJtYXBwaW5ncyI6ICI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUNBLElBQUksSUFBRSxPQUFPLElBQUksT0FBTztBQUFFLFNBQVMsQ0FBQyxDQUFDLEdBQUU7QUFBQSxFQUFDLE9BQU8sRUFBRSxNQUFLLENBQUM7QUFBQTtBQUFFLFNBQVMsQ0FBQyxDQUFDLEtBQUcsVUFBUyxNQUFLLEtBQUcsR0FBRTtBQUFBLEVBQUMsSUFBSSxJQUFFO0FBQUEsRUFBRSxJQUFHLE1BQU0sUUFBUSxDQUFDLE1BQUksRUFBRSxDQUFDO0FBQUEsSUFBRSxJQUFFLEVBQUUsSUFBSSxDQUFDLE1BQUksSUFBSSxLQUFHLElBQUUsRUFBRSxFQUFDLFVBQVMsRUFBQyxDQUFDLENBQUM7QUFBQSxFQUFFLE9BQU8sT0FBTyxPQUFPLENBQUMsR0FBRSxLQUFJLEdBQUUsS0FBSSxFQUFDLEdBQUUsQ0FBQyxHQUFFLEdBQUUsSUFBRyxLQUFFLENBQUM7QUFBQTtBQUFFLFNBQVMsQ0FBQyxDQUFDLEdBQUU7QUFBQSxFQUFDLE9BQU8sTUFBTSxRQUFRLENBQUMsT0FBSyxLQUFLLE1BQUksRUFBRSxDQUFDO0FBQUE7QUFBRyxTQUFTLENBQUMsQ0FBQyxHQUFFO0FBQUEsRUFBQyxPQUFPLElBQUksT0FBSyxVQUFRLE9BQU8sRUFBRSxJQUFJLFNBQU87QUFBQTs7O0FDRHpVLFNBQXdCLE9BQU8sR0FBRztBQUFBLEVBQ2hDLE9BQU8sRUFBZ0IsR0FBbUI7QUFBQSxJQUN4QyxVQUFVLEVBQWdCLE9BQU87QUFBQSxNQUMvQixPQUFPO0FBQUEsTUFDUCxVQUFVLEVBQWdCLE1BQU07QUFBQSxRQUM5QixVQUFVO0FBQUEsTUFDWixHQUFHLFdBQVcsT0FBTyxXQUFXLElBQUk7QUFBQSxJQUN0QyxHQUFHLFdBQVcsT0FBTyxXQUFXLElBQUk7QUFBQSxFQUN0QyxHQUFHLFdBQVcsT0FBTyxXQUFXLElBQUk7QUFBQTsiLAogICJkZWJ1Z0lkIjogIkRFMTlFQjJGNkUyRTdBNkE2NDc1NkUyMTY0NzU2RTIxIiwKICAibmFtZXMiOiBbXQp9

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 200 199" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-1.46627e-05,-0.862671)">
<path d="M124.615,158.942C194.326,134.262 190.916,42.893 140.767,0.863C184.318,20.349 206.756,72.952 198.203,118.905C191.512,154.854 164.558,184.522 129.69,194.524C111.34,199.788 91.695,200.819 73.085,196.201C36.436,187.108 7.653,155.954 1.303,118.778C-3.783,88.998 6.075,58.343 29.922,38.996C54.808,18.806 92.107,14.056 119.324,32.517C146.142,50.708 155.776,89.452 135.231,116.111C124.748,129.714 107.413,137.633 90.191,135.012C72.184,132.272 58.17,116.038 60.606,97.494C63.655,121.563 92.134,130.674 109.243,113.883C125.024,98.394 116.512,74.337 100.517,62.603C87.184,52.821 68.389,55.268 55.26,64.22C39.06,75.266 32.739,94.717 36.289,113.574C44.063,154.866 87.626,172.037 124.615,158.942L124.615,158.942Z" style="fill:url(#_Radial1);"/>
</g>
<defs>
<radialGradient id="_Radial1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(52.186,-99.1373,99.1373,52.186,88.8208,100)"><stop offset="0" style="stop-color:rgb(44,240,204);stop-opacity:1"/><stop offset="0.53" style="stop-color:rgb(44,230,209);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(44,195,228);stop-opacity:1"/></radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
footer {
display: grid;
font-family: "Poppins", sans-serif;
font-size: 27px;
line-height: 1.5;
height: 15vh;
place-items: center;
}

View File

@@ -1 +0,0 @@
/* Navigation Styles */

View File

@@ -1,329 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "Star Kitten Web",
"dependencies": {
"brisa": "0.2.7",
"brisa-tailwindcss": "0.2.7",
"drizzle-orm": "^0.40.0",
"oslo": "^1.2.1",
},
"devDependencies": {
"@types/bun": "latest",
"daisyui": "^5.0.0",
"drizzle-kit": "^0.30.5",
"typescript": "latest",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@emnapi/core": ["@emnapi/core@0.45.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw=="],
"@emnapi/runtime": ["@emnapi/runtime@0.45.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
"@node-rs/argon2": ["@node-rs/argon2@1.7.0", "", { "optionalDependencies": { "@node-rs/argon2-android-arm-eabi": "1.7.0", "@node-rs/argon2-android-arm64": "1.7.0", "@node-rs/argon2-darwin-arm64": "1.7.0", "@node-rs/argon2-darwin-x64": "1.7.0", "@node-rs/argon2-freebsd-x64": "1.7.0", "@node-rs/argon2-linux-arm-gnueabihf": "1.7.0", "@node-rs/argon2-linux-arm64-gnu": "1.7.0", "@node-rs/argon2-linux-arm64-musl": "1.7.0", "@node-rs/argon2-linux-x64-gnu": "1.7.0", "@node-rs/argon2-linux-x64-musl": "1.7.0", "@node-rs/argon2-wasm32-wasi": "1.7.0", "@node-rs/argon2-win32-arm64-msvc": "1.7.0", "@node-rs/argon2-win32-ia32-msvc": "1.7.0", "@node-rs/argon2-win32-x64-msvc": "1.7.0" } }, "sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog=="],
"@node-rs/argon2-android-arm-eabi": ["@node-rs/argon2-android-arm-eabi@1.7.0", "", { "os": "android", "cpu": "arm" }, "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg=="],
"@node-rs/argon2-android-arm64": ["@node-rs/argon2-android-arm64@1.7.0", "", { "os": "android", "cpu": "arm64" }, "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A=="],
"@node-rs/argon2-darwin-arm64": ["@node-rs/argon2-darwin-arm64@1.7.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw=="],
"@node-rs/argon2-darwin-x64": ["@node-rs/argon2-darwin-x64@1.7.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw=="],
"@node-rs/argon2-freebsd-x64": ["@node-rs/argon2-freebsd-x64@1.7.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA=="],
"@node-rs/argon2-linux-arm-gnueabihf": ["@node-rs/argon2-linux-arm-gnueabihf@1.7.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg=="],
"@node-rs/argon2-linux-arm64-gnu": ["@node-rs/argon2-linux-arm64-gnu@1.7.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g=="],
"@node-rs/argon2-linux-arm64-musl": ["@node-rs/argon2-linux-arm64-musl@1.7.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A=="],
"@node-rs/argon2-linux-x64-gnu": ["@node-rs/argon2-linux-x64-gnu@1.7.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ=="],
"@node-rs/argon2-linux-x64-musl": ["@node-rs/argon2-linux-x64-musl@1.7.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA=="],
"@node-rs/argon2-wasm32-wasi": ["@node-rs/argon2-wasm32-wasi@1.7.0", "", { "dependencies": { "@emnapi/core": "^0.45.0", "@emnapi/runtime": "^0.45.0", "@tybys/wasm-util": "^0.8.1", "memfs-browser": "^3.4.13000" }, "cpu": "none" }, "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w=="],
"@node-rs/argon2-win32-arm64-msvc": ["@node-rs/argon2-win32-arm64-msvc@1.7.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA=="],
"@node-rs/argon2-win32-ia32-msvc": ["@node-rs/argon2-win32-ia32-msvc@1.7.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg=="],
"@node-rs/argon2-win32-x64-msvc": ["@node-rs/argon2-win32-x64-msvc@1.7.0", "", { "os": "win32", "cpu": "x64" }, "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q=="],
"@node-rs/bcrypt": ["@node-rs/bcrypt@1.9.0", "", { "optionalDependencies": { "@node-rs/bcrypt-android-arm-eabi": "1.9.0", "@node-rs/bcrypt-android-arm64": "1.9.0", "@node-rs/bcrypt-darwin-arm64": "1.9.0", "@node-rs/bcrypt-darwin-x64": "1.9.0", "@node-rs/bcrypt-freebsd-x64": "1.9.0", "@node-rs/bcrypt-linux-arm-gnueabihf": "1.9.0", "@node-rs/bcrypt-linux-arm64-gnu": "1.9.0", "@node-rs/bcrypt-linux-arm64-musl": "1.9.0", "@node-rs/bcrypt-linux-x64-gnu": "1.9.0", "@node-rs/bcrypt-linux-x64-musl": "1.9.0", "@node-rs/bcrypt-wasm32-wasi": "1.9.0", "@node-rs/bcrypt-win32-arm64-msvc": "1.9.0", "@node-rs/bcrypt-win32-ia32-msvc": "1.9.0", "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" } }, "sha512-u2OlIxW264bFUfvbFqDz9HZKFjwe8FHFtn7T/U8mYjPZ7DWYpbUB+/dkW/QgYfMSfR0ejkyuWaBBe0coW7/7ig=="],
"@node-rs/bcrypt-android-arm-eabi": ["@node-rs/bcrypt-android-arm-eabi@1.9.0", "", { "os": "android", "cpu": "arm" }, "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA=="],
"@node-rs/bcrypt-android-arm64": ["@node-rs/bcrypt-android-arm64@1.9.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A=="],
"@node-rs/bcrypt-darwin-arm64": ["@node-rs/bcrypt-darwin-arm64@1.9.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw=="],
"@node-rs/bcrypt-darwin-x64": ["@node-rs/bcrypt-darwin-x64@1.9.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug=="],
"@node-rs/bcrypt-freebsd-x64": ["@node-rs/bcrypt-freebsd-x64@1.9.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg=="],
"@node-rs/bcrypt-linux-arm-gnueabihf": ["@node-rs/bcrypt-linux-arm-gnueabihf@1.9.0", "", { "os": "linux", "cpu": "arm" }, "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA=="],
"@node-rs/bcrypt-linux-arm64-gnu": ["@node-rs/bcrypt-linux-arm64-gnu@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q=="],
"@node-rs/bcrypt-linux-arm64-musl": ["@node-rs/bcrypt-linux-arm64-musl@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew=="],
"@node-rs/bcrypt-linux-x64-gnu": ["@node-rs/bcrypt-linux-x64-gnu@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ=="],
"@node-rs/bcrypt-linux-x64-musl": ["@node-rs/bcrypt-linux-x64-musl@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg=="],
"@node-rs/bcrypt-wasm32-wasi": ["@node-rs/bcrypt-wasm32-wasi@1.9.0", "", { "dependencies": { "@emnapi/core": "^0.45.0", "@emnapi/runtime": "^0.45.0", "@tybys/wasm-util": "^0.8.1", "memfs-browser": "^3.4.13000" }, "cpu": "none" }, "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw=="],
"@node-rs/bcrypt-win32-arm64-msvc": ["@node-rs/bcrypt-win32-arm64-msvc@1.9.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw=="],
"@node-rs/bcrypt-win32-ia32-msvc": ["@node-rs/bcrypt-win32-ia32-msvc@1.9.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA=="],
"@node-rs/bcrypt-win32-x64-msvc": ["@node-rs/bcrypt-win32-x64-msvc@1.9.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w=="],
"@petamoriken/float16": ["@petamoriken/float16@3.9.1", "", {}, "sha512-j+ejhYwY6PeB+v1kn7lZFACUIG97u90WxMuGosILFsl9d4Ovi0sjk0GlPfoEcx+FzvXZDAfioD+NGnnPamXgMA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.0.9", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.9" } }, "sha512-tOJvdI7XfJbARYhxX+0RArAhmuDcczTC46DGCEziqxzzbIaPnfYaIyRT31n4u8lROrsO7Q6u/K9bmQHL2uL1bQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.9", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.9", "@tailwindcss/oxide-darwin-arm64": "4.0.9", "@tailwindcss/oxide-darwin-x64": "4.0.9", "@tailwindcss/oxide-freebsd-x64": "4.0.9", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.9", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.9", "@tailwindcss/oxide-linux-arm64-musl": "4.0.9", "@tailwindcss/oxide-linux-x64-gnu": "4.0.9", "@tailwindcss/oxide-linux-x64-musl": "4.0.9", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.9", "@tailwindcss/oxide-win32-x64-msvc": "4.0.9" } }, "sha512-eLizHmXFqHswJONwfqi/WZjtmWZpIalpvMlNhTM99/bkHtUs6IqgI1XQ0/W5eO2HiRQcIlXUogI2ycvKhVLNcA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.9", "", { "os": "android", "cpu": "arm64" }, "sha512-YBgy6+2flE/8dbtrdotVInhMVIxnHJPbAwa7U1gX4l2ThUIaPUp18LjB9wEH8wAGMBZUb//SzLtdXXNBHPUl6Q=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pWdl4J2dIHXALgy2jVkwKBmtEb73kqIfMpYmcgESr7oPQ+lbcQ4+tlPeVXaSAmang+vglAfFpXQCOvs/aGSqlw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4Dq3lKp0/C7vrRSkNPtBGVebEyWt9QPPlQctxJ0H3MDyiQYvzVYf8jKow7h5QkWNe8hbatEqljMj/Y0M+ERYJg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-k7U1RwRODta8x0uealtVt3RoWAWqA+D5FAOsvVGpYoI6ObgmnzqWW6pnVwz70tL8UZ/QXjeMyiICXyjzB6OGtQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.9", "", { "os": "linux", "cpu": "arm" }, "sha512-NDDjVweHz2zo4j+oS8y3KwKL5wGCZoXGA9ruJM982uVJLdsF8/1AeKvUwKRlMBpxHt1EdWJSAh8a0Mfhl28GlQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-jk90UZ0jzJl3Dy1BhuFfRZ2KP9wVKMXPjmCtY4U6fF2LvrjP5gWFJj5VHzfzHonJexjrGe1lMzgtjriuZkxagg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-3eMjyTC6HBxh9nRgOHzrc96PYh1/jWOwHZ3Kk0JN0Kl25BJ80Lj9HEvvwVDNTgPg154LdICwuFLuhfgH9DULmg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.9", "", { "os": "linux", "cpu": "x64" }, "sha512-v0D8WqI/c3WpWH1kq/HP0J899ATLdGZmENa2/emmNjubT0sWtEke9W9+wXeEoACuGAhF9i3PO5MeyditpDCiWQ=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.9", "", { "os": "linux", "cpu": "x64" }, "sha512-Kvp0TCkfeXyeehqLJr7otsc4hd/BUPfcIGrQiwsTVCfaMfjQZCG7DjI+9/QqPZha8YapLA9UoIcUILRYO7NE1Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-m3+60T/7YvWekajNq/eexjhV8z10rswcz4BC9bioJ7YaN+7K8W2AmLmG0B79H14m6UHE571qB0XsPus4n0QVgQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.9", "", { "os": "win32", "cpu": "x64" }, "sha512-dpc05mSlqkwVNOUjGu/ZXd5U1XNch1kHFJ4/cHkZFvaW1RzbHmRt24gvM8/HC6IirMxNarzVw4IXVtvrOoZtxA=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.0.9", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.0.9", "@tailwindcss/oxide": "4.0.9", "lightningcss": "^1.29.1", "postcss": "^8.4.41", "tailwindcss": "4.0.9" } }, "sha512-BT/E+pdMqulavEAVM5NCpxmGEwHiLDPpkmg/c/X25ZBW+izTe+aZ+v1gf/HXTrihRoCxrUp5U4YyHsBTzspQKQ=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.8.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q=="],
"@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
"@types/node": ["@types/node@22.13.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"brisa": ["brisa@0.2.7", "", { "dependencies": { "astring": "1.9.0", "csstype": "3.1.3", "diff-dom-streaming": "0.6.5", "meriyah": "6.0.5" }, "bin": { "brisa": "index.js" } }, "sha512-76fjLQkwfbAxdkA7bpQf7RUdvwsuC6l1nsNndn3yL6C2uD+SQ9RvHTuqwt8hWOz37j2zfN31AIxkJkuA9zwFMg=="],
"brisa-tailwindcss": ["brisa-tailwindcss@0.2.7", "", { "peerDependencies": { "@tailwindcss/postcss": "^4.0.0", "postcss": "^8.4.48", "tailwindcss": "^4.0.1" } }, "sha512-ZH4iSEWqY1V+G2sfn7l9YpR/d0AcEK+pWbpIQgoPeFG/qeyDVihHahg7Vp2ZWVp8Bbqq4v0vUhI09zV+OJLFdA=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"daisyui": ["daisyui@5.0.0", "", {}, "sha512-U0K9Bac3Bi3zZGm6ojrw12F0vBHTpEgf46zv/BYxLe07hF0Xnx7emIQliwaRBgJuYhY0BhwQ6wSnq5cJXHA2yA=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"diff-dom-streaming": ["diff-dom-streaming@0.6.5", "", {}, "sha512-S6a7AVm5S0cwOkIYLTg+ilPaCnHM4/QoPol11PLYRKYqud/MfW+wW54+K2zBFqNY3DoNZZXO8yaF3iwFeJkz+A=="],
"drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
"drizzle-orm": ["drizzle-orm@0.40.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7ptk/HQiMSrEZHnAsSlBESXWj52VwgMmyTEfoNmpNN2ZXpcz13LwHfXTIghsAEud7Z5UJhDOp8U07ujcqme7wg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
"fs-monkey": ["fs-monkey@1.0.6", "", {}, "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg=="],
"gel": ["gel@2.0.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-Oq3Fjay71s00xzDc0BF/mpcLmnA+uRqMEJK8p5K4PaZjUEsxaeo+kR9OHBVAf289/qPd+0OcLOLUN0UhqiUCog=="],
"get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
"lightningcss": ["lightningcss@1.29.1", "", { "dependencies": { "detect-libc": "^1.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.1", "lightningcss-darwin-x64": "1.29.1", "lightningcss-freebsd-x64": "1.29.1", "lightningcss-linux-arm-gnueabihf": "1.29.1", "lightningcss-linux-arm64-gnu": "1.29.1", "lightningcss-linux-arm64-musl": "1.29.1", "lightningcss-linux-x64-gnu": "1.29.1", "lightningcss-linux-x64-musl": "1.29.1", "lightningcss-win32-arm64-msvc": "1.29.1", "lightningcss-win32-x64-msvc": "1.29.1" } }, "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.1", "", { "os": "linux", "cpu": "arm" }, "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.1", "", { "os": "win32", "cpu": "x64" }, "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q=="],
"memfs": ["memfs@3.5.3", "", { "dependencies": { "fs-monkey": "^1.0.4" } }, "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw=="],
"memfs-browser": ["memfs-browser@3.5.10302", "", { "dependencies": { "memfs": "3.5.3" } }, "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw=="],
"meriyah": ["meriyah@6.0.5", "", {}, "sha512-SrMqQCox7TTwtftWKHy/ZaVe+ZRpRl20pAgDo+PS9hzcAJrMjYsBJQPPiLXTnjztrqdfGS+Zz99r6Bwvydta1w=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"oslo": ["oslo@1.2.1", "", { "dependencies": { "@node-rs/argon2": "1.7.0", "@node-rs/bcrypt": "1.9.0" } }, "sha512-HfIhB5ruTdQv0XX2XlncWQiJ5SIHZ7NHZhVyHth0CSZ/xzge00etRyYy/3wp/Dsu+PkxMC+6+B2lS/GcKoewkA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"tailwindcss": ["tailwindcss@4.0.9", "", {}, "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw=="],
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
}
}

View File

@@ -1,2 +0,0 @@
[test]
preload = "brisa/test"

View File

@@ -1,25 +0,0 @@
{
"name": "@star-kitten/eve-web",
"module": "src/pages/index.tsx",
"type": "module",
"scripts": {
"dev": "bunx dotenvx run -f .env.development -- brisa dev",
"dev:debug": "brisa dev --debug",
"build": "brisa build",
"start": "bunx dotenvx run -f .env.production -- brisa start"
},
"dependencies": {
"brisa": "0.2.7",
"brisa-tailwindcss": "0.2.7",
"drizzle-orm": "^0.40.0",
"oslo": "^1.2.1",
"@star-kitten/util": "workspace:^0.0.0",
"@star-kitten/eve": "workspace:^0.0.0"
},
"devDependencies": {
"@types/bun": "latest",
"daisyui": "^5.0.0",
"drizzle-kit": "^0.30.5",
"typescript": "latest"
}
}

View File

@@ -1,76 +0,0 @@
import type { RequestContext } from 'brisa';
import type { EVEAuth } from '@/middleware';
import { getCookies, removeCookie, setCookie } from '@/utils';
import { CharacterAPI, characterIdFromToken } from '@star-kitten/eve/esi';
import { CharacterHelper, UserHelper } from '@star-kitten/eve/db';
// GET /api/auth/callback
export async function GET(request: RequestContext) {
const eveauth: EVEAuth = request.store.get('eveauth');
const response = new Response('', { status: 302 });
try {
const cookies = getCookies(request.headers);
const cookieDiscordID = cookies['discordID'];
if (!cookieDiscordID) {
throw new Error(`Missing discordID cookie in /api/auth/callback`);
}
const cookieCharacterID = cookies['characterID'];
const token = await eveauth.validate(response);
const characterID = characterIdFromToken(token.access_token);
if (cookieCharacterID && parseInt(cookieCharacterID) !== characterID) {
throw new Error(`Character ID mismatch: ${cookieCharacterID} !== ${characterID}`);
}
let user = UserHelper.findByDiscordId(cookieDiscordID);
let character = CharacterHelper.findByUserAndEveID(user.id, Number(characterID));
if (!user) {
user = UserHelper.create(cookieDiscordID);
}
if (!user) {
throw new Error(`Something went wrong with creating a user for id: ${cookieDiscordID}`);
}
if (!character) {
const data = await CharacterAPI.getCharacterPublicData(characterID);
if (!data) {
throw new Error(`Failed to retreive character public data for id: ${characterID} - unable to create character`);
}
character = CharacterHelper.create(characterID, data.name || 'UNKNOWN NAME', user, token);
// refetch from db to get id
user = UserHelper.findByDiscordId(cookieDiscordID);
character = CharacterHelper.findByUserAndEveID(user.id, Number(characterID));
if (!character) {
throw new Error(`Failed to retreive character from db for id: ${characterID}`);
}
if (!user.mainCharacterID) {
user.mainCharacterID = character.id;
UserHelper.save(user);
}
} else {
// Update existing character with new token
character.accessToken = token.access_token;
character.expiresAt = new Date(Date.now() + token.expires_in * 1000);
character.refreshToken = token.refresh_token;
CharacterHelper.save(character);
}
setCookie(response, 'currentUser', user.id + '', 60 * 60 * 24 * 30 /* 30 days */);
response.headers.set('location', '/auth/success');
} catch (err) {
console.error(`Error: Callback failed with ${err}`);
response.headers.set('location', '/auth/error');
return response;
} finally {
removeCookie(response, 'discordID');
removeCookie(response, 'characterID');
removeCookie(response, 'state');
}
return response;
}

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