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 dist
node_modules node_modules
.env.keys .env.keys
data data
db db
coverage coverage

View File

@@ -1,179 +1,179 @@
# Memory Bank # 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. 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. 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 ## Memory Bank Structure
The Memory Bank consists of core files and optional context files, all in Markdown format. The Memory Bank consists of core files and optional context files, all in Markdown format.
### Core Files (Required) ### Core Files (Required)
1. `brief.md` 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. 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 - Foundation document that shapes all other files
- Created at project start if it doesn't exist - Created at project start if it doesn't exist
- Defines core requirements and goals - Defines core requirements and goals
- Source of truth for project scope - Source of truth for project scope
2. `product.md` 2. `product.md`
- Why this project exists - Why this project exists
- Problems it solves - Problems it solves
- How it should work - How it should work
- User experience goals - User experience goals
3. `context.md` 3. `context.md`
This file should be short and factual, not creative or speculative. This file should be short and factual, not creative or speculative.
- Current work focus - Current work focus
- Recent changes - Recent changes
- Next steps - Next steps
4. `architecture.md` 4. `architecture.md`
- System architecture - System architecture
- Source Code paths - Source Code paths
- Key technical decisions - Key technical decisions
- Design patterns in use - Design patterns in use
- Component relationships - Component relationships
- Critical implementation paths - Critical implementation paths
5. `tech.md` 5. `tech.md`
- Technologies used - Technologies used
- Development setup - Development setup
- Technical constraints - Technical constraints
- Dependencies - Dependencies
- Tool usage patterns - Tool usage patterns
### Additional Files ### Additional Files
Create additional files/folders within memory-bank/ when they help organize: Create additional files/folders within memory-bank/ when they help organize:
- `tasks.md` - Documentation of repetitive tasks and their workflows - `tasks.md` - Documentation of repetitive tasks and their workflows
- Complex feature documentation - Complex feature documentation
- Integration specifications - Integration specifications
- API documentation - API documentation
- Testing strategies - Testing strategies
- Deployment procedures - Deployment procedures
## Core workflows ## Core workflows
### Memory Bank Initialization ### 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. 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: 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 - All source code files and their relationships
- Configuration files and build system setup - Configuration files and build system setup
- Project structure and organization patterns - Project structure and organization patterns
- Documentation and comments - Documentation and comments
- Dependencies and external integrations - Dependencies and external integrations
- Testing frameworks and patterns - 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. 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. 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 Update
Memory Bank updates occur when: Memory Bank updates occur when:
1. Discovering new project patterns 1. Discovering new project patterns
2. After implementing significant changes 2. After implementing significant changes
3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files) 3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files)
4. When context needs clarification 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?" 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: To execute Memory Bank update, I will:
1. Review ALL project files 1. Review ALL project files
2. Document current state 2. Document current state
3. Document Insights & Patterns 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 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. 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 ### 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**. 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: 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 - Adding support for new AI model versions
- Implementing new API endpoints following established patterns - Implementing new API endpoints following established patterns
- Adding new features that follow existing architecture - 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. 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: To execute Add Task workflow:
1. Create or update `tasks.md` in the memory bank folder 1. Create or update `tasks.md` in the memory bank folder
2. Document the task with: 2. Document the task with:
- Task name and description - Task name and description
- Files that need to be modified - Files that need to be modified
- Step-by-step workflow followed - Step-by-step workflow followed
- Important considerations or gotchas - Important considerations or gotchas
- Example of the completed implementation - Example of the completed implementation
3. Include any context that was discovered during task execution but wasn't previously documented 3. Include any context that was discovered during task execution but wasn't previously documented
Example task entry: Example task entry:
```markdown ```markdown
## Add New Model Support ## Add New Model Support
**Last performed:** [date] **Last performed:** [date]
**Files to modify:** **Files to modify:**
- `/providers/gemini.md` - Add model to documentation - `/providers/gemini.md` - Add model to documentation
- `/src/providers/gemini-config.ts` - Add model configuration - `/src/providers/gemini-config.ts` - Add model configuration
- `/src/constants/models.ts` - Add to model list - `/src/constants/models.ts` - Add to model list
- `/tests/providers/gemini.test.ts` - Add test cases - `/tests/providers/gemini.test.ts` - Add test cases
**Steps:** **Steps:**
1. Add model configuration with proper token limits 1. Add model configuration with proper token limits
2. Update documentation with model capabilities 2. Update documentation with model capabilities
3. Add to constants file for UI display 3. Add to constants file for UI display
4. Write tests for new model configuration 4. Write tests for new model configuration
**Important notes:** **Important notes:**
- Check Google's documentation for exact token limits - Check Google's documentation for exact token limits
- Ensure backward compatibility with existing configurations - Ensure backward compatibility with existing configurations
- Test with actual API calls before committing - Test with actual API calls before committing
``` ```
### Regular Task Execution ### Regular Task Execution
In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional. 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: 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." "[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. 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?" 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. 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 ## Context Window Management
When the context window fills up during an extended session: When the context window fills up during an extended session:
1. I should suggest updating the memory bank to preserve the current state 1. I should suggest updating the memory bank to preserve the current state
2. Recommend starting a fresh conversation/task 2. Recommend starting a fresh conversation/task
3. In the new conversation, I will automatically load the memory bank files to maintain continuity 3. In the new conversation, I will automatically load the memory bank files to maintain continuity
## Technical Implementation ## 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. 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 ## 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. 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. 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. 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 # Architecture Overview - Star Kitten
## System Architecture ## System Architecture
Star Kitten follows a modular monorepo architecture with four main packages and two applications: Star Kitten follows a modular monorepo architecture with four main packages and two applications:
```mermaid ```mermaid
graph TB graph TB
subgraph "Applications" subgraph "Applications"
EB[eve-bot<br/>Discord Bot] EB[eve-bot<br/>Discord Bot]
EW[eve-web<br/>Brisa Web App] EW[eve-web<br/>Brisa Web App]
end end
subgraph "Core Packages" subgraph "Core Packages"
D[@star-kitten/discord<br/>Discord Framework] D[@star-kitten/discord<br/>Discord Framework]
E[@star-kitten/eve<br/>EVE Online Integration] E[@star-kitten/eve<br/>EVE Online Integration]
U[@star-kitten/util<br/>Shared Utilities] U[@star-kitten/util<br/>Shared Utilities]
end end
subgraph "External Services" subgraph "External Services"
DS[Discord API] DS[Discord API]
ESI[EVE ESI API] ESI[EVE ESI API]
JA[Janice API] JA[Janice API]
ET[EveTycoon API] ET[EveTycoon API]
end end
subgraph "Data Layer" subgraph "Data Layer"
DB[(SQLite Database)] DB[(SQLite Database)]
RD[(Reference Data<br/>JSON Files)] RD[(Reference Data<br/>JSON Files)]
end end
EB --> D EB --> D
EB --> E EB --> E
EW --> E EW --> E
EW --> U EW --> U
D --> DS D --> DS
E --> ESI E --> ESI
E --> JA E --> JA
E --> ET E --> ET
E --> DB E --> DB
E --> RD E --> RD
U --> DB U --> DB
``` ```
## Source Code Paths ## Source Code Paths
### Core Packages ### Core Packages
#### [@star-kitten/discord](packages/discord/) #### [@star-kitten/discord](packages/discord/)
- **Entry Point**: [`packages/discord/src/index.ts`](packages/discord/src/index.ts) - **Entry Point**: [`packages/discord/src/index.ts`](packages/discord/src/index.ts)
- **Command System**: [`packages/discord/src/commands/`](packages/discord/src/commands/) - **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 - [`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 - [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) - Command execution handler
- **Localization**: [`packages/discord/src/locales.ts`](packages/discord/src/locales.ts) - **Localization**: [`packages/discord/src/locales.ts`](packages/discord/src/locales.ts)
#### [@star-kitten/eve](packages/eve/) #### [@star-kitten/eve](packages/eve/)
- **Entry Point**: [`packages/eve/src/index.ts`](packages/eve/src/index.ts) - **Entry Point**: [`packages/eve/src/index.ts`](packages/eve/src/index.ts)
- **Database Layer**: [`packages/eve/src/db/`](packages/eve/src/db/) - **Database Layer**: [`packages/eve/src/db/`](packages/eve/src/db/)
- [`schema.ts`](packages/eve/src/db/schema.ts) - Drizzle ORM schema definitions - [`schema.ts`](packages/eve/src/db/schema.ts) - Drizzle ORM schema definitions
- [`index.ts`](packages/eve/src/db/index.ts) - Database connection and exports - [`index.ts`](packages/eve/src/db/index.ts) - Database connection and exports
- [`models/`](packages/eve/src/db/models/) - Character and user model helpers - [`models/`](packages/eve/src/db/models/) - Character and user model helpers
- **ESI Integration**: [`packages/eve/src/esi/`](packages/eve/src/esi/) - **ESI Integration**: [`packages/eve/src/esi/`](packages/eve/src/esi/)
- [`auth.ts`](packages/eve/src/esi/auth.ts) - EVE SSO authentication flow - [`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 - [`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 - [`character.ts`](packages/eve/src/esi/character.ts) - Character API endpoints
- [`fetch.ts`](packages/eve/src/esi/fetch.ts) - ESI API client wrapper - [`fetch.ts`](packages/eve/src/esi/fetch.ts) - ESI API client wrapper
- **Data Models**: [`packages/eve/src/models/`](packages/eve/src/models/) - **Data Models**: [`packages/eve/src/models/`](packages/eve/src/models/)
- [`type.ts`](packages/eve/src/models/type.ts) - EVE item type definitions and utilities - [`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 - [`skill.ts`](packages/eve/src/models/skill.ts) - Skill-related models
- **Third-party APIs**: [`packages/eve/src/third-party/`](packages/eve/src/third-party/) - **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 - [`janice.ts`](packages/eve/src/third-party/janice.ts) - Market appraisal integration
- [`evetycoon.ts`](packages/eve/src/third-party/evetycoon.ts) - Market data integration - [`evetycoon.ts`](packages/eve/src/third-party/evetycoon.ts) - Market data integration
- **Utilities**: [`packages/eve/src/utils/`](packages/eve/src/utils/) - **Utilities**: [`packages/eve/src/utils/`](packages/eve/src/utils/)
- [`markdown.ts`](packages/eve/src/utils/markdown.ts) - EVE markup to Discord formatting - [`markdown.ts`](packages/eve/src/utils/markdown.ts) - EVE markup to Discord formatting
#### [@star-kitten/util](packages/util/) #### [@star-kitten/util](packages/util/)
- **Entry Point**: [`packages/util/src/`](packages/util/src/) (no index.ts, exports individual modules) - **Entry Point**: [`packages/util/src/`](packages/util/src/) (no index.ts, exports individual modules)
- **Core Utilities**: - **Core Utilities**:
- [`text.ts`](packages/util/src/text.ts) - Text processing and formatting - [`text.ts`](packages/util/src/text.ts) - Text processing and formatting
- [`jsonQuery.ts`](packages/util/src/jsonQuery.ts) - Streaming JSON querying for large files - [`jsonQuery.ts`](packages/util/src/jsonQuery.ts) - Streaming JSON querying for large files
- [`time.ts`](packages/util/src/time.ts) - Time utilities - [`time.ts`](packages/util/src/time.ts) - Time utilities
- [`logger.ts`](packages/util/src/logger.ts) - Logging utilities - [`logger.ts`](packages/util/src/logger.ts) - Logging utilities
- **Scheduler System**: [`packages/util/src/scheduler/`](packages/util/src/scheduler/) - **Scheduler System**: [`packages/util/src/scheduler/`](packages/util/src/scheduler/)
- [`scheduler.service.ts`](packages/util/src/scheduler/scheduler.service.ts) - Job scheduling service - [`scheduler.service.ts`](packages/util/src/scheduler/scheduler.service.ts) - Job scheduling service
- [`queue.ts`](packages/util/src/scheduler/queue.ts) - Job queue implementation - [`queue.ts`](packages/util/src/scheduler/queue.ts) - Job queue implementation
- [`workers/`](packages/util/src/scheduler/workers/) - Background worker implementations - [`workers/`](packages/util/src/scheduler/workers/) - Background worker implementations
### Applications ### Applications
#### [eve-bot](packages/eve-bot/) #### [eve-bot](packages/eve-bot/)
- **Entry Point**: [`packages/eve-bot/src/main.ts`](packages/eve-bot/src/main.ts) - **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/) - **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 - [`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) - **Types**: [`packages/eve-bot/src/types/global.d.ts`](packages/eve-bot/src/types/global.d.ts)
#### [eve-web](packages/eve-web/) #### [eve-web](packages/eve-web/)
- **Entry Point**: [`packages/eve-web/src/pages/index.tsx`](packages/eve-web/src/pages/index.tsx) - **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 - **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/) - **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 - [`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 - [`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 - **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) - **Utilities**: [`packages/eve-web/src/utils/cookies.ts`](packages/eve-web/src/utils/cookies.ts)
## Key Technical Decisions ## Key Technical Decisions
### Command System Architecture ### Command System Architecture
- **Auto-discovery**: Commands are automatically discovered using glob patterns (`**/*.command.{js,ts}`) - **Auto-discovery**: Commands are automatically discovered using glob patterns (`**/*.command.{js,ts}`)
- **Convention-based**: Each command exports a default Discord command definition - **Convention-based**: Each command exports a default Discord command definition
- **Event-driven**: Commands handle their own interaction events using global event listeners - **Event-driven**: Commands handle their own interaction events using global event listeners
- **Localized**: Full internationalization support with description translations - **Localized**: Full internationalization support with description translations
### Database Architecture ### Database Architecture
- **ORM**: Drizzle ORM with SQLite for local data storage - **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 - **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 - **Models**: Active Record pattern with helper classes for complex operations
- **Migrations**: Drizzle Kit handles schema migrations - **Migrations**: Drizzle Kit handles schema migrations
### EVE Online Integration ### EVE Online Integration
- **OAuth Flow**: Complete EVE SSO implementation with token management - **OAuth Flow**: Complete EVE SSO implementation with token management
- **Scope Management**: Comprehensive ESI scope definitions and validation - **Scope Management**: Comprehensive ESI scope definitions and validation
- **Token Refresh**: Automatic token refresh with scope preservation - **Token Refresh**: Automatic token refresh with scope preservation
- **Data Caching**: Smart caching of EVE reference data and API responses - **Data Caching**: Smart caching of EVE reference data and API responses
### Web Interface Architecture ### Web Interface Architecture
- **Framework**: Brisa for server-side rendering with progressive enhancement - **Framework**: Brisa for server-side rendering with progressive enhancement
- **Authentication**: EVE SSO integrated at middleware level - **Authentication**: EVE SSO integrated at middleware level
- **Components**: Server-side components with suspense for async data loading - **Components**: Server-side components with suspense for async data loading
- **Styling**: Tailwind CSS with DaisyUI components - **Styling**: Tailwind CSS with DaisyUI components
## Design Patterns ## Design Patterns
### Package Exports ### Package Exports
Each package uses structured exports for different concerns: Each package uses structured exports for different concerns:
```typescript ```typescript
// @star-kitten/eve exports // @star-kitten/eve exports
export * from "./esi"; // ESI API integration export * from "./esi"; // ESI API integration
export * as CharacterAPI from "./esi/character"; export * as CharacterAPI from "./esi/character";
export * as models from "./models"; // Data models export * as models from "./models"; // Data models
export * as db from "./db"; // Database access export * as db from "./db"; // Database access
export * from "./third-party"; // External APIs export * from "./third-party"; // External APIs
``` ```
### Error Handling ### Error Handling
- **API Errors**: Consistent error wrapping for external API calls - **API Errors**: Consistent error wrapping for external API calls
- **Token Validation**: Graceful handling of expired/invalid tokens - **Token Validation**: Graceful handling of expired/invalid tokens
- **Database Errors**: Transaction rollback and error logging - **Database Errors**: Transaction rollback and error logging
### Data Processing ### Data Processing
- **Streaming**: Large JSON files processed via streaming for memory efficiency - **Streaming**: Large JSON files processed via streaming for memory efficiency
- **Caching**: Multi-level caching (in-memory, database, file system) - **Caching**: Multi-level caching (in-memory, database, file system)
- **Type Safety**: Full TypeScript coverage with strict type checking - **Type Safety**: Full TypeScript coverage with strict type checking
## Component Relationships ## Component Relationships
### Discord Bot Flow ### Discord Bot Flow
1. [`main.ts`](packages/eve-bot/src/main.ts) initializes Dysnomia client 1. [`main.ts`](packages/eve-bot/src/main.ts) initializes Dysnomia client
2. [`importCommands()`](packages/discord/src/commands/import-commands.ts) discovers command files 2. [`importCommands()`](packages/discord/src/commands/import-commands.ts) discovers command files
3. Commands register interaction handlers on global client 3. Commands register interaction handlers on global client
4. User interactions trigger command-specific handlers 4. User interactions trigger command-specific handlers
### Web Authentication Flow ### Web Authentication Flow
1. [`middleware.ts`](packages/eve-web/src/middleware.ts) intercepts requests 1. [`middleware.ts`](packages/eve-web/src/middleware.ts) intercepts requests
2. EVE SSO redirect initiated for unauthenticated users 2. EVE SSO redirect initiated for unauthenticated users
3. OAuth callback validates tokens and creates/updates user records 3. OAuth callback validates tokens and creates/updates user records
4. Character data synchronized from ESI API 4. Character data synchronized from ESI API
5. Dashboard components display real-time character statistics 5. Dashboard components display real-time character statistics
### Data Synchronization ### Data Synchronization
1. Reference data downloaded from external sources 1. Reference data downloaded from external sources
2. JSON files processed using streaming query utilities 2. JSON files processed using streaming query utilities
3. Character data fetched from ESI API 3. Character data fetched from ESI API
4. Local database updated with character and user information 4. Local database updated with character and user information
5. Web components display cached data with real-time updates 5. Web components display cached data with real-time updates
## Critical Implementation Paths ## Critical Implementation Paths
### Adding New Commands ### Adding New Commands
1. Create `*.command.ts` file in [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/) 1. Create `*.command.ts` file in [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/)
2. Export Discord command definition as default 2. Export Discord command definition as default
3. Add interaction handlers to global client event listeners 3. Add interaction handlers to global client event listeners
4. Commands automatically discovered and registered 4. Commands automatically discovered and registered
### EVE API Integration ### EVE API Integration
1. Define required scopes in [`scopes.ts`](packages/eve/src/esi/scopes.ts) 1. Define required scopes in [`scopes.ts`](packages/eve/src/esi/scopes.ts)
2. Implement API client in appropriate ESI module 2. Implement API client in appropriate ESI module
3. Add character model methods for data access 3. Add character model methods for data access
4. Create web components for data display 4. Create web components for data display
### Database Schema Changes ### Database Schema Changes
1. Update [`schema.ts`](packages/eve/src/db/schema.ts) with new tables/columns 1. Update [`schema.ts`](packages/eve/src/db/schema.ts) with new tables/columns
2. Generate migration using Drizzle Kit 2. Generate migration using Drizzle Kit
3. Update model classes with new methods 3. Update model classes with new methods
4. Add database exports to [`db/index.ts`](packages/eve/src/db/index.ts) 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, 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. 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 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. 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 # Project Context - Star Kitten
## Current Development State ## Current Development State
### Project Maturity ### Project Maturity
- **Phase**: Active Development - **Phase**: Active Development
- **Version**: 0.0.0 (pre-release) - **Version**: 0.0.0 (pre-release)
- **Status**: Core framework established, basic functionality implemented - **Status**: Core framework established, basic functionality implemented
- **Last Major Update**: Memory bank initialization (January 2025) - **Last Major Update**: Memory bank initialization (January 2025)
### Current Work Focus ### Current Work Focus
- **Primary**: EVE Online Discord bot and web interface development - **Primary**: EVE Online Discord bot and web interface development
- **Secondary**: Framework stabilization and documentation - **Secondary**: Framework stabilization and documentation
- **Future**: Star Citizen integration planning - **Future**: Star Citizen integration planning
### Recent Changes ### Recent Changes
- Memory bank system initialized with comprehensive documentation - Memory bank system initialized with comprehensive documentation
- Core architecture established across four packages - Core architecture established across four packages
- Basic EVE Online integration functional - Basic EVE Online integration functional
- Discord command system with auto-discovery implemented - Discord command system with auto-discovery implemented
- Web interface with EVE SSO authentication working - Web interface with EVE SSO authentication working
## Active Features ## Active Features
### Discord Bot (`eve-bot`) ### Discord Bot (`eve-bot`)
- **Status**: Basic functionality working - **Status**: Basic functionality working
- **Commands**: - **Commands**:
- Market appraisal command (partial implementation) - Market appraisal command (partial implementation)
- **Authentication**: Global client setup with Dysnomia - **Authentication**: Global client setup with Dysnomia
- **Deployment**: Environment-based configuration ready - **Deployment**: Environment-based configuration ready
### Web Interface (`eve-web`) ### Web Interface (`eve-web`)
- **Status**: Basic dashboard working - **Status**: Basic dashboard working
- **Authentication**: EVE SSO OAuth flow implemented - **Authentication**: EVE SSO OAuth flow implemented
- **Components**: - **Components**:
- Skill queue display with progress tracking - Skill queue display with progress tracking
- Wallet balance with daily change calculation - Wallet balance with daily change calculation
- **Framework**: Brisa SSR with Tailwind CSS + DaisyUI - **Framework**: Brisa SSR with Tailwind CSS + DaisyUI
### Core Packages ### Core Packages
- **@star-kitten/discord**: Command auto-discovery and handling - **@star-kitten/discord**: Command auto-discovery and handling
- **@star-kitten/eve**: ESI API integration, database models, third-party APIs - **@star-kitten/eve**: ESI API integration, database models, third-party APIs
- **@star-kitten/util**: Streaming JSON queries, text processing, job scheduling - **@star-kitten/util**: Streaming JSON queries, text processing, job scheduling
## Technical Debt & Known Issues ## Technical Debt & Known Issues
### Architecture Decisions Pending ### Architecture Decisions Pending
- **Web Framework Migration**: Planned switch from Brisa to Elysia + Ripple SPA - **Web Framework Migration**: Planned switch from Brisa to Elysia + Ripple SPA
- **Package Versioning**: All packages at 0.0.0, versioning strategy needed - **Package Versioning**: All packages at 0.0.0, versioning strategy needed
- **Testing Coverage**: Current coverage unknown, target is 80% - **Testing Coverage**: Current coverage unknown, target is 80%
### Implementation Gaps ### Implementation Gaps
- **Command System**: [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) references global commands object not used in current pattern - **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 - **Appraisal Command**: Modal submission handling incomplete
- **Mining Fleet Module**: Database schema exists but no implementation - **Mining Fleet Module**: Database schema exists but no implementation
- **API Error Handling**: Needs standardization across packages - **API Error Handling**: Needs standardization across packages
### Database Schema ### Database Schema
- **Current Tables**: users, characters, resumeCommands, miningFleets, miningFleetParticipants - **Current Tables**: users, characters, resumeCommands, miningFleets, miningFleetParticipants
- **Migration Status**: Drizzle Kit configured but migration history unclear - **Migration Status**: Drizzle Kit configured but migration history unclear
- **Data Location**: SQLite database at configurable path - **Data Location**: SQLite database at configurable path
## Development Environment ## Development Environment
### Setup Requirements ### Setup Requirements
- **Runtime**: Bun (JavaScript runtime and package manager) - **Runtime**: Bun (JavaScript runtime and package manager)
- **Language**: TypeScript with strict type checking - **Language**: TypeScript with strict type checking
- **Database**: SQLite with Drizzle ORM - **Database**: SQLite with Drizzle ORM
- **Development**: VS Code with format-on-save configuration - **Development**: VS Code with format-on-save configuration
### Build System ### Build System
- **Package Manager**: Bun workspaces - **Package Manager**: Bun workspaces
- **Build Tool**: tsdown for TypeScript compilation - **Build Tool**: tsdown for TypeScript compilation
- **Scripts**: Unified commands across packages (build, dev, test) - **Scripts**: Unified commands across packages (build, dev, test)
### External Dependencies ### External Dependencies
- **EVE Online**: ESI API access requires client credentials - **EVE Online**: ESI API access requires client credentials
- **Janice API**: Market appraisal service requires API key - **Janice API**: Market appraisal service requires API key
- **Discord**: Bot token required for Discord API access - **Discord**: Bot token required for Discord API access
## Next Steps Priority ## Next Steps Priority
### Immediate (Current Sprint) ### Immediate (Current Sprint)
1. Complete appraisal command implementation 1. Complete appraisal command implementation
2. Add comprehensive error handling to ESI integration 2. Add comprehensive error handling to ESI integration
3. Implement basic test coverage for core functionality 3. Implement basic test coverage for core functionality
4. Document API authentication setup process 4. Document API authentication setup process
### Short Term (Next Month) ### Short Term (Next Month)
1. Implement mining fleet management functionality 1. Implement mining fleet management functionality
2. Add more Discord commands (character lookup, skill planning) 2. Add more Discord commands (character lookup, skill planning)
3. Expand web interface with more character statistics 3. Expand web interface with more character statistics
4. Establish proper versioning and release process 4. Establish proper versioning and release process
### Medium Term (Next Quarter) ### Medium Term (Next Quarter)
1. Migrate web framework to Elysia + Ripple SPA 1. Migrate web framework to Elysia + Ripple SPA
2. Add support for corporation and alliance management 2. Add support for corporation and alliance management
3. Implement data synchronization and caching strategies 3. Implement data synchronization and caching strategies
4. Create documentation for third-party developers 4. Create documentation for third-party developers
### Long Term (Next Year) ### Long Term (Next Year)
1. Star Citizen integration architecture 1. Star Citizen integration architecture
2. Multi-game framework abstraction 2. Multi-game framework abstraction
3. Plugin system for community extensions 3. Plugin system for community extensions
4. Performance optimization and scaling 4. Performance optimization and scaling
## Key Stakeholders ## Key Stakeholders
### Internal Development ### Internal Development
- **Primary Developer**: j-b-3 (project owner and main developer) - **Primary Developer**: j-b-3 (project owner and main developer)
- **Target Users**: EVE Online corporations and alliances - **Target Users**: EVE Online corporations and alliances
- **Future Contributors**: Open source community (planned) - **Future Contributors**: Open source community (planned)
### External Dependencies ### External Dependencies
- **CCP Games**: EVE Online ESI API provider - **CCP Games**: EVE Online ESI API provider
- **Discord**: Platform and API provider - **Discord**: Platform and API provider
- **Community**: EVE Online player community feedback - **Community**: EVE Online player community feedback
## Risk Assessment ## Risk Assessment
### Technical Risks ### Technical Risks
- **Framework Migration**: Brisa to Elysia migration complexity - **Framework Migration**: Brisa to Elysia migration complexity
- **API Rate Limits**: EVE ESI and third-party API limitations - **API Rate Limits**: EVE ESI and third-party API limitations
- **Database Performance**: SQLite scalability for large datasets - **Database Performance**: SQLite scalability for large datasets
- **Token Management**: OAuth token refresh reliability - **Token Management**: OAuth token refresh reliability
### Project Risks ### Project Risks
- **Single Developer**: No backup maintainer currently - **Single Developer**: No backup maintainer currently
- **Community Adoption**: Uncertain uptake by EVE Online communities - **Community Adoption**: Uncertain uptake by EVE Online communities
- **Game API Changes**: Dependency on external game APIs - **Game API Changes**: Dependency on external game APIs
- **Competition**: Existing EVE Online bot solutions - **Competition**: Existing EVE Online bot solutions
## Success Metrics ## Success Metrics
### Current Measurements ### Current Measurements
- **Code Quality**: Type safety across all packages - **Code Quality**: Type safety across all packages
- **Architecture**: Modular design with clear separation - **Architecture**: Modular design with clear separation
- **Documentation**: Comprehensive memory bank established - **Documentation**: Comprehensive memory bank established
- **Functionality**: Basic bot and web interface working - **Functionality**: Basic bot and web interface working
### Target Metrics ### Target Metrics
- **Test Coverage**: 80% across major functionality - **Test Coverage**: 80% across major functionality
- **Performance**: <2 second Discord command response times - **Performance**: <2 second Discord command response times
- **Reliability**: 99%+ uptime for deployed services - **Reliability**: 99%+ uptime for deployed services
- **Adoption**: Usage by multiple EVE Online communities - **Adoption**: Usage by multiple EVE Online communities

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,73 +1,73 @@
import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers'; import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers';
import type { PartialContext } from './command-context.type'; import type { PartialContext } from './command-context.type';
import type { CommandHandler, ExecutableInteraction } from './command-handler'; import type { CommandHandler, ExecutableInteraction } from './command-handler';
import { injectInteraction } from './command-injection'; import { injectInteraction } from './command-injection';
import { getCommandState } from './command-state'; import { getCommandState } from './command-state';
export async function handleCommands( export async function handleCommands(
interaction: ExecutableInteraction, interaction: ExecutableInteraction,
commands: Record<string, CommandHandler<ApplicationCommandStructure>>, commands: Record<string, CommandHandler<ApplicationCommandStructure>>,
ctx: PartialContext, ctx: PartialContext,
) { ) {
ctx.state = await getCommandState(interaction, ctx); ctx.state = await getCommandState(interaction, ctx);
if (!ctx.state.name) { if (!ctx.state.name) {
ctx.state.name = getCommandName(interaction); ctx.state.name = getCommandName(interaction);
} }
if (isAutocomplete(interaction) && ctx.state.name) { if (isAutocomplete(interaction) && ctx.state.name) {
const acCommand = commands[ctx.state.name]; const acCommand = commands[ctx.state.name];
return acCommand.execute(interaction, ctx as any); return acCommand.execute(interaction, ctx as any);
} }
if (!ctx.state.id) { if (!ctx.state.id) {
console.error(`No command ID found for interaction ${interaction.id}`); console.error(`No command ID found for interaction ${interaction.id}`);
return; return;
} }
const command = commands[ctx.state.name || '']; const command = commands[ctx.state.name || ''];
if (!command) { if (!command) {
console.warn(`No command found for interaction: ${JSON.stringify(interaction, undefined, 2)}`); console.warn(`No command found for interaction: ${JSON.stringify(interaction, undefined, 2)}`);
return; return;
} }
cleanInteractionCustomIds(interaction, ctx.state.id); cleanInteractionCustomIds(interaction, ctx.state.id);
const [injectedInteraction, fullContext] = await injectInteraction(interaction, ctx); const [injectedInteraction, fullContext] = await injectInteraction(interaction, ctx);
return command.execute(injectedInteraction, fullContext); return command.execute(injectedInteraction, fullContext);
} }
export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) { export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) {
ctx.client.on('interactionCreate', async (interaction) => { ctx.client.on('interactionCreate', async (interaction) => {
if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) { if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) {
handleCommands(interaction, commands, ctx); handleCommands(interaction, commands, ctx);
} }
}); });
} }
function cleanInteractionCustomIds(interaction: ExecutableInteraction, id: string) { function cleanInteractionCustomIds(interaction: ExecutableInteraction, id: string) {
if ('components' in interaction && Array.isArray(interaction.components) && id) { if ('components' in interaction && Array.isArray(interaction.components) && id) {
removeCommandIdFromComponentCustomIds(interaction.components, id); removeCommandIdFromComponentCustomIds(interaction.components, id);
} }
if ('data' in interaction && id) { if ('data' in interaction && id) {
if ('custom_id' in interaction.data && typeof interaction.data.custom_id === 'string') { if ('custom_id' in interaction.data && typeof interaction.data.custom_id === 'string') {
interaction.data.custom_id = interaction.data.custom_id.replace(`_${id}`, ''); interaction.data.custom_id = interaction.data.custom_id.replace(`_${id}`, '');
} }
if ('components' in interaction.data && Array.isArray(interaction.data.components)) { if ('components' in interaction.data && Array.isArray(interaction.data.components)) {
removeCommandIdFromComponentCustomIds(interaction.data.components as any, id); removeCommandIdFromComponentCustomIds(interaction.data.components as any, id);
} }
} }
} }
function removeCommandIdFromComponentCustomIds(components: { custom_id?: string; components?: any[] }[], commandId: string) { function removeCommandIdFromComponentCustomIds(components: { custom_id?: string; components?: any[] }[], commandId: string) {
components.forEach((component) => { components.forEach((component) => {
if ('custom_id' in component) { if ('custom_id' in component) {
component.custom_id = component.custom_id.replace(`_${commandId}`, ''); component.custom_id = component.custom_id.replace(`_${commandId}`, '');
} }
if ('components' in component && Array.isArray(component.components)) { if ('components' in component && Array.isArray(component.components)) {
removeCommandIdFromComponentCustomIds(component.components, commandId); removeCommandIdFromComponentCustomIds(component.components, commandId);
} }
if ('component' in component && 'custom_id' in (component as any).component && Array.isArray(component.components)) { 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}`, ''); (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 { Glob } from 'bun';
import { join } from 'node:path'; import { join } from 'node:path';
import type { CommandHandler } from './command-handler'; import type { CommandHandler } from './command-handler';
import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
export async function importCommands( export async function importCommands(
pattern: string = '**/*.command.{js,ts}', pattern: string = '**/*.command.{js,ts}',
baseDir: string = join(process.cwd(), 'src'), baseDir: string = join(process.cwd(), 'src'),
commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {}, commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {},
): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> { ): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> {
const glob = new Glob(pattern); const glob = new Glob(pattern);
for await (const file of glob.scan({ cwd: baseDir, absolute: true })) { for await (const file of glob.scan({ cwd: baseDir, absolute: true })) {
const command = (await import(file)).default as CommandHandler<ApplicationCommandStructure>; const command = (await import(file)).default as CommandHandler<ApplicationCommandStructure>;
commandRegistry[command.definition.name] = command; commandRegistry[command.definition.name] = command;
} }
return commandRegistry; return commandRegistry;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,3 +2,5 @@ export * from './locales';
export * from './commands'; export * from './commands';
export * from './core'; export * from './core';
export * from './jsx'; 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[]) { export function createElement(tag: string, attrs: Record<string, any> = {}, ...children: any[]) {
return { return {
tag, tag,
attrs, attrs,
children, children,
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { createButton, type ButtonStyle } from '@components'; import { createButton, type ButtonStyle } from '@components';
import type { PartialEmoji } from '@projectdysnomia/dysnomia'; import type { PartialEmoji } from '@projectdysnomia/dysnomia';
export function Button(props: { label: string; customId: string; style?: ButtonStyle; emoji?: PartialEmoji; disabled?: boolean }) { 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 }); 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'; import { createContainer } from '@components';
export function Container(props: { accent?: number; spoiler?: boolean; children: any | any[] }) { export function Container(props: { accent?: number; spoiler?: boolean; children: any | any[] }) {
return createContainer( return createContainer(
{ accent_color: props.accent, spoiler: props.spoiler }, { accent_color: props.accent, spoiler: props.spoiler },
...(Array.isArray(props.children) ? props.children : [props.children]), ...(Array.isArray(props.children) ? props.children : [props.children]),
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,40 +1,15 @@
{ {
"extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
// Enable latest features "composite": true,
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "@star-kitten/discord", "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": { "paths": {
"@*": ["./src/*"], "@*": ["./src/*"],
"@types": ["./types/*"] "@types": ["./types/*"]
}, },
"typeRoots": ["src/types", "./node_modules/@types"],
"experimentalDecorators": true, "types": []
"emitDecoratorMetadata": true,
"typeRoots": ["src/types", "./node_modules/@types"]
}, },
"include": ["src", "types", "src/jsx/types.d.ts"], "include": ["src", "types", "src/jsx/types.d.ts"],
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"] "exclude": ["node_modules", "dist", "build", "**/*.test.ts"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
import { startDiscordBot } from '@star-kitten/discord'; import { startDiscordBot } from '@star-kitten/discord';
startDiscordBot(); 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": { "compilerOptions": {
// Enable latest features "composite": true,
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "@star-kitten/discord", "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": { "paths": {
"@*": ["./src/*"] "@*": ["./src/*"]
}, },
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"typeRoots": ["src/types", "./node_modules/@types"] "typeRoots": ["src/types", "./node_modules/@types"]
}, },
"include": ["src", "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