diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..73ce486 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,77 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Git +.git +.gitignore +.github + +# Documentation +README.md +CHANGELOG.md +LICENSE +*.md + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage +.nyc_output + +# IDE +.vscode +.idea +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage + +# Build outputs (exclude .build-cache for multi-stage builds) +# Note: dist/ is included for simple build approach +.build-cache + +# Docker files +Dockerfile* +docker-compose*.yml +.dockerignore + +# Temporary files +.tmp +.temp + +# Cache +.cache +.parcel-cache + +# Storybook build +storybook-static + +# Spec workflow files +.spec-workflow +.serenatoken + +# Astro cache +.astro \ No newline at end of file diff --git a/.gitignore b/.gitignore index d597a22..4144e01 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ pnpm-debug.log* # macOS-specific files .DS_Store -*storybook.log \ No newline at end of file +*storybook.log +data diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..14d86ad --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..6e004bb --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,84 @@ +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp csharp_omnisharp +# dart elixir elm erlang fortran go +# haskell java julia kotlin lua markdown +# nix perl php python python_jedi r +# rego ruby ruby_solargraph rust scala swift +# terraform typescript typescript_vts zig +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# Special requirements: +# - csharp: Requires the presence of a .sln file in the project folder. +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- typescript + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed) on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "moodist" +included_optional_tools: [] diff --git a/.spec-workflow/templates/design-template.md b/.spec-workflow/templates/design-template.md new file mode 100644 index 0000000..54cd5d2 --- /dev/null +++ b/.spec-workflow/templates/design-template.md @@ -0,0 +1,96 @@ +# Design Document + +## Overview + +[High-level description of the feature and its place in the overall system] + +## Steering Document Alignment + +### Technical Standards (tech.md) +[How the design follows documented technical patterns and standards] + +### Project Structure (structure.md) +[How the implementation will follow project organization conventions] + +## Code Reuse Analysis +[What existing code will be leveraged, extended, or integrated with this feature] + +### Existing Components to Leverage +- **[Component/Utility Name]**: [How it will be used] +- **[Service/Helper Name]**: [How it will be extended] + +### Integration Points +- **[Existing System/API]**: [How the new feature will integrate] +- **[Database/Storage]**: [How data will connect to existing schemas] + +## Architecture + +[Describe the overall architecture and design patterns used] + +### Modular Design Principles +- **Single File Responsibility**: Each file should handle one specific concern or domain +- **Component Isolation**: Create small, focused components rather than large monolithic files +- **Service Layer Separation**: Separate data access, business logic, and presentation layers +- **Utility Modularity**: Break utilities into focused, single-purpose modules + +```mermaid +graph TD + A[Component A] --> B[Component B] + B --> C[Component C] +``` + +## Components and Interfaces + +### Component 1 +- **Purpose:** [What this component does] +- **Interfaces:** [Public methods/APIs] +- **Dependencies:** [What it depends on] +- **Reuses:** [Existing components/utilities it builds upon] + +### Component 2 +- **Purpose:** [What this component does] +- **Interfaces:** [Public methods/APIs] +- **Dependencies:** [What it depends on] +- **Reuses:** [Existing components/utilities it builds upon] + +## Data Models + +### Model 1 +``` +[Define the structure of Model1 in your language] +- id: [unique identifier type] +- name: [string/text type] +- [Additional properties as needed] +``` + +### Model 2 +``` +[Define the structure of Model2 in your language] +- id: [unique identifier type] +- [Additional properties as needed] +``` + +## Error Handling + +### Error Scenarios +1. **Scenario 1:** [Description] + - **Handling:** [How to handle] + - **User Impact:** [What user sees] + +2. **Scenario 2:** [Description] + - **Handling:** [How to handle] + - **User Impact:** [What user sees] + +## Testing Strategy + +### Unit Testing +- [Unit testing approach] +- [Key components to test] + +### Integration Testing +- [Integration testing approach] +- [Key flows to test] + +### End-to-End Testing +- [E2E testing approach] +- [User scenarios to test] diff --git a/.spec-workflow/templates/product-template.md b/.spec-workflow/templates/product-template.md new file mode 100644 index 0000000..f806628 --- /dev/null +++ b/.spec-workflow/templates/product-template.md @@ -0,0 +1,51 @@ +# Product Overview + +## Product Purpose +[Describe the core purpose of this product/project. What problem does it solve?] + +## Target Users +[Who are the primary users of this product? What are their needs and pain points?] + +## Key Features +[List the main features that deliver value to users] + +1. **Feature 1**: [Description] +2. **Feature 2**: [Description] +3. **Feature 3**: [Description] + +## Business Objectives +[What are the business goals this product aims to achieve?] + +- [Objective 1] +- [Objective 2] +- [Objective 3] + +## Success Metrics +[How will we measure the success of this product?] + +- [Metric 1]: [Target] +- [Metric 2]: [Target] +- [Metric 3]: [Target] + +## Product Principles +[Core principles that guide product decisions] + +1. **[Principle 1]**: [Explanation] +2. **[Principle 2]**: [Explanation] +3. **[Principle 3]**: [Explanation] + +## Monitoring & Visibility (if applicable) +[How do users track progress and monitor the system?] + +- **Dashboard Type**: [e.g., Web-based, CLI, Desktop app] +- **Real-time Updates**: [e.g., WebSocket, polling, push notifications] +- **Key Metrics Displayed**: [What information is most important to surface] +- **Sharing Capabilities**: [e.g., read-only links, exports, reports] + +## Future Vision +[Where do we see this product evolving in the future?] + +### Potential Enhancements +- **Remote Access**: [e.g., Tunnel features for sharing dashboards with stakeholders] +- **Analytics**: [e.g., Historical trends, performance metrics] +- **Collaboration**: [e.g., Multi-user support, commenting] diff --git a/.spec-workflow/templates/requirements-template.md b/.spec-workflow/templates/requirements-template.md new file mode 100644 index 0000000..8db51e2 --- /dev/null +++ b/.spec-workflow/templates/requirements-template.md @@ -0,0 +1,50 @@ +# Requirements Document + +## Introduction + +[Provide a brief overview of the feature, its purpose, and its value to users] + +## Alignment with Product Vision + +[Explain how this feature supports the goals outlined in product.md] + +## Requirements + +### Requirement 1 + +**User Story:** As a [role], I want [feature], so that [benefit] + +#### Acceptance Criteria + +1. WHEN [event] THEN [system] SHALL [response] +2. IF [precondition] THEN [system] SHALL [response] +3. WHEN [event] AND [condition] THEN [system] SHALL [response] + +### Requirement 2 + +**User Story:** As a [role], I want [feature], so that [benefit] + +#### Acceptance Criteria + +1. WHEN [event] THEN [system] SHALL [response] +2. IF [precondition] THEN [system] SHALL [response] + +## Non-Functional Requirements + +### Code Architecture and Modularity +- **Single Responsibility Principle**: Each file should have a single, well-defined purpose +- **Modular Design**: Components, utilities, and services should be isolated and reusable +- **Dependency Management**: Minimize interdependencies between modules +- **Clear Interfaces**: Define clean contracts between components and layers + +### Performance +- [Performance requirements] + +### Security +- [Security requirements] + +### Reliability +- [Reliability requirements] + +### Usability +- [Usability requirements] diff --git a/.spec-workflow/templates/structure-template.md b/.spec-workflow/templates/structure-template.md new file mode 100644 index 0000000..eb559be --- /dev/null +++ b/.spec-workflow/templates/structure-template.md @@ -0,0 +1,145 @@ +# Project Structure + +## Directory Organization + +``` +[Define your project's directory structure. Examples below - adapt to your project type] + +Example for a library/package: +project-root/ +├── src/ # Source code +├── tests/ # Test files +├── docs/ # Documentation +├── examples/ # Usage examples +└── [build/dist/out] # Build output + +Example for an application: +project-root/ +├── [src/app/lib] # Main source code +├── [assets/resources] # Static resources +├── [config/settings] # Configuration +├── [scripts/tools] # Build/utility scripts +└── [tests/spec] # Test files + +Common patterns: +- Group by feature/module +- Group by layer (UI, business logic, data) +- Group by type (models, controllers, views) +- Flat structure for simple projects +``` + +## Naming Conventions + +### Files +- **Components/Modules**: [e.g., `PascalCase`, `snake_case`, `kebab-case`] +- **Services/Handlers**: [e.g., `UserService`, `user_service`, `user-service`] +- **Utilities/Helpers**: [e.g., `dateUtils`, `date_utils`, `date-utils`] +- **Tests**: [e.g., `[filename]_test`, `[filename].test`, `[filename]Test`] + +### Code +- **Classes/Types**: [e.g., `PascalCase`, `CamelCase`, `snake_case`] +- **Functions/Methods**: [e.g., `camelCase`, `snake_case`, `PascalCase`] +- **Constants**: [e.g., `UPPER_SNAKE_CASE`, `SCREAMING_CASE`, `PascalCase`] +- **Variables**: [e.g., `camelCase`, `snake_case`, `lowercase`] + +## Import Patterns + +### Import Order +1. External dependencies +2. Internal modules +3. Relative imports +4. Style imports + +### Module/Package Organization +``` +[Describe your project's import/include patterns] +Examples: +- Absolute imports from project root +- Relative imports within modules +- Package/namespace organization +- Dependency management approach +``` + +## Code Structure Patterns + +[Define common patterns for organizing code within files. Below are examples - choose what applies to your project] + +### Module/Class Organization +``` +Example patterns: +1. Imports/includes/dependencies +2. Constants and configuration +3. Type/interface definitions +4. Main implementation +5. Helper/utility functions +6. Exports/public API +``` + +### Function/Method Organization +``` +Example patterns: +- Input validation first +- Core logic in the middle +- Error handling throughout +- Clear return points +``` + +### File Organization Principles +``` +Choose what works for your project: +- One class/module per file +- Related functionality grouped together +- Public API at the top/bottom +- Implementation details hidden +``` + +## Code Organization Principles + +1. **Single Responsibility**: Each file should have one clear purpose +2. **Modularity**: Code should be organized into reusable modules +3. **Testability**: Structure code to be easily testable +4. **Consistency**: Follow patterns established in the codebase + +## Module Boundaries +[Define how different parts of your project interact and maintain separation of concerns] + +Examples of boundary patterns: +- **Core vs Plugins**: Core functionality vs extensible plugins +- **Public API vs Internal**: What's exposed vs implementation details +- **Platform-specific vs Cross-platform**: OS-specific code isolation +- **Stable vs Experimental**: Production code vs experimental features +- **Dependencies direction**: Which modules can depend on which + +## Code Size Guidelines +[Define your project's guidelines for file and function sizes] + +Suggested guidelines: +- **File size**: [Define maximum lines per file] +- **Function/Method size**: [Define maximum lines per function] +- **Class/Module complexity**: [Define complexity limits] +- **Nesting depth**: [Maximum nesting levels] + +## Dashboard/Monitoring Structure (if applicable) +[How dashboard or monitoring components are organized] + +### Example Structure: +``` +src/ +└── dashboard/ # Self-contained dashboard subsystem + ├── server/ # Backend server components + ├── client/ # Frontend assets + ├── shared/ # Shared types/utilities + └── public/ # Static assets +``` + +### Separation of Concerns +- Dashboard isolated from core business logic +- Own CLI entry point for independent operation +- Minimal dependencies on main application +- Can be disabled without affecting core functionality + +## Documentation Standards +- All public APIs must have documentation +- Complex logic should include inline comments +- README files for major modules +- Follow language-specific documentation conventions diff --git a/.spec-workflow/templates/tasks-template.md b/.spec-workflow/templates/tasks-template.md new file mode 100644 index 0000000..5e494c0 --- /dev/null +++ b/.spec-workflow/templates/tasks-template.md @@ -0,0 +1,139 @@ +# Tasks Document + +- [ ] 1. Create core interfaces in src/types/feature.ts + - File: src/types/feature.ts + - Define TypeScript interfaces for feature data structures + - Extend existing base interfaces from base.ts + - Purpose: Establish type safety for feature implementation + - _Leverage: src/types/base.ts_ + - _Requirements: 1.1_ + - _Prompt: Role: TypeScript Developer specializing in type systems and interfaces | Task: Create comprehensive TypeScript interfaces for the feature data structures following requirements 1.1, extending existing base interfaces from src/types/base.ts | Restrictions: Do not modify existing base interfaces, maintain backward compatibility, follow project naming conventions | Success: All interfaces compile without errors, proper inheritance from base types, full type coverage for feature requirements_ + +- [ ] 2. Create base model class in src/models/FeatureModel.ts + - File: src/models/FeatureModel.ts + - Implement base model extending BaseModel class + - Add validation methods using existing validation utilities + - Purpose: Provide data layer foundation for feature + - _Leverage: src/models/BaseModel.ts, src/utils/validation.ts_ + - _Requirements: 2.1_ + - _Prompt: Role: Backend Developer with expertise in Node.js and data modeling | Task: Create a base model class extending BaseModel and implementing validation following requirement 2.1, leveraging existing patterns from src/models/BaseModel.ts and src/utils/validation.ts | Restrictions: Must follow existing model patterns, do not bypass validation utilities, maintain consistent error handling | Success: Model extends BaseModel correctly, validation methods implemented and tested, follows project architecture patterns_ + +- [ ] 3. Add specific model methods to FeatureModel.ts + - File: src/models/FeatureModel.ts (continue from task 2) + - Implement create, update, delete methods + - Add relationship handling for foreign keys + - Purpose: Complete model functionality for CRUD operations + - _Leverage: src/models/BaseModel.ts_ + - _Requirements: 2.2, 2.3_ + - _Prompt: Role: Backend Developer with expertise in ORM and database operations | Task: Implement CRUD methods and relationship handling in FeatureModel.ts following requirements 2.2 and 2.3, extending patterns from src/models/BaseModel.ts | Restrictions: Must maintain transaction integrity, follow existing relationship patterns, do not duplicate base model functionality | Success: All CRUD operations work correctly, relationships are properly handled, database operations are atomic and efficient_ + +- [ ] 4. Create model unit tests in tests/models/FeatureModel.test.ts + - File: tests/models/FeatureModel.test.ts + - Write tests for model validation and CRUD methods + - Use existing test utilities and fixtures + - Purpose: Ensure model reliability and catch regressions + - _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_ + - _Requirements: 2.1, 2.2_ + - _Prompt: Role: QA Engineer with expertise in unit testing and Jest/Mocha frameworks | Task: Create comprehensive unit tests for FeatureModel validation and CRUD methods covering requirements 2.1 and 2.2, using existing test utilities from tests/helpers/testUtils.ts and fixtures from tests/fixtures/data.ts | Restrictions: Must test both success and failure scenarios, do not test external dependencies directly, maintain test isolation | Success: All model methods are tested with good coverage, edge cases covered, tests run independently and consistently_ + +- [ ] 5. Create service interface in src/services/IFeatureService.ts + - File: src/services/IFeatureService.ts + - Define service contract with method signatures + - Extend base service interface patterns + - Purpose: Establish service layer contract for dependency injection + - _Leverage: src/services/IBaseService.ts_ + - _Requirements: 3.1_ + - _Prompt: Role: Software Architect specializing in service-oriented architecture and TypeScript interfaces | Task: Design service interface contract following requirement 3.1, extending base service patterns from src/services/IBaseService.ts for dependency injection | Restrictions: Must maintain interface segregation principle, do not expose internal implementation details, ensure contract compatibility with DI container | Success: Interface is well-defined with clear method signatures, extends base service appropriately, supports all required service operations_ + +- [ ] 6. Implement feature service in src/services/FeatureService.ts + - File: src/services/FeatureService.ts + - Create concrete service implementation using FeatureModel + - Add error handling with existing error utilities + - Purpose: Provide business logic layer for feature operations + - _Leverage: src/services/BaseService.ts, src/utils/errorHandler.ts, src/models/FeatureModel.ts_ + - _Requirements: 3.2_ + - _Prompt: Role: Backend Developer with expertise in service layer architecture and business logic | Task: Implement concrete FeatureService following requirement 3.2, using FeatureModel and extending BaseService patterns with proper error handling from src/utils/errorHandler.ts | Restrictions: Must implement interface contract exactly, do not bypass model validation, maintain separation of concerns from data layer | Success: Service implements all interface methods correctly, robust error handling implemented, business logic is well-encapsulated and testable_ + +- [ ] 7. Add service dependency injection in src/utils/di.ts + - File: src/utils/di.ts (modify existing) + - Register FeatureService in dependency injection container + - Configure service lifetime and dependencies + - Purpose: Enable service injection throughout application + - _Leverage: existing DI configuration in src/utils/di.ts_ + - _Requirements: 3.1_ + - _Prompt: Role: DevOps Engineer with expertise in dependency injection and IoC containers | Task: Register FeatureService in DI container following requirement 3.1, configuring appropriate lifetime and dependencies using existing patterns from src/utils/di.ts | Restrictions: Must follow existing DI container patterns, do not create circular dependencies, maintain service resolution efficiency | Success: FeatureService is properly registered and resolvable, dependencies are correctly configured, service lifetime is appropriate for use case_ + +- [ ] 8. Create service unit tests in tests/services/FeatureService.test.ts + - File: tests/services/FeatureService.test.ts + - Write tests for service methods with mocked dependencies + - Test error handling scenarios + - Purpose: Ensure service reliability and proper error handling + - _Leverage: tests/helpers/testUtils.ts, tests/mocks/modelMocks.ts_ + - _Requirements: 3.2, 3.3_ + - _Prompt: Role: QA Engineer with expertise in service testing and mocking frameworks | Task: Create comprehensive unit tests for FeatureService methods covering requirements 3.2 and 3.3, using mocked dependencies from tests/mocks/modelMocks.ts and test utilities | Restrictions: Must mock all external dependencies, test business logic in isolation, do not test framework code | Success: All service methods tested with proper mocking, error scenarios covered, tests verify business logic correctness and error handling_ + +- [ ] 4. Create API endpoints + - Design API structure + - _Leverage: src/api/baseApi.ts, src/utils/apiUtils.ts_ + - _Requirements: 4.0_ + - _Prompt: Role: API Architect specializing in RESTful design and Express.js | Task: Design comprehensive API structure following requirement 4.0, leveraging existing patterns from src/api/baseApi.ts and utilities from src/utils/apiUtils.ts | Restrictions: Must follow REST conventions, maintain API versioning compatibility, do not expose internal data structures directly | Success: API structure is well-designed and documented, follows existing patterns, supports all required operations with proper HTTP methods and status codes_ + +- [ ] 4.1 Set up routing and middleware + - Configure application routes + - Add authentication middleware + - Set up error handling middleware + - _Leverage: src/middleware/auth.ts, src/middleware/errorHandler.ts_ + - _Requirements: 4.1_ + - _Prompt: Role: Backend Developer with expertise in Express.js middleware and routing | Task: Configure application routes and middleware following requirement 4.1, integrating authentication from src/middleware/auth.ts and error handling from src/middleware/errorHandler.ts | Restrictions: Must maintain middleware order, do not bypass security middleware, ensure proper error propagation | Success: Routes are properly configured with correct middleware chain, authentication works correctly, errors are handled gracefully throughout the request lifecycle_ + +- [ ] 4.2 Implement CRUD endpoints + - Create API endpoints + - Add request validation + - Write API integration tests + - _Leverage: src/controllers/BaseController.ts, src/utils/validation.ts_ + - _Requirements: 4.2, 4.3_ + - _Prompt: Role: Full-stack Developer with expertise in API development and validation | Task: Implement CRUD endpoints following requirements 4.2 and 4.3, extending BaseController patterns and using validation utilities from src/utils/validation.ts | Restrictions: Must validate all inputs, follow existing controller patterns, ensure proper HTTP status codes and responses | Success: All CRUD operations work correctly, request validation prevents invalid data, integration tests pass and cover all endpoints_ + +- [ ] 5. Add frontend components + - Plan component architecture + - _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_ + - _Requirements: 5.0_ + - _Prompt: Role: Frontend Architect with expertise in React component design and architecture | Task: Plan comprehensive component architecture following requirement 5.0, leveraging base patterns from src/components/BaseComponent.tsx and theme system from src/styles/theme.ts | Restrictions: Must follow existing component patterns, maintain design system consistency, ensure component reusability | Success: Architecture is well-planned and documented, components are properly organized, follows existing patterns and theme system_ + +- [ ] 5.1 Create base UI components + - Set up component structure + - Implement reusable components + - Add styling and theming + - _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_ + - _Requirements: 5.1_ + - _Prompt: Role: Frontend Developer specializing in React and component architecture | Task: Create reusable UI components following requirement 5.1, extending BaseComponent patterns and using existing theme system from src/styles/theme.ts | Restrictions: Must use existing theme variables, follow component composition patterns, ensure accessibility compliance | Success: Components are reusable and properly themed, follow existing architecture, accessible and responsive_ + +- [ ] 5.2 Implement feature-specific components + - Create feature components + - Add state management + - Connect to API endpoints + - _Leverage: src/hooks/useApi.ts, src/components/BaseComponent.tsx_ + - _Requirements: 5.2, 5.3_ + - _Prompt: Role: React Developer with expertise in state management and API integration | Task: Implement feature-specific components following requirements 5.2 and 5.3, using API hooks from src/hooks/useApi.ts and extending BaseComponent patterns | Restrictions: Must use existing state management patterns, handle loading and error states properly, maintain component performance | Success: Components are fully functional with proper state management, API integration works smoothly, user experience is responsive and intuitive_ + +- [ ] 6. Integration and testing + - Plan integration approach + - _Leverage: src/utils/integrationUtils.ts, tests/helpers/testUtils.ts_ + - _Requirements: 6.0_ + - _Prompt: Role: Integration Engineer with expertise in system integration and testing strategies | Task: Plan comprehensive integration approach following requirement 6.0, leveraging integration utilities from src/utils/integrationUtils.ts and test helpers | Restrictions: Must consider all system components, ensure proper test coverage, maintain integration test reliability | Success: Integration plan is comprehensive and feasible, all system components work together correctly, integration points are well-tested_ + +- [ ] 6.1 Write end-to-end tests + - Set up E2E testing framework + - Write user journey tests + - Add test automation + - _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_ + - _Requirements: All_ + - _Prompt: Role: QA Automation Engineer with expertise in E2E testing and test frameworks like Cypress or Playwright | Task: Implement comprehensive end-to-end tests covering all requirements, setting up testing framework and user journey tests using test utilities and fixtures | Restrictions: Must test real user workflows, ensure tests are maintainable and reliable, do not test implementation details | Success: E2E tests cover all critical user journeys, tests run reliably in CI/CD pipeline, user experience is validated from end-to-end_ + +- [ ] 6.2 Final integration and cleanup + - Integrate all components + - Fix any integration issues + - Clean up code and documentation + - _Leverage: src/utils/cleanup.ts, docs/templates/_ + - _Requirements: All_ + - _Prompt: Role: Senior Developer with expertise in code quality and system integration | Task: Complete final integration of all components and perform comprehensive cleanup covering all requirements, using cleanup utilities and documentation templates | Restrictions: Must not break existing functionality, ensure code quality standards are met, maintain documentation consistency | Success: All components are fully integrated and working together, code is clean and well-documented, system meets all requirements and quality standards_ diff --git a/.spec-workflow/templates/tech-template.md b/.spec-workflow/templates/tech-template.md new file mode 100644 index 0000000..d98ee07 --- /dev/null +++ b/.spec-workflow/templates/tech-template.md @@ -0,0 +1,99 @@ +# Technology Stack + +## Project Type +[Describe what kind of project this is: web application, CLI tool, desktop application, mobile app, library, API service, embedded system, game, etc.] + +## Core Technologies + +### Primary Language(s) +- **Language**: [e.g., Python 3.11, Go 1.21, TypeScript, Rust, C++] +- **Runtime/Compiler**: [if applicable] +- **Language-specific tools**: [package managers, build tools, etc.] + +### Key Dependencies/Libraries +[List the main libraries and frameworks your project depends on] +- **[Library/Framework name]**: [Purpose and version] +- **[Library/Framework name]**: [Purpose and version] + +### Application Architecture +[Describe how your application is structured - this could be MVC, event-driven, plugin-based, client-server, standalone, microservices, monolithic, etc.] + +### Data Storage (if applicable) +- **Primary storage**: [e.g., PostgreSQL, files, in-memory, cloud storage] +- **Caching**: [e.g., Redis, in-memory, disk cache] +- **Data formats**: [e.g., JSON, Protocol Buffers, XML, binary] + +### External Integrations (if applicable) +- **APIs**: [External services you integrate with] +- **Protocols**: [e.g., HTTP/REST, gRPC, WebSocket, TCP/IP] +- **Authentication**: [e.g., OAuth, API keys, certificates] + +### Monitoring & Dashboard Technologies (if applicable) +- **Dashboard Framework**: [e.g., React, Vue, vanilla JS, terminal UI] +- **Real-time Communication**: [e.g., WebSocket, Server-Sent Events, polling] +- **Visualization Libraries**: [e.g., Chart.js, D3, terminal graphs] +- **State Management**: [e.g., Redux, Vuex, file system as source of truth] + +## Development Environment + +### Build & Development Tools +- **Build System**: [e.g., Make, CMake, Gradle, npm scripts, cargo] +- **Package Management**: [e.g., pip, npm, cargo, go mod, apt, brew] +- **Development workflow**: [e.g., hot reload, watch mode, REPL] + +### Code Quality Tools +- **Static Analysis**: [Tools for code quality and correctness] +- **Formatting**: [Code style enforcement tools] +- **Testing Framework**: [Unit, integration, and/or end-to-end testing tools] +- **Documentation**: [Documentation generation tools] + +### Version Control & Collaboration +- **VCS**: [e.g., Git, Mercurial, SVN] +- **Branching Strategy**: [e.g., Git Flow, GitHub Flow, trunk-based] +- **Code Review Process**: [How code reviews are conducted] + +### Dashboard Development (if applicable) +- **Live Reload**: [e.g., Hot module replacement, file watchers] +- **Port Management**: [e.g., Dynamic allocation, configurable ports] +- **Multi-Instance Support**: [e.g., Running multiple dashboards simultaneously] + +## Deployment & Distribution (if applicable) +- **Target Platform(s)**: [Where/how the project runs: cloud, on-premise, desktop, mobile, embedded] +- **Distribution Method**: [How users get your software: download, package manager, app store, SaaS] +- **Installation Requirements**: [Prerequisites, system requirements] +- **Update Mechanism**: [How updates are delivered] + +## Technical Requirements & Constraints + +### Performance Requirements +- [e.g., response time, throughput, memory usage, startup time] +- [Specific benchmarks or targets] + +### Compatibility Requirements +- **Platform Support**: [Operating systems, architectures, versions] +- **Dependency Versions**: [Minimum/maximum versions of dependencies] +- **Standards Compliance**: [Industry standards, protocols, specifications] + +### Security & Compliance +- **Security Requirements**: [Authentication, encryption, data protection] +- **Compliance Standards**: [GDPR, HIPAA, SOC2, etc. if applicable] +- **Threat Model**: [Key security considerations] + +### Scalability & Reliability +- **Expected Load**: [Users, requests, data volume] +- **Availability Requirements**: [Uptime targets, disaster recovery] +- **Growth Projections**: [How the system needs to scale] + +## Technical Decisions & Rationale +[Document key architectural and technology choices] + +### Decision Log +1. **[Technology/Pattern Choice]**: [Why this was chosen, alternatives considered] +2. **[Architecture Decision]**: [Rationale, trade-offs accepted] +3. **[Tool/Library Selection]**: [Reasoning, evaluation criteria] + +## Known Limitations +[Document any technical debt, limitations, or areas for improvement] + +- [Limitation 1]: [Impact and potential future solutions] +- [Limitation 2]: [Why it exists and when it might be addressed] diff --git a/.spec-workflow/user-templates/README.md b/.spec-workflow/user-templates/README.md new file mode 100644 index 0000000..ad36a48 --- /dev/null +++ b/.spec-workflow/user-templates/README.md @@ -0,0 +1,64 @@ +# User Templates + +This directory allows you to create custom templates that override the default Spec Workflow templates. + +## How to Use Custom Templates + +1. **Create your custom template file** in this directory with the exact same name as the default template you want to override: + - `requirements-template.md` - Override requirements document template + - `design-template.md` - Override design document template + - `tasks-template.md` - Override tasks document template + - `product-template.md` - Override product steering template + - `tech-template.md` - Override tech steering template + - `structure-template.md` - Override structure steering template + +2. **Template Loading Priority**: + - The system first checks this `user-templates/` directory + - If a matching template is found here, it will be used + - Otherwise, the default template from `templates/` will be used + +## Example Custom Template + +To create a custom requirements template: + +1. Create a file named `requirements-template.md` in this directory +2. Add your custom structure, for example: + +```markdown +# Requirements Document + +## Executive Summary +[Your custom section] + +## Business Requirements +[Your custom structure] + +## Technical Requirements +[Your custom fields] + +## Custom Sections +[Add any sections specific to your workflow] +``` + +## Template Variables + +Templates can include placeholders that will be replaced when documents are created: +- `{{projectName}}` - The name of your project +- `{{featureName}}` - The name of the feature being specified +- `{{date}}` - The current date +- `{{author}}` - The document author + +## Best Practices + +1. **Start from defaults**: Copy a default template from `../templates/` as a starting point +2. **Keep structure consistent**: Maintain similar section headers for tool compatibility +3. **Document changes**: Add comments explaining why sections were added/modified +4. **Version control**: Track your custom templates in version control +5. **Test thoroughly**: Ensure custom templates work with the spec workflow tools + +## Notes + +- Custom templates are project-specific and not included in the package distribution +- The `templates/` directory contains the default templates which are updated with each version +- Your custom templates in this directory are preserved during updates +- If a custom template has errors, the system will fall back to the default template diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d8434..0362dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,592 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## 2.2.0 (2025-11-17) + + +### ✅ Testing + +* add Vitest and some tests ([def9a57](https://github.com/wheesys/moodist/commit/def9a57e0c6454f0e3ffd74b29153a01b33866be)) +* write more tests ([9cc0ccd](https://github.com/wheesys/moodist/commit/9cc0ccd325cf769d64779f133bd2d59e6ba7ca58)) +* write tests for motion lib ([d356d77](https://github.com/wheesys/moodist/commit/d356d77aa951b84a6ccbd0b1c6590286c042957b)) +* write tests for random helper ([cad85c7](https://github.com/wheesys/moodist/commit/cad85c76676cff7fe8c47ccb8d332809f7276e28)) + + +### ⚡️ Performance Improvements + +* improve the breathing cricle ([3d83a14](https://github.com/wheesys/moodist/commit/3d83a1427feaec1e858953899870da06d35b4631)) + + +### ♻️ Code Refactoring + +* add constants ([81678ea](https://github.com/wheesys/moodist/commit/81678ea384bfdc00925e674c988fad85710d705a)) +* add description for events ([2c8135d](https://github.com/wheesys/moodist/commit/2c8135db43b1a1dad789277926af0d1be3e987fc)) +* add JSDoc for custom hooks ([0f50e6a](https://github.com/wheesys/moodist/commit/0f50e6ae8b3d1615ed52fb168a48bbb2149090ac)) +* add JSDoc for helper functions ([4ae0504](https://github.com/wheesys/moodist/commit/4ae05049377506f79f5ef9f68fa7cf396d7d0528)) +* add Radix ([ddae0b6](https://github.com/wheesys/moodist/commit/ddae0b660ff2bb0bc33400ad59159f4525d80429)) +* add variant to container ([831a9c8](https://github.com/wheesys/moodist/commit/831a9c8ea019a3d86e994ff0e060dd4337a84d1f)) +* better item structure for menu ([26bf016](https://github.com/wheesys/moodist/commit/26bf01690cfcc105b661951bcb2347394a67fb68)) +* better name ([2192335](https://github.com/wheesys/moodist/commit/219233523827ed47a8ebea88a4ce73bb3c027e0c)) +* better shortcut handling ([f81ea9e](https://github.com/wheesys/moodist/commit/f81ea9e7bdf7c7253587da9312e6fb6caaf14590)) +* better tooltip ([5fecd38](https://github.com/wheesys/moodist/commit/5fecd383aaf757dbb563a1abd7eee0e64905902c)) +* change data file structure ([c9e8bd4](https://github.com/wheesys/moodist/commit/c9e8bd41fd79f6c73c11e6fcdbe8b24c6c0bbeb4)) +* change ordering config ([a43c679](https://github.com/wheesys/moodist/commit/a43c679e214b24c7f547e182aea6e2fbf826228f)) +* change stores structure ([096251e](https://github.com/wheesys/moodist/commit/096251ec0a459efbbe08d88cabab75c4ad775976)) +* migrate to Astro components ([ffe260f](https://github.com/wheesys/moodist/commit/ffe260f4a02238cb83cf92ed06c4f9c75ba189a4)) +* move donation to React ([c505c57](https://github.com/wheesys/moodist/commit/c505c574a885004e071da63d8255062befc29921)) +* move footer to React ([52176bc](https://github.com/wheesys/moodist/commit/52176bc3f9eac43d701de0e7f0ca103eeca46858)) +* reduce dependency ([c893e2a](https://github.com/wheesys/moodist/commit/c893e2a6adc68bdd40f8e5dd1e2b3ab6642a0145)) +* refactor the breathing tool ([d56f8be](https://github.com/wheesys/moodist/commit/d56f8be448aa746874c38ba0cc7e00e38d339f59)) +* relocate folders ([f3cea66](https://github.com/wheesys/moodist/commit/f3cea668470ca06b2114a03b54660475cc560d44)) +* relocate generic components ([4adfb3d](https://github.com/wheesys/moodist/commit/4adfb3ddc938a2720c26b9107c8cccdf66c0b913)) +* relocate sections ([d672bf6](https://github.com/wheesys/moodist/commit/d672bf6f85fe7b3a5c20fc53668705ab3d7827c5)) +* remove extra hook ([a4a31dd](https://github.com/wheesys/moodist/commit/a4a31dd43eef5c3e1d2b62cf4bb6e491e382f988)) +* remove extra types ([e490a1d](https://github.com/wheesys/moodist/commit/e490a1da84d948c9db2e689414f432aaf53bc0b2)) +* remove hide delay for tooltips ([48291a6](https://github.com/wheesys/moodist/commit/48291a645776b235918485b737b9272113f838a0)) +* remove media session ([1f63534](https://github.com/wheesys/moodist/commit/1f635348e3e5cf73ee76e1c5fac7b5f5b7f7ea6a)) +* remove sections ([3f45be3](https://github.com/wheesys/moodist/commit/3f45be3942bfeff74f3f0126de5e61037a749e61)) +* remove seperate favorite store ([d7fd17e](https://github.com/wheesys/moodist/commit/d7fd17ea8bb79ab44220bedfd62c98f9abf1d9f6)) +* remove seperate playing context ([daee746](https://github.com/wheesys/moodist/commit/daee7465bc4460a11b6aa5885cbd0eb7191c0026)) +* remove the timer store ([5ffb06b](https://github.com/wheesys/moodist/commit/5ffb06be036acb1fe5d8fa4b91e4cbede39ebcc0)) +* remove unmute and media session ([b77c817](https://github.com/wheesys/moodist/commit/b77c817db25e1a738b6770b1ae86d792e0d42240)) +* rename component ([f5cdb8c](https://github.com/wheesys/moodist/commit/f5cdb8c06b44f9cdde27e6e7c7e3d4d156c21dca)) +* rename components ([d73b2bc](https://github.com/wheesys/moodist/commit/d73b2bc1ff7689ff85c6453710b2d89927973066)) +* rename hook file ([2f84268](https://github.com/wheesys/moodist/commit/2f84268017aa4592684c8e3ac47399d0f100669d)) +* rename some functions ([0533460](https://github.com/wheesys/moodist/commit/05334606673a6268ca35083ea31e28cdb11f1b86)) +* rename stores folder ([2a86a88](https://github.com/wheesys/moodist/commit/2a86a88ed6a232c4a8c2a10bbb06f586361f732d)) +* reorder menu items ([ae0cbf1](https://github.com/wheesys/moodist/commit/ae0cbf1aa3392ae775bfee9404c21ed7c145166e)) +* rewrite menu with floating ui ([8beb42c](https://github.com/wheesys/moodist/commit/8beb42cb1b92c99aa9656b35cd7d82094e5baf72)) +* rewrite timer logic ([7c57fb6](https://github.com/wheesys/moodist/commit/7c57fb686b50fa106ad0663a44f4831295d235c3)) +* separate sounds ([a1ea9a1](https://github.com/wheesys/moodist/commit/a1ea9a19e64f062c1d63ecef7fb200fbba063fe4)) +* separate the migration ([c35409c](https://github.com/wheesys/moodist/commit/c35409ce0a95d8376f0d84c96ed0975c9f3a1301)) +* seperate buttons ([b117a4b](https://github.com/wheesys/moodist/commit/b117a4b495bed8d7b034c42a70e080bc062ad672)) +* seperate common types ([bad2d31](https://github.com/wheesys/moodist/commit/bad2d31b2dfa6a1f01c1c9cd767209c2c6f58f5c)) +* seperate favorite button ([4124beb](https://github.com/wheesys/moodist/commit/4124beb5b4818f1eee322fa6a4777f2e422d04ba)) +* seperate irrelevant logic ([f1688cb](https://github.com/wheesys/moodist/commit/f1688cb53ccf7199759b8a60f1d05787edd05790)) +* seperate motion variants ([7fce9e1](https://github.com/wheesys/moodist/commit/7fce9e1dff3dfe2b17a92872125bb29f61fee23f)) +* seperate range input ([89149dc](https://github.com/wheesys/moodist/commit/89149dca78069affadb5633ba1354dd50fb616ae)) +* sort interface keys ([c5240ff](https://github.com/wheesys/moodist/commit/c5240ff507fba8d979ef842ceba05b712b76220d)) +* turn footer into Astro component ([a67083c](https://github.com/wheesys/moodist/commit/a67083c0e9812acc1dd71fade41a81f307669116)) +* turn hero into Astro component ([77f9fcc](https://github.com/wheesys/moodist/commit/77f9fcc50e54cecb31877eaccb3a578c291f99fe)) +* turn sections into Astro components ([9398ae0](https://github.com/wheesys/moodist/commit/9398ae0eddb4fac9695569a97a829bd518500363)) +* use nullish operator ([9d633a9](https://github.com/wheesys/moodist/commit/9d633a963772c3444b6e9effc7920fe190b0614d)) +* use scrollIntoView instead of link ([4d2645f](https://github.com/wheesys/moodist/commit/4d2645f06c846eea791f182224be0bc6e3db76dc)) +* use the ID instead of index ([7658842](https://github.com/wheesys/moodist/commit/7658842324a92210a6a612c70c5479c6bb7f3c05)) +* write JSDoc for libs ([fddf75c](https://github.com/wheesys/moodist/commit/fddf75cdca1f121160f9054c82a7a1ddedd6f2fa)) + + +### 🐛 Bug Fixes + +* add aria label to shuffle button ([6d02cfb](https://github.com/wheesys/moodist/commit/6d02cfb134bc925b9824040307b1b40626312fd1)) +* add aria labels ([85768d8](https://github.com/wheesys/moodist/commit/85768d8bca10f2732e98d138a3d83ec3116816d4)) +* add audio element ([889962b](https://github.com/wheesys/moodist/commit/889962babe6e940ff283a41b145620d2a0477c70)) +* add correct count to description ([81e6666](https://github.com/wheesys/moodist/commit/81e66667765879da624544c5d915c1562f2ab34c)) +* add default value ([14c331a](https://github.com/wheesys/moodist/commit/14c331ab6e692ea3fcdaa056e32728f0a1cd2772)) +* add delay to cipher text ([4895a72](https://github.com/wheesys/moodist/commit/4895a7266d1b7458bc09a77dd6922058a247ea98)) +* add key to categories ([38c11f1](https://github.com/wheesys/moodist/commit/38c11f124e2235bc32de1e469b00ccaa22467a7e)) +* add keys to filler elements ([b7c7d40](https://github.com/wheesys/moodist/commit/b7c7d40bf9c47c4a2793335e406ac4173d98a1e0)) +* add media session ([81f33d9](https://github.com/wheesys/moodist/commit/81f33d9d375f63b4dd0bf58ad28a72354d85706e)) +* add media session ([216b913](https://github.com/wheesys/moodist/commit/216b913ccd0a7dfe0d03575f842aac9711ef0216)) +* add min-width to inputs ([dfd6c1f](https://github.com/wheesys/moodist/commit/dfd6c1fc4a41845e686fc6ee96f71b696213fe69)) +* add unmute for iOS ([e422b52](https://github.com/wheesys/moodist/commit/e422b52436c7dfc0b6cf866afa2b74dc219dcf2f)) +* allow empty inputs ([601ba6d](https://github.com/wheesys/moodist/commit/601ba6def7954ca8f961c461abacfb076863ae18)) +* better implement shortcuts ([e77c67b](https://github.com/wheesys/moodist/commit/e77c67bc24f1831bb6de80a4335c51e5b84009ed)) +* change completion condition ([1ac5286](https://github.com/wheesys/moodist/commit/1ac52861d1de651f8245d1e343307c6cf7a13dde)) +* change default times ([158cffc](https://github.com/wheesys/moodist/commit/158cffca8c4b138f33e2df03e046706d2b122478)) +* change default values ([34d3c72](https://github.com/wheesys/moodist/commit/34d3c72f3512664ac8f26a637b0d0be86b5499df)) +* change icon path ([09c0a6c](https://github.com/wheesys/moodist/commit/09c0a6ce93f8b0f62149928218532201e0de16c5)) +* change icon path ([28c3c40](https://github.com/wheesys/moodist/commit/28c3c404ad790869b13731e4c3622abe33f1dda2)) +* change icon path ([8cceb6e](https://github.com/wheesys/moodist/commit/8cceb6ecd1d0183e0d5f0aeb7af4d80b2dc41b34)) +* change icon path ([dc6a9e1](https://github.com/wheesys/moodist/commit/dc6a9e120a0617761c9a36a3f1268c50d4a1b7c5)) +* change icon path ([c184246](https://github.com/wheesys/moodist/commit/c184246a1280e9e8cf85c77d1de8d32bf1d7592b)) +* change initial value ([a7e5368](https://github.com/wheesys/moodist/commit/a7e53685918187c47d4fc2935418786b772c189e)) +* change link address ([1b4d216](https://github.com/wheesys/moodist/commit/1b4d216b0813f8d336fba93c2e3bb794a988f834)) +* change page title ([3bebb3e](https://github.com/wheesys/moodist/commit/3bebb3e9d259dd7f87d17f29ea85df67c5e2ada5)) +* change shortcuts ([edd15f4](https://github.com/wheesys/moodist/commit/edd15f4b9a0291b9794102fbb41048de10b0fd69)) +* change z-index values ([79afb8d](https://github.com/wheesys/moodist/commit/79afb8d92f9cb8e551bf101267018af1ab58815d)) +* close all modals ([f025213](https://github.com/wheesys/moodist/commit/f025213ef2e8ddbc5e6603d045c8bd4d08ad8b7b)) +* coffee typo ([8e02910](https://github.com/wheesys/moodist/commit/8e0291004a90e55b67a921b9ffb483b409109ae4)) +* comment out toolbox section ([a8a8c36](https://github.com/wheesys/moodist/commit/a8a8c3643478d3da531e1da6c3640eb327acad3b)) +* complete donation links ([e6f768a](https://github.com/wheesys/moodist/commit/e6f768a5e6dc983ae04b70f6c434fd4c13aeb506)) +* **component:** update oscillators frequency on preset change ([dcc91e0](https://github.com/wheesys/moodist/commit/dcc91e038d806994382baa19b3d238da4a8ecaae)) +* connect audio context to audio element ([463667c](https://github.com/wheesys/moodist/commit/463667c868371540c46c9007e686961f9a4be7e5)) +* correct link ([496c831](https://github.com/wheesys/moodist/commit/496c831552442047d5556376a212698c8931b698)) +* disable the sleep timer when no sound is selected ([d42eb25](https://github.com/wheesys/moodist/commit/d42eb25f7be64b5e77cd0bacd1538949d331aff7)) +* fix button disabled and reset to 0 ([58bf28b](https://github.com/wheesys/moodist/commit/58bf28bb24fd12bc28f4f5e3e79058df60095fd4)) +* fix icon imports ([a3eb479](https://github.com/wheesys/moodist/commit/a3eb47914024eb7b9493adae95f916be591bb748)) +* fix some animation issues ([eccba87](https://github.com/wheesys/moodist/commit/eccba87557e0f444adb740e8d6488adad8a2ce42)) +* fix some types ([04061e2](https://github.com/wheesys/moodist/commit/04061e23c3063279afa493a1e120817f80447840)) +* fixate the binary pattern ([4996cc8](https://github.com/wheesys/moodist/commit/4996cc893c480ab77cf27a27801dba96771eadc5)) +* focus on the first new sound ([54c7772](https://github.com/wheesys/moodist/commit/54c777276deccfda20bb7f027cef40d141a445b1)) +* icons path ([1a1359c](https://github.com/wheesys/moodist/commit/1a1359c989268a22cfdba20f198af192726ac2ce)) +* increase decimal ([a33ae45](https://github.com/wheesys/moodist/commit/a33ae450cf2c883228c76d04df8df75839c12753)) +* make inputs full width ([cc77f9e](https://github.com/wheesys/moodist/commit/cc77f9e9c0b0a0d7734353486c93b4ca1509bf36)) +* make share hotkey conditional ([9ad49d0](https://github.com/wheesys/moodist/commit/9ad49d021a34d47160575ae1349f866ecb26c077)) +* make sound count dynamic ([f66a6ff](https://github.com/wheesys/moodist/commit/f66a6ffde770992353a5b21fe65c20fe50ab4328)) +* make sound count dynamic ([79458bb](https://github.com/wheesys/moodist/commit/79458bba54189147af8b8e3f38b34c756d4bd58e)) +* play sounds when starting timer if not already playing ([2e375ad](https://github.com/wheesys/moodist/commit/2e375ad40a8001ee00c9553ba46d70f3bbe6636c)) +* refocus on show more button ([b955fc9](https://github.com/wheesys/moodist/commit/b955fc93f42c1bd71d5fb5ff46f9e3a039c7fe83)) +* rehydrate store only on mount ([2c443d3](https://github.com/wheesys/moodist/commit/2c443d3f33d9d9f4d00ed1e99a8b092597abce97)) +* relocate focus trap ([8596a00](https://github.com/wheesys/moodist/commit/8596a0014cbbac25ec93b1bb9136219a096cb21f)) +* remove auto focus on load ([3b0c229](https://github.com/wheesys/moodist/commit/3b0c22968e4209fa5a37a88b69f55492615ec389)) +* remove console log ([7c6f068](https://github.com/wheesys/moodist/commit/7c6f068d158cda0f8b0fe6bd6352a65002db0e25)) +* remove dropdown menu item from slider ([99e6941](https://github.com/wheesys/moodist/commit/99e694161f16a3be03cbda0854687a244df42f21)) +* remove extra headings ([7390a9b](https://github.com/wheesys/moodist/commit/7390a9b3de0c52163d63b42ad48a882087886b65)) +* remove extra hook ([3ef4a07](https://github.com/wheesys/moodist/commit/3ef4a076a2b48911d37f75067dc60ea15dd28405)) +* remove extra play calls ([e0164c3](https://github.com/wheesys/moodist/commit/e0164c362d72fea7587f67470e4d295007e5ad5e)) +* remove fading ([653d309](https://github.com/wheesys/moodist/commit/653d309e64b770c2b270d2435bcd02345b686fec)) +* remove fading ([d96461d](https://github.com/wheesys/moodist/commit/d96461d1ea83c72bfe651d84cf34fabc029c200e)) +* remove history on favorite toggle ([190f06a](https://github.com/wheesys/moodist/commit/190f06aa78b1aff931348a65da864404b2d0f4d5)) +* remove history on select ([5bd1dd3](https://github.com/wheesys/moodist/commit/5bd1dd3016cf97ad397b4371015605473c55dee8)) +* remove media session ([9338b1d](https://github.com/wheesys/moodist/commit/9338b1d30a4ae4602b339bc5c5a391a462a03de2)) +* remove media session ([8d01d74](https://github.com/wheesys/moodist/commit/8d01d74bd356adce782b95065fadad332ed99e48)) +* remove time from tabs array ([110356b](https://github.com/wheesys/moodist/commit/110356b2da82e0f1e971ee9dc486664027d886ff)) +* remove tooltip ([b634d6f](https://github.com/wheesys/moodist/commit/b634d6f3c354a51e4403374b2e3505e4f2c09351)) +* remove word counter dependency ([ae3ea8c](https://github.com/wheesys/moodist/commit/ae3ea8c74f9a94ae56a0eb4b165bc36d990dea7b)) +* replace generator with static silent audio ([af09607](https://github.com/wheesys/moodist/commit/af096077aed6c42d4ff77303e6f3c1d39cd87209)) +* replace the animation on button ([8307657](https://github.com/wheesys/moodist/commit/8307657628c0afc7ef11c3a829344a64777dc1d3)) +* reset values on cancel ([89a8308](https://github.com/wheesys/moodist/commit/89a83089c568c619fd76a28c268ad9af9913babc)) +* resume audio ([8e4d053](https://github.com/wheesys/moodist/commit/8e4d0531e0e9aaf4e52b3b3a8666b74ff0c0222e)) +* rotate the spinner when unselected ([cf7600e](https://github.com/wheesys/moodist/commit/cf7600e6c72d9d9638c3a9ad0513675d353422cd)) +* set aria label to ID ([7e0a9af](https://github.com/wheesys/moodist/commit/7e0a9afb179d228301effe00575c2f67b426e3da)) +* stringify dependency ([1a23e00](https://github.com/wheesys/moodist/commit/1a23e004a65960ce169990211f150db25762fead)) +* take remvze comments into account ([0517c31](https://github.com/wheesys/moodist/commit/0517c31fc13e0b82391e18a7d16341421488f1c2)) +* turn off spell check ([c66cddc](https://github.com/wheesys/moodist/commit/c66cddc4c98c19a8c0ef46ed0ee7555a30fd5059)) +* typo ([5cfb9a8](https://github.com/wheesys/moodist/commit/5cfb9a8293a215b83a826c403d996d00108d49b5)) +* typo in README file ([06d0dfb](https://github.com/wheesys/moodist/commit/06d0dfbe7eb0660a97c84627b1751b9a43d2e033)) +* undo changes ([32da26c](https://github.com/wheesys/moodist/commit/32da26ccfc0c5bdbe031e26ea48363ea0d8a7b23)) + + +### 🚚 Chores + +* add accessibility addon ([0300df3](https://github.com/wheesys/moodist/commit/0300df3852838135245882a8aa1c59dd1a3f8af7)) +* add animation to countdown timer ([73a5c21](https://github.com/wheesys/moodist/commit/73a5c21be918e1e105214078eaef8d76b168333b)) +* add autodocs for button ([3f3bcdd](https://github.com/wheesys/moodist/commit/3f3bcdda21b631683028ea1c65e674973c78291d)) +* add banner ([fb82117](https://github.com/wheesys/moodist/commit/fb82117742c2a0beb8937a76fcd5f313230cd418)) +* add binaural beats ([f1d212a](https://github.com/wheesys/moodist/commit/f1d212abc8b69a614bbdc4a23876e2eab7cbb574)) +* add Commitizen ([9d7cdde](https://github.com/wheesys/moodist/commit/9d7cddeb8b7156033a0b5f1a9012d34de60032bb)) +* add Commitlint ([50341d1](https://github.com/wheesys/moodist/commit/50341d19bbed1b75d5e9fff5948c1792e5110e52)) +* add contributing guide ([5899d1b](https://github.com/wheesys/moodist/commit/5899d1bbbb8eb621882e2cbacc1bc1dc9ae2ee06)) +* add contribution section to README file ([b990778](https://github.com/wheesys/moodist/commit/b9907781424ccd43babd31dd1d939d2e78ba4a11)) +* add divider ([3e44516](https://github.com/wheesys/moodist/commit/3e445165090472859573e69fad0fdeec87ca858f)) +* add donation link to README file ([1f806c4](https://github.com/wheesys/moodist/commit/1f806c4e561d79a00850130eda09376299d85ed2)) +* add Editor Config ([a7d3495](https://github.com/wheesys/moodist/commit/a7d3495fd0cec97c8b497feb1e5435f76ffc3539)) +* add emojis ([d09e598](https://github.com/wheesys/moodist/commit/d09e598297fb29f005873eb5e1cfad62774fc7f0)) +* add emojis ([781adcf](https://github.com/wheesys/moodist/commit/781adcf17eecea61bc03b832d8c81f3aac304848)) +* add ESLint ([be2a66e](https://github.com/wheesys/moodist/commit/be2a66e207d95b35b1aeacaf2c09f5c5206f2689)) +* add features to README file ([c614e3d](https://github.com/wheesys/moodist/commit/c614e3d4f54f814fe3813bc8788a23ecba5e38c8)) +* add Husky ([3bed00a](https://github.com/wheesys/moodist/commit/3bed00a1eeba7675df8873a986fa533a39f8314f)) +* add library sound ([309dd89](https://github.com/wheesys/moodist/commit/309dd89a8c13eb2647217c81d7fc0a82eb3ebaae)) +* add licenses to README file ([dcef777](https://github.com/wheesys/moodist/commit/dcef77729579391706047ad68afd73a07acf5122)) +* add link to issue ([6fe9ce8](https://github.com/wheesys/moodist/commit/6fe9ce8915600e5ec0140b5bb635ac1a2b092339)) +* add link to story ([f8fb1ed](https://github.com/wheesys/moodist/commit/f8fb1ed61e071baeba7981773e4dbd1e345c29b1)) +* add Lint Staged ([6cad460](https://github.com/wheesys/moodist/commit/6cad46040d15d839c56ff6efdf54f7a93bfc7611)) +* add more sounds ([095e3c7](https://github.com/wheesys/moodist/commit/095e3c795ef699e9e99c5eb364badaadce8a884b)) +* add more sounds ([38f6f7d](https://github.com/wheesys/moodist/commit/38f6f7dbe6898ed78e51eb3f0c7936f003ddca08)) +* add more sounds ([937bf29](https://github.com/wheesys/moodist/commit/937bf29d09cbce20ea0b6b0c87879f3a7dd1d497)) +* add more sounds ([e2172fd](https://github.com/wheesys/moodist/commit/e2172fd2bbd0e12a705c9efc98c72ad99d86d006)) +* add more sounds ([1f12afa](https://github.com/wheesys/moodist/commit/1f12afa3943154d70145ef6adc6aeee79f7a7af3)) +* add more sounds ([cd05704](https://github.com/wheesys/moodist/commit/cd05704a73ffb33aa0ccf5d789328a4cefc320f1)) +* add more sounds ([01b4bdb](https://github.com/wheesys/moodist/commit/01b4bdbb572285984bcdc9bb94c1a1b6dd2630c5)) +* add more sounds ([e3864be](https://github.com/wheesys/moodist/commit/e3864bede129c102ef5b7258b4688d9177dd284c)) +* add more sounds ([55e7f05](https://github.com/wheesys/moodist/commit/55e7f05892f6d3200b56a7e06b371bed4b4c4554)) +* add more sounds ([318e87c](https://github.com/wheesys/moodist/commit/318e87c9f1f3e2509c2b8eeb3a7f6875dd1c02fd)) +* add more sounds ([eed5a13](https://github.com/wheesys/moodist/commit/eed5a1329d6fc36d1e6375feaeaf2bba26167bf5)) +* add more sounds ([5a7936f](https://github.com/wheesys/moodist/commit/5a7936f11c4510886d14400e088ac0d8977a4806)) +* add more sounds ([8c75f87](https://github.com/wheesys/moodist/commit/8c75f875f0e39d392f8394d67b64d3d6d4e6f4a0)) +* add npm commands to README file ([8d90344](https://github.com/wheesys/moodist/commit/8d90344b26d3d52d1649074486d10c7b0bc68b66)) +* add npm config ([297f7a7](https://github.com/wheesys/moodist/commit/297f7a77af1a20ac09d0faf57008d44cc2dd2178)) +* add path alias ([123839d](https://github.com/wheesys/moodist/commit/123839d166948aa9283b0342ed268399eea59cf4)) +* add places category ([5970012](https://github.com/wheesys/moodist/commit/5970012fa6cbd8222c2be8ce426065f928d81b2b)) +* add PostCSS ([332bd49](https://github.com/wheesys/moodist/commit/332bd496f7dc9565e05a6e467d9f76831168eb12)) +* add Prettier ([110359b](https://github.com/wheesys/moodist/commit/110359b9158758f7c77d09bc884ee18f686e513f)) +* add robots.txt file ([6bdf28a](https://github.com/wheesys/moodist/commit/6bdf28afdcf218c02f3bddc2a55fc1b6b88ebcff)) +* add Standard Version ([afc330e](https://github.com/wheesys/moodist/commit/afc330eef05bd9fa5aeb6d12dd968e2434fd19b5)) +* add story for snackbar provider ([f19d151](https://github.com/wheesys/moodist/commit/f19d151f4a5292668e87abb04111e142482baf1e)) +* add Stylelint ([0e5948f](https://github.com/wheesys/moodist/commit/0e5948f0588e2c545f6557e3c8971fb961464f86)) +* add support section ([672988c](https://github.com/wheesys/moodist/commit/672988c36e8630fe775fdf0707bfa3e1a8956231)) +* add tech stack to README file ([8e6e690](https://github.com/wheesys/moodist/commit/8e6e6900069775df5c29c53b2d2b9a00457ad8f8)) +* add toolbox copy ([cfd2744](https://github.com/wheesys/moodist/commit/cfd2744e92b7a2948597a750275bf9c900248d55)) +* add transport category ([c1c3945](https://github.com/wheesys/moodist/commit/c1c3945d43e84e3011de52bffa5116d58283c473)) +* add washing machine sound ([7e65bb7](https://github.com/wheesys/moodist/commit/7e65bb75f9871603c30ecfc578ad109a969a2a58)) +* change docker workflow ([cb4bfea](https://github.com/wheesys/moodist/commit/cb4bfea5ab4326dee17c78554f12a08ffcb9dd0e)) +* change docker-compose file ([660ee07](https://github.com/wheesys/moodist/commit/660ee07a2359ec77c9d56bbe552541246e0f79c5)) +* change GitHub workflow ([faf7f78](https://github.com/wheesys/moodist/commit/faf7f78b8c10cd7b3688ed5bba3d0c077c020ad2)) +* change heartbeat audio ([f43a378](https://github.com/wheesys/moodist/commit/f43a378697437f671c0c33122b1c9ec5a1e173ff)) +* change README banner ([c450028](https://github.com/wheesys/moodist/commit/c450028ac7e58e961204de4789231d357d129ca1)) +* change README file ([85e42f3](https://github.com/wheesys/moodist/commit/85e42f3606f9fba281f2177d0dbffc86851603f9)) +* comment out the banner ([c5adffb](https://github.com/wheesys/moodist/commit/c5adffb4d777eda1e2a092e382c1cac616dd60f1)) +* complete tech stack ([aeccf2d](https://github.com/wheesys/moodist/commit/aeccf2dabd7528ff7984b50b7e7c7b8f46d4cef7)) +* install Storybook ([65ca7e1](https://github.com/wheesys/moodist/commit/65ca7e1c942455a41f8af794861a1875bd6190be)) +* refine logo ([755c442](https://github.com/wheesys/moodist/commit/755c4422635e475b8d3b0f26e3cf493a59ff3065)) +* **release:** 0.0.1 ([17cbd6e](https://github.com/wheesys/moodist/commit/17cbd6eb38ef386a124c98ee0d95f5593b62b0f0)) +* **release:** 1.0.0 ([df1a21f](https://github.com/wheesys/moodist/commit/df1a21f109863da9cf47e0ff05f2dfa26a545b12)) +* **release:** 1.1.0 ([69b8519](https://github.com/wheesys/moodist/commit/69b85199bb0b4ba16039d267c4bf13818f77bb99)) +* **release:** 1.2.0 ([b8bc9c8](https://github.com/wheesys/moodist/commit/b8bc9c8b4c80f8ee4401eea739b98e5464591d51)) +* **release:** 1.3.0 ([f877e49](https://github.com/wheesys/moodist/commit/f877e49763fdae78a00d0dfe2d4240b55ab60c3e)) +* **release:** 1.3.1 ([75ff67c](https://github.com/wheesys/moodist/commit/75ff67c9e635847d3da93c59c57c5b4a0414f257)) +* **release:** 1.4.0 ([6dfa998](https://github.com/wheesys/moodist/commit/6dfa998ffe571886ad15e6865092870dd0db492e)) +* **release:** 1.4.1 ([42bd47b](https://github.com/wheesys/moodist/commit/42bd47bbeacbe5b215152109dea39c948a878c7e)) +* **release:** 1.4.2 ([73a8e03](https://github.com/wheesys/moodist/commit/73a8e03d66ec5a440c961a71bd2599b00fff6b1a)) +* **release:** 1.4.3 ([4b5456a](https://github.com/wheesys/moodist/commit/4b5456a51d8aba9b93f34c2d1064854520a74778)) +* **release:** 1.5.0 ([78656bb](https://github.com/wheesys/moodist/commit/78656bb61f95791e63ffb93abc11866a7a11a429)) +* **release:** 1.5.1 ([c60dcc7](https://github.com/wheesys/moodist/commit/c60dcc74edbd23c7d1052ca965ac638341aa4a63)) +* **release:** 2.0.0 ([87f64e6](https://github.com/wheesys/moodist/commit/87f64e6574fe2d90153d44ecf3f2e1e01f68b600)) +* **release:** 2.0.1 ([df210a1](https://github.com/wheesys/moodist/commit/df210a1246d3395c7f1fa5aef8433f52e76f40ea)) +* **release:** 2.1.0 ([4c8d577](https://github.com/wheesys/moodist/commit/4c8d5775274ad0573c73e30e5aae4fc87361e0e9)) +* relocate underwater audio ([37bad81](https://github.com/wheesys/moodist/commit/37bad8149e1f5170426dc745322c0e65cb9a41ff)) +* remove arm/v6 ([017c27f](https://github.com/wheesys/moodist/commit/017c27fb2b20402553011db8f417717dcca6d447)) +* remove arm/v7 ([fa9711a](https://github.com/wheesys/moodist/commit/fa9711a1e09e6e979b420556160c3cd69a8c3775)) +* remove extra sound ([9ad1630](https://github.com/wheesys/moodist/commit/9ad16306cf534ff27e99a537589c0d3c2c483d81)) +* remove heartbeat audio ([121a8f2](https://github.com/wheesys/moodist/commit/121a8f204c6b61490a7ab0e732779031278e6e8c)) +* update banner ([a0a7f94](https://github.com/wheesys/moodist/commit/a0a7f94c3328c65d4fc756ca52455461a05657ab)) +* update banner ([2f994c6](https://github.com/wheesys/moodist/commit/2f994c6094ad1948c14346badbc4462ae7782904)) +* update GitHub action ([ee60613](https://github.com/wheesys/moodist/commit/ee606139a80121fd6ee1b8233f82af994c4e1178)) +* update logos ([7a47282](https://github.com/wheesys/moodist/commit/7a472821652d1359126568836b3040ce1fa454c5)) +* update logos ([2b85b27](https://github.com/wheesys/moodist/commit/2b85b276eb11d862bf1abd1e6f099740d9b85c10)) +* update README file ([a9fe7f7](https://github.com/wheesys/moodist/commit/a9fe7f7b4f9ca91704d76a314e3c3368fbc4f1cf)) +* update README file ([629f0a5](https://github.com/wheesys/moodist/commit/629f0a514ec1ac96f1874b8d6a466bf05577cd4d)) +* update README file ([de49d37](https://github.com/wheesys/moodist/commit/de49d37f08a90523e9b9b298189b10103e833e15)) +* update README file ([7cb0f1c](https://github.com/wheesys/moodist/commit/7cb0f1c7521775578bb6d794f43d04aa0da2fcba)) +* update README file ([dc139e4](https://github.com/wheesys/moodist/commit/dc139e41e628a75756cea99bdca0252267541014)) +* update README file ([954a1b1](https://github.com/wheesys/moodist/commit/954a1b1ce2c9f334d349fcd140ec18a5c78b7dd7)) +* update README file ([383f898](https://github.com/wheesys/moodist/commit/383f8981250d2fe646b4f642b36b28b3dbdd178f)) +* update the logo ([348fc1e](https://github.com/wheesys/moodist/commit/348fc1e8c4561481e5ad1d4528e8ee480d0e2fb4)) +* upgrade Astro ([72fa516](https://github.com/wheesys/moodist/commit/72fa516316cf1077cf5ab09bc59b76de147c6d38)) +* write story for button ([603d318](https://github.com/wheesys/moodist/commit/603d318e68ec786cfbeaad57835a812ca8918fb9)) + + +### 💄 Styling + +* add animation on active ([50687c9](https://github.com/wheesys/moodist/commit/50687c97ca483f4de3ee7633d333dfcb4def0c4d)) +* add animation to modal ([7823dc7](https://github.com/wheesys/moodist/commit/7823dc7ff473278ef8ee401e69796c17b33da794)) +* add animation to more/less button ([b849b3a](https://github.com/wheesys/moodist/commit/b849b3aecd6178114b3b27a2daa014b0795ddf42)) +* add animation to presets ([787a9b6](https://github.com/wheesys/moodist/commit/787a9b60b51334ec2a7423d489f71c305661039e)) +* add base and global styles ([05d68e4](https://github.com/wheesys/moodist/commit/05d68e4de6f55ebbc08817ed553f7760f570208b)) +* add binary pattern ([ba3cd5c](https://github.com/wheesys/moodist/commit/ba3cd5ca5be8435f32b93d5a499e37388340bff8)) +* add effect to about ([1a499be](https://github.com/wheesys/moodist/commit/1a499be2446730d5333dd0d0d6a06bbd47564979)) +* add focus state ([af075b3](https://github.com/wheesys/moodist/commit/af075b32e64a6ab923d60282558250b79cc12da3)) +* add gradient background ([77fed03](https://github.com/wheesys/moodist/commit/77fed0308ad55ca32f07b4f30e7a7936063d842a)) +* add gradient line ([ea722ea](https://github.com/wheesys/moodist/commit/ea722eabd24cb966c65fa45d41f55e1e1a049939)) +* add hover state to button ([ebb35de](https://github.com/wheesys/moodist/commit/ebb35deaf982348ccea49e3830af77521fbed207)) +* add hover states ([2c74dd0](https://github.com/wheesys/moodist/commit/2c74dd0d604af86f99edcba2eb573641ac2010fd)) +* add icon to menu items ([131ab29](https://github.com/wheesys/moodist/commit/131ab296215812e45a0c60486d75683f3de25d16)) +* add line to titles ([ec1def0](https://github.com/wheesys/moodist/commit/ec1def041934d8a9f98084299a0606c5690ef23d)) +* add margin to donate section ([6d30a01](https://github.com/wheesys/moodist/commit/6d30a0123e0feb509b6c560f405b98d20a89467a)) +* add min width ([18987cc](https://github.com/wheesys/moodist/commit/18987cc33997c7b010aea2d4f1546ddcabe1a46b)) +* add more icons ([41c5ae5](https://github.com/wheesys/moodist/commit/41c5ae5db8e72f15f5cc1b7501f397239ba9368a)) +* add new font weight for links ([287d7b3](https://github.com/wheesys/moodist/commit/287d7b33fb107e81034a17a60e1cd6cd5d40d935)) +* add outline for better accessibility ([e7d7a37](https://github.com/wheesys/moodist/commit/e7d7a37a12dd79f12933b3ffa91fe6e0557c4f9e)) +* add outlines to toolbar buttons ([a3cfbb9](https://github.com/wheesys/moodist/commit/a3cfbb98db8a70d8055e86071a4dab4d2b7ab952)) +* add pattern ([69eb883](https://github.com/wheesys/moodist/commit/69eb8832dae026706f76ba21a74fcb248ba4309d)) +* add polka dot pattern ([dc22b51](https://github.com/wheesys/moodist/commit/dc22b51548f0d6830fcee79eebd75650f3f19dc1)) +* add scroll lock in modals ([def69de](https://github.com/wheesys/moodist/commit/def69de6e4e11e373280c90f93af0b0369b85ac8)) +* add shine to donation button ([ac24da2](https://github.com/wheesys/moodist/commit/ac24da294008a34c49dc3502374f1fcf55db5be8)) +* add smooth transition ([3b33e09](https://github.com/wheesys/moodist/commit/3b33e095479340496a7a11b057daef029f40b70a)) +* add smooth transition ([e7fc951](https://github.com/wheesys/moodist/commit/e7fc9513109ae48ce407745549085c9449cf3324)) +* add style to generators ([5c53678](https://github.com/wheesys/moodist/commit/5c536786ea64e9722a67289ab2d7e56e7a259404)) +* add text animation ([7810d21](https://github.com/wheesys/moodist/commit/7810d212259cfe19befafab33d51110126089a83)) +* add theme color ([6de1394](https://github.com/wheesys/moodist/commit/6de1394628ccb6b58aec02bcd164e56e9ca0f30a)) +* add title to timer ([a3c384d](https://github.com/wheesys/moodist/commit/a3c384d1054b81e056265eecd9344496c9b0b5ce)) +* add wrap balancer to desc ([276639b](https://github.com/wheesys/moodist/commit/276639b0d3a70ead87dc61e2c8cb7cd621261c3e)) +* better line alignment ([1f24812](https://github.com/wheesys/moodist/commit/1f24812efa3b64fdbfc794bcb546226cc2ef07d4)) +* better outlines for accessibility ([3c8d75b](https://github.com/wheesys/moodist/commit/3c8d75b018e657b2c2e13d967b90b635360225fe)) +* center icons ([1cf9a85](https://github.com/wheesys/moodist/commit/1cf9a85e13d50d3c5335dfb78fa57543ce6fda44)) +* change border color ([85b627e](https://github.com/wheesys/moodist/commit/85b627ecb96a4f52ecacdb53ed4484c050adba5e)) +* change border radius ([5c9a2aa](https://github.com/wheesys/moodist/commit/5c9a2aa23aa04f9386e7d7ac9a20759a2ed87acc)) +* change border to shadow ([a53800c](https://github.com/wheesys/moodist/commit/a53800c6b194e7520d2e7ee13c5e00f77db9f5f7)) +* change button animation ([6983559](https://github.com/wheesys/moodist/commit/6983559032d731ad6264ad56f0786b1a84f7cf4e)) +* change button animation ([c44a863](https://github.com/wheesys/moodist/commit/c44a86361ebf3a77d68148564a2983e60b522c29)) +* change button style ([8a79ccf](https://github.com/wheesys/moodist/commit/8a79ccf018cd7ee86b27b8bd187975376abea953)) +* change button style ([8efb1ce](https://github.com/wheesys/moodist/commit/8efb1cee00ec0e0dcd9677729d9136ca8d69073f)) +* change button styles ([e674738](https://github.com/wheesys/moodist/commit/e674738ce70d1c240c57433824a0b509f24deb88)) +* change copy ([c51acd6](https://github.com/wheesys/moodist/commit/c51acd62618cc705902dc01f0574a2c9124264c5)) +* change copy ([6242308](https://github.com/wheesys/moodist/commit/624230843c3328fdfb42e0e2f23084cef4dec614)) +* change cursor ([6ac65c1](https://github.com/wheesys/moodist/commit/6ac65c1948ad93fed012a8203fc8c6c2b2898b5b)) +* change description ([9208663](https://github.com/wheesys/moodist/commit/9208663050c340fdecf486b4835d30353852fd22)) +* change description ([8930e7b](https://github.com/wheesys/moodist/commit/8930e7b76abffafd0ace5926de6c1d3f7629febd)) +* change dividers ([8471a3c](https://github.com/wheesys/moodist/commit/8471a3ca493b0c738ed7de900e82403f0b1ce2b7)) +* change favicon ([a82dc3f](https://github.com/wheesys/moodist/commit/a82dc3f36af098071b6be09491e9e25bda190b74)) +* change font path ([43ba975](https://github.com/wheesys/moodist/commit/43ba9754085d7a710d3d629e70f873b16f267507)) +* change gradient ([9e38a8f](https://github.com/wheesys/moodist/commit/9e38a8fd7da2d68c8c04c4c21cbda6444e9e247b)) +* change icon backgrounds ([ef825ca](https://github.com/wheesys/moodist/commit/ef825cae68f3cd4ef58016212a45820d3b272f96)) +* change icon color on selection ([e6abca6](https://github.com/wheesys/moodist/commit/e6abca61fe9eb36ca6968339a4cb67beeb5f8fdc)) +* change icons ([2e1fce4](https://github.com/wheesys/moodist/commit/2e1fce46695b693c4b6aa11f18506e2f2cd9bb59)) +* change input styles ([8fe90da](https://github.com/wheesys/moodist/commit/8fe90daf1e96def534c62f3241438cf62ea00b18)) +* change item order ([9198315](https://github.com/wheesys/moodist/commit/919831538fea639eb60c8fb84fa93a79ec2cd9c5)) +* change label cursor ([15953ef](https://github.com/wheesys/moodist/commit/15953ef8565a27da2b41330753fbc40931987aa7)) +* change like color ([d8c9806](https://github.com/wheesys/moodist/commit/d8c9806a1964042b787baabf43e2852bab23dcfa)) +* change logo ([4a92d2f](https://github.com/wheesys/moodist/commit/4a92d2f1c12c12b4166500149937be51e6442f71)) +* change logo ([9f702db](https://github.com/wheesys/moodist/commit/9f702dbfa74b524b4553bd1686532bc7d35d9985)) +* change logo color ([4b01501](https://github.com/wheesys/moodist/commit/4b015016e7c531afc3f3b1f51d62bf96232e3ea8)) +* change notice ([9d1d8f8](https://github.com/wheesys/moodist/commit/9d1d8f80359097b9122673564d3d57c0827ff3db)) +* change other assets ([11e0ba2](https://github.com/wheesys/moodist/commit/11e0ba2f938fc08984e4acba1ba6b4ac3239cacf)) +* change outline color ([6f9c941](https://github.com/wheesys/moodist/commit/6f9c941a8749f2b006c3f352e0a047c5dc1d3d21)) +* change pattern ([f3e7224](https://github.com/wheesys/moodist/commit/f3e72242670317d938cc8d9619729be95df0f4f0)) +* change position for toolbar ([e7fd84b](https://github.com/wheesys/moodist/commit/e7fd84bd4e8637e34eb0a59e97fd9c49875f8776)) +* change primary color ([ed9a027](https://github.com/wheesys/moodist/commit/ed9a0271f7c49b499ab07487072cfd7bab5277db)) +* change reason copy ([69c4ec1](https://github.com/wheesys/moodist/commit/69c4ec150849a15e2aa222ac4b6f2982cc9536df)) +* change snackbar styles ([1e5bda7](https://github.com/wheesys/moodist/commit/1e5bda707cc202407b179e2d1b95dec34bfe9420)) +* change sound counter ([e1b9a17](https://github.com/wheesys/moodist/commit/e1b9a1736c1d11827faf900838769194364afbd3)) +* change sound counter ([00fc5f3](https://github.com/wheesys/moodist/commit/00fc5f3a872be51eb875744e254c75ea58e93281)) +* change spacing ([cc26f68](https://github.com/wheesys/moodist/commit/cc26f68097bd137bea1f62a9eba566844b1cb069)) +* change tagline ([f3603e8](https://github.com/wheesys/moodist/commit/f3603e84318a9b69145ae69d3aa02447ed1235e6)) +* change the about style ([4515aa8](https://github.com/wheesys/moodist/commit/4515aa8e7a7f6d0fbc839625f74f0583e1a20d18)) +* change the pattern slightly ([5fc3e7e](https://github.com/wheesys/moodist/commit/5fc3e7e5d048cb4aa189683d147b181fdf2a94b6)) +* change theme ([bd517f8](https://github.com/wheesys/moodist/commit/bd517f88c01202eb7e7e5acf70bf4af2e6f91d75)) +* change to primary color ([c8e5122](https://github.com/wheesys/moodist/commit/c8e51226e57bfa72ad91318de25fc5f9b5751634)) +* change unselected style ([586e502](https://github.com/wheesys/moodist/commit/586e502c3cc81314bc1e83f4e088a0d9289390fc)) +* decorate paragraphs ([1a6ecd8](https://github.com/wheesys/moodist/commit/1a6ecd82abe89e1686538c42b31826ccc8a43b2d)) +* decrease background opacity ([a071ba0](https://github.com/wheesys/moodist/commit/a071ba04c7e86b3056049492386516b58c4210c0)) +* decrease dots ([182a8c7](https://github.com/wheesys/moodist/commit/182a8c7aadc9a253261c56ae7faf8ac5c3c82707)) +* decrease dots ([0ad4bb7](https://github.com/wheesys/moodist/commit/0ad4bb72e15e8f7d52e7d4b036b71fdb837e3554)) +* decrease dots ([2b84374](https://github.com/wheesys/moodist/commit/2b843747b41111908bbe5fb8f5abc407114e4f15)) +* decrease font size ([69cb45b](https://github.com/wheesys/moodist/commit/69cb45bff74d36f654d521e9e7f6b2149b01d630)) +* decrease gradient shine ([8f58794](https://github.com/wheesys/moodist/commit/8f587944fd1ad5e11bb6bc3afc7e9380afa43a6c)) +* decrease margin ([d700195](https://github.com/wheesys/moodist/commit/d7001952f9ce323d746118583e0b34e001a8a517)) +* decrease opacity ([56802b6](https://github.com/wheesys/moodist/commit/56802b67f2db751dbede9aa58b2158dd250a1420)) +* decrease opacity ([2078648](https://github.com/wheesys/moodist/commit/2078648c6687aab79a725490335b8ae751f3d4ee)) +* decrease opacity ([82e4ea7](https://github.com/wheesys/moodist/commit/82e4ea72f4ddb8658824813a64e14970400b1820)) +* decrease padding ([98d2f76](https://github.com/wheesys/moodist/commit/98d2f764383eaba0dd6163b93826459b614b67d2)) +* decrease scale animation ([7e668e5](https://github.com/wheesys/moodist/commit/7e668e5b393c7df71bec8bf11696acbae22d70e4)) +* decrease shine ([0f32de3](https://github.com/wheesys/moodist/commit/0f32de3c0ca9f553c8917b786ddcdfb6feccf0c8)) +* fix margins ([99775b7](https://github.com/wheesys/moodist/commit/99775b7c6487b009bbf87fbd834ed8730508d1ce)) +* fix pointer event ([12d3255](https://github.com/wheesys/moodist/commit/12d3255d57083ff72ae919b6161922620dc1d6e2)) +* fix snackbar pointer ([14c9e88](https://github.com/wheesys/moodist/commit/14c9e88bfbef4b68dce0a1a8e570c1a9d9894dfd)) +* fix tooltip z-index ([fb061c3](https://github.com/wheesys/moodist/commit/fb061c3d66d3fa7c3fce63bae1e04e502fcbb891)) +* fix z-index ([fa71709](https://github.com/wheesys/moodist/commit/fa71709f897cc2b7a5ba03dbc1cb60a3198bf9f4)) +* hide about and features ([400ea0a](https://github.com/wheesys/moodist/commit/400ea0aeafe48587fe8596d1b5fe90755995d1c3)) +* hide features ([9028675](https://github.com/wheesys/moodist/commit/902867505743dd1dcd3f1e2afef010a186586615)) +* increase border radius ([e2bb4dd](https://github.com/wheesys/moodist/commit/e2bb4dd55fbf17e777ddbb6825e400bd023da328)) +* increase dots ([405fccc](https://github.com/wheesys/moodist/commit/405fcccd95d9ce720f0731e8040006bd1d9b8bd2)) +* increase line height ([a179c09](https://github.com/wheesys/moodist/commit/a179c09d0c637d33d310960dbf3e92af4b5c526b)) +* increase menu width ([96ca376](https://github.com/wheesys/moodist/commit/96ca3768856806bbe761e773d5ef626dcd12c968)) +* increase opacity ([882d440](https://github.com/wheesys/moodist/commit/882d44079cfba8c7536c3713f0abeaa075ecb069)) +* increase padding ([8e50013](https://github.com/wheesys/moodist/commit/8e500136cec6ba5580146306f25a5956aa3a2a4b)) +* increase padding ([eedbf53](https://github.com/wheesys/moodist/commit/eedbf53e0e07ba75161e9f397dc0554204bc004a)) +* increase pattern opacity ([5b83710](https://github.com/wheesys/moodist/commit/5b83710c75515352b88c7bd361694d3067cb08fb)) +* increase sounds per row ([cd8ec5e](https://github.com/wheesys/moodist/commit/cd8ec5e8649f8808d0a89a74c1426b92628efbc7)) +* increase text color ([d11a6ab](https://github.com/wheesys/moodist/commit/d11a6ab062061da5809ebddd6eb39b17c2cd3862)) +* lower opacity ([d4cc24e](https://github.com/wheesys/moodist/commit/d4cc24e468230df51e5676abbd828b2f2edd97f3)) +* minor change ([302a71c](https://github.com/wheesys/moodist/commit/302a71cdc6472dd29d75372ddc6a3ef214dd68c4)) +* minor change ([b73fd0b](https://github.com/wheesys/moodist/commit/b73fd0b16e57140350d0743aa98ec6933bdc5c64)) +* minor changes ([536db4c](https://github.com/wheesys/moodist/commit/536db4cd156cb391a0b1ef9bf3e4fbbac06ccc11)) +* minor changes ([7f3ac26](https://github.com/wheesys/moodist/commit/7f3ac26b982e629eef891f706004eca5f14e11c4)) +* minor changes ([4cc8597](https://github.com/wheesys/moodist/commit/4cc85975e54cfd8195596e017c351a227184806b)) +* minor changes ([b27f24d](https://github.com/wheesys/moodist/commit/b27f24d37484a04495a043170ccaf4b4923b31ac)) +* minor changes ([a29e2c2](https://github.com/wheesys/moodist/commit/a29e2c20e4bac276495b409b20a6ffaa079122e2)) +* relocate the play button ([403a755](https://github.com/wheesys/moodist/commit/403a755ca7a9d93ef6940d1954fcde058505c1b8)) +* remove animation on change ([41845ff](https://github.com/wheesys/moodist/commit/41845ffe5e282c07b3c4cdea56607f1668c636bd)) +* remove animations ([28abc16](https://github.com/wheesys/moodist/commit/28abc16b9cbbc3986f7fb3feb17e57e553cda5dd)) +* remove cipher animation ([3feb9c1](https://github.com/wheesys/moodist/commit/3feb9c1a09b52a35d79cebb7ece54989e9faf481)) +* remove extra colors ([38f05a3](https://github.com/wheesys/moodist/commit/38f05a3e757ab0c8d91b1f84938bfb8443450769)) +* remove gradient line ([de03cac](https://github.com/wheesys/moodist/commit/de03cac6b374e836da65d00b7fe732bf17600554)) +* remove gradient line ([6720e86](https://github.com/wheesys/moodist/commit/6720e86a0af14c8c05d73f305ee12664f3b264b7)) +* remove hero pattern ([8f36c86](https://github.com/wheesys/moodist/commit/8f36c863d7f7489979691475947dbc8f29f26b39)) +* remove layout animation ([ef952d0](https://github.com/wheesys/moodist/commit/ef952d0a03b2cc3490b65535f1c5707b6578836d)) +* remove layout animation ([efd6f99](https://github.com/wheesys/moodist/commit/efd6f9941d1483e6a6df8db861ba221084a1f298)) +* remove opacity effect on disabled ([4266557](https://github.com/wheesys/moodist/commit/4266557366977534a4fba24922904ac51aaae74d)) +* remove pointer event ([c12ef12](https://github.com/wheesys/moodist/commit/c12ef12b79c6db93c457b77f4bfccb2848dc8067)) +* reorder menu items ([0052b91](https://github.com/wheesys/moodist/commit/0052b917a817ca7f83fe23521077d99ae78e81cd)) +* reverse gradient line ([87f3a2b](https://github.com/wheesys/moodist/commit/87f3a2b51104d635dcaf6e48281b99193a7d931a)) +* revert changes ([341a896](https://github.com/wheesys/moodist/commit/341a896924a6ace70114ad2ae3349f8934a329ba)) +* revert pattern ([5916e86](https://github.com/wheesys/moodist/commit/5916e86d3c6de9912b2c9bd490fa04cd9a0958dd)) +* show about and features ([37505a6](https://github.com/wheesys/moodist/commit/37505a6b3f86919ac04b69519e56ddbaf5234843)) +* widen the container ([7ec7ea7](https://github.com/wheesys/moodist/commit/7ec7ea74d53db85cffa3af646c03270793453009)) +* widen the container ([e7c786f](https://github.com/wheesys/moodist/commit/e7c786f25986436606fa723441338588a84b00b3)) +* widen the menu ([37a0736](https://github.com/wheesys/moodist/commit/37a0736a0e7edd09c33940099c884e5b48afbbf1)) + + +### ✨ Features + +* 完整实现中英文双语支持并修复所有声音翻译问题 ([65958f8](https://github.com/wheesys/moodist/commit/65958f84827dc2fc02f74dbc2b8a6174e90c1586)) +* add about section ([d725d59](https://github.com/wheesys/moodist/commit/d725d597034ead0bb63c5f0667b64ce459477662)) +* add about section ([4e84419](https://github.com/wheesys/moodist/commit/4e84419ab19f4f0c129a76a91be194bbab7f6b11)) +* add active indicator for sleep timer ([82d8240](https://github.com/wheesys/moodist/commit/82d8240b9708a9d522f67ae305dc44e004ced6de)) +* add active indicators ([240fd9c](https://github.com/wheesys/moodist/commit/240fd9c6e05c7385c0de92b8b57776988b902fae)) +* add alarm for pomodoro timer ([0eb47ba](https://github.com/wheesys/moodist/commit/0eb47ba2e1accaee7dd7d6114ca9a69cbc0656c4)) +* add animation for labels ([48a85b2](https://github.com/wheesys/moodist/commit/48a85b26016a8f3cc934e1b2298b0d897ffd9b43)) +* add animation to menu box ([17027e2](https://github.com/wheesys/moodist/commit/17027e299bb9bf958aebaf735c40e7664ad71e8b)) +* add aria-disabled to play button ([f390f38](https://github.com/wheesys/moodist/commit/f390f3801604c49799078298637ea63a06eb9721)) +* add auto pause to play button ([7c901b2](https://github.com/wheesys/moodist/commit/7c901b2bdc525d02b80a0c42eb2f81f766947ca3)) +* add auto play on select ([17d1b23](https://github.com/wheesys/moodist/commit/17d1b23c8f1a6c717d846c12d38945e7d3b47be1)) +* add autofocus for note ([24a53c8](https://github.com/wheesys/moodist/commit/24a53c81dffc1a4ba0b46244a87fb49bf562e755)) +* add basic animations with Framer Motion ([fa7b90e](https://github.com/wheesys/moodist/commit/fa7b90eeec5b697446fa5871f8b499a85d0d150f)) +* add basic audio player ([5a7a58e](https://github.com/wheesys/moodist/commit/5a7a58e883fbb0122d8d6e2c777049a8fc0d9609)) +* add basic categories ([8d7e4d2](https://github.com/wheesys/moodist/commit/8d7e4d26fd7b53a16f7ce39551b31484eefbe461)) +* add basic fading effect ([6ce766a](https://github.com/wheesys/moodist/commit/6ce766af47389e9e3e57226b956b8593a4af06d4)) +* add basic form ([c272914](https://github.com/wheesys/moodist/commit/c27291441625eb6528b28f55af3f88e1debd8a55)) +* add basic pomodoro structure ([9f7de33](https://github.com/wheesys/moodist/commit/9f7de336e5add254b40c5694fc4c619ee00602ba)) +* add basic sound components ([4adb8bf](https://github.com/wheesys/moodist/commit/4adb8bfdbc86a475d59e1d636927539592ec4b6c)) +* add basic sounds for prototyping ([5791346](https://github.com/wheesys/moodist/commit/5791346a881a9f451b967f782257317d8fb04d63)) +* add better aria labels ([98e5021](https://github.com/wheesys/moodist/commit/98e5021f561458465a544e2b86194e7f52a62169)) +* add better aria labels ([9774532](https://github.com/wheesys/moodist/commit/977453230847790de86aa7721c059d4fe3ec7eeb)) +* add binary animation ([699f49b](https://github.com/wheesys/moodist/commit/699f49bfa33420698962b56db23b49c8e14bb354)) +* add binaural beat generator without styles ([f40e820](https://github.com/wheesys/moodist/commit/f40e8206f8126f1988e0e39ca522ac3c5eb8139f)) +* add breathing exercise ([1f2b6b9](https://github.com/wheesys/moodist/commit/1f2b6b952c65c04828f19506134d783a7491df23)) +* add breathing exercise shortcut ([a3b794d](https://github.com/wheesys/moodist/commit/a3b794d9748d4a9877e5727269178f207fbc03d5)) +* add breathing exercises and other tools ([eee7553](https://github.com/wheesys/moodist/commit/eee755378a14d93d1363e8c265a908d50b9cc332)) +* add breathing exercises tool ([27f2578](https://github.com/wheesys/moodist/commit/27f25785e1cfc0482d7ddd625ac1219fd5bb6863)) +* add cipher animation ([29bebb3](https://github.com/wheesys/moodist/commit/29bebb3ec74d969fb42968696e470db00a07766e)) +* add close event for modals ([af92b1e](https://github.com/wheesys/moodist/commit/af92b1ed902b4bf221e53315ba431f834915d7c2)) +* add color noise ([7363e8d](https://github.com/wheesys/moodist/commit/7363e8d51a347adf3c53cbef9ec3e181912ecc6b)) +* add comprehensive Docker deployment support and Chinese documentation ([a8718df](https://github.com/wheesys/moodist/commit/a8718df8d2c3c70325165c4057239590a470cb61)) +* add confetti ([ace0d6e](https://github.com/wheesys/moodist/commit/ace0d6eeccc65c96275a24c8a96e63988cf76134)) +* add controls to pomodoro ([7ed016d](https://github.com/wheesys/moodist/commit/7ed016d8558a73d8d2bf05823cf80633882c1d69)) +* add copy for productivity toolbox ([3205145](https://github.com/wheesys/moodist/commit/3205145d5425c7a7a8660b46aa9de0b273a04ff0)) +* add countdown timer ([edd53d8](https://github.com/wheesys/moodist/commit/edd53d8102871d53b0a11eaa9bae7323f874d988)) +* add countdown timer button ([5f066a4](https://github.com/wheesys/moodist/commit/5f066a4eff91996b165de3b86549fffe93800d38)) +* add countdown timer structure ([c5657d0](https://github.com/wheesys/moodist/commit/c5657d06425aea84a4ba9a4b2f48e312be8b0271)) +* add counter to notepad ([2424523](https://github.com/wheesys/moodist/commit/24245235b14f9d59f86d2e988657a45a8a6d1eb7)) +* add CTA button ([0e12a52](https://github.com/wheesys/moodist/commit/0e12a5203ef836bd262b3d4cc02aaeb9048f461e)) +* add custom checkbox ([cb340c5](https://github.com/wheesys/moodist/commit/cb340c53a39917722137a8ee05b779af04a1203d)) +* add custom presets ([2484e01](https://github.com/wheesys/moodist/commit/2484e01273cf5e7ef5b44395cab26095891118fd)) +* add custom slider ([3b77c12](https://github.com/wheesys/moodist/commit/3b77c12114e5e37c0a3a17c945a0e69e034a35a4)) +* add deep merge to Zustand Persist ([01f4031](https://github.com/wheesys/moodist/commit/01f40318124ad1e6e09b1f0572f623900192ba9d)) +* add description for sleep timer ([77e2ec5](https://github.com/wheesys/moodist/commit/77e2ec5e798771b7719b36882bc68c10265c06f6)) +* add desktop notice ([07f37ef](https://github.com/wheesys/moodist/commit/07f37ef17f8be893d3ceba8fbe4427a9ecda5c15)) +* add disabled state ([ff26597](https://github.com/wheesys/moodist/commit/ff26597d22d444d18d2874a5c278eccc288972de)) +* add dividers to menu items ([408734d](https://github.com/wheesys/moodist/commit/408734d49fd89fbd47d29527c03927e49c834f30)) +* add donate item ([f12ca48](https://github.com/wheesys/moodist/commit/f12ca4806c9279f69f298bef770f8cac69a0860a)) +* add donate section ([d449c29](https://github.com/wheesys/moodist/commit/d449c29321024a43517e92cc59223b4b22fe2e82)) +* add donation header ([17b4f25](https://github.com/wheesys/moodist/commit/17b4f25ff10e09a917203e67cf963cac8358de1a)) +* add done counter ([aa8161a](https://github.com/wheesys/moodist/commit/aa8161aac5eb238048c713500a091e9af1c98e6a)) +* add fade in/out ([663cb92](https://github.com/wheesys/moodist/commit/663cb921350c083f1991665d147a3820bcdd9321)) +* add fading to intro and outro ([5467bbb](https://github.com/wheesys/moodist/commit/5467bbbc2437a5504e157122a995ad7a565ff0b8)) +* add features section ([e4e332a](https://github.com/wheesys/moodist/commit/e4e332ad75aad1a58fd97acb71660b8dec9dfa09)) +* add footer component ([262bb1a](https://github.com/wheesys/moodist/commit/262bb1a9c6153a53e259e5bd9123b8035bd6b6d1)) +* add form to sleep timer ([9d458fb](https://github.com/wheesys/moodist/commit/9d458fb60e8b84210f492541bab2c5dc94adcc8b)) +* add global volume ([3b829fc](https://github.com/wheesys/moodist/commit/3b829fce07ed7adf11ca9993c33e33caab285763)) +* add gradient line decoration ([5559152](https://github.com/wheesys/moodist/commit/5559152a8492dac335f0e6620ca4ca2d9233c889)) +* add header to todos ([c6cc61a](https://github.com/wheesys/moodist/commit/c6cc61a17fcb8542ece3caccc0de536d8003b106)) +* add help text ([c3521a7](https://github.com/wheesys/moodist/commit/c3521a798611aa0ad7297493aa5a790a27bbc991)) +* add hero section ([dc33c2c](https://github.com/wheesys/moodist/commit/dc33c2cf9cdcb251b7a0cc4dabdd7aafae154aa9)) +* add hidden selection indicator ([e2cd75a](https://github.com/wheesys/moodist/commit/e2cd75a332fab318a529f4f6e04ecf1867be7fd1)) +* add Howler for sounds ([735d9eb](https://github.com/wheesys/moodist/commit/735d9ebebfa36dd3e7596e70a0549b24b7b9fc4d)) +* add icon for sounds ([1994004](https://github.com/wheesys/moodist/commit/199400446cc241fb66722c79e74f882a7ef6a26c)) +* add ID to presets ([78222be](https://github.com/wheesys/moodist/commit/78222be011cf93998faed0b7926a5b49dcdeb470)) +* add isochronic tone generator without styles ([d759064](https://github.com/wheesys/moodist/commit/d759064373fe791f641db39549e05341068ae8a2)) +* add keyboard shortcut for play button ([d3a2a12](https://github.com/wheesys/moodist/commit/d3a2a12e1fdcca502c0d3d6dc60d3e4c577165f2)) +* add keyboard shortcut for unselect button ([99f3a41](https://github.com/wheesys/moodist/commit/99f3a41598ea237d2f509825d0b3c0ee27e789d7)) +* add keyboard shortcuts ([669df1f](https://github.com/wheesys/moodist/commit/669df1f08264e63c0892e7d4fdd2ee7dbcb96b2e)) +* add link to reasons ([e2b6eaf](https://github.com/wheesys/moodist/commit/e2b6eaf8f3278768ce142ed58594958dcc7821ad)) +* add loader for favorites ([f682a91](https://github.com/wheesys/moodist/commit/f682a910da97eb53cfb90ce955e953f05088e686)) +* add loading state for sounds ([aaccbee](https://github.com/wheesys/moodist/commit/aaccbee3d7dd1d4469ee26cea14df7132e8e9e0d)) +* add local storage support ([856b3e6](https://github.com/wheesys/moodist/commit/856b3e668ed6f24c8aefe532ac673af5e99141d1)) +* add lock while fading ([d9246b6](https://github.com/wheesys/moodist/commit/d9246b692bcb75018653cb6f437b1f46af1f925d)) +* add lofi music play ([fcbe50c](https://github.com/wheesys/moodist/commit/fcbe50c78c30e4422aea2ed698fff777fcaea1c4)) +* add lofi radios ([bb39b4b](https://github.com/wheesys/moodist/commit/bb39b4ba98f20da13e1e7a440441f5474a823f32)) +* add media session ([5e0a842](https://github.com/wheesys/moodist/commit/5e0a84259ff5586700c4e10087485d905be7ccee)) +* add media session (wip) ([34d3f07](https://github.com/wheesys/moodist/commit/34d3f075816eb821979f1d51a1177ecfa03920f3)) +* add media session (wip) ([cf4870b](https://github.com/wheesys/moodist/commit/cf4870b0d6b172bd4e6b79ff517af06b2aeac7a5)) +* add media session (wip) ([9f0a28d](https://github.com/wheesys/moodist/commit/9f0a28d9305954486d4f609f85811982df9710f3)) +* add media session (wip) ([56b0e9b](https://github.com/wheesys/moodist/commit/56b0e9bf1a16d4e7e2c8d7a552b652f8d30dd800)) +* add media session (wip) ([4f752bb](https://github.com/wheesys/moodist/commit/4f752bb6d048c0260ff6b2aada59c227624b2d17)) +* add media session (wip) ([1547b0a](https://github.com/wheesys/moodist/commit/1547b0a436bd9a77c19fc5d37be3cb3e123e6117)) +* add media session (wip) ([f311ec1](https://github.com/wheesys/moodist/commit/f311ec114e3a8ca61954819334e43195d0980219)) +* add media session (wip) ([df1b05f](https://github.com/wheesys/moodist/commit/df1b05f7ce3e26128d0bc4a9a022b5300ea88f85)) +* add media session (wip) ([ea0dfff](https://github.com/wheesys/moodist/commit/ea0dfff9c1e7d8e6e03bccdc0ab15d098b31a10d)) +* add media session (wip) ([fc1bd07](https://github.com/wheesys/moodist/commit/fc1bd07b7de9532383c66d7e59cc13bbe41f415a)) +* add media session (wip) ([f79e941](https://github.com/wheesys/moodist/commit/f79e941527e09e96b5eba6ca8c4e2e3df583c071)) +* add media session (wip) ([11a4514](https://github.com/wheesys/moodist/commit/11a4514a0f63f09954361fdef8145869d369fd29)) +* add menu button ([184bb09](https://github.com/wheesys/moodist/commit/184bb09f5ab09fcf877e6a904023d9de72be9a89)) +* add Moodist description to tools ([5b3972b](https://github.com/wheesys/moodist/commit/5b3972b3470f3c43903d9a20925ed49321f07440)) +* add more and less button for sounds ([13cd72a](https://github.com/wheesys/moodist/commit/13cd72a0655d90f0a6b7658b3357d1e8902f8fb7)) +* add more sounds ([d2e289e](https://github.com/wheesys/moodist/commit/d2e289e5d5cccd050ca94860f05f00740b3cf139)) +* add more sounds ([554309e](https://github.com/wheesys/moodist/commit/554309ebd87da2bce4555f09e5c9f34735d0b794)) +* add more sounds ([be38b92](https://github.com/wheesys/moodist/commit/be38b92647209ce17032987b3d6f5d1800322db5)) +* add more sounds ([b497d16](https://github.com/wheesys/moodist/commit/b497d16fd8b7d6ccf34c0c91b596fca75dff2f34)) +* add more sounds ([0888aaa](https://github.com/wheesys/moodist/commit/0888aaa0f09ed549afdb21166ad6d2f048604275)) +* add more sounds ([63ed396](https://github.com/wheesys/moodist/commit/63ed396a5a74ed2b6e25882a72511ee93935fe04)) +* add move up and down functionality ([3e11fb6](https://github.com/wheesys/moodist/commit/3e11fb6123e4c6b6be9668ef4c274390a5acd16a)) +* add new logo ([c1ece58](https://github.com/wheesys/moodist/commit/c1ece582f445906308a0d856181ebaca464ec25a)) +* add new sounds ([759e6b0](https://github.com/wheesys/moodist/commit/759e6b0ce8f0acc3eb0eed508f7c587804097748)) +* add notepad tool ([a80289d](https://github.com/wheesys/moodist/commit/a80289db57c1b002edd586b323444d3a474587ad)) +* add notepad tool page ([1fd02f9](https://github.com/wheesys/moodist/commit/1fd02f927c55155ecd8d1af6325995c4635e0a29)) +* add open-source section ([f7302de](https://github.com/wheesys/moodist/commit/f7302dec5b7e182ad465bc30b63457a6e629a5b3)) +* add persist mode to the modal ([4c0f417](https://github.com/wheesys/moodist/commit/4c0f417469fb15adbe33cab9bb66459225653e68)) +* add play button ([31c087e](https://github.com/wheesys/moodist/commit/31c087ebc8e66220d488226029dcc1435667ce04)) +* add pomodoro timer ([d2edeb4](https://github.com/wheesys/moodist/commit/d2edeb48becef62f1002359a41ebe8ebfa1f34bb)) +* add pomodoro timer tool ([bee391a](https://github.com/wheesys/moodist/commit/bee391acfecdaf36488c48ef1022b16a83059d58)) +* add PWA ([761c730](https://github.com/wheesys/moodist/commit/761c7301295a3e5645326be804225431f823f808)) +* add ready section ([e372d2f](https://github.com/wheesys/moodist/commit/e372d2f398dbdcfad1069b50911ba840f0c9a1fe)) +* add reverse timer ([105f53e](https://github.com/wheesys/moodist/commit/105f53ea028fadae4bd2ff7d8a1856e94f070b1a)) +* add scroll for lower heights ([758f2f4](https://github.com/wheesys/moodist/commit/758f2f48dc6a4e520b7a1e937f96eed28c8e8c20)) +* add scroll to top component ([3c1c27b](https://github.com/wheesys/moodist/commit/3c1c27b2fd378eb0f7351a3f511375cbc62f2a7b)) +* add share modal ([35e3215](https://github.com/wheesys/moodist/commit/35e32152b153f4dfaf9e071f526f6d7602ea97fc)) +* add share placeholder ([fe2357c](https://github.com/wheesys/moodist/commit/fe2357c995713cd0fb8335b325266859dc47a769)) +* add shine effect ([d9df0d4](https://github.com/wheesys/moodist/commit/d9df0d4b2c5071c12cecc6452acc0f160c57deb5)) +* add shortcut for breathing exercise ([60cb453](https://github.com/wheesys/moodist/commit/60cb453847f0968a4d1abc0fbb66773a54ebdfd9)) +* add shortcuts list ([60f167c](https://github.com/wheesys/moodist/commit/60f167c4d734bc6238f7c2bb7b39c89ed45ed9eb)) +* add shortcuts to items ([42f82ab](https://github.com/wheesys/moodist/commit/42f82ab95d684163826e76231fb1dd554f773d68)) +* add simple breathing exercise tool ([fc4f521](https://github.com/wheesys/moodist/commit/fc4f52146e2142a0c711b6d6a334c0107b1e1daa)) +* add simple notepad ([e923559](https://github.com/wheesys/moodist/commit/e923559709796698257772cced4e20de584c6c80)) +* add simple tooltip ([f2efe3c](https://github.com/wheesys/moodist/commit/f2efe3c490ab5429824d10e97979694a4de11dd4)) +* add singing bowl sound ([0b49f66](https://github.com/wheesys/moodist/commit/0b49f66e5879642da10054046700a160411448a3)) +* add sleep timer ([71b62ed](https://github.com/wheesys/moodist/commit/71b62ed3dd365744435dc4499b9c53684f72849c)) +* add sound count to hero ([42ccc7a](https://github.com/wheesys/moodist/commit/42ccc7ada780fd5db5c038fa9210ec0e3e75be6e)) +* add source code item ([d055e66](https://github.com/wheesys/moodist/commit/d055e66dd9dd5789c88d1a002686457ea89db073)) +* add special button ([a514a36](https://github.com/wheesys/moodist/commit/a514a364ec5b6e2e34e7901ad51219d7be2aee86)) +* add store to the notepad ([47a63a7](https://github.com/wheesys/moodist/commit/47a63a774ebede5db65f17a29a36f0b76d9ed85a)) +* add story for modal ([9b7d3c6](https://github.com/wheesys/moodist/commit/9b7d3c645b8c3469231641e6ec8bbdef88732bbc)) +* add story for snackbar ([43f6245](https://github.com/wheesys/moodist/commit/43f62452275573f948449190dcfcef89faa4ec51)) +* add timer for breathing exercises ([5865fc8](https://github.com/wheesys/moodist/commit/5865fc867dc97e03d0f0c79ea8c465e0c0f27411)) +* add titles ([5f40435](https://github.com/wheesys/moodist/commit/5f40435c0ccfec0cb87d9ac0c14723fb8265fa8d)) +* add toolbar and portal ([ede4801](https://github.com/wheesys/moodist/commit/ede480186c4b3f187c82e1d64e4d521ee59da31a)) +* add toolbar to notepad ([7463334](https://github.com/wheesys/moodist/commit/7463334053ecd35a53cae535674169f5b50c81c2)) +* add tooltip to scroll button ([d4401fa](https://github.com/wheesys/moodist/commit/d4401faaffcb4351be1a152b89f94c9db63ca213)) +* add why section ([3ed610b](https://github.com/wheesys/moodist/commit/3ed610bb468293f6b08c2b2444bc47cd570383eb)) +* allow using spacebar or enter to trigger buttons ([60cc2e9](https://github.com/wheesys/moodist/commit/60cc2e9369aff3a374458cf1c3234eec8cd0530e)) +* basic structure for share link ([ef81f19](https://github.com/wheesys/moodist/commit/ef81f198baeb927e3b1768570f75e6638a7bd0b6)) +* better heading ([10259d0](https://github.com/wheesys/moodist/commit/10259d013f7cb1ae41808f7a78e836ddee3b07f1)) +* bring back all tools ([6a4dc1e](https://github.com/wheesys/moodist/commit/6a4dc1ed95072c402cb553fa5b1becb646062c45)) +* bring back all tools ([e1de5c4](https://github.com/wheesys/moodist/commit/e1de5c48b299e815f071f15c00424ba1b0189419)) +* change alignments ([1a01a00](https://github.com/wheesys/moodist/commit/1a01a0086648c7564ecab30b79df0d67e93eb392)) +* change and add shortcuts ([a59db41](https://github.com/wheesys/moodist/commit/a59db41dc5eaa7be5ab86c5cc407274eb7b57dfe)) +* change lofi icon ([066af9e](https://github.com/wheesys/moodist/commit/066af9e2f31bc9201d349d888c6dc19cd5ad7750)) +* change logos ([3d1d45c](https://github.com/wheesys/moodist/commit/3d1d45cd4933335cfbe20381c0e758969a3bdcb9)) +* change shortcuts ([4f45279](https://github.com/wheesys/moodist/commit/4f45279938f60ee6934c3e6047898b9833c2b9c6)) +* change shortcuts ([251f309](https://github.com/wheesys/moodist/commit/251f30930c72a50120412c6b2182fdf4183b9d62)) +* change shortcuts to shift ([837826f](https://github.com/wheesys/moodist/commit/837826fbc13599e51bb7b65cf8b7bdcb1f1fc503)) +* change sound count from round to exact ([8c49453](https://github.com/wheesys/moodist/commit/8c49453011d127669774f46720ce6e98ca01aa13)) +* change the copy for features ([38da02a](https://github.com/wheesys/moodist/commit/38da02a0d3b08e8f8802d6cf76a04ae656e10b76)) +* change tooltip content ([941e1f0](https://github.com/wheesys/moodist/commit/941e1f024189143340d663a0c117c08a0b315599)) +* close notepad on escape ([583578b](https://github.com/wheesys/moodist/commit/583578b31592b3c0e7f5ae6ad3f83e99e64fb6ff)) +* complete meta tags ([1cfbf0d](https://github.com/wheesys/moodist/commit/1cfbf0dd092d35d2f098c29baf6d6adbc1107cc0)) +* create reusable tooltip ([c637e2d](https://github.com/wheesys/moodist/commit/c637e2d63109e12886b6f688c643146707967c7a)) +* **docker:** add dockerfile ([a234bc1](https://github.com/wheesys/moodist/commit/a234bc17a66331acbbc1d980cd1f53d58646f534)) +* extract the provider for the tooltip ([95b641a](https://github.com/wheesys/moodist/commit/95b641a88f2eee264b59b5bd62206bb84119da57)) +* fix modal and scrollbar layout shift ([e399673](https://github.com/wheesys/moodist/commit/e3996734621b33c0598db29e82371f1258396147)) +* implement basic snackbar ([8090599](https://github.com/wheesys/moodist/commit/8090599f2bc9ce58cdb36a6a04555afdb7af2bb2)) +* implement basic Zustand store ([22bb65d](https://github.com/wheesys/moodist/commit/22bb65de0d4ea9f485e4923b9c8715233df3114e)) +* implement countdown timer functionality ([2bfb9b1](https://github.com/wheesys/moodist/commit/2bfb9b181c490c9836e2410199e6a1cf8687e7aa)) +* implement favorite sounds functionality ([cb34b59](https://github.com/wheesys/moodist/commit/cb34b59d864fb80b930c0c9e1c1269bb7e9c2b18)) +* implement override feature ([0f62f07](https://github.com/wheesys/moodist/commit/0f62f0795c5a9e06fa4e62b6b7b1e6c0774dfe0f)) +* implement sharing URL ([93ff72a](https://github.com/wheesys/moodist/commit/93ff72a052484b36c9ac821b94b632865b4a3550)) +* implement shuffle functionality ([26ba017](https://github.com/wheesys/moodist/commit/26ba017815d7338f49d2017eda75f86f493bf050)) +* implement shuffling functionality ([3ac211e](https://github.com/wheesys/moodist/commit/3ac211e3554d26c48fb8e0a588a67f1a4901e9b9)) +* implement time setting ([f3cb2a1](https://github.com/wheesys/moodist/commit/f3cb2a1b63e40f4f742ee2591b9353aa373f9783)) +* implement unselect all functionality ([8966d59](https://github.com/wheesys/moodist/commit/8966d59d758496cc94247364833788dcc555ce8b)) +* make sound file addresses relative ([81d9d7c](https://github.com/wheesys/moodist/commit/81d9d7ca03f6c7410ca750e069c9c8b935114950)) +* make the modal more accessible ([0252fa9](https://github.com/wheesys/moodist/commit/0252fa96abed18de71472ffc671b13c263754ed9)) +* media session support ([18ed2e6](https://github.com/wheesys/moodist/commit/18ed2e6f055d7e32b4a9df33cdb724eaf1f930aa)) +* migrate to motion and fix some animations ([b191e60](https://github.com/wheesys/moodist/commit/b191e6067ddc3233689a34946c602db36d6133ba)) +* persist pomodoro setting ([665e217](https://github.com/wheesys/moodist/commit/665e2173f4083128687a6302a6f2fd82674f07c1)) +* persist presets ([38a9a23](https://github.com/wheesys/moodist/commit/38a9a23790248d5af522fc0d3cf6e99970e59637)) +* remove all extra tools ([973e0df](https://github.com/wheesys/moodist/commit/973e0df6fb3a6749fd4b0f8d1cd976c67a7e8c43)) +* remove all tools ([2bbdc7e](https://github.com/wheesys/moodist/commit/2bbdc7e09e053bd6e8bb052abb7aff723cb14eaa)) +* remove all tools ([b32d8b2](https://github.com/wheesys/moodist/commit/b32d8b28034e018eeaf1c544e4128b91f4a95172)) +* remove lofi modal ([13d26b3](https://github.com/wheesys/moodist/commit/13d26b3337b2e79d52c774807795b5924a4dcb76)) +* remove pre-made binaurals ([b8ed79f](https://github.com/wheesys/moodist/commit/b8ed79f48ad2a315b93aedf1f932b6c5f075b157)) +* remove the breathing exercises ([76fdc74](https://github.com/wheesys/moodist/commit/76fdc747100bc15ced92b77b1fefc8cba519d37f)) +* remove the countdown timer ([d6ed3fd](https://github.com/wheesys/moodist/commit/d6ed3fd251df029100caba5df304996e723acd78)) +* reorder sounds in favorites ([dc9da85](https://github.com/wheesys/moodist/commit/dc9da85e6825b3cb70e2e6ad4f35c0db3aeb26c2)) +* replace reverse timer ([a6c7ac4](https://github.com/wheesys/moodist/commit/a6c7ac41ad5210b9a98e0fe62f5cb387fe9c4e9a)) +* scroll into view after marking favorite ([74f6b58](https://github.com/wheesys/moodist/commit/74f6b5851d3a0fac5f97d97cd24f12507c2c3b35)) +* scroll the new timer into view ([f4c66e3](https://github.com/wheesys/moodist/commit/f4c66e309277414951b191e627b1f52aab79af6f)) +* update the menu items ([1768ba1](https://github.com/wheesys/moodist/commit/1768ba1548a444c57dbfd5e351d77838238aed0d)) +* use custom slider in binaural and isochronic ([e61307a](https://github.com/wheesys/moodist/commit/e61307a30263dca8cc016ec5136d52c4b18e5c3c)) + ## [2.1.0](https://github.com/remvze/moodist/compare/v2.0.1...v2.1.0) (2025-07-19) diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md new file mode 100644 index 0000000..7a3ee4c --- /dev/null +++ b/DOCKER_DEPLOY.md @@ -0,0 +1,305 @@ +# Moodist Docker 部署指南 + +## 🐳 Docker 镜像构建和部署 + +### 📋 镜像信息 + +- **镜像名称**: `walllee/moodist` +- **Docker Hub**: https://hub.docker.com/r/walllee/moodist +- **支持平台**: `linux/amd64`, `linux/arm64` +- **基础镜像**: `nginx:alpine` +- **镜像大小**: ~30MB + +### 🚀 快速开始 + +#### 1. 直接拉取并运行 +```bash +# 拉取镜像 +docker pull walllee/moodist:latest + +# 运行容器 +docker run -d \ + --name moodist \ + -p 8080:8080 \ + --restart unless-stopped \ + walllee/moodist:latest +``` + +#### 2. 使用 Docker Compose +```bash +# 简单版本 +docker-compose up -d + +# 或使用优化版本 +docker-compose -f docker-compose.optimized.yml up -d + +# 查看日志 +docker-compose logs -f +# 或 +docker-compose -f docker-compose.optimized.yml logs -f + +# 停止服务 +docker-compose down +``` + +### 🔨 自定义构建 + +#### 1. 简化本地构建(推荐) +```bash +# 克隆仓库 +git clone https://github.com/wheesys/moodist.git +cd moodist + +# 简化构建(推荐,兼容性最好) +npm run docker:push + +# 或带版本号构建 +./scripts/build-docker-simple.sh 2.1.0 + +# 构建并推送到 Docker Hub +npm run docker:push-and-upload +``` + +**特点:** +- ✅ 完全兼容,不依赖 Docker Buildx +- ✅ 先本地构建再打包,避免容器内依赖问题 +- ✅ 构建速度快,使用缓存优化 +- ✅ 支持版本标签和自动 latest 标签 + +#### 2. 多平台构建 +```bash +# 克隆仓库 +git clone https://github.com/wheesys/moodist.git +cd moodist + +# 本地构建和测试 +./scripts/build-local.sh + +# 查看构建结果 +docker images | grep moodist +``` + +#### 3. 推送到 Docker Hub +```bash +# 登录 Docker Hub +docker login + +# 使用简化脚本推送 +npm run docker:push-and-upload + +# 或手动推送指定版本 +docker push walllee/moodist:2.1.0 +docker push walllee/moodist:latest +``` + +### 📦 部署配置 + +#### 生产环境配置 +```yaml +version: '3.8' +services: + moodist: + image: wheeysys/moodist:latest + container_name: moodist-prod + restart: always + ports: + - "80:8080" + environment: + - NODE_ENV=production + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/"] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +``` + +#### 开发环境配置 +```bash +# 使用开发配置 +docker-compose -f docker-compose.dev.yml up -d + +# 或者使用开发工具 +docker-compose -f docker-compose.dev.yml --profile tools up -d +``` + +### 🔧 环境变量 + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `NODE_ENV` | `production` | 运行环境 | +| `TZ` | `Asia/Shanghai` | 时区设置 | + +### 📊 性能优化 + +#### 镜像特性 +- ✅ **多阶段构建**: 优化镜像大小 +- ✅ **多平台支持**: AMD64 + ARM64 +- ✅ **非root用户**: 提高安全性 +- ✅ **健康检查**: 自动监控应用状态 +- ✅ **静态优化**: Nginx + Gzip 压缩 + +#### 资源使用 +- **内存占用**: ~32MB (运行时) +- **CPU占用**: < 0.1 (空闲时) +- **启动时间**: ~2秒 +- **镜像大小**: ~30MB + +### 🔍 监控和日志 + +#### 查看容器状态 +```bash +# 查看容器状态 +docker ps | grep moodist + +# 查看健康检查状态 +docker inspect moodist | grep Health -A 10 + +# 查看资源使用 +docker stats moodist +``` + +#### 日志管理 +```bash +# 查看实时日志 +docker logs -f moodist + +# 查看最近日志 +docker logs --tail 100 moodist + +# 日志轮转(在 docker-compose 中配置) +logging: + options: + max-size: "10m" + max-file: "3" +``` + +### 🛠️ 故障排除 + +#### 常见问题 + +1. **容器无法启动** + ```bash + # 检查端口占用 + netstat -tlnp | grep 8080 + + # 查看容器日志 + docker logs moodist + ``` + +2. **健康检查失败** + ```bash + # 手动检查应用是否响应 + curl -f http://localhost:8080/ + + # 查看健康检查状态 + docker inspect moodist | grep Health + ``` + +3. **构建失败** + ```bash + # 清理Docker缓存 + docker system prune -a + + # 重新构建 + docker build --no-cache -f Dockerfile.optimized -t moodist:test . + ``` + +### 🔄 更新部署 + +#### 滚动更新 +```bash +# 拉取新版本 +docker pull wheeysys/moodist:latest + +# 停止旧容器 +docker stop moodist + +# 启动新容器 +docker run -d \ + --name moodist \ + -p 8080:8080 \ + --restart unless-stopped \ + wheeysys/moodist:latest + +# 删除旧容器 +docker rm $(docker ps -aq --filter "status=exited") +``` + +#### 使用 Docker Compose 更新 +```bash +# 拉取新镜像 +docker-compose -f docker-compose.optimized.yml pull + +# 重启服务 +docker-compose -f docker-compose.optimized.yml up -d + +# 清理旧镜像 +docker image prune -f +``` + +### 🔐 安全配置 + +#### 生产环境安全建议 +```yaml +services: + moodist: + image: wheeysys/moodist:latest + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /var/cache/nginx + - /var/run + user: "nginx" + cap_drop: + - ALL + cap_add: + - CHOWN + - SETGID + - SETUID +``` + +### 📈 扩展部署 + +#### 使用反向代理 +```nginx +server { + listen 80; + server_name moodist.example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +#### 负载均衡配置 +```yaml +version: '3.8' +services: + moodist: + image: wheeysys/moodist:latest + deploy: + replicas: 3 + # ... 其他配置 +``` + +### 📞 支持 + +- **GitHub**: https://github.com/wheesys/moodist +- **Docker Hub**: https://hub.docker.com/r/walllee/moodist +- **问题反馈**: 请在 GitHub Issues 中提交 + +--- + +*最后更新: 2024-11-16* \ No newline at end of file diff --git a/Dockerfile.dev-server b/Dockerfile.dev-server new file mode 100644 index 0000000..933d659 --- /dev/null +++ b/Dockerfile.dev-server @@ -0,0 +1,56 @@ +# 开发模式 Dockerfile - 完整开发环境 +FROM node:18-alpine + +# 安装必要的系统依赖 +RUN apk add --no-cache \ + curl \ + git \ + bash + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV NODE_ENV=development +ENV PORT=8080 +ENV HOST=0.0.0.0 +ENV PATH="/app/node_modules/.bin:${PATH}" + +# 复制package文件 +COPY package*.json ./ + +# 安装所有依赖(包括devDependencies),跳过 prepare 脚本 +RUN npm ci --ignore-scripts && \ + npm cache clean --force && \ + npm install husky --save-dev + +# 复制所有源代码 +COPY . . + +# 创建数据目录 +RUN mkdir -p /app/data + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +# 启动开发服务器(类似生产模式,稳定运行) +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8080"] + +# 定义构建参数 +ARG VERSION=latest +ARG BUILD_DATE +ARG VCS_REF + +# 添加标签信息 +LABEL maintainer="walllee" \ + org.opencontainers.image.title="Moodist Development" \ + org.opencontainers.image.description="Ambient sounds for focus and calm - 开发环境(完整功能)" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.source="https://github.com/wheesys/moodist" \ + org.opencontainers.image.licenses="MIT" diff --git a/Dockerfile.multiplatform b/Dockerfile.multiplatform new file mode 100644 index 0000000..b3384b4 --- /dev/null +++ b/Dockerfile.multiplatform @@ -0,0 +1,83 @@ +# 多平台构建 Dockerfile +# 使用: docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile.multiplatform -t wheesys/moodist:latest --push . + +# 使用官方Node.js镜像作为构建环境 +FROM --platform=linux/amd64,linux/arm64 node:20-alpine AS base + +# 安装必要的系统依赖 +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# 复制package文件 +COPY package*.json ./ + +# 安装依赖(使用npm ci进行更快、更可靠的安装) +RUN npm ci --only=production && npm cache clean --force + +# 构建阶段 +FROM base AS builder +COPY . . + +# 设置构建环境变量 +ARG NODE_ENV=production +ARG BUILD_DATE +ARG VERSION +ARG VCS_REF + +ENV NODE_ENV=$NODE_ENV +ENV BUILD_DATE=$BUILD_DATE +ENV VERSION=$VERSION +ENV VCS_REF=$VCS_REF + +# 构建应用 +RUN npm run build + +# 生产运行阶段 - 使用轻量级Nginx +FROM nginx:alpine AS runtime + +# 安装curl用于健康检查 +RUN apk add --no-cache curl + +# 创建非root用户提高安全性 +RUN addgroup -g 1001 -S nginx && \ + adduser -S nginx -u 1001 -G nginx + +# 复制自定义nginx配置 +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf + +# 从构建阶段复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 设置正确的权限 +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d + +# 创建nginx运行时需要的目录 +RUN touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +# 切换到非root用户 +USER nginx + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +# 启动nginx +CMD ["nginx", "-g", "daemon off;"] + +# 添加标签信息 +LABEL maintainer="walllee" \ + org.opencontainers.image.title="Moodist" \ + org.opencontainers.image.description="Ambient sounds for focus and calm - 多语言环境音应用" \ + org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.revision=$VCS_REF \ + org.opencontainers.image.source="https://github.com/wheesys/moodist" \ + org.opencontainers.image.licenses="MIT" \ No newline at end of file diff --git a/Dockerfile.optimized b/Dockerfile.optimized new file mode 100644 index 0000000..b655251 --- /dev/null +++ b/Dockerfile.optimized @@ -0,0 +1,89 @@ +# 使用官方Node.js镜像作为构建环境 +FROM node:20-alpine AS base + +# 安装必要的系统依赖 +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# 复制package文件 +COPY package*.json ./ + +# 构建阶段 +FROM node:20-alpine AS builder + +WORKDIR /app + +# 复制package文件 +COPY package*.json ./ + +# 设置构建环境变量 +ARG NODE_ENV=production +ARG BUILD_DATE +ARG VERSION +ARG VCS_REF + +ENV NODE_ENV=$NODE_ENV +ENV BUILD_DATE=$BUILD_DATE +ENV VERSION=$VERSION +ENV VCS_REF=$VCS_REF + +# 安装所有依赖(构建需要所有依赖,包括devDependencies) +RUN npm ci --ignore-scripts && \ + npm install --save-dev autoprefixer postcss postcss-nesting && \ + npm cache clean --force + +# 复制源代码 +COPY . . + +# 构建应用 +RUN npm run build + +# 生产运行阶段 - 使用轻量级Nginx +FROM nginx:alpine AS runtime + +# 安装curl用于健康检查 +RUN apk add --no-cache curl + +# 创建非root用户提高安全性 +RUN addgroup -g 1001 -S nginx && \ + adduser -S nginx -u 1001 -G nginx + +# 复制自定义nginx配置 +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf + +# 从构建阶段复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 设置正确的权限 +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d + +# 创建nginx运行时需要的目录 +RUN touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +# 切换到非root用户 +USER nginx + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +# 启动nginx +CMD ["nginx", "-g", "daemon off;"] + +# 添加标签信息 +LABEL maintainer="walllee" \ + org.opencontainers.image.title="Moodist" \ + org.opencontainers.image.description="Ambient sounds for focus and calm - 多语言环境音应用" \ + org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.revision=$VCS_REF \ + org.opencontainers.image.source="https://github.com/wheesys/moodist" \ + org.opencontainers.image.licenses="MIT" \ No newline at end of file diff --git a/Dockerfile.prod-like b/Dockerfile.prod-like new file mode 100644 index 0000000..27d2dee --- /dev/null +++ b/Dockerfile.prod-like @@ -0,0 +1,58 @@ +# 类生产模式 Dockerfile - 开发服务器但无热重载 +FROM node:18-alpine + +# 安装必要的系统依赖 +RUN apk add --no-cache \ + curl \ + git \ + bash + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV NODE_ENV=development +ENV PORT=8080 +ENV HOST=0.0.0.0 +ENV PATH="/app/node_modules/.bin:${PATH}" + +# 复制package文件 +COPY package*.json ./ + +# 安装所有依赖 +RUN npm ci && \ + npm cache clean --force + +# 在容器内构建应用 +COPY . . + +# 创建数据目录 +RUN mkdir -p /app/data + +# 构建应用 +RUN npm run build + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +# 启动应用(类似生产模式,无热重载) +CMD ["node", "./dist/server/entry.mjs"] + +# 定义构建参数 +ARG VERSION=latest +ARG BUILD_DATE +ARG VCS_REF + +# 添加标签信息 +LABEL maintainer="walllee" \ + org.opencontainers.image.title="Moodist Production-like" \ + org.opencontainers.image.description="Ambient sounds for focus and calm - 类生产模式(无热重载)" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.source="https://github.com/wheesys/moodist" \ + org.opencontainers.image.licenses="MIT" \ No newline at end of file diff --git a/Dockerfile.server b/Dockerfile.server new file mode 100644 index 0000000..3c0ea66 --- /dev/null +++ b/Dockerfile.server @@ -0,0 +1,58 @@ +# 使用现有的本地构建成果 - Node.js 服务器版本 +FROM node:20-alpine + +# 安装必要的系统依赖 +RUN apk add --no-cache curl + +# 创建应用用户 +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV NODE_ENV=production +ENV PORT=8080 + +# 复制package文件 +COPY package*.json ./ + +# 安装所有依赖(运行时需要adapter) +RUN npm ci --ignore-scripts && \ + npm cache clean --force + +# 复制本地构建的完整产物 +COPY --chown=nodejs:nodejs dist/ ./dist + +# 创建数据目录 +RUN mkdir -p /app/data && \ + chown -R nodejs:nodejs /app + +# 切换到非root用户 +USER nodejs + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +# 启动应用 +CMD ["node", "./dist/server/entry.mjs"] + +# 定义构建参数 +ARG VERSION=latest +ARG BUILD_DATE +ARG VCS_REF + +# 添加标签信息 +LABEL maintainer="walllee" \ + org.opencontainers.image.title="Moodist" \ + org.opencontainers.image.description="Ambient sounds for focus and calm - 多语言环境音应用 (Full Stack)" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.source="https://github.com/wheesys/moodist" \ + org.opencontainers.image.licenses="MIT" \ No newline at end of file diff --git a/Dockerfile.simple b/Dockerfile.simple new file mode 100644 index 0000000..aa5c81e --- /dev/null +++ b/Dockerfile.simple @@ -0,0 +1,49 @@ +# 简化的Dockerfile - 使用本地构建产物 +FROM nginx:alpine + +# 安装curl用于健康检查 +RUN apk add --no-cache curl + +# 复制自定义nginx配置 +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf + +# 复制本地构建的静态文件 +COPY dist/ /usr/share/nginx/html + +# 设置正确的权限 +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d + +# 创建nginx运行时需要的目录 +RUN touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +# 切换到非root用户 +USER nginx + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/ || exit 1 + +# 启动nginx +CMD ["nginx", "-g", "daemon off;"] + +# 定义构建参数 +ARG VERSION=latest +ARG BUILD_DATE +ARG VCS_REF + +# 添加标签信息 +LABEL maintainer="walllee" \ + org.opencontainers.image.title="Moodist" \ + org.opencontainers.image.description="Ambient sounds for focus and calm - 多语言环境音应用" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.source="https://github.com/wheesys/moodist" \ + org.opencontainers.image.licenses="MIT" \ No newline at end of file diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..a96b99e --- /dev/null +++ b/README.en.md @@ -0,0 +1,95 @@ +## 🌍 Language / 语言 + +**[English](README.en.md)** | **[简体中文](README.md)** + +--- + +
+ Moodist Logo Banner +

Moodist 🌲

+

Ambient sounds for focus and calm.

+ Visit Moodist | Buy Me a Coffee +
+ +## Table of Contents + +- ⚡ [Features](#features) +- 🧰 [Tools](#tools) +- 🔮 [Commands](#commands) +- 🚧 [Contributing](#contributing) +- ⭐ [Support](#support-moodist) +- 📜 [License](#license) + +## Features + +1. 🎵 Over 75 ambient sounds. +1. 📝 Persistent sound selection. +1. ✈️ Sharing sound selections with others. +1. 🧰 Custom sound presets. +1. 🌙 Sleep timer for sounds. +1. 📓 Notepad for quick notes. +1. 🍅 Pomodoro timer. +1. ✅ Simple to-do list (soon). +1. ⏯️ Media controls. +1. ⌨️ Keyboard shortcuts for everything. +1. 🥷 Privacy focused: no data collection. +1. 💰 Completely free, open-source, and self-hostable. + +## Tools + +- ⚡ **TypeScript**: Programming Language +- 🔨 **React**: UI Library +- 🧑‍🚀 **Astro**: Meta Framework +- 🎨 **CSS Modules**: Styling +- 🐻 **Zustand**: State Management +- 🎭 **Framer Motion**: Animation Library +- ⚙️ **Radix**: Accessible Components +- 📕 **Storybook**: Component Documentation +- 🧪 **Vitest**: Unit Testing (soon) +- 🔭 **Playwright**: End-To-End Testing (soon) +- 🔍 **ESLint**: Code Linting +- 🧹 **Prettier**: Code Formatting +- 🧼 **Stylelint**: CSS Linting +- 🐶 **Husky**: Git Hooks +- 📝 **Lint Staged**: Running Linters on Staged Files +- 🧽 **Commitlint**: Git Commit Linting +- 🧭 **Commitizen**: Git Commit Message Helper +- 📓 **Standard Version**: Versioning and CHANGLOG Generation +- 🧰 **PostCSS**: CSS Transformations + +## Commands + +- `npm run dev`: run development server +- `npm run build`: build for production +- `npm run preview`: preview the built app +- `npm run lint`: lint files using ESLint +- `npm run lint:fix`: lint and fix using ESLint +- `npm run lint:style`: lint styles using Stylelint +- `npm run lint:style:fix`: lint and fix styles using Stylelint +- `npm run format`: format files using Prettier +- `npm run commit`: commit message using Commitizen +- `npm run release:major`: release major version +- `npm run release:minor`: release minor version +- `npm run release:patch`: release patch version +- `npm run storybook`: run Storybook + +## Contributing + +🚧 Please check [CONTRIBUTING.md](CONTRIBUTING.md) file. + +## Support Moodist + +⭐ Give a star if you liked this project. + +☕ [Buy Me a Coffee](https://buymeacoffee.com/remvze) to help me maintain Moodist. + +## License + +This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. + +### ⚠️ Third-Party Assets + +Some sounds used in this project are sourced from third-party providers and **are subject to different licenses**: + +- Sounds licensed under the **Pixabay Content License**: [Pixabay Content License](https://pixabay.com/service/license-summary/) +- Sounds licensed under **CC0**: [Creative Commons Zero License](https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/README.md b/README.md index a875755..a50582d 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,157 @@ +## 🌍 Language / 语言 + +**[English](README.en.md)** | **[简体中文](README.md)** + +--- +
Moodist Logo Banner

Moodist 🌲

-

Ambient sounds for focus and calm.

- Visit Moodist | Buy Me a Coffee +

环境音工具,助你专注与平静

+ 访问 Moodist | 在线体验地址 | 支持开发者
-## Table of Contents +## 目录 -- ⚡ [Features](#features) -- 🧰 [Tools](#tools) -- 🔮 [Commands](#commands) -- 🚧 [Contributing](#contributing) -- ⭐ [Support](#support-moodist) -- 📜 [License](#license) +- ⚡ [功能特性](#features) +- 🎮 [使用说明](#-使用说明) +- 🐳 [Docker 部署](#-docker-部署) +- 🌐 [在线体验](#-在线体验) +- 🧰 [技术栈](#tools) +- 🔮 [命令](#commands) +- 🚧 [贡献指南](#contributing) +- ⭐ [支持项目](#support-moodist) +- 📜 [许可证](#license) -## Features +## 功能特性 -1. 🎵 Over 75 ambient sounds. -1. 📝 Persistent sound selection. -1. ✈️ Sharing sound selections with others. -1. 🧰 Custom sound presets. -1. 🌙 Sleep timer for sounds. -1. 📓 Notepad for quick notes. -1. 🍅 Pomodoro timer. -1. ✅ Simple to-do list (soon). -1. ⏯️ Media controls. -1. ⌨️ Keyboard shortcuts for everything. -1. 🥷 Privacy focused: no data collection. -1. 💰 Completely free, open-source, and self-hostable. +1. 🎵 75+ 种环境音效 +2. 📝 声音选择持久化存储 +3. ✈️ 分享声音组合给他人 +4. 🧰 自定义声音预设 +5. 🌙 声音睡眠定时器 +6. 📓 便签快速记录 +7. 🍅 番茄钟计时器 +8. ✅ 简单待办事项(即将推出) +9. ⏯️ 媒体控制键 +10. ⌨️ 全功能快捷键支持 +11. 🥷 隐私保护:无数据收集 +12. 💰 完全免费、开源、可自托管 -## Tools +## 🎮 使用说明 + +### 基本操作 +- **播放/暂停声音**:点击声音卡片即可播放,再次点击暂停 +- **音量调节**:拖动声音卡片下方的音量进度条 +- **速度调节**:拖动第二条进度条调整播放速度 +- **音调调节**:拖动第三条进度条调整音调(Rate) + +### 高级功能 +- **收藏功能**:点击声音卡片右上角的❤️图标收藏常用声音 +- **随机效果**:点击❤️下方的🔀图标启用随机变化: + - 每次只随机调整一个参数(速度/音调/音量) + - 随机变化频率约为1分钟一次 + - 速度和音调:默认值 ±0.25 范围内随机 + - 音量:30%-70% 范围内随机 +- **键盘快捷键**: + - 空格键:播放/暂停 + - 方向键:调节选中声音的音量 + - 数字键:快速选择声音 + +### 主题切换 +- **昼夜模式**:点击右上角的🌞/🌙按钮切换主题 +- **自动适配**:系统会根据您的设备主题自动选择合适的颜色方案 +- **全面适配**:主题切换会影响整个页面背景及所有组件的颜色 + +## 🐳 Docker 部署 + +### 使用 Docker Compose(推荐) + +1. **克隆项目** + ```bash + git clone https://github.com/your-username/moodist.git + cd moodist + ``` + +2. **创建 docker-compose.yml 文件** + ```yaml + version: '3.8' + + services: + moodist: + build: . + ports: + - "4321:4321" + environment: + - NODE_ENV=production + restart: unless-stopped + ``` + +3. **启动服务** + ```bash + docker-compose up -d + ``` + +4. **访问应用** + + 打开浏览器访问:http://localhost:4321 + +### 使用 Docker 命令 + +1. **构建镜像** + ```bash + docker build -t moodist . + ``` + +2. **运行容器** + ```bash + docker run -d -p 4321:4321 --name moodist moodist + ``` + +3. **访问应用** + + 打开浏览器访问:http://localhost:4321 + +### 生产环境部署 + +对于生产环境,建议使用反向代理(如 Nginx)并配置 HTTPS: + +```nginx +server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl; + server_name your-domain.com; + + ssl_certificate /path/to/certificate.crt; + ssl_certificate_key /path/to/private.key; + + location / { + proxy_pass http://localhost:4321; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### 环境变量配置 + +- `NODE_ENV`: 设置为 `production` 以启用生产模式优化 +- `PORT`: 应用运行端口(默认:4321) + +## 🌐 在线体验 + +- **官方站点**:https://moodist.mvze.net +- **体验地址**:https://calm.zlext.com(可直接使用) +- **完全免费**:无需注册,即开即用 + +## 技术栈 - ⚡ **TypeScript**: Programming Language - 🔨 **React**: UI Library @@ -51,33 +173,33 @@ - 📓 **Standard Version**: Versioning and CHANGLOG Generation - 🧰 **PostCSS**: CSS Transformations -## Commands +## 命令 -- `npm run dev`: run development server -- `npm run build`: build for production -- `npm run preview`: preview the built app -- `npm run lint`: lint files using ESLint -- `npm run lint:fix`: lint and fix using ESLint -- `npm run lint:style`: lint styles using Stylelint -- `npm run lint:style:fix`: lint and fix styles using Stylelint -- `npm run format`: format files using Prettier -- `npm run commit`: commit message using Commitizen -- `npm run release:major`: release major version -- `npm run release:minor`: release minor version -- `npm run release:patch`: release patch version -- `npm run storybook`: run Storybook +- `npm run dev`: 启动开发服务器 +- `npm run build`: 构建生产版本 +- `npm run preview`: 预览构建的应用 +- `npm run lint`: 使用 ESLint 检查代码 +- `npm run lint:fix`: 使用 ESLint 检查并修复代码 +- `npm run lint:style`: 使用 Stylelint 检查样式 +- `npm run lint:style:fix`: 使用 Stylelint 检查并修复样式 +- `npm run format`: 使用 Prettier 格式化代码 +- `npm run commit`: 使用 Commitizen 提交代码 +- `npm run release:major`: 发布主版本 +- `npm run release:minor`: 发布次版本 +- `npm run release:patch`: 发布补丁版本 +- `npm run storybook`: 运行 Storybook -## Contributing +## 贡献指南 -🚧 Please check [CONTRIBUTING.md](CONTRIBUTING.md) file. +🚧 请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 文件。 -## Support Moodist +## 支持项目 -⭐ Give a star if you liked this project. +⭐ 如果您喜欢这个项目,请给我们一个星标。 -☕ [Buy Me a Coffee](https://buymeacoffee.com/remvze) to help me maintain Moodist. +☕ [请我喝咖啡](https://buymeacoffee.com/remvze) 来帮助我维护 Moodist。 -## License +## 许可证 This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..c7b3f37 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,168 @@ +## 🌍 Language / 语言 + +**[English](README.md)** | **[简体中文](README.zh-CN.md)** + +--- + +
+ Moodist Logo Banner +

Moodist 🌲

+

环境音生成器 - 专注与平静的声音伴侣。

+ 访问 Moodist | 请我喝杯咖啡 +
+ +## 目录 + +- ⚡ [功能特性](#功能特性) +- 🚀 [快速开始](#快速开始) +- 🐳 [Docker 部署](#docker-部署) +- 🧰 [技术工具](#技术工具) +- 🔮 [命令说明](#命令说明) +- 🚧 [贡献指南](#贡献指南) +- ⭐ [支持项目](#支持-moodist) +- 📜 [许可证](#许可证) + +## 功能特性 + +1. 🎵 超过 75 种环境音 +1. 📝 持久化的声音选择 +1. ✈️ 与他人分享声音选择 +1. 🧰 自定义声音预设 +1. 🌙 睡眠定时器 +1. 📓 快速记事本 +1. 🍅 番茄钟计时器 +1. ✅ 简单的待办事项列表(即将推出) +1. ⏯️ 媒体控制 +1. ⌨️ 全功能键盘快捷键 +1. 🥷 注重隐私:不收集任何数据 +1. 💰 完全免费、开源、可自托管 + +## 🚀 快速开始 + +### 本地开发 + +```bash +# 克隆项目 +git clone https://github.com/wheesys/moodist.git +cd moodist + +# 安装依赖 +npm install + +# 启动开发服务器 +npm run dev +``` + +访问 [http://localhost:4321](http://localhost:4321) 查看应用。 + +### 构建生产版本 + +```bash +# 构建应用 +npm run build + +# 预览构建结果 +npm run preview +``` + +## 🐳 Docker 部署 + +### 直接运行 + +```bash +# 拉取镜像 +docker pull walllee/moodist:latest + +# 运行容器 +docker run -d \ + --name moodist \ + -p 8080:8080 \ + --restart unless-stopped \ + walllee/moodist:latest +``` + +### 使用 Docker Compose + +```bash +# 简单版本 +docker-compose up -d + +# 或使用优化版本 +docker-compose -f docker-compose.optimized.yml up -d +``` + +访问 [http://localhost:8080](http://localhost:8080) 查看应用。 + +### 自定义构建 + +```bash +# 构建镜像 +npm run docker:push + +# 带版本号构建 +./scripts/build-docker-simple.sh 2.1.0 + +# 构建并推送到 Docker Hub +npm run docker:push-and-upload +``` + +📖 详细部署指南请查看 [DOCKER_DEPLOY.md](DOCKER_DEPLOY.md) + +## 技术工具 + +- ⚡ **TypeScript**: 编程语言 +- 🔨 **React**: UI 库 +- 🧑‍🚀 **Astro**: 元框架 +- 🎨 **CSS Modules**: 样式方案 +- 🐻 **Zustand**: 状态管理 +- 🎭 **Framer Motion**: 动画库 +- ⚙️ **Radix**: 无障碍组件 +- 📕 **Storybook**: 组件文档 +- 🧪 **Vitest**: 单元测试(即将推出) +- 🔭 **Playwright**: 端到端测试(即将推出) +- 🔍 **ESLint**: 代码检查 +- 🧹 **Prettier**: 代码格式化 +- 🧼 **Stylelint**: CSS 检查 +- 🐶 **Husky**: Git 钩子 +- 📝 **Lint Staged**: 暂存文件检查器 +- 🧽 **Commitlint**: Git 提交信息检查 +- 🧭 **Commitizen**: Git 提交信息助手 +- 📓 **Standard Version**: 版本管理和更新日志生成 +- 🧰 **PostCSS**: CSS 转换 + +## 命令说明 + +- `npm run dev`: 运行开发服务器 +- `npm run build`: 构建生产版本 +- `npm run preview`: 预览构建的应用 +- `npm run lint`: 使用 ESLint 检查代码 +- `npm run lint:fix`: 使用 ESLint 检查并修复代码 +- `npm run lint:style`: 使用 Stylelint 检查样式 +- `npm run lint:style:fix`: 使用 Stylelint 检查并修复样式 +- `npm run format`: 使用 Prettier 格式化文件 +- `npm run commit`: 使用 Commitizen 提交代码 +- `npm run release:major`: 发布主版本 +- `npm run release:minor`: 发布次版本 +- `npm run release:patch`: 发布补丁版本 +- `npm run storybook`: 运行 Storybook + +## 贡献指南 + +🚧 请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 文件。 + +## 支持 Moodist + +⭐ 如果您喜欢这个项目,请给它一个星标。 + +☕ [请我喝杯咖啡](https://buymeacoffee.com/remvze) 来帮助我维护 Moodist。 + +## 许可证 + +本项目基于 **MIT 许可证** - 详情请查看 [LICENSE](LICENSE) 文件。 + +### ⚠️ 第三方资源 + +本项目使用的部分声音来源于第三方提供商,并**遵循不同的许可证**: + +- 遵循 **Pixabay 内容许可证** 的声音:[Pixabay 内容许可证](https://pixabay.com/service/license-summary/) +- 遵循 **CC0** 的声音:[知识共享署名许可协议](https://creativecommons.org/publicdomain/zero/1.0/) \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index 82f6cc6..9e97f61 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,9 +1,14 @@ import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; +import node from '@astrojs/node'; import AstroPWA from '@vite-pwa/astro'; export default defineConfig({ + output: 'server', + adapter: node({ + mode: 'standalone' + }), integrations: [ react(), AstroPWA({ @@ -33,4 +38,9 @@ export default defineConfig({ }, }), ], + vite: { + define: { + global: 'globalThis', + }, + }, }); diff --git a/data/users.db b/data/users.db new file mode 100644 index 0000000..b4ae37a Binary files /dev/null and b/data/users.db differ diff --git a/data/users.db-shm b/data/users.db-shm new file mode 100644 index 0000000..a9d0947 Binary files /dev/null and b/data/users.db-shm differ diff --git a/data/users.db-wal b/data/users.db-wal new file mode 100644 index 0000000..6aad829 Binary files /dev/null and b/data/users.db-wal differ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..50c5e2a --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,90 @@ +version: '3.8' + +services: + moodist-dev: + # 构建上下文 + build: + context: . + dockerfile: Dockerfile.optimized + target: builder # 只构建到builder阶段用于开发 + args: + - NODE_ENV=development + - BUILD_DATE=${BUILD_DATE:-$(date -u +'%Y-%m-%dT%H:%M:%SZ')} + - VERSION=dev + - VCS_REF=${VCS_REF:-dev} + + container_name: moodist-dev + restart: unless-stopped + + # 开发端口映射 + ports: + - "3000:3000" # Astro开发服务器 + - "8080:8080" # 预览服务器 + + # 开发环境变量 + environment: + - NODE_ENV=development + - TZ=Asia/Shanghai + + # 卷挂载用于开发 + volumes: + - .:/app + - /app/node_modules # 防止node_modules被覆盖 + - moodist-dist:/app/dist + # 挂载 SQLite 数据库文件目录 + - ./data:/app/data:rw + + # 工作目录 + working_dir: /app + + # 开发命令 + command: npm run dev + + # 健康检查 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + # 开发资源配置 + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + + # 日志配置 + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + + networks: + - moodist-network + + # 开发工具容器 + dev-tools: + image: node:20-alpine + container_name: moodist-dev-tools + working_dir: /app + volumes: + - .:/app + networks: + - moodist-network + profiles: + - tools + command: sh -c "npm install && tail -f /dev/null" + +volumes: + moodist-dist: + driver: local + +networks: + moodist-network: + driver: bridge \ No newline at end of file diff --git a/docker-compose.optimized.yml b/docker-compose.optimized.yml new file mode 100644 index 0000000..aecdf00 --- /dev/null +++ b/docker-compose.optimized.yml @@ -0,0 +1,95 @@ +version: '3.8' + +services: + moodist: + # 使用优化的镜像名称 + image: walllee/moodist:latest + container_name: moodist-app + + # 重启策略 + restart: unless-stopped + + # 端口映射 + ports: + - "8080:8080" + + # 环境变量 + environment: + - NODE_ENV=production + - TZ=Asia/Shanghai + + # 健康检查 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # 资源限制 + deploy: + resources: + limits: + cpus: '0.5' + memory: 128M + reservations: + cpus: '0.1' + memory: 32M + + # 日志配置 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # 网络配置 + networks: + - moodist-network + + # 安全选项 + security_opt: + - no-new-privileges:true + + # 数据卷挂载 + volumes: + # 挂载 SQLite 数据库文件目录(需要读写权限) + - ./data:/app/data:rw + # 挂载临时目录用于 SQLite WAL 文件 + - moodist-temp:/tmp:rw + + # 只读根文件系统(除了数据目录) + read_only: true + tmpfs: + - /var/cache/nginx + - /var/run + - /var/log/nginx + + # Nginx反向代理(可选) + nginx-proxy: + image: nginx:alpine + container_name: moodist-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./docker/nginx-proxy/nginx.conf:/etc/nginx/nginx.conf:ro + - ./docker/nginx-proxy/ssl:/etc/nginx/ssl:ro + depends_on: + - moodist + networks: + - moodist-network + profiles: + - proxy + +volumes: + moodist-temp: + driver: local + +networks: + moodist-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8ef1cf0..e81b37e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,26 @@ -version: '3.9' services: moodist: - image: ghcr.io/remvze/moodist + image: walllee/moodist:latest logging: options: max-size: 1g - restart: always + restart: unless-stopped ports: - - '8080:8080' + - '11081:8080' + volumes: + # 挂载源代码用于热重载(保持用户权限) + - .:/app:cached + # 使用独立的 node_modules 避免权限冲突 + - node_modules_volume:/app/node_modules + # 挂载 SQLite 数据库文件和 WAL 文件 + - ./data:/app/data:rw + environment: + - NODE_ENV=development + - PORT=8080 + stdin_open: true + tty: true + # 启动时确保安装了必要的开发依赖 + command: sh -c "cd /app && npm install @astrojs/node autoprefixer --no-save && npm run dev -- --host 0.0.0.0 --port 8080" + +volumes: + node_modules_volume: diff --git a/docker-database-mount.md b/docker-database-mount.md new file mode 100644 index 0000000..463d793 --- /dev/null +++ b/docker-database-mount.md @@ -0,0 +1,113 @@ +# Docker 数据库挂载说明 + +## 概述 + +本项目已配置 SQLite 数据库文件挂载,确保数据在容器重启后不会丢失。 + +## 数据库文件位置 + +SQLite 数据库文件位于项目的 `./data` 目录中: +- `./data/users.db` - 主数据库文件 +- `./data/users.db-wal` - Write-Ahead Log 文件 +- `./data/users.db-shm` - 共享内存文件 + +## Docker Compose 配置 + +### 1. 基础配置 (`docker-compose.yml`) + +```yaml +services: + moodist: + volumes: + # 挂载 SQLite 数据库文件和 WAL 文件 + - ./data:/app/data:rw + environment: + - NODE_ENV=production +``` + +### 2. 优化配置 (`docker-compose.optimized.yml`) + +```yaml +services: + moodist: + volumes: + # 挂载 SQLite 数据库文件目录(需要读写权限) + - ./data:/app/data:rw + # 挂载临时目录用于 SQLite WAL 文件 + - moodist-temp:/tmp:rw + +volumes: + moodist-temp: + driver: local +``` + +### 3. 开发配置 (`docker-compose.dev.yml`) + +```yaml +services: + moodist-dev: + volumes: + # 挂载 SQLite 数据库文件目录 + - ./data:/app/data:rw +``` + +## 使用方法 + +### 启动服务 + +```bash +# 生产环境 +docker-compose up -d + +# 优化环境 +docker-compose -f docker-compose.optimized.yml up -d + +# 开发环境 +docker-compose -f docker-compose.dev.yml up -d +``` + +### 数据持久化 + +- 数据库文件会自动创建在 `./data` 目录中 +- 容器重启或重新创建后数据不会丢失 +- 支持数据库备份和迁移 + +### 备份数据库 + +```bash +# 备份数据库 +cp ./data/users.db ./data/users.db.backup.$(date +%Y%m%d_%H%M%S) + +# 查看数据库文件 +ls -la ./data/ +``` + +## 注意事项 + +1. **权限问题**: 确保 `./data` 目录有正确的读写权限 +2. **WAL 模式**: SQLite 使用 WAL (Write-Ahead Logging) 模式,会产生额外的 WAL 和 SHM 文件 +3. **并发访问**: Docker 挂载确保文件系统的一致性 +4. **备份策略**: 建议定期备份数据库文件 + +## 故障排除 + +### 数据库锁定问题 +如果遇到数据库锁定错误: +1. 停止容器:`docker-compose down` +2. 删除 WAL 文件:`rm ./data/users.db-wal ./data/users.db-shm` +3. 重新启动容器:`docker-compose up -d` + +### 权限问题 +如果遇到权限错误: +```bash +# 设置正确的目录权限 +sudo chown -R 1000:1000 ./data +chmod 755 ./data +``` + +## 开发环境注意事项 + +开发环境中,数据库文件会被实时同步到本地文件系统,便于: +- 调试和测试 +- 数据分析 +- 快速重置测试数据 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a4d5785..58c8975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "moodist", - "version": "2.1.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "moodist", - "version": "2.1.0", + "version": "3.0.0", "dependencies": { "@astrojs/react": "3.6.0", "@floating-ui/react": "0.26.0", @@ -15,20 +15,27 @@ "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-slider": "1.2.3", "@radix-ui/react-tooltip": "1.2.8", + "@types/bcryptjs": "2.4.6", + "@types/better-sqlite3": "7.6.13", "@types/howler": "2.2.10", "@types/react": "^18.2.25", "@types/react-dom": "^18.2.10", "@vite-pwa/astro": "0.5.0", "astro": "4.10.3", + "astro-i18next": "1.0.0-beta.21", + "bcryptjs": "3.0.3", + "better-sqlite3": "11.10.0", "deepmerge": "4.3.1", "focus-trap-react": "10.2.3", "framer-motion": "10.16.4", "howler": "2.2.4", + "i18next": "25.6.2", "js-confetti": "0.12.0", "motion": "12.23.24", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "3.2.1", + "react-i18next": "16.3.3", "react-icons": "4.11.0", "react-wrap-balancer": "1.1.0", "react-youtube": "10.1.0", @@ -36,6 +43,7 @@ "zustand": "4.4.3" }, "devDependencies": { + "@astrojs/node": "8.3.4", "@chromatic-com/storybook": "1.3.3", "@commitlint/cli": "17.7.2", "@commitlint/config-conventional": "17.7.0", @@ -147,6 +155,79 @@ "vfile": "^6.0.1" } }, + "node_modules/@astrojs/node": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-8.3.4.tgz", + "integrity": "sha512-xzQs39goN7xh9np9rypGmbgZj3AmmjNxEMj9ZWz5aBERlqqFF3n8A/w/uaJeZ/bkHS60l1BXVS0tgsQt9MFqBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "send": "^0.19.0", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^4.2.0" + } + }, + "node_modules/@astrojs/node/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@astrojs/node/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@astrojs/node/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@astrojs/node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@astrojs/node/node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/@astrojs/prism": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.1.0.tgz", @@ -2044,12 +2125,10 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -4163,69 +4242,6 @@ "tar-fs": "^2.1.1" } }, - "node_modules/@ndelangen/get-tarball/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@ndelangen/get-tarball/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@ndelangen/get-tarball/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/@ndelangen/get-tarball/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4288,6 +4304,28 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@proload/core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@proload/core/-/core-0.3.3.tgz", + "integrity": "sha512-7dAFWsIK84C90AMl24+N/ProHKm4iw0akcnoKjRvbfHifJZBLhaDsDus1QJmhG12lXj4e/uB/8mB/0aduCW+NQ==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escalade": "^3.1.1" + } + }, + "node_modules/@proload/plugin-tsm": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@proload/plugin-tsm/-/plugin-tsm-0.2.1.tgz", + "integrity": "sha512-Ex1sL2BxU+g8MHdAdq9SZKz+pU34o8Zcl9PHWo2WaG9hrnlZme607PU6gnpoAYsDBpHX327+eu60wWUk+d/b+A==", + "license": "MIT", + "dependencies": { + "tsm": "^2.1.4" + }, + "peerDependencies": { + "@proload/core": "^0.3.2" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -6498,41 +6536,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@storybook/cli/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@storybook/cli/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/@storybook/cli/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8649,6 +8652,21 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -8849,8 +8867,7 @@ "node_modules/@types/node": { "version": "20.5.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", - "integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==", - "devOptional": true + "integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.2", @@ -10171,6 +10188,52 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/astro-i18next": { + "version": "1.0.0-beta.21", + "resolved": "https://registry.npmjs.org/astro-i18next/-/astro-i18next-1.0.0-beta.21.tgz", + "integrity": "sha512-1YPqwexumHpK/d9afEoi52CBFTu6k4MYv/oHjsaAasZDvFClU6U5VPttC/OgZcXRYggCM6ee2LOnyHqlmXOeLA==", + "license": "MIT", + "dependencies": { + "@proload/core": "^0.3.3", + "@proload/plugin-tsm": "^0.2.1", + "i18next": "^22.4.10", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-fs-backend": "^2.1.1", + "i18next-http-backend": "^2.1.1", + "iso-639-1": "^2.1.15", + "locale-emoji": "^0.3.0", + "pathe": "^1.1.0" + }, + "bin": { + "astro-i18next": "dist/cli/index.js" + }, + "peerDependencies": { + "astro": ">=1.0.0" + } + }, + "node_modules/astro-i18next/node_modules/i18next": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz", + "integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, "node_modules/astro/node_modules/@esbuild/android-arm": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", @@ -10758,7 +10821,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -10772,7 +10834,17 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } }, "node_modules/better-opn": { "version": "3.0.2", @@ -10839,6 +10911,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -10856,6 +10939,26 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -11041,6 +11144,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -11298,8 +11425,7 @@ "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromatic": { "version": "11.3.0", @@ -13550,6 +13676,15 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -13733,6 +13868,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -13783,6 +13933,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -14014,7 +14173,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "optional": true, "engines": { "node": ">=8" } @@ -14512,7 +14670,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -14761,6 +14918,262 @@ "@esbuild/win32-x64": "0.19.8" } }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/esbuild-plugin-alias": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", @@ -14779,6 +15192,70 @@ "esbuild": ">=0.12 <1" } }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -15739,6 +16216,15 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -15972,6 +16458,12 @@ "ramda": "0.29.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -16265,7 +16757,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "11.1.1", @@ -17271,6 +17763,12 @@ "ini": "^1.3.2" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -17911,6 +18409,15 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -17995,6 +18502,61 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.2.tgz", + "integrity": "sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.2.tgz", + "integrity": "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.1.tgz", + "integrity": "sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==", + "license": "MIT" + }, + "node_modules/i18next-http-backend": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.7.3.tgz", + "integrity": "sha512-FgZxrXdRA5u44xfYsJlEBL4/KH3f2IluBpgV/7riW0YW2VEyM8FzVt2XHAOi6id0Ppj7vZvCZVpp5LrGXnc8Ig==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -18017,7 +18579,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -18031,7 +18592,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.2.4", @@ -18114,8 +18676,7 @@ "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/inquirer": { "version": "8.2.5", @@ -18182,41 +18743,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/inquirer/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/inquirer/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/inquirer/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -19063,6 +19589,15 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/iso-639-1": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.1.15.tgz", + "integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -19774,6 +20309,12 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locale-emoji": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/locale-emoji/-/locale-emoji-0.3.0.tgz", + "integrity": "sha512-JGm8+naU49CBDnH1jksS3LecPdfWQLxFgkLN6ZhYONKa850pJ0Xt8DPGJnYK0ZuJI8jTuiDDPCDtSL3nyacXwg==", + "license": "CC0-1.0" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -20993,6 +21534,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -21025,6 +21579,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -21049,7 +21615,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -21132,8 +21697,7 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "node_modules/mlly": { "version": "1.7.1", @@ -21260,6 +21824,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -21299,6 +21869,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -21315,7 +21897,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -21953,8 +22534,7 @@ "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" }, "node_modules/pathval": { "version": "1.1.1", @@ -22272,6 +22852,32 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/preferred-pm": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.1.3.tgz", @@ -22495,7 +23101,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -22629,6 +23234,21 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -22746,6 +23366,42 @@ "react-dom": ">=16.8.1" } }, + "node_modules/react-i18next": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.3.tgz", + "integrity": "sha512-IaY2W+ueVd/fe7H6Wj2S4bTuLNChnajFUlZFfCTrTHWzGcOrUHlVzW55oXRSl+J51U8Onn6EvIhQ+Bar9FUcjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-i18next/node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-icons": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz", @@ -22922,7 +23578,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -23011,11 +23666,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -24081,7 +24731,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -24096,7 +24745,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -24107,8 +24755,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -24149,18 +24796,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -24191,6 +24826,13 @@ "node": ">= 0.8.0" } }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true, + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -24351,6 +24993,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -24783,7 +25470,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -25047,6 +25733,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-literal": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", @@ -25475,6 +26170,34 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -25761,8 +26484,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/trim-lines": { "version": "3.0.1", @@ -25930,6 +26652,90 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tsm": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tsm/-/tsm-2.3.0.tgz", + "integrity": "sha512-++0HFnmmR+gMpDtKTnW3XJ4yv9kVGi20n+NfyQWB9qwJvTaIWY9kBmzek2YUQK5APTQ/1DTrXmm4QtFPmW9Rzw==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.16" + }, + "bin": { + "tsm": "bin.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tsm/node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsm/node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsm/node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", @@ -25951,6 +26757,18 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tween-functions": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", @@ -26478,8 +27296,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -27155,6 +27972,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -27189,8 +28015,7 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack-sources": { "version": "3.2.3", @@ -27211,7 +28036,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index a47e597..5463e99 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "moodist", "type": "module", - "version": "2.1.0", + "version": "3.0.0", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -21,7 +21,14 @@ "release:minor": "npm run release -- --release-as minor", "release:patch": "npm run release -- --release-as patch", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "docker:build": "./scripts/build-local.sh", + "docker:push": "./scripts/build-docker-simple.sh", + "docker:push-and-upload": "./scripts/build-docker-simple.sh latest push", + "docker:multi": "./scripts/build-docker-compatible.sh", + "docker:dev": "docker-compose -f docker-compose.dev.yml up -d", + "docker:prod": "docker-compose -f docker-compose.optimized.yml up -d", + "docker:logs": "docker-compose -f docker-compose.optimized.yml logs -f" }, "dependencies": { "@astrojs/react": "3.6.0", @@ -31,20 +38,27 @@ "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-slider": "1.2.3", "@radix-ui/react-tooltip": "1.2.8", + "@types/bcryptjs": "2.4.6", + "@types/better-sqlite3": "7.6.13", "@types/howler": "2.2.10", "@types/react": "^18.2.25", "@types/react-dom": "^18.2.10", "@vite-pwa/astro": "0.5.0", "astro": "4.10.3", + "astro-i18next": "1.0.0-beta.21", + "bcryptjs": "3.0.3", + "better-sqlite3": "11.10.0", "deepmerge": "4.3.1", "focus-trap-react": "10.2.3", "framer-motion": "10.16.4", "howler": "2.2.4", + "i18next": "25.6.2", "js-confetti": "0.12.0", "motion": "12.23.24", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "3.2.1", + "react-i18next": "16.3.3", "react-icons": "4.11.0", "react-wrap-balancer": "1.1.0", "react-youtube": "10.1.0", @@ -52,6 +66,7 @@ "zustand": "4.4.3" }, "devDependencies": { + "@astrojs/node": "8.3.4", "@chromatic-com/storybook": "1.3.3", "@commitlint/cli": "17.7.2", "@commitlint/config-conventional": "17.7.0", diff --git a/scripts/build-docker-compatible.sh b/scripts/build-docker-compatible.sh new file mode 100755 index 0000000..133c54e --- /dev/null +++ b/scripts/build-docker-compatible.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +# Moodist Docker 构建脚本 - 兼容版本 +# 支持标准Docker和Docker Buildx(可选) + +set -e + +# 配置变量 +IMAGE_NAME="walllee/moodist" +VERSION=${1:-latest} +BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') +VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") +USE_BUILDX=${2:-false} + +echo "🐳 开始构建 Moodist Docker 镜像..." +echo "📦 镜像名称: ${IMAGE_NAME}" +echo "🏷️ 版本标签: ${VERSION}" +echo "📅 构建时间: ${BUILD_DATE}" +echo "🔗 Git提交: ${VCS_REF}" +echo "🔧 使用Buildx: ${USE_BUILDX}" + +# 检查Docker是否安装并运行 +if ! docker info &> /dev/null; then + echo "❌ Docker未运行,请启动Docker服务" + exit 1 +fi + +# 构建参数 +BUILD_ARGS="--build-arg BUILD_DATE=${BUILD_DATE}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VERSION=${VERSION}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VCS_REF=${VCS_REF}" +BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_ENV=production" + +# 构建函数 +build_image() { + local tag=$1 + local push_flag=$2 + local build_cmd="docker build" + + if [ "$USE_BUILDX" = "true" ]; then + # 检查buildx是否可用 + if docker buildx version &> /dev/null; then + echo "🔨 使用Docker Buildx构建..." + build_cmd="docker buildx build" + + # 创建buildx构建器(如果不存在) + if ! docker buildx ls | grep -q "moodist-builder"; then + echo "🔨 创建Docker Buildx构建器..." + docker buildx create --name moodist-builder --use 2>/dev/null || true + docker buildx inspect --bootstrap 2>/dev/null || true + fi + + # 多平台构建参数 + PLATFORM_ARGS="--platform linux/amd64,linux/arm64" + PUSH_FLAG="--push" + else + echo "⚠️ Docker Buildx不可用,回退到标准Docker构建..." + USE_BUILDX="false" + fi + fi + + echo "🏗️ 开始构建镜像: ${tag}" + + if [ "$USE_BUILDX" = "true" ]; then + # 使用buildx + $build_cmd \ + $PLATFORM_ARGS \ + --tag "${tag}" \ + $BUILD_ARGS \ + --file ./Dockerfile.optimized \ + $PUSH_FLAG \ + . + else + # 使用标准Docker + $build_cmd \ + --tag "${tag}" \ + $BUILD_ARGS \ + --file ./Dockerfile.optimized \ + . + + # 如果需要推送,使用标准Docker push + if [ "$push_flag" = "true" ]; then + echo "📤 推送镜像: ${tag}" + docker push "${tag}" + fi + fi +} + +# 检查是否需要登录Docker Hub +if [ "$USE_BUILDX" = "true" ] || [ "${2}" = "push" ]; then + if ! docker info 2>/dev/null | grep -q "Username"; then + echo "⚠️ 未检测到Docker登录,请先运行: docker login" + echo "💡 如果您有Docker Hub账号,请使用以下命令登录:" + echo " docker login" + read -p "是否继续构建?(y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "❌ 构建已取消" + exit 1 + fi + fi +fi + +# 构建镜像 +if [ "$USE_BUILDX" = "true" ]; then + # Buildx模式(支持多平台) + build_image "${IMAGE_NAME}:${VERSION}" true + + # 如果指定了特定版本,同时创建version-specific标签 + if [ "$VERSION" != "latest" ]; then + echo "🏷️ 添加版本标签: ${VERSION}" + build_image "${IMAGE_NAME}:${VERSION}" true + fi +else + # 标准Docker模式 + build_image "${IMAGE_NAME}:${VERSION}" false + + # 如果指定了特定版本,创建额外的标签 + if [ "$VERSION" != "latest" ]; then + docker tag "${IMAGE_NAME}:${VERSION}" "${IMAGE_NAME}:latest" + fi + + # 如果需要推送 + if [ "${2}" = "push" ]; then + echo "📤 推送镜像到Docker Hub..." + docker push "${IMAGE_NAME}:${VERSION}" + + if [ "$VERSION" != "latest" ]; then + docker push "${IMAGE_NAME}:latest" + fi + fi +fi + +echo "" +echo "🎉 镜像构建完成!" +echo "📋 镜像信息:" +echo " 🔗 镜像名称: ${IMAGE_NAME}" +echo " 🏷️ 标签: ${VERSION}${VERSION != "latest" ? ", latest" : ""}" +echo " 📅 构建时间: ${BUILD_DATE}" +echo " 🔗 Git提交: ${VCS_REF}" +echo "" +echo "🚀 使用方法:" +echo " docker run -d -p 8080:8080 ${IMAGE_NAME}:${VERSION}" +echo "" +echo "📝 查看镜像信息:" +echo " docker images | grep ${IMAGE_NAME}" +echo " docker inspect ${IMAGE_NAME}:${VERSION}" + +# 显示镜像大小 +echo "" +echo "📊 镜像大小:" +docker images ${IMAGE_NAME}:${VERSION} --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" \ No newline at end of file diff --git a/scripts/build-docker-simple.sh b/scripts/build-docker-simple.sh new file mode 100755 index 0000000..e69442a --- /dev/null +++ b/scripts/build-docker-simple.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Moodist Docker 简化构建脚本 +# 先本地构建,再打包成Docker镜像 + +set -e + +# 配置变量 +IMAGE_NAME="walllee/moodist" +VERSION=${1:-latest} +BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') +VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + +echo "🐳 开始构建 Moodist Docker 镜像(简化版)..." +echo "📦 镜像名称: ${IMAGE_NAME}" +echo "🏷️ 版本标签: ${VERSION}" +echo "📅 构建时间: ${BUILD_DATE}" +echo "🔗 Git提交: ${VCS_REF}" + +# 检查Docker是否安装并运行 +if ! docker info &> /dev/null; then + echo "❌ Docker未运行,请启动Docker服务" + exit 1 +fi + +# 步骤1: 本地构建 +echo "" +echo "📦 步骤 1: 本地构建应用..." +npm run build + +if [ $? -ne 0 ]; then + echo "❌ 本地构建失败" + exit 1 +fi + +echo "✅ 本地构建完成!" + +# 步骤2: 构建Docker镜像 +echo "" +echo "🐳 步骤 2: 构建 Docker镜像..." + +# 构建参数 +BUILD_ARGS="--build-arg BUILD_DATE=${BUILD_DATE}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VERSION=${VERSION}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VCS_REF=${VCS_REF}" + +# 构建镜像 +docker build \ + ${BUILD_ARGS} \ + --tag "${IMAGE_NAME}:${VERSION}" \ + --file ./Dockerfile.simple \ + . + +echo "✅ Docker镜像构建完成!" + +# 步骤3: 可选推送 +if [ "${2}" = "push" ]; then + echo "" + echo "📤 步骤 3: 推送镜像到Docker Hub..." + + # 检查是否登录Docker Hub + if ! docker info 2>/dev/null | grep -q "Username"; then + echo "⚠️ 未检测到Docker登录,请先运行: docker login" + read -p "是否继续推送?(y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "❌ 推送已取消" + exit 1 + fi + fi + + # 推送镜像 + docker push "${IMAGE_NAME}:${VERSION}" + + # 如果指定了特定版本,同时创建latest标签 + if [ "$VERSION" != "latest" ]; then + docker tag "${IMAGE_NAME}:${VERSION}" "${IMAGE_NAME}:latest" + docker push "${IMAGE_NAME}:latest" + fi + + echo "✅ 镜像推送完成!" +fi + +echo "" +echo "🎉 简化构建完成!" +echo "📋 镜像信息:" +echo " 🔗 镜像名称: ${IMAGE_NAME}" +if [ "$VERSION" != "latest" ]; then + echo " 🏷️ 标签: ${VERSION}, latest" +else + echo " 🏷️ 标签: ${VERSION}" +fi +echo " 📅 构建时间: ${BUILD_DATE}" +echo " 🔗 Git提交: ${VCS_REF}" +echo "" +echo "🚀 使用方法:" +echo " docker run -d -p 8080:8080 ${IMAGE_NAME}:${VERSION}" +echo "" +echo "📝 查看镜像信息:" +echo " docker images | grep ${IMAGE_NAME}" +echo " docker inspect ${IMAGE_NAME}:${VERSION}" + +# 显示镜像大小 +echo "" +echo "📊 镜像大小:" +docker images ${IMAGE_NAME}:${VERSION} --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" + +# 显示构建产物大小 +echo "" +echo "📦 构建产物大小:" +du -sh dist/ | tail -1 \ No newline at end of file diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh new file mode 100755 index 0000000..38ad0ef --- /dev/null +++ b/scripts/build-docker.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# Moodist Docker 构建和推送脚本 +# 支持多平台构建并推送到 Docker Hub + +set -e + +# 配置变量 +IMAGE_NAME="walllee/moodist" +VERSION=${1:-latest} +BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') +VCS_REF=$(git rev-parse --short HEAD) + +echo "🐳 开始构建 Moodist Docker 镜像..." +echo "📦 镜像名称: ${IMAGE_NAME}" +echo "🏷️ 版本标签: ${VERSION}" +echo "📅 构建时间: ${BUILD_DATE}" +echo "🔗 Git提交: ${VCS_REF}" + +# 检查Docker是否安装并运行 +if ! docker info &> /dev/null; then + echo "❌ Docker未运行,请启动Docker服务" + exit 1 +fi + +# 检查是否登录Docker Hub +if ! docker info | grep -q "Username"; then + echo "⚠️ 未检测到Docker登录,请先运行: docker login" + echo "💡 如果您有Docker Hub账号,请使用以下命令登录:" + echo " docker login" + echo " # 输入您的用户名和密码或访问令牌" + read -p "是否继续构建?(y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "❌ 构建已取消" + exit 1 + fi +fi + +# 创建buildx构建器(如果不存在) +if ! docker buildx ls | grep -q "moodist-builder"; then + echo "🔨 创建Docker Buildx构建器..." + docker buildx create --name moodist-builder --use + docker buildx inspect --bootstrap +fi + +# 构建参数 +BUILD_ARGS="--build-arg BUILD_DATE=${BUILD_DATE}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VERSION=${VERSION}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VCS_REF=${VCS_REF}" +BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_ENV=production" + +echo "🏗️ 开始构建多平台镜像..." + +# 构建并推送多平台镜像 +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag "${IMAGE_NAME}:${VERSION}" \ + --tag "${IMAGE_NAME}:latest" \ + ${BUILD_ARGS} \ + --file ./Dockerfile.optimized \ + --push \ + . + +echo "✅ 构建完成!" + +# 如果指定了特定版本,同时创建version-specific标签 +if [ "$VERSION" != "latest" ]; then + echo "🏷️ 添加版本标签: ${VERSION}" + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag "${IMAGE_NAME}:${VERSION}" \ + ${BUILD_ARGS} \ + --file ./Dockerfile.optimized \ + --push \ + . +fi + +echo "" +echo "🎉 镜像构建和推送完成!" +echo "📋 镜像信息:" +echo " 🔗 Docker Hub: https://hub.docker.com/r/${IMAGE_NAME}" +echo " 🏷️ 标签: ${VERSION}, latest" +echo " 🏗️ 平台: linux/amd64, linux/arm64" +echo "" +echo "🚀 使用方法:" +echo " docker run -d -p 8080:8080 ${IMAGE_NAME}:${VERSION}" +echo "" +echo "📝 查看镜像信息:" +echo " docker pull ${IMAGE_NAME}:${VERSION}" +echo " docker inspect ${IMAGE_NAME}:${VERSION}" \ No newline at end of file diff --git a/scripts/build-local.sh b/scripts/build-local.sh new file mode 100755 index 0000000..c2d0450 --- /dev/null +++ b/scripts/build-local.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Moodist Docker 本地构建脚本 +# 用于本地测试和开发 + +set -e + +# 配置变量 +IMAGE_NAME="moodist-local" +VERSION=${1:-dev} +BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') +VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + +echo "🐳 开始本地构建 Moodist Docker 镜像..." +echo "📦 镜像名称: ${IMAGE_NAME}" +echo "🏷️ 版本标签: ${VERSION}" +echo "📅 构建时间: ${BUILD_DATE}" +echo "🔗 Git提交: ${VCS_REF}" + +# 检查Docker是否安装并运行 +if ! docker info &> /dev/null; then + echo "❌ Docker未运行,请启动Docker服务" + exit 1 +fi + +# 构建参数 +BUILD_ARGS="--build-arg BUILD_DATE=${BUILD_DATE}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VERSION=${VERSION}" +BUILD_ARGS="${BUILD_ARGS} --build-arg VCS_REF=${VCS_REF}" +BUILD_ARGS="${BUILD_ARGS} --build-arg NODE_ENV=production" + +echo "🏗️ 开始本地构建..." + +# 构建本地镜像 +docker build \ + ${BUILD_ARGS} \ + --tag "${IMAGE_NAME}:${VERSION}" \ + --file ./Dockerfile.optimized \ + . + +echo "✅ 本地构建完成!" + +# 运行容器进行测试 +echo "🧪 启动测试容器..." + +# 停止并删除现有容器(如果存在) +docker stop moodist-test 2>/dev/null || true +docker rm moodist-test 2>/dev/null || true + +# 启动新容器 +docker run -d \ + --name moodist-test \ + -p 8081:8080 \ + --restart unless-stopped \ + "${IMAGE_NAME}:${VERSION}" + +echo "🚀 测试容器已启动!" +echo "" +echo "📋 访问信息:" +echo " 🌐 本地访问: http://localhost:8081" +echo " 🐳 容器名称: moodist-test" +echo " 🏷️ 镜像标签: ${IMAGE_NAME}:${VERSION}" +echo "" +echo "🔧 常用命令:" +echo " 查看日志: docker logs moodist-test" +echo " 停止容器: docker stop moodist-test" +echo " 删除容器: docker rm moodist-test" +echo " 进入容器: docker exec -it moodist-test /bin/sh" +echo "" +echo "⏳ 等待容器启动..." +sleep 5 + +# 健康检查 +echo "🔍 执行健康检查..." +if curl -f http://localhost:8081/ &> /dev/null; then + echo "✅ 健康检查通过!应用正常运行" +else + echo "⚠️ 健康检查失败,请查看容器日志" + echo " docker logs moodist-test" +fi + +echo "" +echo "🎉 本地构建和测试完成!" \ No newline at end of file diff --git a/src/components/about-unified.astro b/src/components/about-unified.astro new file mode 100644 index 0000000..8a0b5af --- /dev/null +++ b/src/components/about-unified.astro @@ -0,0 +1,119 @@ +--- +import { Container } from '@/components/container'; +import { count as soundCount } from '@/lib/sounds'; +import { getTranslation } from '@/data/i18n'; + +// Get language from URL path +const url = Astro.url; +const pathname = url.pathname; +const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/'); +const lang = isZhPage ? 'zh-CN' : 'en'; +const t = getTranslation(lang); +const count = soundCount(); +--- + +
+
+ + + +
+ + \ No newline at end of file diff --git a/src/components/about-zh.astro b/src/components/about-zh.astro new file mode 100644 index 0000000..c9fe004 --- /dev/null +++ b/src/components/about-zh.astro @@ -0,0 +1,173 @@ +--- +import { Container } from '@/components/container'; + +import { count as soundCount } from '@/lib/sounds'; + +const count = soundCount(); +--- + +
+
+ + +
+
+ 01 / 04 +
+

免费环境音

+

+ 渴望从日常繁杂中获得片刻宁静?需要完美的声音环境来提升专注力或帮助入眠? + Moodist 就是您的最佳选择——免费开源的环境音生成器!无需订阅注册,使用 Moodist, + 您可以免费享受舒缓沉浸的音频体验。 +

+
+ +
+
+ 02 / 04 +
+

精心挑选的声音

+

+ 探索包含 {count} 个精心挑选声音的庞大音库。 + 自然爱好者可以在溪流的轻柔潺潺声中、海浪的节拍拍岸声中、或篝火的温暖噼啪声中获得慰藉。 + 城市景观在咖啡馆的轻柔嗡嗡声、火车的节拍咔嗒声、或交通的舒缓白噪声中变得生动。 + 对于寻求更深专注或放松的人,Moodist 提供了专门设计来增强心境的双节拍和色彩噪声。 +

+
+ +
+
+ 03 / 04 +
+

创造您的声音景观

+

+ Moodist 的美妙之处在于其简洁性和自定义性。没有复杂的菜单或令人困惑的选项——只需选择您喜欢的声音, + 调整音量平衡,然后点击播放。想要将鸟儿的轻柔啾鸣与雨水的舒缓声音融合?没问题! + 随心所欲地叠加多个声音,创建个性化的声音绿洲。 +

+
+ +
+
+ 04 / 04 +
+

适合每个时刻的声音

+

+ 无论您是想在漫长一天后放松身心,在工作中提升专注力,还是让自己进入宁静的睡眠, + Moodist 都有完美的声音景观等着您。最棒的是什么?它完全免费开源,您可以毫无负担地享受它的好处。 + 今天就开始使用 Moodist,发现您新的宁静和专注天堂吧! +

+
+ + +
+
+ + + + \ No newline at end of file diff --git a/src/components/about.astro b/src/components/about.astro index 72fd439..fdff981 100644 --- a/src/components/about.astro +++ b/src/components/about.astro @@ -4,45 +4,53 @@ import { Container } from '@/components/container'; import { count as soundCount } from '@/lib/sounds'; const count = soundCount(); - -const paragraphs = [ - { - body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations – with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.', - title: 'Free Ambient Sounds', - }, - { - body: `Dive into an expansive library of ${count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.`, - title: 'Carefully Curated Sounds', - }, - { - body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options – just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.', - title: 'Create Your Soundscape', - }, - { - body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!", - title: 'Sounds for Every Moment', - }, -]; ---
- { - paragraphs.map((paragraph, index) => ( -
-
- 0{index + 1} / 0{paragraphs.length} -
+
+
+ 01 / 04 +
+

Free Ambient Sounds

+

+ Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations – with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free. +

+
-

{paragraph.title}

-

{paragraph.body}

-
- )) - } +
+
+ 02 / 04 +
+

Carefully Curated Sounds

+

+ Dive into an expansive library of {count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind. +

+
- +
+
+ 03 / 04 +
+

Create Your Soundscape

+

+ The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options – just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis. +

+
+ +
+
+ 04 / 04 +
+

Sounds for Every Moment

+

+ Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus! +

+
+ +
diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index c5f890d..f2cf1be 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -4,10 +4,13 @@ import { BiSolidHeart } from 'react-icons/bi/index'; import { Howler } from 'howler'; import { useSoundStore } from '@/stores/sound'; +import { useLocalizedSounds } from '@/hooks/useLocalizedSounds'; +import { useTranslation } from '@/hooks/useTranslation'; import { Container } from '@/components/container'; import { StoreConsumer } from '@/components/store-consumer'; import { Buttons } from '@/components/buttons'; +import { SelectedSoundsDisplay } from '@/components/selected-sounds-display'; import { Categories } from '@/components/categories'; import { SharedModal } from '@/components/modals/shared'; import { Toolbar } from '@/components/toolbar'; @@ -21,15 +24,17 @@ import type { Sound } from '@/data/types'; import { subscribe } from '@/lib/event'; export function App() { - const categories = useMemo(() => sounds.categories, []); + const localizedCategories = useLocalizedSounds(); + const { t } = useTranslation(); + const categories = useMemo(() => sounds.categories, []); const favorites = useSoundStore(useShallow(state => state.getFavorites())); const pause = useSoundStore(state => state.pause); const lock = useSoundStore(state => state.lock); const unlock = useSoundStore(state => state.unlock); const favoriteSounds = useMemo(() => { - const favoriteSounds = categories + const favoriteSounds = localizedCategories .map(category => category.sounds) .flat() .filter(sound => favorites.includes(sound.id)); @@ -40,7 +45,7 @@ export function App() { return favorites.map(favorite => favoriteSounds.find(sound => sound.id === favorite), ); - }, [favorites, categories]); + }, [favorites, localizedCategories]); useEffect(() => { const onChange = () => { @@ -79,12 +84,12 @@ export function App() { icon: , id: 'favorites', sounds: favoriteSounds as Array, - title: 'Favorites', + title: t('favorite'), }); } - return [...favorites, ...categories]; - }, [favoriteSounds, categories]); + return [...favorites, ...localizedCategories]; + }, [favoriteSounds, localizedCategories, t]); return ( @@ -93,6 +98,7 @@ export function App() {
+ diff --git a/src/components/auth-button/auth-button.module.css b/src/components/auth-button/auth-button.module.css new file mode 100644 index 0000000..8453d28 --- /dev/null +++ b/src/components/auth-button/auth-button.module.css @@ -0,0 +1,285 @@ +.authButton { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background: transparent; + border: none; + border-radius: 50%; + cursor: pointer; + color: var(--color-foreground, #1e293b); + transition: all 0.2s ease; + font-size: 16px; + line-height: 1; + width: 40px; + height: 40px; + position: fixed; + top: 1rem; + right: 4rem; /* 主题切换按钮左侧 */ + z-index: 1000; + background: var(--bg-secondary); + border: 1px solid var(--color-border); +} + +.authButton:hover { + background: var(--bg-tertiary); + transform: scale(1.05); +} + +.authButton:focus { + outline: 2px solid var(--color-foreground); + outline-offset: 2px; +} + +.authButton:active { + transform: scale(0.95); +} + +.userIndicator { + position: absolute; + bottom: 2px; + right: 2px; + width: 8px; + height: 8px; + background: #10b981; + border-radius: 50%; + border: 2px solid var(--bg-secondary); +} + +/* 暗色主题下的特殊样式 */ +:global(.dark-theme) .authButton { + background: var(--bg-secondary); + border: 1px solid var(--color-border); +} + +:global(.dark-theme) .authButton:hover { + background: var(--bg-tertiary); +} + +:global(.dark-theme) .userIndicator { + border-color: var(--bg-secondary); +} + +/* 认证表单样式 */ +.authFormOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1001; + padding: 16px; +} + +.authForm { + width: 100%; + max-width: 320px; + padding: 16px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + color: var(--color-foreground); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.authForm h3 { + margin: 0 0 16px 0; + font-size: var(--font-lg); + font-weight: 600; + text-align: center; + color: var(--color-foreground); +} + +.authForm form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.authInput { + padding: 8px 12px; + border: 1px solid var(--color-border); + border-radius: 6px; + font-size: var(--font-sm); + background: var(--input-bg); + color: var(--color-foreground); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.authInput::placeholder { + color: var(--color-foreground-subtler); +} + +.authInput:focus { + outline: none; + border-color: var(--color-muted); + box-shadow: 0 0 0 2px var(--color-muted); +} + +.authButtons { + display: flex; + gap: 8px; +} + +.authSubmitButton { + flex: 1; + padding: 8px 16px; + background: var(--color-foreground); + color: var(--bg-primary); + border: none; + border-radius: 6px; + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.authSubmitButton:hover:not(:disabled) { + background: var(--color-foreground-subtle); +} + +.authSubmitButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.authCancelButton { + padding: 8px 16px; + background: transparent; + color: var(--color-foreground-subtler); + border: 1px solid var(--color-border); + border-radius: 6px; + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; +} + +.authCancelButton:hover { + background: var(--component-hover); + border-color: var(--color-foreground-subtle); +} + +.authToggle { + margin-top: 8px; +} + +.authToggleButton { + width: 100%; + padding: 6px 12px; + background: transparent; + color: var(--color-foreground-subtle); + border: none; + border-radius: 6px; + font-size: var(--font-xsm); + cursor: pointer; + text-decoration: underline; + transition: color 0.2s; +} + +.authToggleButton:hover { + color: var(--color-foreground); +} + +/* 用户菜单样式 */ +.userMenu { + position: fixed; + top: 4rem; + right: 4rem; + z-index: 1000; +} + +.userInfo { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + min-width: 160px; +} + +.userAvatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--color-foreground); + color: var(--bg-primary); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-sm); + font-weight: 600; + flex-shrink: 0; +} + +.userName { + font-weight: 500; + color: var(--color-foreground); + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.logoutButton { + padding: 4px 8px; + font-size: var(--font-xsm); + color: #ef4444; + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.logoutButton:hover { + background: rgba(239, 68, 68, 0.1); +} + +/* 暗色主题下的样式优化 */ +:global(.dark-theme) .authForm { + background: var(--component-bg); + border-color: var(--color-border); +} + +:global(.dark-theme) .authInput { + background: var(--input-bg); + border-color: var(--color-border); +} + +:global(.dark-theme) .userInfo { + background: var(--component-bg); + border-color: var(--color-border); +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .authButton { + top: 0.75rem; + right: 3.5rem; + width: 36px; + height: 36px; + font-size: 14px; + } + + .userMenu { + top: 3.5rem; + right: 3.5rem; + } + + .authFormOverlay { + padding: 12px; + } + + .authForm { + padding: 12px; + } +} \ No newline at end of file diff --git a/src/components/auth-button/auth-button.tsx b/src/components/auth-button/auth-button.tsx new file mode 100644 index 0000000..784357a --- /dev/null +++ b/src/components/auth-button/auth-button.tsx @@ -0,0 +1,143 @@ +import { useState } from 'react'; +import { FaUser } from 'react-icons/fa/index'; +import { motion } from 'motion/react'; +import { useAuthStore } from '@/stores/auth'; +import styles from './auth-button.module.css'; + +export function AuthButton() { + const { isAuthenticated, user, login, logout, isLoading } = useAuthStore(); + const [showAuthForm, setShowAuthForm] = useState(false); + const [isLogin, setIsLogin] = useState(true); + const [formData, setFormData] = useState({ + username: '', + password: '', + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (isLogin) { + await login(formData); + } else { + // 注册功能先用登录代替 + await login(formData); + } + setShowAuthForm(false); + setFormData({ username: '', password: '' }); + } catch (error) { + console.error('认证失败:', error); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const handleLogout = () => { + logout(); + }; + + const handleClick = () => { + if (isAuthenticated) { + // 如果已登录,显示用户菜单 + return; + } else { + // 如果未登录,显示登录表单 + setShowAuthForm(true); + } + }; + + return ( + <> + + + {isAuthenticated && user && ( + + )} + + + {showAuthForm && ( +
setShowAuthForm(false)}> +
e.stopPropagation()}> +

{isLogin ? '登录' : '注册'}

+
+ + +
+ + +
+
+ +
+
+
+
+ )} + + {isAuthenticated && ( +
+
+
+ {user?.username.charAt(0).toUpperCase()} +
+ {user?.username} + +
+
+ )} + + ); +} \ No newline at end of file diff --git a/src/components/auth-button/index.ts b/src/components/auth-button/index.ts new file mode 100644 index 0000000..b5a5a0e --- /dev/null +++ b/src/components/auth-button/index.ts @@ -0,0 +1 @@ +export { AuthButton } from './auth-button'; \ No newline at end of file diff --git a/src/components/auth/auth-form.module.css b/src/components/auth/auth-form.module.css new file mode 100644 index 0000000..02f10b0 --- /dev/null +++ b/src/components/auth/auth-form.module.css @@ -0,0 +1,146 @@ +.authContainer { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + backdrop-filter: blur(4px); +} + +.authCard { + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 12px; + padding: 32px; + width: 100%; + max-width: 400px; + margin: 0 16px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} + +.title { + font-size: var(--font-xl); + font-weight: 600; + color: var(--color-foreground); + text-align: center; + margin-bottom: 24px; +} + +.form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.formGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.label { + font-size: var(--font-sm); + font-weight: 500; + color: var(--color-foreground-subtle); +} + +.input { + padding: 12px 16px; + border: 1px solid var(--color-border); + border-radius: 8px; + font-size: var(--font-base); + background: var(--bg-primary); + color: var(--color-foreground); + transition: border-color 0.2s, box-shadow 0.2s; + + &:focus { + outline: none; + border-color: var(--color-muted); + box-shadow: 0 0 0 2px var(--color-muted); + } + + &::placeholder { + color: var(--color-foreground-subtler); + } +} + +.submitButton { + padding: 14px 24px; + background: var(--color-foreground); + color: var(--bg-primary); + border: none; + border-radius: 8px; + font-size: var(--font-base); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; + + &:hover:not(:disabled) { + background: var(--color-foreground-subtle); + } + + &:active:not(:disabled) { + transform: translateY(1px); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.successMessage { + background: #10b981; + color: white; + padding: 12px 16px; + border-radius: 8px; + font-size: var(--font-sm); + text-align: center; + margin-bottom: 16px; +} + +.errorMessage { + background: #ef4444; + color: white; + padding: 12px 16px; + border-radius: 8px; + font-size: var(--font-sm); + text-align: center; + margin-bottom: 16px; +} + +.toggleSection { + text-align: center; + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; +} + +.toggleText { + font-size: var(--font-sm); + color: var(--color-foreground-subtler); +} + +.toggleButton { + background: none; + border: none; + color: var(--color-foreground); + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + text-decoration: underline; + transition: color 0.2s; + + &:hover { + color: var(--color-foreground-subtle); + } +} \ No newline at end of file diff --git a/src/components/auth/auth-form.tsx b/src/components/auth/auth-form.tsx new file mode 100644 index 0000000..a11bd58 --- /dev/null +++ b/src/components/auth/auth-form.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { useAuthStore } from '@/stores/auth'; +import { useTranslation } from '@/hooks/useTranslation'; +import styles from './auth-form.module.css'; + +export function AuthForm() { + const { t } = useTranslation(); + const [isLogin, setIsLogin] = useState(true); + const [formData, setFormData] = useState({ + username: '', + password: '', + }); + const [showSuccess, setShowSuccess] = useState(false); + const [successMessage, setSuccessMessage] = useState(''); + + const { login, register, isLoading, error, clearError } = useAuthStore(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + clearError(); + + try { + if (isLogin) { + await login(formData); + setSuccessMessage('登录成功!'); + } else { + await register(formData); + setSuccessMessage('注册成功!'); + } + + setShowSuccess(true); + setTimeout(() => { + setShowSuccess(false); + }, 3000); + } catch (error) { + // 错误已经在 store 中处理了 + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const toggleMode = () => { + setIsLogin(!isLogin); + clearError(); + setFormData({ username: '', password: '' }); + }; + + return ( +
+
+

+ {isLogin ? '登录' : '注册'} +

+ + {showSuccess && ( +
+ {successMessage} +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ + +
+ +
+ + {isLogin ? '还没有账号?' : '已有账号?'} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/auth/index.ts b/src/components/auth/index.ts new file mode 100644 index 0000000..2ee3eae --- /dev/null +++ b/src/components/auth/index.ts @@ -0,0 +1,3 @@ +export { AuthForm } from './auth-form'; +export { UserInfo } from './user-info'; +export { LoginTrigger } from './login-trigger'; \ No newline at end of file diff --git a/src/components/auth/login-trigger.module.css b/src/components/auth/login-trigger.module.css new file mode 100644 index 0000000..ff74f8b --- /dev/null +++ b/src/components/auth/login-trigger.module.css @@ -0,0 +1,26 @@ +.loginButton { + padding: 8px 16px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + color: var(--color-foreground); + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; + + &:hover { + background: var(--component-hover); + border-color: var(--color-foreground-subtle); + } +} + +.backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; + z-index: 9998; +} \ No newline at end of file diff --git a/src/components/auth/login-trigger.tsx b/src/components/auth/login-trigger.tsx new file mode 100644 index 0000000..cd85fc3 --- /dev/null +++ b/src/components/auth/login-trigger.tsx @@ -0,0 +1,26 @@ +import { useState } from 'react'; +import { AuthForm } from './auth-form'; +import styles from './login-trigger.module.css'; + +export function LoginTrigger() { + const [showAuth, setShowAuth] = useState(false); + + const openAuth = () => { + setShowAuth(true); + }; + + const closeAuth = () => { + setShowAuth(false); + }; + + return ( + <> + + + {showAuth && } + {showAuth &&
} + + ); +} \ No newline at end of file diff --git a/src/components/auth/user-info.module.css b/src/components/auth/user-info.module.css new file mode 100644 index 0000000..f9d61d3 --- /dev/null +++ b/src/components/auth/user-info.module.css @@ -0,0 +1,140 @@ +.userContainer { + position: relative; + display: inline-block; +} + +.userButton { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; + color: var(--color-foreground); + font-size: var(--font-sm); +} + +.userButton:hover { + background: var(--component-hover); + border-color: var(--color-foreground-subtle); +} + +.userAvatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--color-foreground); + color: var(--bg-primary); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-sm); + font-weight: 600; +} + +.userName { + font-weight: 500; + color: var(--color-foreground); +} + +.dropdownArrow { + font-size: 10px; + color: var(--color-foreground-subtler); + transition: transform 0.2s; +} + +.userButton:hover .dropdownArrow { + transform: translateY(1px); +} + +.dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 280px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 12px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + z-index: 1000; + overflow: hidden; +} + +.userInfo { + padding: 20px; + display: flex; + align-items: center; + gap: 16px; +} + +.userAvatarLarge { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--color-foreground); + color: var(--bg-primary); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-lg); + font-weight: 600; + flex-shrink: 0; +} + +.userDetails { + flex: 1; + min-width: 0; +} + +.userFullname { + font-size: var(--font-base); + font-weight: 600; + color: var(--color-foreground); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.userJoined { + font-size: var(--font-xsm); + color: var(--color-foreground-subtler); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.divider { + height: 1px; + background: var(--color-border); +} + +.logoutButton { + width: 100%; + padding: 12px 20px; + background: none; + border: none; + color: #ef4444; + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + text-align: left; + + &:hover { + background: rgba(239, 68, 68, 0.1); + } +} + +.backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; + z-index: 999; +} \ No newline at end of file diff --git a/src/components/auth/user-info.tsx b/src/components/auth/user-info.tsx new file mode 100644 index 0000000..2fa5108 --- /dev/null +++ b/src/components/auth/user-info.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { useAuthStore } from '@/stores/auth'; +import styles from './user-info.module.css'; + +export function UserInfo() { + const { user, logout } = useAuthStore(); + const [showDropdown, setShowDropdown] = useState(false); + + const handleLogout = () => { + logout(); + setShowDropdown(false); + }; + + if (!user) { + return null; + } + + return ( +
+ + + {showDropdown && ( +
+
+
+ {user.username.charAt(0).toUpperCase()} +
+
+
{user.username}
+
+ 加入时间: {new Date(user.created_at).toLocaleDateString()} +
+
+
+
+ +
+ )} + + {showDropdown && ( +
setShowDropdown(false)} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/src/components/buttons/delete-music/delete-music.module.css b/src/components/buttons/delete-music/delete-music.module.css new file mode 100644 index 0000000..f7bbd4c --- /dev/null +++ b/src/components/buttons/delete-music/delete-music.module.css @@ -0,0 +1,155 @@ +.deleteDropdownContainer { + position: relative; + display: inline-block; +} + +.deleteButton { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + width: 60px; + height: 32px; /* 与输入框内容区域高度一致 */ + padding: 6px 8px; + background: #ef4444; + color: white; + border: none; + border-radius: 6px; /* 与输入框圆角一致 */ + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.deleteButton:hover:not(:disabled) { + background: #dc2626; + transform: translateY(-1px); +} + +.deleteButton:disabled { + background: var(--color-muted); + cursor: not-allowed; + opacity: 0.6; +} + +.deleteButton.disabled { + background: var(--color-muted); + cursor: not-allowed; + opacity: 0.6; +} + +.deleteDropdown { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + width: 280px; + max-height: 320px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + overflow: hidden; +} + +.dropdownHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--color-border); +} + +.dropdownHeader h4 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--color-foreground); +} + +.closeButton { + background: none; + border: none; + color: var(--color-foreground-subtle); + font-size: 18px; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; +} + +.closeButton:hover { + background: var(--component-hover); + color: var(--color-foreground); +} + +.loading, +.empty { + padding: 16px; + text-align: center; + color: var(--color-foreground-subtle); + font-size: 14px; +} + +.musicList { + max-height: 240px; + overflow-y: auto; +} + +.musicItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--color-border); + transition: background-color 0.2s ease; +} + +.musicItem:last-child { + border-bottom: none; +} + +.musicItem:hover { + background: var(--component-hover); +} + +.musicName { + flex: 1; + font-size: 14px; + color: var(--color-foreground); + margin-right: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.deleteItemButton { + background: #ef4444; + color: white; + border: none; + border-radius: 4px; + padding: 6px 12px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.deleteItemButton:hover:not(:disabled) { + background: #dc2626; +} + +.deleteItemButton:disabled { + background: var(--color-muted); + color: var(--color-foreground-subtle); + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/components/buttons/delete-music/delete-music.tsx b/src/components/buttons/delete-music/delete-music.tsx new file mode 100644 index 0000000..bb5ac6e --- /dev/null +++ b/src/components/buttons/delete-music/delete-music.tsx @@ -0,0 +1,186 @@ +import { useState, useCallback, useEffect } from 'react'; +import { FaTrash } from 'react-icons/fa'; +import { useSoundStore } from '@/stores/sound'; +import { useAuthStore } from '@/stores/auth'; +import { useSnackbar } from '@/contexts/snackbar'; +import { useLocalizedSounds } from '@/hooks/useLocalizedSounds'; +import { ApiClient } from '@/lib/api-client'; +import { cn } from '@/helpers/styles'; + +import styles from './delete-music.module.css'; + +interface SavedMusic { + id: number; + name: string; + sounds: string[]; + created_at: string; +} + +export function DeleteMusicButton() { + const { isAuthenticated, user } = useAuthStore(); + const sounds = useSoundStore(state => state.sounds); + const selectedSoundIds = useSoundStore(state => + Object.keys(state.sounds).filter(id => state.sounds[id].isSelected) + ); + const showSnackbar = useSnackbar(); + const localizedCategories = useLocalizedSounds(); + + const [isDeleting, setIsDeleting] = useState(false); + const [showDeleteDropdown, setShowDeleteDropdown] = useState(false); + const [savedMusicList, setSavedMusicList] = useState([]); + const [isLoadingMusic, setIsLoadingMusic] = useState(false); + + // 获取选中的声音详细信息 + const selectedSounds = selectedSoundIds + .map(id => { + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + return allSounds.find(sound => sound.id === id); + }) + .filter(Boolean); + + const noSelected = selectedSounds.length === 0; + const hasSelected = selectedSounds.length > 0; + + // 获取用户保存的音乐列表 + const fetchSavedMusic = useCallback(async () => { + if (!isAuthenticated || !user) { + setSavedMusicList([]); + return; + } + + setIsLoadingMusic(true); + + try { + const response = await ApiClient.post('/api/auth/music/list'); + + if (!response.ok) { + throw new Error('获取音乐列表失败'); + } + + const data = await response.json(); + if (data.success) { + setSavedMusicList(data.musicList || []); + } + } catch (error) { + console.error('❌ 获取音乐列表失败:', error); + setSavedMusicList([]); + } finally { + setIsLoadingMusic(false); + } + }, [isAuthenticated, user]); + + // 删除音乐 + const deleteMusic = useCallback(async (musicId: string, musicName: string) => { + if (!isAuthenticated || !user) return; + if (!confirm(`确定要删除"${musicName}"吗?`)) return; + + setIsDeleting(true); + + try { + const response = await ApiClient.post('/api/auth/music/delete', { + musicId + }); + + if (!response.ok) { + throw new Error('删除失败'); + } + + const data = await response.json(); + if (data.success) { + setSavedMusicList(prev => prev.filter(music => music.id !== parseInt(musicId))); + showSnackbar(`已删除音乐: ${musicName}`); + console.log('✅ 音乐删除成功'); + } else { + showSnackbar(data.error || '删除失败'); + } + } catch (error) { + console.error('❌ 删除音乐失败:', error); + showSnackbar('删除失败'); + } finally { + setIsDeleting(false); + } + }, [isAuthenticated, user, showSnackbar]); + + // 当用户认证状态改变时,获取音乐列表 + const handleToggleDropdown = useCallback(() => { + if (!isAuthenticated) { + showSnackbar('请先登录后再删除音乐'); + return; + } + + if (!showDeleteDropdown && savedMusicList.length === 0) { + fetchSavedMusic(); + } + setShowDeleteDropdown(!showDeleteDropdown); + }, [isAuthenticated, showDeleteDropdown, savedMusicList.length, fetchSavedMusic, showSnackbar]); + + // 点击外部关闭下拉菜单 + const handleDocumentClick = useCallback((event: MouseEvent) => { + const target = event.target as Element; + if (showDeleteDropdown && !target.closest(`.${styles.deleteDropdownContainer}`)) { + setShowDeleteDropdown(false); + } + }, [showDeleteDropdown]); + + // 添加和移除事件监听器 + useEffect(() => { + if (showDeleteDropdown) { + document.addEventListener('click', handleDocumentClick); + } + return () => { + document.removeEventListener('click', handleDocumentClick); + }; + }, [showDeleteDropdown, handleDocumentClick]); + + return ( +
+ + + {/* 删除下拉菜单 */} + {showDeleteDropdown && ( +
+
+

删除音乐

+ +
+ + {isLoadingMusic ? ( +
加载中...
+ ) : savedMusicList.length === 0 ? ( +
没有可删除的音乐
+ ) : ( +
+ {savedMusicList.map((music) => ( +
+ {music.name} + +
+ ))} +
+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/buttons/play/play.module.css b/src/components/buttons/play/play.module.css index ea64f37..7ed74fc 100644 --- a/src/components/buttons/play/play.module.css +++ b/src/components/buttons/play/play.module.css @@ -7,15 +7,15 @@ font-family: var(--font-heading); font-size: var(--font-base); line-height: 0; - color: var(--color-neutral-200); + color: var(--color-foreground); cursor: pointer; - background-color: var(--color-neutral-950); - border: 1px solid var(--color-neutral-50); + background-color: var(--bg-secondary); + border: 1px solid var(--color-border); border-radius: 100px; transition: 0.2s; &:hover { - background-color: var(--color-neutral-800); + background-color: var(--bg-tertiary); } &:not(.disabled):active { @@ -32,7 +32,7 @@ } &:focus-visible { - outline: 2px solid var(--color-neutral-400); + outline: 2px solid var(--color-muted); outline-offset: 2px; } } diff --git a/src/components/buttons/play/play.tsx b/src/components/buttons/play/play.tsx index cd4afa9..c467af2 100644 --- a/src/components/buttons/play/play.tsx +++ b/src/components/buttons/play/play.tsx @@ -4,11 +4,13 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useSoundStore } from '@/stores/sound'; import { useSnackbar } from '@/contexts/snackbar'; +import { useTranslation } from '@/hooks/useTranslation'; import { cn } from '@/helpers/styles'; import styles from './play.module.css'; export function PlayButton() { + const { t } = useTranslation(); const isPlaying = useSoundStore(state => state.isPlaying); const pause = useSoundStore(state => state.pause); const toggle = useSoundStore(state => state.togglePlay); @@ -42,14 +44,14 @@ export function PlayButton() { {' '} - Pause + {t('pause')} ) : ( <> {' '} - Play + {t('play')} )} diff --git a/src/components/buttons/save-music/save-music.module.css b/src/components/buttons/save-music/save-music.module.css new file mode 100644 index 0000000..206d921 --- /dev/null +++ b/src/components/buttons/save-music/save-music.module.css @@ -0,0 +1,48 @@ +.saveButton { + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 32px; /* 与输入框内容区域高度一致 (14px字体 + 8px*2 = 30px) */ + padding: 6px 8px; + font-family: var(--font-heading); + font-size: 12px; + line-height: 1; + color: var(--color-foreground); + cursor: pointer; + background-color: var(--bg-secondary); + border: 1px solid var(--color-border); + border-radius: 6px; /* 与输入框圆角一致 */ + transition: 0.2s; + gap: 4px; + flex-shrink: 0; + + &:hover:not(:disabled) { + background-color: var(--bg-tertiary); + } + + &:not(.disabled):active { + transform: scale(0.97); + } + + &:disabled, + &.disabled { + cursor: not-allowed; + opacity: 0.6; + } + + & span { + font-size: var(--font-base); + } + + &:focus-visible { + outline: 2px solid var(--color-muted); + outline-offset: 2px; + } + + &.saving { + background-color: var(--color-muted); + } +} + +/* 使用统一的 .loginPrompt 样式,定义在 sounds.module.css 中 */ \ No newline at end of file diff --git a/src/components/buttons/save-music/save-music.tsx b/src/components/buttons/save-music/save-music.tsx new file mode 100644 index 0000000..0f310e3 --- /dev/null +++ b/src/components/buttons/save-music/save-music.tsx @@ -0,0 +1,156 @@ +import { useState, useCallback } from 'react'; +import { FaSave } from 'react-icons/fa'; +import { useSoundStore } from '@/stores/sound'; +import { useAuthStore } from '@/stores/auth'; +import { useNotification } from '@/hooks/useNotification'; +import { useLocalizedSounds } from '@/hooks/useLocalizedSounds'; +import { Notification } from '@/components/notification/notification'; +import { ApiClient } from '@/lib/api-client'; +import { cn } from '@/helpers/styles'; + +import styles from './save-music.module.css'; +import soundsStyles from '@/components/sounds/sounds.module.css'; + +export function SaveMusicButton() { + const { isAuthenticated, user } = useAuthStore(); + const sounds = useSoundStore(state => state.sounds); + const selectedSoundIds = useSoundStore(state => + Object.keys(state.sounds).filter(id => state.sounds[id].isSelected) + ); + const { showNotificationMessage, ...notificationState } = useNotification(); + const localizedCategories = useLocalizedSounds(); + + const [isSaving, setIsSaving] = useState(false); + const [showLoginPrompt, setShowLoginPrompt] = useState(false); + + // 获取选中的声音详细信息 + const selectedSounds = selectedSoundIds + .map(id => { + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + return allSounds.find(sound => sound.id === id); + }) + .filter(Boolean); + + const noSelected = selectedSounds.length === 0; + + // 获取音乐名称输入框的值 + const getMusicName = useCallback(() => { + const musicInput = document.querySelector('input[placeholder="音乐名称"]') as HTMLInputElement; + return musicInput?.value?.trim() || ''; + }, []); + + const handleSave = useCallback(async () => { + if (noSelected) return showNotificationMessage('请先选择声音', 'error'); + + if (!isAuthenticated) { + setShowLoginPrompt(true); + return; + } + + // 验证音乐名称输入 + const musicName = getMusicName(); + if (!musicName) { + showNotificationMessage('请输入音乐名称', 'error'); + const musicInput = document.querySelector('input[placeholder="音乐名称"]') as HTMLInputElement; + musicInput?.focus(); + return; + } + + setIsSaving(true); + + try { + // 准备保存的数据 + const volume: Record = {}; + const speed: Record = {}; + const rate: Record = {}; + const random_effects: Record = {}; + + selectedSounds.forEach(sound => { + if (sound) { + volume[sound.id] = sounds[sound.id]?.volume || 50; + speed[sound.id] = sounds[sound.id]?.speed || 1; + rate[sound.id] = sounds[sound.id]?.rate || 1; + random_effects[sound.id] = sounds[sound.id]?.isRandomSpeed || sounds[sound.id]?.isRandomVolume || sounds[sound.id]?.isRandomRate || false; + } + }); + + const musicData = { + name: musicName, + sounds: selectedSoundIds, + volume, + speed, + rate, + random_effects + }; + + const response = await ApiClient.post('/api/auth/music/save', musicData); + + if (response.ok) { + const result = await response.json(); + showNotificationMessage('音乐保存成功!', 'success'); + console.log('✅ 音乐保存成功:', result.music); + } else { + const errorData = await response.json(); + console.error('❌ 保存音乐失败:', errorData.error); + + if (response.status === 401) { + // JWT认证失败,显示登录提示 + setShowLoginPrompt(true); + } + showNotificationMessage(errorData.error || '保存失败', 'error'); + } + } catch (error) { + console.error('❌ 保存音乐失败:', error); + if (error instanceof Error && error.message.includes('401')) { + setShowLoginPrompt(true); + } + showNotificationMessage('保存失败,请重试', 'error'); + } finally { + setIsSaving(false); + } + }, [noSelected, isAuthenticated, user, selectedSounds, selectedSoundIds, sounds, showNotificationMessage, getMusicName]); + + return ( + <> + + + {/* 登录提示 */} + {showLoginPrompt && ( +
+

请先登录后再保存音乐

+ + +
+ )} + + + {/* 通用通知组件 */} + + + ); +} \ No newline at end of file diff --git a/src/components/buttons/unselect/unselect.module.css b/src/components/buttons/unselect/unselect.module.css index 7115a4f..f228e31 100644 --- a/src/components/buttons/unselect/unselect.module.css +++ b/src/components/buttons/unselect/unselect.module.css @@ -9,8 +9,8 @@ line-height: 0; color: var(--color-foreground); cursor: pointer; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-300); + background-color: var(--bg-secondary); + border: 1px solid var(--color-border); border-radius: 100px; transition: 0.2s; @@ -25,11 +25,11 @@ &:hover, &:focus-visible { - background-color: var(--color-neutral-200); + background-color: var(--bg-tertiary); } &:focus-visible { - outline: 2px solid var(--color-neutral-400); + outline: 2px solid var(--color-muted); outline-offset: 2px; } } @@ -37,7 +37,7 @@ .tooltip { padding: 6px 12px; font-size: var(--font-xsm); - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-200); + background-color: var(--bg-secondary); + border: 1px solid var(--color-border); border-radius: 100px; } diff --git a/src/components/categories/categories.tsx b/src/components/categories/categories.tsx index 8e2f558..bd9fc0a 100644 --- a/src/components/categories/categories.tsx +++ b/src/components/categories/categories.tsx @@ -1,7 +1,6 @@ import { AnimatePresence } from 'motion/react'; import { Category } from './category'; -import { Donate } from './donate'; import type { Categories } from '@/data/types'; @@ -12,12 +11,8 @@ interface CategoriesProps { export function Categories({ categories }: CategoriesProps) { return ( - {categories.map((category, index) => ( -
- - - {index === 3 && } -
+ {categories.map((category) => ( + ))}
); diff --git a/src/components/categories/category/category.module.css b/src/components/categories/category/category.module.css index e4f997f..d8e3130 100644 --- a/src/components/categories/category/category.module.css +++ b/src/components/categories/category/category.module.css @@ -12,7 +12,7 @@ & .tail { width: 1px; height: 75px; - background: linear-gradient(transparent, var(--color-neutral-300)); + background: linear-gradient(transparent, var(--color-muted)); } & .icon { @@ -23,10 +23,10 @@ height: 45px; font-size: var(--font-md); background: linear-gradient( - var(--color-neutral-50), - var(--color-neutral-100) + var(--bg-secondary), + var(--bg-tertiary) ); - border: 1px solid var(--color-neutral-300); + border: 1px solid var(--color-border); border-radius: 50%; } } diff --git a/src/components/categories/donate/donate.tsx b/src/components/categories/donate/donate.tsx index 93a1774..67b5444 100644 --- a/src/components/categories/donate/donate.tsx +++ b/src/components/categories/donate/donate.tsx @@ -1,10 +1,13 @@ import { FaCoffee } from 'react-icons/fa/index'; import { SpecialButton } from '@/components/special-button'; +import { useTranslation } from '@/hooks/useTranslation'; import styles from './donate.module.css'; export function Donate() { + const { t } = useTranslation(); + return (
@@ -15,14 +18,14 @@ export function Donate() {
- Support Me + {t('supportMe')}
-

Help me keep Moodist ad-free.

+

{t('helpKeepAdFree')}

- Donate Today + {t('donateToday')}
); diff --git a/src/components/donate.astro b/src/components/donate.astro deleted file mode 100644 index 631a846..0000000 --- a/src/components/donate.astro +++ /dev/null @@ -1,57 +0,0 @@ ---- -import { Container } from './container'; ---- - - -
-

- Enjoy Moodist?{' '} - - Support with a donation! - -

-
-
- - diff --git a/src/components/footer.astro b/src/components/footer.astro index bfed23f..e93885a 100644 --- a/src/components/footer.astro +++ b/src/components/footer.astro @@ -1,11 +1,19 @@ --- import { Container } from './container'; +import { getTranslation } from '@/data/i18n'; + +// Get language from URL path +const url = Astro.url; +const pathname = url.pathname; +const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/'); +const lang = isZhPage ? 'zh-CN' : 'en'; +const t = getTranslation(lang); --- diff --git a/src/components/hero.astro b/src/components/hero.astro index 5afe3e7..6f872d4 100644 --- a/src/components/hero.astro +++ b/src/components/hero.astro @@ -2,9 +2,15 @@ import { BsSoundwave } from 'react-icons/bs/index'; import { Container } from './container'; - +import { getTranslation } from '@/data/i18n'; import { count as soundCount } from '@/lib/sounds'; +// Get language from URL path +const url = Astro.url; +const pathname = url.pathname; +const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/'); +const lang = isZhPage ? 'zh-CN' : 'en'; +const t = getTranslation(lang); const count = soundCount(); --- @@ -24,15 +30,15 @@ const count = soundCount();

- Ambient SoundsFor Focus and Calm + {t.heroTitle}{t.heroSubtitle}

-

Free and Open-Source.

+

{t.heroDescription}

- {count} Sounds + {count}{t.soundsCount}

diff --git a/src/components/language-switcher/index.ts b/src/components/language-switcher/index.ts new file mode 100644 index 0000000..faf1bf2 --- /dev/null +++ b/src/components/language-switcher/index.ts @@ -0,0 +1 @@ +export * from './language-switcher'; \ No newline at end of file diff --git a/src/components/language-switcher/language-switcher.module.css b/src/components/language-switcher/language-switcher.module.css new file mode 100644 index 0000000..e4cf672 --- /dev/null +++ b/src/components/language-switcher/language-switcher.module.css @@ -0,0 +1,492 @@ +/* 头部控制容器 */ +.headerControls { + position: fixed; + top: 20px; + right: 20px; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 4px; + z-index: 1000; + background: var(--bg-secondary); + backdrop-filter: blur(10px); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 8px; + min-width: 160px; +} + +/* 通用控制按钮样式 */ +.controlButton { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + padding: 10px 12px; + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + color: var(--color-foreground); + transition: all 0.2s ease; + font-size: 14px; + line-height: 1; + width: 100%; + height: auto; + min-height: 40px; + position: relative; + text-align: left; +} + +.controlButton:hover { + background: var(--bg-tertiary); +} + +.controlButton:focus { + outline: 2px solid var(--color-foreground); + outline-offset: 2px; +} + +.controlButton:active { + transform: scale(0.95); +} + +/* 用户指示器 */ +.userIndicator { + position: absolute; + bottom: 2px; + right: 2px; + width: 8px; + height: 8px; + background: #10b981; + border-radius: 50%; + border: 2px solid var(--bg-secondary); +} + +/* 语言切换器样式 */ +.languageSwitcher { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + padding: 10px 12px; + border: none; + border-radius: 6px; + background-color: transparent; + color: var(--color-foreground); + font-size: 14px; + transition: all 0.2s ease; + cursor: pointer; + width: 100%; + text-align: left; +} + +.languageSwitcher:hover { + background: var(--bg-tertiary); +} + +.icon { + color: var(--color-foreground-subtle); + font-size: 14px; + flex-shrink: 0; +} + +.select { + background: transparent; + border: none; + color: var(--color-foreground); + font-size: 14px; + cursor: pointer; + outline: none; + padding: 0; + border-radius: 4px; + min-width: 70px; + flex: 1; +} + +.select:hover { + background-color: transparent; +} + +.select:focus-visible { + outline: 2px solid var(--color-muted); + outline-offset: 2px; +} + +/* 认证表单样式 */ +.authFormOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1001; + padding: 16px; +} + +.authForm { + width: 100%; + max-width: 320px; + padding: 16px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + color: var(--color-foreground); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.authForm h3 { + margin: 0 0 16px 0; + font-size: var(--font-lg); + font-weight: 600; + text-align: center; + color: var(--color-foreground); +} + +.authForm form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.authInput { + padding: 8px 12px; + border: 1px solid var(--color-border); + border-radius: 6px; + font-size: var(--font-sm); + background: var(--input-bg); + color: var(--color-foreground); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.authInput::placeholder { + color: var(--color-foreground-subtler); +} + +.authInput:focus { + outline: none; + border-color: var(--color-muted); + box-shadow: 0 0 0 2px var(--color-muted); +} + +.authButtons { + display: flex; + gap: 8px; +} + +.authSubmitButton { + flex: 1; + padding: 8px 16px; + background: var(--color-foreground); + color: var(--bg-primary); + border: none; + border-radius: 6px; + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.authSubmitButton:hover:not(:disabled) { + background: var(--color-foreground-subtle); +} + +.authSubmitButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.authCancelButton { + padding: 8px 16px; + background: transparent; + color: var(--color-foreground-subtler); + border: 1px solid var(--color-border); + border-radius: 6px; + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; +} + +.authCancelButton:hover { + background: var(--component-hover); + border-color: var(--color-foreground-subtle); +} + +.authToggle { + margin-top: 8px; +} + +.authToggleButton { + width: 100%; + padding: 6px 12px; + background: transparent; + color: var(--color-foreground-subtle); + border: none; + border-radius: 6px; + font-size: var(--font-xsm); + cursor: pointer; + text-decoration: underline; + transition: color 0.2s; +} + +.authToggleButton:hover { + color: var(--color-foreground); +} + +/* 用户菜单样式 */ +.userMenu { + position: fixed; + top: 20px; + right: 180px; /* 改为左侧展开,在headerControls的左边 */ + z-index: 1001; /* 提高层级,确保在最上层 */ +} + +.userInfo { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + min-width: 160px; +} + +.userAvatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--color-foreground); + color: var(--bg-primary); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-sm); + font-weight: 600; + flex-shrink: 0; +} + +.userName { + font-weight: 500; + color: var(--color-foreground); + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.userActions { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--color-border); +} + +.userActionButton { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + padding: 8px 12px; + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + color: var(--color-foreground); + transition: all 0.2s ease; + font-size: 14px; + width: 100%; + text-align: left; +} + +.userActionButton:hover { + background: var(--component-hover); +} + +.userActionButton.logoutButton { + color: #ef4444; +} + +.userActionButton.logoutButton:hover { + background: rgba(239, 68, 68, 0.1); +} + +.userActionButton .icon { + font-size: 14px; + width: 16px; + text-align: center; +} + +/* 暗色主题下的特殊样式 */ +:global(.dark-theme) .headerControls { + background: var(--bg-secondary); + border-color: var(--color-border); +} + +:global(.dark-theme) .controlButton { + background: transparent; +} + +:global(.dark-theme) .controlButton:hover { + background: var(--bg-tertiary); +} + +:global(.dark-theme) .userIndicator { + border-color: var(--bg-secondary); +} + +:global(.dark-theme) .authForm { + background: var(--component-bg); + border-color: var(--color-border); +} + +:global(.dark-theme) .authInput { + background: var(--input-bg); + border-color: var(--color-border); +} + +:global(.dark-theme) .userInfo { + background: var(--component-bg); + border-color: var(--color-border); +} + +/* 提示通知样式 */ +.notification { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 1002; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + animation: notificationSlideIn 0.3s ease-out; + max-width: 400px; + width: calc(100vw - 40px); +} + +.notificationContent { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + gap: 12px; +} + +.notificationMessage { + flex: 1; + font-size: 14px; + font-weight: 500; + line-height: 1.4; +} + +.notificationClose { + background: none; + border: none; + font-size: 18px; + font-weight: bold; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + color: inherit; + opacity: 0.7; + transition: opacity 0.2s; +} + +.notificationClose:hover { + opacity: 1; +} + +/* 成功通知样式 */ +.notification.success { + background: linear-gradient(135deg, #10b981, #059669); + color: white; + border: 1px solid #059669; +} + +/* 错误通知样式 */ +.notification.error { + background: linear-gradient(135deg, #ef4444, #dc2626); + color: white; + border: 1px solid #dc2626; +} + +/* 通知动画 */ +@keyframes notificationSlideIn { + from { + transform: translateX(-50%) translateY(-100%); + opacity: 0; + } + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .headerControls { + top: 15px; + right: 15px; + padding: 6px; + gap: 3px; + min-width: 140px; + } + + .controlButton { + padding: 8px 10px; + font-size: 13px; + min-height: 36px; + gap: 10px; + } + + .languageSwitcher { + padding: 8px 10px; + gap: 10px; + font-size: 13px; + } + + .icon { + font-size: 12px; + } + + .select { + font-size: 13px; + min-width: 60px; + } + + .userMenu { + top: 15px; + right: 155px; /* 移动端适配左侧展开 */ + } + + .authFormOverlay { + padding: 12px; + } + + .authForm { + padding: 12px; + } + + .notification { + top: 15px; + max-width: calc(100vw - 30px); + width: calc(100vw - 30px); + } + + .notificationContent { + padding: 10px 14px; + } + + .notificationMessage { + font-size: 13px; + } +} \ No newline at end of file diff --git a/src/components/language-switcher/language-switcher.tsx b/src/components/language-switcher/language-switcher.tsx new file mode 100644 index 0000000..3fa29fe --- /dev/null +++ b/src/components/language-switcher/language-switcher.tsx @@ -0,0 +1,394 @@ +import { useState, useEffect } from 'react'; +import { FaGlobe, FaSun, FaMoon, FaUser, FaSignOutAlt, FaCog } from 'react-icons/fa/index'; +import { AnimatePresence, motion } from 'motion/react'; +import { useTranslation } from '@/hooks/useTranslation'; +import { useAuthStore } from '@/stores/auth'; + +import styles from './language-switcher.module.css'; +import { fade } from '@/lib/motion'; + +interface LanguageSwitcherProps { + className?: string; +} + +export function LanguageSwitcher({ className }: LanguageSwitcherProps) { + const { currentLang, changeLanguage, t } = useTranslation(); + const { isAuthenticated, user, login, register, logout, isLoading, checkAuth, error, clearError } = useAuthStore(); + const [isDarkTheme, setIsDarkTheme] = useState(null); // 使用 null 表示未初始化 + const [isClient, setIsClient] = useState(false); // 跟踪是否在客户端 + const [showAuthForm, setShowAuthForm] = useState(false); + const [isLogin, setIsLogin] = useState(true); + const [showNotification, setShowNotification] = useState(false); + const [notificationMessage, setNotificationMessage] = useState(''); + const [notificationType, setNotificationType] = useState<'success' | 'error'>('success'); + const [showUserMenu, setShowUserMenu] = useState(false); + const [formData, setFormData] = useState({ + username: '', + password: '', + }); + + // 客户端检测 + useEffect(() => { + setIsClient(true); + }, []); + + // 认证状态检查 + useEffect(() => { + if (isClient) { + checkAuth(); + } + }, [isClient]); + + // 点击外部关闭用户菜单 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (showUserMenu && !target.closest(`.${styles.headerControls}`) && !target.closest(`.${styles.userMenu}`)) { + setShowUserMenu(false); + } + }; + + if (showUserMenu) { + document.addEventListener('click', handleClickOutside); + } + + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [showUserMenu]); + + // 监听显示登录表单的自定义事件 + useEffect(() => { + const handleShowLoginForm = () => { + setShowAuthForm(true); + setIsLogin(true); // 默认显示登录表单 + }; + + document.addEventListener('showLoginForm', handleShowLoginForm); + + return () => { + document.removeEventListener('showLoginForm', handleShowLoginForm); + }; + }, []); + + // 主题切换逻辑 - 确保只在客户端执行 + useEffect(() => { + // 避免在 SSR 环境下执行 + if (typeof window === 'undefined' || typeof localStorage === 'undefined') { + return; + } + + const savedTheme = localStorage.getItem('theme'); + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const initialDarkTheme = savedTheme ? savedTheme === 'dark' : systemPrefersDark; + setIsDarkTheme(initialDarkTheme); + applyTheme(initialDarkTheme); + }, []); + + const applyTheme = (isDark: boolean) => { + const root = document.documentElement; + const body = document.body; + + if (isDark) { + root.classList.add('dark-theme'); + root.style.setProperty('--bg-primary', '#0d1117'); + root.style.setProperty('--bg-secondary', '#161b22'); + root.style.setProperty('--bg-tertiary', '#21262d'); + root.style.setProperty('--bg-quaternary', '#30363d'); + root.style.setProperty('--color-foreground', '#f0f6fc'); + root.style.setProperty('--color-foreground-subtle', '#8b949e'); + root.style.setProperty('--color-foreground-subtler', '#6e7681'); + root.style.setProperty('--color-muted', '#484f58'); + root.style.setProperty('--color-border', '#30363d'); + root.style.setProperty('--component-bg', '#161b22'); + root.style.setProperty('--component-hover', '#21262d'); + root.style.setProperty('--component-active', '#30363d'); + root.style.setProperty('--modal-bg', '#0d1117'); + root.style.setProperty('--input-bg', '#0d1117'); + body.style.backgroundColor = '#0d1117'; + } else { + root.classList.remove('dark-theme'); + root.style.setProperty('--bg-primary', '#ffffff'); + root.style.setProperty('--bg-secondary', '#f8fafc'); + root.style.setProperty('--bg-tertiary', '#f1f5f9'); + root.style.setProperty('--bg-quaternary', '#e2e8f0'); + root.style.setProperty('--color-foreground', '#1e293b'); + root.style.setProperty('--color-foreground-subtle', '#475569'); + root.style.setProperty('--color-foreground-subtler', '#64748b'); + root.style.setProperty('--color-muted', '#94a3b8'); + root.style.setProperty('--color-border', '#cbd5e1'); + root.style.setProperty('--component-bg', '#ffffff'); + root.style.setProperty('--component-hover', '#f8fafc'); + root.style.setProperty('--component-active', '#f1f5f9'); + root.style.setProperty('--modal-bg', '#ffffff'); + root.style.setProperty('--input-bg', '#ffffff'); + body.style.backgroundColor = '#ffffff'; + } + }; + + const toggleTheme = () => { + // 确保主题已初始化且在客户端环境 + if (isDarkTheme === null || typeof window === 'undefined' || typeof localStorage === 'undefined') { + return; + } + + const newTheme = !isDarkTheme; + setIsDarkTheme(newTheme); + applyTheme(newTheme); + localStorage.setItem('theme', newTheme ? 'dark' : 'light'); + }; + + // 显示提示信息 + const showNotificationMessage = (message: string, type: 'success' | 'error') => { + setNotificationMessage(message); + setNotificationType(type); + setShowNotification(true); + + // 3秒后自动关闭 + setTimeout(() => { + setShowNotification(false); + }, 3000); + }; + + // 认证逻辑 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + clearError(); // 清除之前的错误 + + try { + if (isLogin) { + await login(formData); + showNotificationMessage('登录成功!', 'success'); + setShowAuthForm(false); + setFormData({ username: '', password: '' }); + } else { + await register(formData); + showNotificationMessage('注册成功!', 'success'); + setShowAuthForm(false); + setFormData({ username: '', password: '' }); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '认证失败'; + showNotificationMessage(errorMessage, 'error'); + console.error('认证失败:', error); + // 认证失败时不关闭弹窗,让用户重新尝试 + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const handleLogout = () => { + logout(); + }; + + const handleAuthClick = () => { + if (isAuthenticated) { + setShowUserMenu(!showUserMenu); + } else { + setShowAuthForm(true); + } + }; + + const handleLanguageChange = (e: React.ChangeEvent) => { + changeLanguage(e.target.value); + }; + + const variants = fade(); + + return ( + <> +
+ {/* 语言切换器 */} +
+ + +
+ + {/* 主题切换按钮 */} + + + {/* 登录按钮 */} + + + {isClient && isAuthenticated && user && ( + + )} + + {isClient ? (isAuthenticated ? (user?.username || '用户') : '登录') : '登录'} + + +
+ + {/* 认证表单模态框 */} + {showAuthForm && ( +
setShowAuthForm(false)}> +
e.stopPropagation()}> +

{isLogin ? '登录' : '注册'}

+
+ + +
+ + +
+
+ +
+
+
+
+ )} + + {/* 用户菜单 - 左侧展开菜单 */} + + {isAuthenticated && showUserMenu && ( + +
+
+ {user?.username.charAt(0).toUpperCase()} +
+ {user?.username} +
+ +
+ + + +
+
+ )} +
+ + {/* 提示通知 */} + {showNotification && ( +
+
+ + {notificationMessage} + + +
+
+ )} + + ); +} \ No newline at end of file diff --git a/src/components/modals/share-link/share-link.tsx b/src/components/modals/share-link/share-link.tsx index 8a75960..76b0e0c 100644 --- a/src/components/modals/share-link/share-link.tsx +++ b/src/components/modals/share-link/share-link.tsx @@ -51,14 +51,16 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) { return ( -

Share your sound selection!

-

+

+ Share your sound selection! +

+

Copy and send the following link to the person you want to share your selection with.

-
diff --git a/src/components/notification/notification.module.css b/src/components/notification/notification.module.css new file mode 100644 index 0000000..7e743b7 --- /dev/null +++ b/src/components/notification/notification.module.css @@ -0,0 +1,90 @@ +.notification { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 1002; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + animation: notificationSlideIn 0.3s ease-out; + max-width: 400px; + width: calc(100vw - 40px); +} + +.notificationContent { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + gap: 12px; +} + +.notificationMessage { + font-size: 14px; + color: white; + font-weight: 500; +} + +.notificationClose { + background: none; + border: none; + color: white; + font-size: 18px; + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + transition: background-color 0.2s ease; + opacity: 0.7; +} + +.notificationClose:hover { + opacity: 1; +} + +/* 成功通知样式 */ +.notification.success { + background: linear-gradient(135deg, #10b981, #059669); + color: white; + border: 1px solid #059669; +} + +/* 错误通知样式 */ +.notification.error { + background: linear-gradient(135deg, #ef4444, #dc2626); + color: white; + border: 1px solid #dc2626; +} + +/* 通知动画 */ +@keyframes notificationSlideIn { + from { + transform: translateX(-50%) translateY(-100%); + opacity: 0; + } + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .notification { + top: 15px; + max-width: calc(100vw - 30px); + width: calc(100vw - 30px); + } + + .notificationContent { + padding: 10px 14px; + } + + .notificationMessage { + font-size: 13px; + } +} \ No newline at end of file diff --git a/src/components/notification/notification.tsx b/src/components/notification/notification.tsx new file mode 100644 index 0000000..be23ae9 --- /dev/null +++ b/src/components/notification/notification.tsx @@ -0,0 +1,32 @@ +import { AnimatePresence } from 'motion/react'; +import styles from './notification.module.css'; + +interface NotificationProps { + show: boolean; + message: string; + type: 'success' | 'error'; + onClose: () => void; +} + +export function Notification({ show, message, type, onClose }: NotificationProps) { + return ( + + {show && ( +
+
+ + {message} + + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/saved-music-list/index.ts b/src/components/saved-music-list/index.ts new file mode 100644 index 0000000..3c83bfa --- /dev/null +++ b/src/components/saved-music-list/index.ts @@ -0,0 +1 @@ +export { SavedMusicList } from './saved-music-list'; \ No newline at end of file diff --git a/src/components/saved-music-list/saved-music-list.module.css b/src/components/saved-music-list/saved-music-list.module.css new file mode 100644 index 0000000..05c5033 --- /dev/null +++ b/src/components/saved-music-list/saved-music-list.module.css @@ -0,0 +1,331 @@ +.savedMusicList { + margin-bottom: 20px; +} + +.title { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: var(--color-foreground); +} + +.titleIcon { + color: var(--color-muted); + font-size: 14px; +} + +.error { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + margin-bottom: 16px; + background: var(--bg-error, rgba(239, 68, 68, 0.1)); + color: var(--color-error, #ef4444); + border: 1px solid var(--color-error, #ef4444); + border-radius: 6px; + font-size: 14px; +} + +.errorClose { + background: none; + border: none; + color: var(--color-error, #ef4444); + font-size: 18px; + cursor: pointer; + padding: 0; + margin-left: 8px; + font-weight: bold; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.errorClose:hover { + background: rgba(239, 68, 68, 0.2); +} + +.loading { + text-align: center; + padding: 20px; + color: var(--color-foreground-subtle); + font-size: 14px; +} + +.empty { + text-align: center; + padding: 32px 20px; + color: var(--color-foreground-subtle); +} + +.emptyIcon { + font-size: 32px; + color: var(--color-muted); + margin-bottom: 12px; + opacity: 0.7; +} + +.empty p { + margin: 8px 0; + font-size: 14px; +} + +.emptyHint { + font-size: 13px !important; + color: var(--color-muted) !important; + margin-top: 8px !important; +} + +.musicItems { + display: flex; + flex-direction: column; + gap: 8px; +} + +.musicItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + transition: all 0.2s ease; + min-height: 48px; +} + +.musicItem:hover { + background: var(--component-hover); + border-color: var(--color-foreground-subtle); +} + +.musicInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.playButton { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: var(--color-foreground); + color: var(--bg-primary); + border: none; + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; + flex-shrink: 0; +} + +.playButton:hover { + background: var(--color-foreground-subtle); + transform: scale(1.05); +} + +.playButton:active { + transform: scale(0.95); +} + +.musicName { + flex: 1; + font-size: 14px; + color: var(--color-foreground); + cursor: pointer; + transition: color 0.2s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.musicName:hover { + color: var(--color-accent); +} + +.musicActions { + display: flex; + align-items: center; + gap: 4px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.musicItem:hover .musicActions { + opacity: 1; +} + +.actionButton { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: transparent; + color: var(--color-foreground-subtle); + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; +} + +.actionButton:hover { + background: var(--component-hover); + color: var(--color-foreground); +} + +.actionButton.deleteButton:hover { + background: rgba(239, 68, 68, 0.1); + color: var(--color-error, #ef4444); +} + +.editForm { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} + +.editInput { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--color-border); + border-radius: 4px; + background: var(--bg-primary); + color: var(--color-foreground); + font-size: 14px; + min-width: 0; + outline: none; +} + +.editInput:focus { + border-color: var(--color-accent); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); +} + +.editButtons { + display: flex; + align-items: center; + gap: 4px; +} + +.editButton { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: all 0.2s ease; +} + +.saveButton { + background: var(--color-foreground); + color: var(--bg-primary); +} + +.saveButton:hover { + background: var(--color-foreground-subtle); + transform: scale(1.05); +} + +.cancelButton { + background: var(--color-muted); + color: var(--color-foreground); +} + +.cancelButton:hover { + background: var(--color-foreground-subtle); + color: var(--bg-primary); + transform: scale(1.05); +} + +/* 响应式设计 */ +@media (max-width: 640px) { + .title { + font-size: 15px; + } + + .musicItem { + padding: 10px 12px; + } + + .playButton { + width: 28px; + height: 28px; + font-size: 11px; + } + + .musicName { + font-size: 13px; + } + + .actionButton { + width: 24px; + height: 24px; + font-size: 11px; + } + + .musicActions { + opacity: 1; /* 移动端始终显示操作按钮 */ + } + + .empty { + padding: 24px 16px; + } + + .emptyIcon { + font-size: 28px; + } +} + +/* 动画效果 */ +@keyframes slideIn { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.musicItem { + animation: slideIn 0.3s ease-out; +} + +/* 焦点可访问性 */ +.musicItem:focus-within { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.editInput:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.editButton:focus-visible, +.actionButton:focus-visible, +.playButton:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} \ No newline at end of file diff --git a/src/components/saved-music-list/saved-music-list.tsx b/src/components/saved-music-list/saved-music-list.tsx new file mode 100644 index 0000000..9539d8d --- /dev/null +++ b/src/components/saved-music-list/saved-music-list.tsx @@ -0,0 +1,309 @@ +import { useState, useEffect } from 'react'; +import { FaMusic, FaEdit, FaTrash, FaPlay } from 'react-icons/fa'; +import { AnimatePresence } from 'motion/react'; + +import { useAuthStore } from '@/stores/auth'; +import { useSoundStore } from '@/stores/sound'; +import { useTranslation } from '@/hooks/useTranslation'; +import { ApiClient } from '@/lib/api-client'; + +import type { SavedMusic } from '@/lib/database'; + +import styles from './saved-music-list.module.css'; + +interface SavedMusicListProps { + onMusicSelect?: (music: SavedMusic) => void; +} + +export function SavedMusicList({ onMusicSelect }: SavedMusicListProps) { + const { t } = useTranslation(); + const { isAuthenticated, user } = useAuthStore(); + const [savedMusicList, setSavedMusicList] = useState([]); + const [loading, setLoading] = useState(false); + const [editingId, setEditingId] = useState(null); + const [editingName, setEditingName] = useState(''); + const [error, setError] = useState(null); + + // 获取声音store的操作函数 + const unselectAll = useSoundStore(state => state.unselectAll); + const select = useSoundStore(state => state.select); + const setVolume = useSoundStore(state => state.setVolume); + const setSpeed = useSoundStore(state => state.setSpeed); + const setRate = useSoundStore(state => state.setRate); + const toggleRandomSpeed = useSoundStore(state => state.toggleRandomSpeed); + const toggleRandomVolume = useSoundStore(state => state.toggleRandomVolume); + const toggleRandomRate = useSoundStore(state => state.toggleRandomRate); + const play = useSoundStore(state => state.play); + + // 获取用户保存的音乐列表 + const fetchSavedMusic = async () => { + if (!isAuthenticated || !user) return; + + setLoading(true); + setError(null); + + try { + const response = await ApiClient.post('/api/auth/music/list'); + + if (!response.ok) { + throw new Error('获取音乐列表失败'); + } + + const data = await response.json(); + if (data.success) { + setSavedMusicList(data.musicList || []); + } else { + setError(data.error || '获取音乐列表失败'); + } + } catch (err) { + console.error('获取音乐列表错误:', err); + setError('获取音乐列表失败,请稍后再试'); + } finally { + setLoading(false); + } + }; + + // 重命名音乐 + const renameMusic = async (musicId: string, newName: string) => { + if (!isAuthenticated || !user) return; + + try { + const response = await ApiClient.post('/api/auth/music/rename', { + musicId, + name: newName + }); + + if (!response.ok) { + throw new Error('重命名失败'); + } + + const data = await response.json(); + if (data.success) { + // 更新本地状态 + setSavedMusicList(prev => + prev.map(music => + music.id === musicId ? { ...music, name: newName } : music + ) + ); + setEditingId(null); + setEditingName(''); + } else { + setError(data.error || '重命名失败'); + } + } catch (err) { + console.error('重命名音乐错误:', err); + setError('重命名失败,请稍后再试'); + } + }; + + // 删除音乐 + const deleteMusic = async (musicId: string) => { + if (!isAuthenticated || !user) return; + + if (!confirm('确定要删除这首音乐吗?')) { + return; + } + + try { + const response = await ApiClient.post('/api/auth/music/delete', { + musicId + }); + + if (!response.ok) { + throw new Error('删除失败'); + } + + const data = await response.json(); + if (data.success) { + // 从本地状态中移除 + setSavedMusicList(prev => prev.filter(music => music.id !== musicId)); + } else { + setError(data.error || '删除失败'); + } + } catch (err) { + console.error('删除音乐错误:', err); + setError('删除失败,请稍后再试'); + } + }; + + // 播放保存的音乐 + const playSavedMusic = async (music: SavedMusic) => { + // 清除当前所有声音选择 + unselectAll(true); + + // 延迟一下确保清除完成后再开始播放 + setTimeout(() => { + // 选择音乐中的所有声音 + music.sounds.forEach((soundId: string) => { + // 选择声音 + select(soundId); + + // 设置音量 + const volume = music.volume[soundId] || 50; + setVolume(soundId, volume / 100); // store中存储的是0-1的范围 + + // 设置速度 + const speed = music.speed[soundId] || 1; + setSpeed(soundId, speed); + + // 设置速率 + const rate = music.rate[soundId] || 1; + setRate(soundId, rate); + + // 设置随机效果 + const randomEffects = music.random_effects[soundId]; + if (randomEffects) { + if (randomEffects.volume) { + toggleRandomVolume(soundId); + } + if (randomEffects.speed) { + toggleRandomSpeed(soundId); + } + if (randomEffects.rate) { + toggleRandomRate(soundId); + } + } + }); + + // 开始播放 + play(); + + // 通知父组件音乐已被选中 + if (onMusicSelect) { + onMusicSelect(music); + } + }, 100); + }; + + // 开始编辑名称 + const startEditing = (music: SavedMusic) => { + setEditingId(music.id); + setEditingName(music.name); + }; + + // 保存编辑 + const saveEdit = () => { + if (editingId && editingName.trim()) { + renameMusic(editingId, editingName.trim()); + } + }; + + // 取消编辑 + const cancelEdit = () => { + setEditingId(null); + setEditingName(''); + setError(null); + }; + + // 当用户认证状态改变时,获取音乐列表 + useEffect(() => { + if (isAuthenticated && user) { + fetchSavedMusic(); + } else { + setSavedMusicList([]); + } + }, [isAuthenticated, user]); + + // 如果用户未登录,不显示组件 + if (!isAuthenticated) { + return null; + } + + return ( +
+

+ + 我的音乐 +

+ + {error && ( +
+ {error} + +
+ )} + + {loading ? ( +
加载中...
+ ) : savedMusicList.length === 0 ? ( +
+ +

还没有保存的音乐

+

选择声音并点击保存按钮来创建你的第一首音乐

+
+ ) : ( +
+ + {savedMusicList.map((music) => ( +
+ {editingId === music.id ? ( +
+ setEditingName(e.target.value)} + className={styles.editInput} + placeholder="输入音乐名称" + maxLength={50} + /> +
+ + +
+
+ ) : ( + <> +
+ + startEditing(music)} + title="点击编辑名称" + > + {music.name} + +
+
+ + +
+ + )} +
+ ))} +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/selected-sounds-display/index.ts b/src/components/selected-sounds-display/index.ts new file mode 100644 index 0000000..865cf33 --- /dev/null +++ b/src/components/selected-sounds-display/index.ts @@ -0,0 +1 @@ +export { SelectedSoundsDisplay } from './selected-sounds-display'; \ No newline at end of file diff --git a/src/components/selected-sounds-display/selected-sounds-display.tsx b/src/components/selected-sounds-display/selected-sounds-display.tsx new file mode 100644 index 0000000..f4c7117 --- /dev/null +++ b/src/components/selected-sounds-display/selected-sounds-display.tsx @@ -0,0 +1,615 @@ +import { useMemo, useState, useEffect, useRef } from 'react'; +import { AnimatePresence, motion } from 'motion/react'; +import { FaSave, FaPlay, FaStop, FaTrash, FaEdit, FaCog, FaSignOutAlt, FaMusic, FaChevronDown, FaChevronRight } from 'react-icons/fa/index'; +import { SaveMusicButton } from '@/components/buttons/save-music/save-music'; +import { DeleteMusicButton } from '@/components/buttons/delete-music/delete-music'; + +import { useSoundStore } from '@/stores/sound'; +import { useLocalizedSounds } from '@/hooks/useLocalizedSounds'; +import { useTranslation } from '@/hooks/useTranslation'; +import { useAuthStore } from '@/stores/auth'; +import { ApiClient } from '@/lib/api-client'; +import { Howl } from 'howler'; + +import { Sound } from '@/components/sounds/sound'; +import styles from '../sounds/sounds.module.css'; + +interface SavedMusic { + id: number; + name: string; + sounds: string[]; + volume: Record; + speed: Record; + rate: Record; + random_effects: Record; + created_at: string; + updated_at: string; +} + +export function SelectedSoundsDisplay() { + const { t } = useTranslation(); + const localizedCategories = useLocalizedSounds(); + const { isAuthenticated, user, login, sessionPassword } = useAuthStore(); + const [isSaving, setIsSaving] = useState(false); + const [showLoginPrompt, setShowLoginPrompt] = useState(false); + const [showSaveSuccess, setShowSaveSuccess] = useState(false); + const [savedMusicList, setSavedMusicList] = useState([]); + const [isLoadingMusic, setIsLoadingMusic] = useState(false); + const [editingId, setEditingId] = useState(null); + const [editingName, setEditingName] = useState(''); + const [expandedMusic, setExpandedMusic] = useState>(new Set()); // 跟踪展开的音乐项 + const [expandedCurrent, setExpandedCurrent] = useState(true); // 跟踪当前选中声音的展开状态,默认展开 + const [expandedMyMusic, setExpandedMyMusic] = useState(true); // 跟踪音乐列表展开状态,默认展开 + const [error, setError] = useState(null); + const [musicName, setMusicName] = useState(''); + + // 独立的音乐播放状态 + const [currentlyPlayingMusic, setCurrentlyPlayingMusic] = useState(null); + const musicHowlInstances = useRef>({}); + const [isPlayingMusic, setIsPlayingMusic] = useState(false); + + // 获取声音store + const sounds = useSoundStore(state => state.sounds); + + // 获取选中的声音 + const selectedSoundIds = useSoundStore(state => + Object.keys(state.sounds).filter(id => state.sounds[id].isSelected) + ); + + // 独立展开逻辑:展开当前选中声音时收起所有展开的音乐 + const toggleExpandedCurrent = () => { + setExpandedCurrent(!expandedCurrent); + if (!expandedCurrent) { + setExpandedMusic(new Set()); // 展开时收起所有展开的音乐项 + } + }; + + const toggleExpandedMyMusic = () => { + setExpandedMyMusic(!expandedMyMusic); + if (!expandedMyMusic) { + setExpandedMusic(new Set()); // 展开时收起所有展开的音乐项 + } + }; + + // 获取声音store的操作函数(仅用于控制主要播放状态) + const play = useSoundStore(state => state.play); + const pause = useSoundStore(state => state.pause); + + // 停止音乐播放 + const stopMusic = () => { + console.log('🛑 停止音乐播放'); + + // 停止所有音乐相关的 Howl 实例 + Object.values(musicHowlInstances.current).forEach(howlInstance => { + if (howlInstance) { + howlInstance.stop(); + howlInstance.unload(); + } + }); + + musicHowlInstances.current = {}; + setCurrentlyPlayingMusic(null); + setIsPlayingMusic(false); + }; + + // 播放音乐记录 - 使用独立的音乐播放系统,不影响当前选中声音 + const playMusicRecord = async (music: SavedMusic) => { + try { + console.log('🎵 开始播放音乐:', music.name); + console.log('🎵 音乐数据:', { + sounds: music.sounds, + volume: music.volume, + speed: music.speed, + rate: music.rate, + random_effects: music.random_effects + }); + + // 先停止当前播放的音乐 + stopMusic(); + + // 停止主要的选中声音播放(但不改变选中状态) + pause(); + + // 获取所有声音数据 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + + // 创建所有声音的 Howl 实例 + const howlPromises: Promise[] = []; + + for (const soundId of music.sounds) { + const soundData = allSounds.find(s => s.id === soundId); + if (!soundData || !soundData.src) continue; + + const volume = music.volume[soundId] || 0.5; + const rate = music.rate[soundId] || 1; + const speed = music.speed[soundId] || 1; + + console.log(`🔊 创建音乐声音: ${soundId}`, { volume, rate, speed }); + + // 创建 Howl 实例的 Promise + const howlPromise = new Promise((resolve, reject) => { + const howl = new Howl({ + src: [soundData.src], + loop: true, + volume: volume, + rate: rate, + preload: true, + onload: () => { + console.log(`✅ 声音加载完成: ${soundId}`); + resolve(howl); + }, + onloaderror: (id, error) => { + console.error(`❌ 声音加载失败: ${soundId}`, error); + reject(error); + } + }); + + // 保存实例引用 + musicHowlInstances.current[soundId] = howl; + }); + + howlPromises.push(howlPromise); + } + + // 等待所有声音加载完成 + console.log('⏳ 等待所有声音加载...'); + await Promise.all(howlPromises); + console.log('✅ 所有声音加载完成,开始播放'); + + // 播放所有声音 + Object.values(musicHowlInstances.current).forEach(howlInstance => { + if (howlInstance && howlInstance.state() === 'loaded') { + howlInstance.play(); + } + }); + + // 设置播放状态 + setCurrentlyPlayingMusic(music); + setIsPlayingMusic(true); + + // 展开对应的音乐记录 + setExpandedMusic(new Set([music.id])); + setExpandedCurrent(false); // 收起当前选中声音模块 + + console.log(`✅ 播放音乐记录完成: ${music.name}`); + } catch (error) { + console.error('❌ 播放音乐记录失败:', error); + stopMusic(); + } + }; + + // 切换音乐项的展开/收起状态 + const toggleMusicExpansion = (musicId: number) => { + setExpandedMusic(prev => { + const newSet = new Set(prev); + if (newSet.has(musicId)) { + // 如果点击已展开的音乐,直接收起 + newSet.delete(musicId); + } else { + // 如果点击未展开的音乐,收起其他所有展开的项目,只展开当前这个 + return new Set([musicId]); + } + return newSet; + }); + + // 展开音乐时,同时收起当前选中声音模块 + if (!expandedMusic.has(musicId)) { + setExpandedCurrent(false); + } + }; + + // 根据选中的声音ID获取声音对象 + const selectedSounds = useMemo(() => { + return selectedSoundIds.map(id => { + // 从 localizedCategories 中查找对应的声音数据 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + const soundData = allSounds.find(s => s.id === id); + + if (!soundData) return null; + + return { + id, + ...soundData, + ...sounds[id] // 合并状态信息(volume, speed 等) + }; + }).filter(Boolean); + }, [selectedSoundIds, sounds, localizedCategories]); + + // 获取音乐列表 + const fetchMusicList = async () => { + if (!isAuthenticated || !user) return; + + setIsLoadingMusic(true); + setError(null); + + try { + console.log('🎵 开始获取音乐列表...'); + console.log('👤 用户信息:', { id: user.id, username: user.username }); + + const response = await ApiClient.post('/api/auth/music/list', { + userId: user.id + }); + + console.log('📡 响应状态:', response.status); + console.log('📡 响应头:', response.headers); + + if (!response.ok) { + const errorText = await response.text(); + console.error('❌ API响应错误:', response.status, errorText); + throw new Error(`获取音乐列表失败 (${response.status}): ${errorText}`); + } + + const data = await response.json(); + console.log('📋 音乐列表数据:', data); + + if (data.success) { + console.log('✅ 设置音乐列表:', data.musicList || [], '数量:', (data.musicList || []).length); + setSavedMusicList(data.musicList || []); + console.log('✅ savedMusicList状态更新完成'); + } else { + setError(data.error || '获取音乐列表失败'); + console.error('❌ 音乐列表API返回错误:', data.error); + } + } catch (error) { + console.error('❌ 获取音乐列表失败:', error); + setError('获取音乐列表失败,请稍后再试'); + setSavedMusicList([]); + } finally { + setIsLoadingMusic(false); + } + }; + + // 重命名音乐 + const renameMusic = async (musicId: string, newName: string) => { + if (!isAuthenticated || !user) return; + + try { + const response = await ApiClient.post('/api/auth/music/rename', { + musicId, + name: newName + }); + + if (!response.ok) { + throw new Error('重命名失败'); + } + + const data = await response.json(); + if (data.success) { + setSavedMusicList(prev => + prev.map(music => + music.id === parseInt(musicId) ? { ...music, name: newName } : music + ) + ); + setEditingId(null); + setEditingName(''); + } else { + setError(data.error || '重命名失败'); + } + } catch (error) { + console.error('❌ 重命名失败:', error); + setError('重命名失败,请稍后再试'); + } + }; + + // 删除音乐 + const deleteMusic = async (musicId: string) => { + if (!isAuthenticated || !user) return; + + if (!confirm('确定要删除这首音乐吗?')) return; + + try { + console.log('🗑️ 开始删除音乐:', musicId); + const response = await ApiClient.post('/api/auth/music/delete', { + musicId, + userId: user.id + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('❌ 删除失败:', response.status, errorText); + throw new Error(`删除失败 (${response.status}): ${errorText}`); + } + + const data = await response.json(); + console.log('📋 删除响应:', data); + + if (data.success) { + setSavedMusicList(prev => prev.filter(music => music.id !== parseInt(musicId))); + console.log('✅ 音乐删除成功'); + } else { + setError(data.error || '删除失败'); + console.error('❌ 删除API返回错误:', data.error); + } + } catch (error) { + console.error('❌ 删除音乐失败:', error); + setError('删除失败,请稍后再试'); + } + }; + + // 初始加载音乐列表 + useEffect(() => { + if (isAuthenticated && user) { + fetchMusicList(); + } + }, [isAuthenticated, user]); + + // 组件卸载时清理音乐播放 + useEffect(() => { + return () => { + stopMusic(); + }; + }, []); + + // 监听音乐列表数量,超过5个时默认收起 + useEffect(() => { + if (savedMusicList.length > 5) { + setExpandedMyMusic(false); + } else { + setExpandedMyMusic(true); + } + }, [savedMusicList.length]); + + // 如果既没有选中声音,也没有音乐列表,则不渲染组件 + if (selectedSounds.length === 0 && (!isAuthenticated || savedMusicList.length === 0)) { + return null; + } + + return ( +
+ {/* 当前选中声音模块 - 只有选中声音时才显示 */} + {selectedSounds.length > 0 && ( +
+
+

+ + 当前选中的声音 +

+ +
+ + {/* 音乐名称配置区域 */} + {expandedCurrent && ( +
+ setMusicName(e.target.value)} + placeholder="音乐名称" + className={styles.musicNameInput} + maxLength={50} + /> + +
+ )} + + {/* 选中的声音展示 */} + {expandedCurrent && ( +
+ + {selectedSounds.map((sound) => ( + +
+ )} +
+ )} + + {/* 音乐列表模块 - 只有登录用户且有音乐时才显示 */} + {isAuthenticated && savedMusicList.length > 0 && ( +
+
+

+ + 音乐列表 +

+
+ + {/* 错误提示 */} + {error && ( +
+ {error} + +
+ )} + + {/* 保存成功提示 */} + {showSaveSuccess && ( +
+

✓ 音乐保存成功!

+ +
+ )} + + {/* 音乐列表 - 展开时显示 */} + {expandedMyMusic && ( +
0 ? styles.hasExpanded : ''}`}> + {console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })} + {isLoadingMusic ? ( +
加载中...
+ ) : savedMusicList.length === 0 ? ( +
+ +

还没有保存的音乐

+

选择声音并点击保存按钮来创建你的第一首音乐

+
+ ) : ( + + {savedMusicList.map((music) => ( +
+ {editingId === music.id.toString() ? ( +
+ setEditingName(e.target.value)} + className={styles.editInput} + placeholder="输入音乐名称" + maxLength={50} + /> +
+ + +
+
+ ) : ( +
+ +
+
{music.name}
+
+ {music.sounds && music.sounds.length > 0 ? ( + music.sounds.map((soundId, index) => { + // 从所有声音中查找对应的声音名称 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + const sound = allSounds.find(s => s.id === soundId); + return sound ? ( + + {sound.label}{index < music.sounds.length - 1 ? ', ' : ''} + + ) : null; + }) + ) : ( + 暂无声音 + )} +
+
+
+ + +
+
+ )} + + {/* 展开时显示的声音内容 */} + {expandedMusic.has(music.id) && ( +
+ {/* 声音组件展示 */} +
+ + {music.sounds.map((soundId) => { + // 从所有声音中查找对应的声音 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + const sound = allSounds.find(s => s.id === soundId); + + if (!sound) return null; + + return ( + +
+
+ )} +
+ ))} +
+ )} +
+ )} +
+ )} + + {/* 登录提示 */} + {showLoginPrompt && ( +
+

请先登录后再保存音乐

+ + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/slider/slider.module.css b/src/components/slider/slider.module.css index cb713e3..9db2653 100644 --- a/src/components/slider/slider.module.css +++ b/src/components/slider/slider.module.css @@ -11,14 +11,14 @@ position: relative; flex-grow: 1; height: 4px; - background: var(--color-neutral-200); + background: var(--color-control-bg); border-radius: 9999px; } .sliderRange { position: absolute; height: 100%; - background: var(--color-neutral-800); + background: var(--color-control-progress); border-radius: 9999px; } @@ -27,16 +27,18 @@ width: 16px; height: 16px; cursor: pointer; - background: var(--color-neutral-950); + background: var(--color-control-progress); + border: 2px solid var(--color-control-progress); border-radius: 50%; - box-shadow: 0 0 3px var(--color-neutral-50); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); } .sliderThumb:hover { - background: var(--color-neutral-800); + background: var(--color-control-bg-hover); + border-color: var(--color-control-bg-active); } .sliderThumb:focus { outline: none; - box-shadow: 0 0 0 3px var(--color-neutral-400); + box-shadow: 0 0 0 3px var(--color-foreground-subtler); } diff --git a/src/components/sounds/sound/favorite/favorite.module.css b/src/components/sounds/sound/favorite/favorite.module.css index fb74e40..c647e59 100644 --- a/src/components/sounds/sound/favorite/favorite.module.css +++ b/src/components/sounds/sound/favorite/favorite.module.css @@ -1,7 +1,4 @@ .favoriteButton { - position: absolute; - top: 10px; - right: 10px; display: flex; align-items: center; justify-content: center; @@ -10,23 +7,24 @@ line-height: 0; color: var(--color-foreground-subtle); cursor: pointer; - background-color: black; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-200); + background-color: var(--component-bg); + border: 1px solid var(--color-border); border-radius: 50%; transition: 0.2s; &:hover, &:focus-visible { color: var(--color-foreground); + background-color: var(--component-hover); } &:focus-visible { - outline: 2px solid var(--color-neutral-400); + outline: 2px solid var(--color-muted); outline-offset: 2px; } &.isFavorite { color: var(--color-foreground); + background-color: var(--component-active); } } diff --git a/src/components/sounds/sound/random-speed/index.ts b/src/components/sounds/sound/random-speed/index.ts new file mode 100644 index 0000000..a72a033 --- /dev/null +++ b/src/components/sounds/sound/random-speed/index.ts @@ -0,0 +1 @@ +export { RandomSpeed } from './random-speed'; \ No newline at end of file diff --git a/src/components/sounds/sound/random-speed/random-speed.module.css b/src/components/sounds/sound/random-speed/random-speed.module.css new file mode 100644 index 0000000..99cf400 --- /dev/null +++ b/src/components/sounds/sound/random-speed/random-speed.module.css @@ -0,0 +1,30 @@ +.randomSpeedButton { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + line-height: 0; + color: var(--color-foreground-subtle); + cursor: pointer; + background-color: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 50%; + transition: 0.2s; + + &:hover, + &:focus-visible { + color: var(--color-foreground); + background-color: var(--component-hover); + } + + &:focus-visible { + outline: 2px solid var(--color-muted); + outline-offset: 2px; + } + + &.isRandomSpeed { + color: var(--color-foreground); + background-color: var(--component-active); + } +} \ No newline at end of file diff --git a/src/components/sounds/sound/random-speed/random-speed.tsx b/src/components/sounds/sound/random-speed/random-speed.tsx new file mode 100644 index 0000000..7edc8e6 --- /dev/null +++ b/src/components/sounds/sound/random-speed/random-speed.tsx @@ -0,0 +1,89 @@ +import { FiShuffle } from 'react-icons/fi/index'; +import { AnimatePresence, motion } from 'motion/react'; + +import { useSoundStore } from '@/stores/sound'; +import { cn } from '@/helpers/styles'; +import { fade } from '@/lib/motion'; + +import styles from './random-speed.module.css'; + +import { useKeyboardButton } from '@/hooks/use-keyboard-button'; +import { random } from '@/helpers/random'; + +interface RandomSpeedProps { + id: string; + label: string; + baseSpeed: number; + baseRate: number; + baseVolume: number; +} + +export function RandomSpeed({ id, label, baseSpeed, baseRate, baseVolume }: RandomSpeedProps) { + const isRandomSpeed = useSoundStore(state => state.sounds[id].isRandomSpeed); + const isRandomVolume = useSoundStore(state => state.sounds[id].isRandomVolume); + const isRandomRate = useSoundStore(state => state.sounds[id].isRandomRate); + const toggleAllRandom = useSoundStore(state => state.toggleAllRandom); + const setSpeed = useSoundStore(state => state.setSpeed); + const setRate = useSoundStore(state => state.setRate); + const setVolume = useSoundStore(state => state.setVolume); + + const hasAnyRandom = isRandomSpeed || isRandomVolume || isRandomRate; + + const handleToggle = () => { + toggleAllRandom(id); + + if (!hasAnyRandom) { + // 启用随机时,立即设置随机值 + if (isRandomSpeed) { + const randomSpeed = random(baseSpeed - 0.25, baseSpeed + 0.25); + setSpeed(id, Math.max(0.5, Math.min(2.0, randomSpeed))); + } + if (isRandomRate) { + const randomRate = random(baseRate - 0.25, baseRate + 0.25); + setRate(id, Math.max(0.5, Math.min(2.0, randomRate))); + } + if (isRandomVolume) { + const randomVolume = random(baseVolume * 0.3, baseVolume * 0.7); + setVolume(id, Math.max(0.0, Math.min(1.0, randomVolume))); + } + } else { + // 禁用随机时,恢复基础值 + setSpeed(id, baseSpeed); + setRate(id, baseRate); + setVolume(id, baseVolume); + } + }; + + const variants = fade(); + + const handleKeyDown = useKeyboardButton(handleToggle); + + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/sounds/sound/range/range.module.css b/src/components/sounds/sound/range/range.module.css index d195001..b6a8186 100644 --- a/src/components/sounds/sound/range/range.module.css +++ b/src/components/sounds/sound/range/range.module.css @@ -1,72 +1,45 @@ -.range { - width: 100%; - max-width: 120px; +.controlsContainer { + display: flex; + flex-direction: column; + gap: 8px; margin-top: 10px; - - /********** Range Input Styles **********/ - - /* Range Reset */ - appearance: none; - cursor: pointer; - background: transparent; - - /* Removes default focus */ - &:focus { - outline: none; - } - - &:disabled { - pointer-events: none; - cursor: default; - opacity: 0.5; - } - - /***** Chrome, Safari, Opera and Edge Chromium styles *****/ - - &::-webkit-slider-runnable-track { - height: 0.5rem; - background-color: #27272a; - border-radius: 0.5rem; - } - - &::-webkit-slider-thumb { - width: 14px; - height: 14px; - margin-top: -3px; - appearance: none; - background-color: #3f3f46; - border: 1px solid #52525b; - border-radius: 50%; - } - - &:not(:disabled):focus::-webkit-slider-thumb { - border: 1px solid #053a5f; - outline: 3px solid #053a5f; - outline-offset: 0.125rem; - } - - /******** Firefox styles ********/ - - &::-moz-range-track { - height: 0.5rem; - background-color: #27272a; - border-radius: 0.5rem; - } - - &::-moz-range-thumb { - width: 14px; - height: 14px; - margin-top: -3px; - background-color: #3f3f46; - border: none; - border: 1px solid #52525b; - border-radius: 0; - border-radius: 50%; - } - - &:not(:disabled):focus::-moz-range-thumb { - border: 1px solid #053a5f; - outline: 3px solid #053a5f; - outline-offset: 0.125rem; - } + width: 100%; + max-width: 150px; +} + +.volumeContainer, +.speedContainer, +.rateContainer { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} + +.volumeIcon, +.speedIcon, +.rateIcon { + color: var(--color-foreground-subtle); + font-size: 14px; + flex-shrink: 0; + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.volumeContainer:hover .volumeIcon, +.speedContainer:hover .speedIcon, +.rateContainer:hover .rateIcon { + opacity: 1; +} + +/* 当滑块禁用时,容器内的图标也要相应调整 */ +.volumeContainer:has(.slider:disabled) .volumeIcon, +.speedContainer:has(.slider:disabled) .speedIcon, +.rateContainer:has(.slider:disabled) .rateIcon { + opacity: 0.4; +} + +.slider { + width: 100%; + flex: 1; } diff --git a/src/components/sounds/sound/range/range.tsx b/src/components/sounds/sound/range/range.tsx index d4022fb..460b490 100644 --- a/src/components/sounds/sound/range/range.tsx +++ b/src/components/sounds/sound/range/range.tsx @@ -1,4 +1,7 @@ +import { FaVolumeUp, FaTachometerAlt, FaMusic } from 'react-icons/fa/index'; import { useSoundStore } from '@/stores/sound'; +import { useTranslation } from '@/hooks/useTranslation'; +import { Slider } from '@/components/slider'; import styles from './range.module.css'; @@ -8,25 +11,54 @@ interface RangeProps { } export function Range({ id, label }: RangeProps) { + const { t } = useTranslation(); const setVolume = useSoundStore(state => state.setVolume); + const setSpeed = useSoundStore(state => state.setSpeed); + const setRate = useSoundStore(state => state.setRate); const volume = useSoundStore(state => state.sounds[id].volume); + const speed = useSoundStore(state => state.sounds[id].speed); + const rate = useSoundStore(state => state.sounds[id].rate); const isSelected = useSoundStore(state => state.sounds[id].isSelected); const locked = useSoundStore(state => state.locked); return ( - e.stopPropagation()} - onChange={e => - !locked && isSelected && setVolume(id, Number(e.target.value) / 100) - } - /> +
+
+ + !locked && isSelected && setVolume(id, value)} + className={styles.slider} + /> +
+
+ + !locked && isSelected && setSpeed(id, value)} + className={styles.slider} + /> +
+
+ + !locked && isSelected && setRate(id, value)} + className={styles.slider} + /> +
+
); } diff --git a/src/components/sounds/sound/sound.module.css b/src/components/sounds/sound/sound.module.css index d7bfb49..c6d5769 100644 --- a/src/components/sounds/sound/sound.module.css +++ b/src/components/sounds/sound/sound.module.css @@ -7,13 +7,13 @@ padding: 25px 20px; text-align: center; cursor: pointer; - background: linear-gradient(rgb(24 24 27 / 50%), transparent); - border: 1px solid var(--color-neutral-200); + background: var(--color-component-bg); + border: 1px solid var(--color-border); border-radius: 12px; transition: 0.2s; &:focus-visible { - outline: 2px solid var(--color-neutral-400); + outline: 2px solid var(--color-muted); outline-offset: 2px; } @@ -31,7 +31,7 @@ background: linear-gradient( 90deg, transparent, - var(--color-neutral-400), + var(--color-muted), transparent ); } @@ -60,7 +60,7 @@ width: 100%; height: 100%; content: ''; - background-color: var(--color-neutral-50); + background-color: var(--color-control-bg); border-radius: 50%; } @@ -73,8 +73,8 @@ height: calc(100% + 2px); content: ''; background: linear-gradient( - var(--color-neutral-300), - var(--color-neutral-100) + var(--bg-quaternary), + var(--bg-secondary) ); border-radius: 50%; } @@ -95,7 +95,7 @@ &.selected { border-color: transparent; - box-shadow: 0 0 0 2px var(--color-neutral-800); + box-shadow: 0 0 0 2px var(--color-border); & .icon { color: var(--color-foreground); @@ -111,6 +111,17 @@ } } +.controlsContainer { + position: absolute; + top: 8px; + right: 8px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 10; + align-items: flex-end; +} + @keyframes spinner { 0% { transform: rotate(0deg); diff --git a/src/components/sounds/sound/sound.tsx b/src/components/sounds/sound/sound.tsx index 8ba190f..488ff9f 100644 --- a/src/components/sounds/sound/sound.tsx +++ b/src/components/sounds/sound/sound.tsx @@ -3,6 +3,7 @@ import { ImSpinner9 } from 'react-icons/im/index'; import { Range } from './range'; import { Favorite } from './favorite'; +import { RandomSpeed } from './random-speed'; import { useSound } from '@/hooks/use-sound'; import { useSoundStore } from '@/stores/sound'; @@ -20,10 +21,11 @@ interface SoundProps extends SoundType { hidden: boolean; selectHidden: (key: string) => void; unselectHidden: (key: string) => void; + displayMode?: boolean; // 新增:展示模式参数 } export const Sound = forwardRef(function Sound( - { functional, hidden, icon, id, label, selectHidden, src, unselectHidden }, + { functional, hidden, icon, id, label, selectHidden, src, unselectHidden, displayMode = false }, ref, ) { const isPlaying = useSoundStore(state => state.isPlaying); @@ -31,35 +33,96 @@ export const Sound = forwardRef(function Sound( const selectSound = useSoundStore(state => state.select); const unselectSound = useSoundStore(state => state.unselect); const setVolume = useSoundStore(state => state.setVolume); + const setSpeed = useSoundStore(state => state.setSpeed); + const setRate = useSoundStore(state => state.setRate); const isSelected = useSoundStore(state => state.sounds[id].isSelected); const locked = useSoundStore(state => state.locked); const volume = useSoundStore(state => state.sounds[id].volume); + const speed = useSoundStore(state => state.sounds[id].speed); + const rate = useSoundStore(state => state.sounds[id].rate); + const isRandomSpeed = useSoundStore(state => state.sounds[id].isRandomSpeed); + const isRandomVolume = useSoundStore(state => state.sounds[id].isRandomVolume); + const isRandomRate = useSoundStore(state => state.sounds[id].isRandomRate); const globalVolume = useSoundStore(state => state.globalVolume); const adjustedVolume = useMemo( () => volume * globalVolume, [volume, globalVolume], ); + const actualPlaybackRate = useMemo( + () => speed * rate, + [speed, rate], + ); - const isLoading = useLoadingStore(state => state.loaders[src]); + const isLoading = src ? useLoadingStore(state => state.loaders[src]) : false; - const sound = useSound(src, { loop: true, volume: adjustedVolume }); + // 确保 src 存在才创建声音实例 + const sound = useSound(src || '', { loop: true, volume: adjustedVolume, speed: actualPlaybackRate }); useEffect(() => { if (locked) return; - if (isSelected && isPlaying && functional) { + // 在展示模式下或者功能模式下,只要选中且在播放就应该播放 + const shouldPlay = isSelected && isPlaying && (functional || displayMode); + + if (shouldPlay) { sound?.play(); } else { sound?.pause(); } - }, [isSelected, sound, isPlaying, functional, locked]); + }, [isSelected, sound, isPlaying, functional, displayMode, locked]); useEffect(() => { if (hidden && isSelected) selectHidden(label); else if (hidden && !isSelected) unselectHidden(label); }, [label, isSelected, hidden, selectHidden, unselectHidden]); + // 改进的随机逻辑 - 每次只随机调整一个参数,频率为1分钟 + useEffect(() => { + const hasAnyRandom = isRandomSpeed || isRandomVolume || isRandomRate; + const isActiveMode = functional || displayMode; + if (!hasAnyRandom || !isSelected || !isPlaying || !isActiveMode) return; + + const interval = setInterval(() => { + // 获取当前启用的随机选项列表 + const randomOptions = []; + if (isRandomSpeed) randomOptions.push('speed'); + if (isRandomRate) randomOptions.push('rate'); + if (isRandomVolume) randomOptions.push('volume'); + + if (randomOptions.length === 0) return; + + // 随机选择一个要调整的参数 + const selectedOption = randomOptions[Math.floor(Math.random() * randomOptions.length)]; + + switch (selectedOption) { + case 'speed': { + const baseSpeed = 1.0; + const randomSpeed = Math.random() * 0.5 + baseSpeed - 0.25; // baseSpeed ± 0.25 + const clampedSpeed = Math.max(0.5, Math.min(2.0, randomSpeed)); + setSpeed(id, clampedSpeed); + break; + } + case 'rate': { + const baseRate = 1.0; + const randomRate = Math.random() * 0.5 + baseRate - 0.25; // baseRate ± 0.25 + const clampedRate = Math.max(0.5, Math.min(2.0, randomRate)); + setRate(id, clampedRate); + break; + } + case 'volume': { + const baseVolume = 0.5; + const randomVolume = Math.random() * 0.4 + baseVolume * 0.3; // 30% - 70% 范围 + const clampedVolume = Math.max(0.0, Math.min(1.0, randomVolume)); + setVolume(id, clampedVolume); + break; + } + } + }, 60000 + Math.random() * 30000); // 每 60-90 秒更新一次,大约1分钟 + + return () => clearInterval(interval); + }, [isRandomSpeed, isRandomVolume, isRandomRate, isSelected, isPlaying, id, setSpeed, setRate, setVolume]); + const select = useCallback(() => { if (locked) return; selectSound(id); @@ -70,7 +133,17 @@ export const Sound = forwardRef(function Sound( if (locked) return; unselectSound(id); setVolume(id, 0.5); - }, [unselectSound, setVolume, id, locked]); + setSpeed(id, 1.0); + setRate(id, 1.0); + + // 确保所有随机模式都被重置 + const { toggleRandomSpeed, toggleRandomVolume, toggleRandomRate } = useSoundStore.getState(); + const sound = useSoundStore.getState().sounds[id]; + + if (sound?.isRandomSpeed) toggleRandomSpeed(id); + if (sound?.isRandomVolume) toggleRandomVolume(id); + if (sound?.isRandomRate) toggleRandomRate(id); + }, [unselectSound, setVolume, setSpeed, setRate, id, locked]); const toggle = useCallback(() => { if (locked) return; @@ -100,7 +173,10 @@ export const Sound = forwardRef(function Sound( onClick={handleClick} onKeyDown={handleKeyDown} > - +
+ + +
{isLoading ? (
+
+ )} + + ); +} \ No newline at end of file diff --git a/src/components/toolbar/menu/items/binaural.tsx b/src/components/toolbar/menu/items/binaural.tsx index cd75f3c..7f451a1 100644 --- a/src/components/toolbar/menu/items/binaural.tsx +++ b/src/components/toolbar/menu/items/binaural.tsx @@ -1,13 +1,16 @@ import { FaHeadphonesAlt } from 'react-icons/fa/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface BinauralProps { open: () => void; } export function Binaural({ open }: BinauralProps) { + const { t } = useTranslation(); + return ( - } label="Binaural Beats" onClick={open} /> + } label={t('binauralBeats')} onClick={open} /> ); } diff --git a/src/components/toolbar/menu/items/breathing-exercise.tsx b/src/components/toolbar/menu/items/breathing-exercise.tsx index 5695afb..53d2eb3 100644 --- a/src/components/toolbar/menu/items/breathing-exercise.tsx +++ b/src/components/toolbar/menu/items/breathing-exercise.tsx @@ -1,16 +1,19 @@ import { IoMdFlower } from 'react-icons/io/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface BreathingExerciseProps { open: () => void; } export function BreathingExercise({ open }: BreathingExerciseProps) { + const { t } = useTranslation(); + return ( } - label="Breathing Exercise" + label={t('breathingExercise')} shortcut="Shift + B" onClick={open} /> diff --git a/src/components/toolbar/menu/items/countdown.tsx b/src/components/toolbar/menu/items/countdown.tsx index cd3b1a6..f6b10c6 100644 --- a/src/components/toolbar/menu/items/countdown.tsx +++ b/src/components/toolbar/menu/items/countdown.tsx @@ -1,16 +1,19 @@ import { MdOutlineTimer } from 'react-icons/md/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface CountdownProps { open: () => void; } export function Countdown({ open }: CountdownProps) { + const { t } = useTranslation(); + return ( } - label="Countdown Timer" + label={t('countdownTimer')} shortcut="Shift + C" onClick={open} /> diff --git a/src/components/toolbar/menu/items/donate.tsx b/src/components/toolbar/menu/items/donate.tsx index ee21235..1cfb309 100644 --- a/src/components/toolbar/menu/items/donate.tsx +++ b/src/components/toolbar/menu/items/donate.tsx @@ -1,13 +1,16 @@ import { SiBuymeacoffee } from 'react-icons/si/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; export function Donate() { + const { t } = useTranslation(); + return ( } - label="Buy Me a Coffee" + label={t('buyMeACoffee')} /> ); } diff --git a/src/components/toolbar/menu/items/index.ts b/src/components/toolbar/menu/items/index.ts index c83f5ee..04121ec 100644 --- a/src/components/toolbar/menu/items/index.ts +++ b/src/components/toolbar/menu/items/index.ts @@ -13,3 +13,4 @@ export { Countdown as CountdownItem } from './countdown'; export { Binaural as BinauralItem } from './binaural'; export { Isochronic as IsochronicItem } from './isochronic'; export { Lofi as LofiItem } from './lofi'; +export { AuthItem } from './auth-item'; diff --git a/src/components/toolbar/menu/items/isochronic.tsx b/src/components/toolbar/menu/items/isochronic.tsx index 71427ac..aed0544 100644 --- a/src/components/toolbar/menu/items/isochronic.tsx +++ b/src/components/toolbar/menu/items/isochronic.tsx @@ -1,11 +1,14 @@ import { TbWaveSine } from 'react-icons/tb/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface IsochronicProps { open: () => void; } export function Isochronic({ open }: IsochronicProps) { - return } label="Isochronic Tones" onClick={open} />; + const { t } = useTranslation(); + + return } label={t('isochronicTones')} onClick={open} />; } diff --git a/src/components/toolbar/menu/items/item.module.css b/src/components/toolbar/menu/items/item.module.css new file mode 100644 index 0000000..92d36be --- /dev/null +++ b/src/components/toolbar/menu/items/item.module.css @@ -0,0 +1,201 @@ +.item { + width: 100%; + padding: 8px 12px; + text-align: left; + font-size: var(--font-sm); + color: var(--color-foreground); + background: none; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: var(--color-neutral-200); + } + + &:focus-visible { + outline: 2px solid var(--color-neutral-400); + outline-offset: 2px; + } +} + +.authContainer { + width: 100%; + padding: 8px; +} + +.userInfo { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 12px; +} + +.userAvatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--color-foreground); + color: var(--bg-primary); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-sm); + font-weight: 600; + flex-shrink: 0; +} + +.userName { + font-weight: 500; + color: var(--color-foreground); + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.logoutButton { + width: 100%; + padding: 8px 12px; + text-align: left; + font-size: var(--font-sm); + color: #ef4444; + background: none; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: rgba(239, 68, 68, 0.1); + } + + &:focus-visible { + outline: 2px solid var(--color-neutral-400); + outline-offset: 2px; + } +} + +.authForm { + width: 100%; + padding: 16px; + background: var(--component-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + color: var(--color-foreground); + + h3 { + margin: 0 0 16px 0; + font-size: var(--font-lg); + font-weight: 600; + text-align: center; + color: var(--color-foreground); + } + + form { + display: flex; + flex-direction: column; + gap: 12px; + } +} + +.authInput { + padding: 8px 12px; + border: 1px solid var(--color-border); + border-radius: 6px; + font-size: var(--font-sm); + background: var(--bg-primary); + color: var(--color-foreground); + + &::placeholder { + color: var(--color-foreground-subtler); + } + + &:focus { + outline: none; + border-color: var(--color-muted); + box-shadow: 0 0 0 2px var(--color-muted); + } +} + +.authButtons { + display: flex; + gap: 8px; +} + +.authSubmitButton { + flex: 1; + padding: 8px 16px; + background: var(--color-foreground); + color: var(--bg-primary); + border: none; + border-radius: 6px; + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + + &:hover:not(:disabled) { + background: var(--color-foreground-subtle); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.authCancelButton { + padding: 8px 16px; + background: transparent; + color: var(--color-foreground-subtler); + border: 1px solid var(--color-border); + border-radius: 6px; + font-size: var(--font-sm); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; + + &:hover { + background: var(--component-hover); + border-color: var(--color-foreground-subtle); + } +} + +.authToggle { + margin-top: 8px; +} + +.authToggleButton { + width: 100%; + padding: 6px 12px; + background: transparent; + color: var(--color-foreground-subtle); + border: none; + border-radius: 6px; + font-size: var(--font-xsm); + cursor: pointer; + text-decoration: underline; + transition: color 0.2s; + + &:hover { + color: var(--color-foreground); + } +} + +.authFormOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 16px; +} \ No newline at end of file diff --git a/src/components/toolbar/menu/items/lofi.tsx b/src/components/toolbar/menu/items/lofi.tsx index ea9d9ab..a412ecd 100644 --- a/src/components/toolbar/menu/items/lofi.tsx +++ b/src/components/toolbar/menu/items/lofi.tsx @@ -1,16 +1,19 @@ import { IoIosMusicalNote } from 'react-icons/io/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface LofiProps { open: () => void; } export function Lofi({ open }: LofiProps) { + const { t } = useTranslation(); + return ( } - label="Lofi Music Player" + label={t('lofiMusicPlayer')} onClick={open} /> ); diff --git a/src/components/toolbar/menu/items/notepad.tsx b/src/components/toolbar/menu/items/notepad.tsx index ef0ff39..d626c6a 100644 --- a/src/components/toolbar/menu/items/notepad.tsx +++ b/src/components/toolbar/menu/items/notepad.tsx @@ -3,19 +3,21 @@ import { MdNotes } from 'react-icons/md/index'; import { Item } from '../item'; import { useNoteStore } from '@/stores/note'; +import { useTranslation } from '@/hooks/useTranslation'; interface NotepadProps { open: () => void; } export function Notepad({ open }: NotepadProps) { + const { t } = useTranslation(); const note = useNoteStore(state => state.note); return ( } - label="Notepad" + label={t('notepad')} shortcut="Shift + N" onClick={open} /> diff --git a/src/components/toolbar/menu/items/pomodoro.tsx b/src/components/toolbar/menu/items/pomodoro.tsx index 43473b5..ea2754c 100644 --- a/src/components/toolbar/menu/items/pomodoro.tsx +++ b/src/components/toolbar/menu/items/pomodoro.tsx @@ -3,19 +3,21 @@ import { MdOutlineAvTimer } from 'react-icons/md/index'; import { Item } from '../item'; import { usePomodoroStore } from '@/stores/pomodoro'; +import { useTranslation } from '@/hooks/useTranslation'; interface PomodoroProps { open: () => void; } export function Pomodoro({ open }: PomodoroProps) { + const { t } = useTranslation(); const running = usePomodoroStore(state => state.running); return ( } - label="Pomodoro" + label={t('pomodoro')} shortcut="Shift + P" onClick={open} /> diff --git a/src/components/toolbar/menu/items/presets.tsx b/src/components/toolbar/menu/items/presets.tsx index a78b17d..33c5283 100644 --- a/src/components/toolbar/menu/items/presets.tsx +++ b/src/components/toolbar/menu/items/presets.tsx @@ -1,16 +1,20 @@ import { RiPlayListFill } from 'react-icons/ri/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface PresetsProps { open: () => void; } export function Presets({ open }: PresetsProps) { + const { t } = useTranslation(); + return ( } - label="Your Presets" + label={t('presets')} + data-i18n="navigation.presets" shortcut="Shift + Alt + P" onClick={open} /> diff --git a/src/components/toolbar/menu/items/share.tsx b/src/components/toolbar/menu/items/share.tsx index 7bda116..43c1fe4 100644 --- a/src/components/toolbar/menu/items/share.tsx +++ b/src/components/toolbar/menu/items/share.tsx @@ -3,19 +3,21 @@ import { IoShareSocialSharp } from 'react-icons/io5/index'; import { Item } from '../item'; import { useSoundStore } from '@/stores/sound'; +import { useTranslation } from '@/hooks/useTranslation'; interface ShareProps { open: () => void; } export function Share({ open }: ShareProps) { + const { t } = useTranslation(); const noSelected = useSoundStore(state => state.noSelected()); return ( } - label="Share Sounds" + label={t('share')} shortcut="Shift + S" onClick={open} /> diff --git a/src/components/toolbar/menu/items/shortcuts.tsx b/src/components/toolbar/menu/items/shortcuts.tsx index fc9f4e1..5b3fb77 100644 --- a/src/components/toolbar/menu/items/shortcuts.tsx +++ b/src/components/toolbar/menu/items/shortcuts.tsx @@ -1,16 +1,19 @@ import { MdKeyboardCommandKey } from 'react-icons/md/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface ShortcutsProps { open: () => void; } export function Shortcuts({ open }: ShortcutsProps) { + const { t } = useTranslation(); + return ( } - label="Shortcuts" + label={t('shortcuts')} shortcut="Shift + H" onClick={open} /> diff --git a/src/components/toolbar/menu/items/shuffle.tsx b/src/components/toolbar/menu/items/shuffle.tsx index 722853d..7c7f497 100644 --- a/src/components/toolbar/menu/items/shuffle.tsx +++ b/src/components/toolbar/menu/items/shuffle.tsx @@ -3,8 +3,10 @@ import { BiShuffle } from 'react-icons/bi/index'; import { useSoundStore } from '@/stores/sound'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; export function Shuffle() { + const { t } = useTranslation(); const shuffle = useSoundStore(state => state.shuffle); const locked = useSoundStore(state => state.locked); @@ -12,7 +14,7 @@ export function Shuffle() { } - label="Shuffle Sounds" + label={t('shuffleSounds')} onClick={shuffle} /> ); diff --git a/src/components/toolbar/menu/items/sleep-timer.tsx b/src/components/toolbar/menu/items/sleep-timer.tsx index 0ecfa9b..a66da25 100644 --- a/src/components/toolbar/menu/items/sleep-timer.tsx +++ b/src/components/toolbar/menu/items/sleep-timer.tsx @@ -2,19 +2,21 @@ import { IoMoonSharp } from 'react-icons/io5/index'; import { useSleepTimerStore } from '@/stores/sleep-timer'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface SleepTimerProps { open: () => void; } export function SleepTimer({ open }: SleepTimerProps) { + const { t } = useTranslation(); const active = useSleepTimerStore(state => state.active); return ( } - label="Sleep Timer" + label={t('sleepTimer')} shortcut="Shift + Alt + T" onClick={open} /> diff --git a/src/components/toolbar/menu/items/source.tsx b/src/components/toolbar/menu/items/source.tsx index 668fcde..cab5e6a 100644 --- a/src/components/toolbar/menu/items/source.tsx +++ b/src/components/toolbar/menu/items/source.tsx @@ -1,13 +1,16 @@ import { LuGithub } from 'react-icons/lu/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; export function Source() { + const { t } = useTranslation(); + return ( } - label="Source Code" + label={t('sourceCode')} /> ); } diff --git a/src/components/toolbar/menu/items/todo.tsx b/src/components/toolbar/menu/items/todo.tsx index f2dbc1e..d1f62bc 100644 --- a/src/components/toolbar/menu/items/todo.tsx +++ b/src/components/toolbar/menu/items/todo.tsx @@ -1,16 +1,19 @@ import { MdTaskAlt } from 'react-icons/md/index'; import { Item } from '../item'; +import { useTranslation } from '@/hooks/useTranslation'; interface TodoProps { open: () => void; } export function Todo({ open }: TodoProps) { + const { t } = useTranslation(); + return ( } - label="Todo Checklist" + label={t('todoChecklist')} shortcut="Shift + T" onClick={open} /> diff --git a/src/components/toolbar/menu/menu.module.css b/src/components/toolbar/menu/menu.module.css index 6f1f399..11fd840 100644 --- a/src/components/toolbar/menu/menu.module.css +++ b/src/components/toolbar/menu/menu.module.css @@ -9,19 +9,19 @@ color: var(--color-foreground); pointer-events: auto; cursor: pointer; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-300); + background-color: var(--bg-secondary); + border: 1px solid var(--color-border); border-radius: 50%; transition: 0.2s; &:focus-visible { - outline: 2px solid var(--color-neutral-400); + outline: 2px solid var(--color-muted); outline-offset: 2px; } &:hover, &:focus-visible { - background-color: var(--color-neutral-200); + background-color: var(--bg-tertiary); } } } @@ -36,8 +36,8 @@ max-height: var(--radix-dropdown-menu-content-available-height); padding: 4px; overflow: auto; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-300); + background-color: var(--component-bg); + border: 1px solid var(--color-border); border-radius: 8px; } diff --git a/src/components/toolbar/menu/menu.tsx b/src/components/toolbar/menu/menu.tsx index 33d8f51..c931cd3 100644 --- a/src/components/toolbar/menu/menu.tsx +++ b/src/components/toolbar/menu/menu.tsx @@ -11,27 +11,13 @@ import { SourceItem, PresetsItem, ShortcutsItem, - SleepTimerItem, - BreathingExerciseItem, - PomodoroItem, - NotepadItem, - TodoItem, - CountdownItem, - BinauralItem, - IsochronicItem, - LofiItem, } from './items'; import { Divider } from './divider'; import { ShareLinkModal } from '@/components/modals/share-link'; import { PresetsModal } from '@/components/modals/presets'; import { ShortcutsModal } from '@/components/modals/shortcuts'; -import { SleepTimerModal } from '@/components/modals/sleep-timer'; -import { BreathingExerciseModal } from '@/components/modals/breathing'; -import { BinauralModal } from '@/components/modals/binaural'; -import { IsochronicModal } from '@/components/modals/isochronic'; -import { LofiModal } from '@/components/modals/lofi'; -import { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox'; import { Slider } from '@/components/slider'; +import { useTranslation } from '@/hooks/useTranslation'; import { fade, mix, slideY } from '@/lib/motion'; import { useSoundStore } from '@/stores/sound'; @@ -41,6 +27,7 @@ import { useCloseListener } from '@/hooks/use-close-listener'; import { closeModals } from '@/lib/modal'; export function Menu() { + const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const noSelected = useSoundStore(state => state.noSelected()); @@ -49,18 +36,9 @@ export function Menu() { const initial = useMemo( () => ({ - binaural: false, - breathing: false, - countdown: false, - isochronic: false, - lofi: false, - notepad: false, - pomodoro: false, presets: false, shareLink: false, shortcuts: false, - sleepTimer: false, - todo: false, }), [], ); @@ -86,13 +64,7 @@ export function Menu() { useHotkeys('shift+m', () => setIsOpen(prev => !prev)); useHotkeys('shift+alt+p', () => open('presets')); useHotkeys('shift+h', () => open('shortcuts')); - useHotkeys('shift+b', () => open('breathing')); - useHotkeys('shift+n', () => open('notepad')); - useHotkeys('shift+p', () => open('pomodoro')); - useHotkeys('shift+t', () => open('todo')); - useHotkeys('shift+c', () => open('countdown')); useHotkeys('shift+s', () => open('shareLink'), { enabled: !noSelected }); - useHotkeys('shift+alt+t', () => open('sleepTimer')); useCloseListener(closeAll); @@ -103,7 +75,7 @@ export function Menu() {
setIsOpen(o)}> - @@ -128,26 +100,13 @@ export function Menu() { open('presets')} /> open('shareLink')} /> - open('sleepTimer')} /> - - - open('countdown')} /> - open('pomodoro')} /> - open('notepad')} /> - open('todo')} /> - open('breathing')} /> - - - open('binaural')} /> - open('isochronic')} /> - open('lofi')} /> open('shortcuts')} />
- + close('shareLink')} /> - close('breathing')} - /> close('shortcuts')} /> - open('pomodoro')} - show={modals.pomodoro} - onClose={() => close('pomodoro')} - /> - close('notepad')} /> - close('todo')} /> - close('countdown')} /> close('presets')} /> - close('sleepTimer')} - /> - close('binaural')} /> - close('isochronic')} - /> - close('lofi')} /> ); } diff --git a/src/components/toolbar/scroll-to-top/scroll-to-top.module.css b/src/components/toolbar/scroll-to-top/scroll-to-top.module.css index 4e11b80..009e286 100644 --- a/src/components/toolbar/scroll-to-top/scroll-to-top.module.css +++ b/src/components/toolbar/scroll-to-top/scroll-to-top.module.css @@ -8,18 +8,18 @@ color: var(--color-foreground); pointer-events: auto; cursor: pointer; - background-color: var(--color-neutral-100); - border: 1px solid var(--color-neutral-300); + background-color: var(--bg-secondary); + border: 1px solid var(--color-border); border-radius: 50%; transition: 0.2s; &:focus-visible { - outline: 2px solid var(--color-neutral-400); + outline: 2px solid var(--color-muted); outline-offset: 2px; } &:hover, &:focus-visible { - background-color: var(--color-neutral-200); + background-color: var(--bg-tertiary); } } diff --git a/src/data/i18n.ts b/src/data/i18n.ts new file mode 100644 index 0000000..6ff4de9 --- /dev/null +++ b/src/data/i18n.ts @@ -0,0 +1,525 @@ +export interface Translations { + // Navigation & UI + presets: string; + share: string; + useMoodist: string; + + // Hero section + heroTitle: string; + heroSubtitle: string; + heroDescription: string; + soundsCount: string; + + // About section + freeAmbientSounds: { + title: string; + body: string; + }; + carefullyCuratedSounds: { + title: string; + body: string; + }; + createYourSoundscape: { + title: string; + body: string; + }; + soundsForEveryMoment: { + title: string; + body: string; + }; + + // Categories + categories: Record; + + // Sounds + sounds: Record>; + + // Common + play: string; + pause: string; + favorite: string; + volume: string; + selected_sounds: string; + + // Support & Donate + supportMe: string; + helpKeepAdFree: string; + donateToday: string; + buyMeACoffee: string; + createdBy: string; + enjoyMoodist: string; + supportWithDonation: string; + + // UI Actions + showMore: string; + showLess: string; + + // Settings + globalVolume: string; + menu: string; + + // Menu Items + breathingExercise: string; + countdownTimer: string; + sleepTimer: string; + pomodoro: string; + notepad: string; + todoChecklist: string; + lofiMusicPlayer: string; + binauralBeats: string; + isochronicTones: string; + shortcuts: string; + shuffleSounds: string; + sourceCode: string; +} + +export const translations: Record = { + en: { + // Navigation & UI + presets: 'Your Presets', + share: 'Share', + useMoodist: 'Use Moodist', + + // Hero section + heroTitle: 'Ambient Sounds', + heroSubtitle: 'For Focus and Calm', + heroDescription: 'Free and Open-Source.', + soundsCount: 'Sounds', + + // About section + freeAmbientSounds: { + title: 'Free Ambient Sounds', + body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations – with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.' + }, + carefullyCuratedSounds: { + title: 'Carefully Curated Sounds', + body: 'Dive into an expansive library of {{count}} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.' + }, + createYourSoundscape: { + title: 'Create Your Soundscape', + body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options – just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.' + }, + soundsForEveryMoment: { + title: 'Sounds for Every Moment', + body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!" + }, + + // Categories + categories: { + nature: 'Nature', + rain: 'Rain', + animals: 'Animals', + urban: 'Urban', + places: 'Places', + transport: 'Transport', + things: 'Things', + noise: 'Noise' + }, + + // Sounds + sounds: { + nature: { + river: 'River', + waves: 'Waves', + campfire: 'Campfire', + wind: 'Wind', + howlingWind: 'Howling Wind', + windInTrees: 'Wind in Trees', + waterfall: 'Waterfall', + walkInSnow: 'Walk in Snow', + walkOnLeaves: 'Walk on Leaves', + walkOnGravel: 'Walk on Gravel', + droplets: 'Droplets', + jungle: 'Jungle' + }, + rain: { + lightRain: 'Light Rain', + moderateRain: 'Moderate Rain', + heavyRain: 'Heavy Rain', + storm: 'Storm', + thunder: 'Thunder', + distantStorm: 'Distant Storm', + fire: 'Fire', + oceanWaves: 'Ocean Waves', + rainOnLeaves: 'Rain on Leaves', + rainOnPavement: 'Rain on Pavement', + rainOnWindow: 'Rain on Window', + rainOnUmbrella: 'Rain on Umbrella', + rainOnTent: 'Rain on Tent', + insideRain: 'Inside the Rain', + carRain: 'Car Rain' + }, + animals: { + birds: 'Birds', + seagulls: 'Seagulls', + crickets: 'Crickets', + wolves: 'Wolves', + owl: 'Owl', + frogs: 'Frogs', + dogs: 'Dogs', + horses: 'Horses', + cats: 'Cats', + crows: 'Crows', + whale: 'Whale', + beehive: 'Beehive', + woodpecker: 'Woodpecker', + chickens: 'Chickens', + cows: 'Cows', + sheep: 'Sheep', + rooster: 'Rooster', + birdsMorning: 'Birds in Morning', + birdsEvening: 'Birds in Evening' + }, + urban: { + cityStreet: 'City Street', + traffic: 'Traffic', + highway: 'Highway', + road: 'Road', + ambulanceSiren: 'Ambulance Siren', + busyStreet: 'Busy Street', + crowd: 'Crowd', + fireworks: 'Fireworks' + }, + places: { + forest: 'Forest', + beach: 'Beach', + park: 'Park', + mountain: 'Mountain', + desert: 'Desert', + cave: 'Cave', + meadow: 'Meadow', + lake: 'Lake', + campsite: 'Campsite', + temple: 'Temple', + airport: 'Airport', + church: 'Church', + underwater: 'Underwater', + crowdedBar: 'Crowded Bar', + nightVillage: 'Night Village', + carousel: 'Carousel', + laboratory: 'Laboratory', + laundryRoom: 'Laundry Room', + subwayStation: 'Subway Station', + cafe: 'Cafe', + constructionSite: 'Construction Site', + office: 'Office', + supermarket: 'Supermarket', + restaurant: 'Restaurant', + library: 'Library' + }, + transport: { + car: 'Car', + bus: 'Bus', + train: 'Train', + subway: 'Subway', + airplane: 'Airplane', + boat: 'Boat', + bicycle: 'Bicycle', + motorcycle: 'Motorcycle', + helicopter: 'Helicopter', + steamTrain: 'Steam Train', + insideTrain: 'Inside a Train', + submarine: 'Submarine', + sailboat: 'Sailboat', + rowingBoat: 'Rowing Boat' + }, + things: { + fan: 'Fan', + clock: 'Clock', + typewriter: 'Typewriter', + keyboard: 'Keyboard', + printer: 'Printer', + refrigerator: 'Refrigerator', + washingMachine: 'Washing Machine', + vacuum: 'Vacuum', + airConditioner: 'Air Conditioner', + microwave: 'Microwave', + paper: 'Paper', + windChimes: 'Wind Chimes', + singingBowl: 'Singing Bowl', + ceilingFan: 'Ceiling Fan', + dryer: 'Dryer', + slideProjector: 'Slide Projector', + boilingWater: 'Boiling Water', + bubbles: 'Bubbles', + tuningRadio: 'Tuning Radio', + morseCode: 'Morse Code', + vinylEffect: 'Vinyl Effect', + windshieldWipers: 'Windshield Wipers' + }, + noise: { + whiteNoise: 'White Noise', + pinkNoise: 'Pink Noise', + brownNoise: 'Brown Noise', + blueNoise: 'Blue Noise', + violetNoise: 'Violet Noise', + greyNoise: 'Grey Noise' + } + }, + + // Common + play: 'Play', + pause: 'Pause', + favorite: 'Favorite', + volume: 'Volume', + selected_sounds: 'Current Sounds', + + // Support & Donate + supportMe: 'Support Me', + helpKeepAdFree: 'Help me keep Moodist ad-free.', + donateToday: 'Donate Today', + buyMeACoffee: 'Buy Me a Coffee', + createdBy: 'Created by', + enjoyMoodist: 'Enjoy Moodist?', + supportWithDonation: 'Support with a donation!', + + // UI Actions + showMore: 'Show More', + showLess: 'Show Less', + + // Settings + globalVolume: 'Global Volume', + menu: 'Menu', + + // Menu Items + breathingExercise: 'Breathing Exercise', + countdownTimer: 'Countdown Timer', + sleepTimer: 'Sleep Timer', + pomodoro: 'Pomodoro', + notepad: 'Notepad', + todoChecklist: 'Todo Checklist', + lofiMusicPlayer: 'Lofi Music Player', + binauralBeats: 'Binaural Beats', + isochronicTones: 'Isochronic Tones', + shortcuts: 'Shortcuts', + shuffleSounds: 'Shuffle Sounds', + sourceCode: 'Source Code' + }, + + 'zh-CN': { + // Navigation & UI + app: { language: '语言' }, + presets: '我的预设', + share: '分享', + useMoodist: '使用 Moodist', + + // Hero section + heroTitle: '环境音', + heroSubtitle: '专注与宁静', + heroDescription: '免费开源。', + soundsCount: '个声音', + + // About section + freeAmbientSounds: { + title: '免费环境音', + body: '渴望从日常繁杂中获得片刻宁静?需要完美的声音环境来提升专注力或帮助入眠?Moodist 就是您的最佳选择——免费开源的环境音生成器!无需订阅注册,使用 Moodist,您可以免费享受舒缓沉浸的音频体验。' + }, + carefullyCuratedSounds: { + title: '精心挑选的声音', + body: '探索包含 {{count}} 个精心挑选声音的庞大音库。自然爱好者可以在溪流的轻柔潺潺声中、海浪的节拍拍岸声中、或篝火的温暖噼啪声中获得慰藉。城市景观在咖啡馆的轻柔嗡嗡声、火车的节拍咔嗒声、或交通的舒缓白噪声中变得生动。对于寻求更深专注或放松的人,Moodist 提供了专门设计来增强心境的双节拍和色彩噪声。' + }, + createYourSoundscape: { + title: '创造您的声音景观', + body: 'Moodist 的美妙之处在于其简洁性和自定义性。没有复杂的菜单或令人困惑的选项——只需选择您喜欢的声音,调整音量平衡,然后点击播放。想要将鸟儿的轻柔啾鸣与雨水的舒缓声音融合?没问题!随心所欲地叠加多个声音,创建个性化的声音绿洲。' + }, + soundsForEveryMoment: { + title: '适合每个时刻的声音', + body: '无论您是想在漫长一天后放松身心,在工作中提升专注力,还是让自己进入宁静的睡眠,Moodist 都有完美的声音景观等着您。最棒的是什么?它完全免费开源,您可以毫无负担地享受它的好处。今天就开始使用 Moodist,发现您新的宁静和专注天堂吧!' + }, + + // Categories + categories: { + nature: '自然', + rain: '雨声', + animals: '动物', + urban: '城市', + places: '地点', + transport: '交通', + things: '物品', + noise: '噪声' + }, + + // Sounds + sounds: { + nature: { + river: '河流', + waves: '波浪', + campfire: '篝火', + wind: '风声', + howlingWind: '呼啸的风', + windInTrees: '林中风声', + waterfall: '瀑布', + walkInSnow: '雪地漫步', + walkOnLeaves: '落叶漫步', + walkOnGravel: '碎石漫步', + droplets: '水滴', + jungle: '丛林' + }, + rain: { + lightRain: '小雨', + moderateRain: '中雨', + heavyRain: '大雨', + storm: '暴风雨', + thunder: '雷声', + distantStorm: '远处的暴风雨', + fire: '火焰', + oceanWaves: '海浪', + rainOnLeaves: '雨打叶子', + rainOnPavement: '雨打路面', + rainOnWindow: '雨打窗户', + rainOnUmbrella: '雨打雨伞', + rainOnTent: '雨打帐篷', + insideRain: '室内雨声', + carRain: '车中雨声' + }, + animals: { + birds: '鸟儿', + seagulls: '海鸥', + crickets: '蟋蟀', + wolves: '狼群', + owl: '猫头鹰', + frogs: '青蛙', + dogs: '狗狗', + horses: '马儿', + cats: '猫咪', + crows: '乌鸦', + whale: '鲸鱼', + beehive: '蜂箱', + woodpecker: '啄木鸟', + chickens: '小鸡', + cows: '奶牛', + sheep: '绵羊', + rooster: '公鸡', + birdsMorning: '清晨鸟鸣', + birdsEvening: '傍晚鸟鸣' + }, + urban: { + cityStreet: '城市街道', + traffic: '交通', + highway: '高速公路', + road: '马路', + ambulanceSiren: '救护车警报', + busyStreet: '繁忙街道', + crowd: '人群', + fireworks: '烟花' + }, + places: { + forest: '森林', + beach: '海滩', + park: '公园', + mountain: '山脉', + desert: '沙漠', + cave: '洞穴', + meadow: '草原', + lake: '湖泊', + campsite: '露营地', + temple: '寺庙', + airport: '机场', + church: '教堂', + underwater: '水下', + crowdedBar: '拥挤酒吧', + nightVillage: '夜晚村庄', + carousel: '旋转木马', + laboratory: '实验室', + laundryRoom: '洗衣房', + subwayStation: '地铁站', + cafe: '咖啡馆', + constructionSite: '施工现场', + office: '办公室', + supermarket: '超市', + restaurant: '餐厅', + library: '图书馆' + }, + transport: { + car: '汽车', + bus: '公交车', + train: '火车', + subway: '地铁', + airplane: '飞机', + boat: '小船', + bicycle: '自行车', + motorcycle: '摩托车', + helicopter: '直升机', + steamTrain: '蒸汽火车', + insideTrain: '火车内部', + submarine: '潜水艇', + sailboat: '帆船', + rowingBoat: '划船' + }, + things: { + fan: '风扇', + clock: '时钟', + typewriter: '打字机', + keyboard: '键盘', + printer: '打印机', + refrigerator: '冰箱', + washingMachine: '洗衣机', + vacuum: '吸尘器', + airConditioner: '空调', + microwave: '微波炉', + paper: '纸张', + windChimes: '风铃', + singingBowl: '颂钵', + ceilingFan: '吊扇', + dryer: '干衣机', + slideProjector: '幻灯机', + boilingWater: '沸腾的水', + bubbles: '泡泡', + tuningRadio: '调谐收音机', + morseCode: '摩斯电码', + vinylEffect: '黑胶唱片效果', + windshieldWipers: '雨刮器' + }, + noise: { + whiteNoise: '白噪声', + pinkNoise: '粉噪声', + brownNoise: '棕噪声', + blueNoise: '蓝噪声', + violetNoise: '紫噪声', + greyNoise: '灰噪声' + } + }, + + // Common + play: '播放', + pause: '暂停', + favorite: '收藏', + volume: '音量', + selected_sounds: '当前声音', + + // Support & Donate + supportMe: '支持我', + helpKeepAdFree: '帮助我保持 Moodist 无广告。', + donateToday: '立即捐赠', + buyMeACoffee: '请我喝杯咖啡', + createdBy: '作者', + enjoyMoodist: '喜欢 Moodist 吗?', + supportWithDonation: '欢迎捐赠支持!', + + // UI Actions + showMore: '更多', + showLess: '收起', + + // Settings + globalVolume: '全局音量', + menu: '菜单', + + // Menu Items + breathingExercise: '呼吸练习', + countdownTimer: '倒计时器', + sleepTimer: '睡眠定时器', + pomodoro: '番茄钟', + notepad: '记事本', + todoChecklist: '待办清单', + lofiMusicPlayer: 'Lofi 音乐播放器', + binauralBeats: '双节拍', + isochronicTones: '等时音', + shortcuts: '快捷键', + shuffleSounds: '随机播放', + sourceCode: '源代码' + } +}; + +export function getTranslation(lang: string = 'en'): Translations { + return translations[lang] || translations.en; +} \ No newline at end of file diff --git a/src/data/sounds/animals.tsx b/src/data/sounds/animals.tsx index c248930..c587c95 100644 --- a/src/data/sounds/animals.tsx +++ b/src/data/sounds/animals.tsx @@ -31,98 +31,115 @@ export const animals: Category = { icon: , id: 'birds', label: 'Birds', + dataI18n: 'sounds.animals.birds', src: getAssetPath('/sounds/animals/birds.mp3'), }, { icon: , id: 'seagulls', label: 'Seagulls', + dataI18n: 'sounds.animals.seagulls', src: getAssetPath('/sounds/animals/seagulls.mp3'), }, { icon: , id: 'crickets', label: 'Crickets', + dataI18n: 'sounds.animals.crickets', src: getAssetPath('/sounds/animals/crickets.mp3'), }, { icon: , id: 'wolf', label: 'Wolf', + dataI18n: 'sounds.animals.wolves', src: getAssetPath('/sounds/animals/wolf.mp3'), }, { icon: , id: 'owl', label: 'Owl', + dataI18n: 'sounds.animals.owl', src: getAssetPath('/sounds/animals/owl.mp3'), }, { icon: , id: 'frog', label: 'Frog', + dataI18n: 'sounds.animals.frogs', src: getAssetPath('/sounds/animals/frog.mp3'), }, { icon: , id: 'dog-barking', label: 'Dog Barking', + dataI18n: 'sounds.animals.dogs', src: getAssetPath('/sounds/animals/dog-barking.mp3'), }, { icon: , id: 'horse-gallop', label: 'Horse Gallop', + dataI18n: 'sounds.animals.horses', src: getAssetPath('/sounds/animals/horse-gallop.mp3'), }, { icon: , id: 'cat-purring', label: 'Cat Purring', + dataI18n: 'sounds.animals.cats', src: getAssetPath('/sounds/animals/cat-purring.mp3'), }, { icon: , id: 'crows', label: 'Crows', + dataI18n: 'sounds.animals.crows', src: getAssetPath('/sounds/animals/crows.mp3'), }, { icon: , id: 'whale', label: 'Whale', + dataI18n: 'sounds.animals.whale', src: getAssetPath('/sounds/animals/whale.mp3'), }, { icon: , id: 'beehive', label: 'Beehive', + dataI18n: 'sounds.animals.beehive', src: getAssetPath('/sounds/animals/beehive.mp3'), }, { icon: , id: 'woodpecker', label: 'Woodpecker', + dataI18n: 'sounds.animals.woodpecker', src: getAssetPath('/sounds/animals/woodpecker.mp3'), }, { icon: , id: 'chickens', label: 'Chickens', + dataI18n: 'sounds.animals.chickens', src: getAssetPath('/sounds/animals/chickens.mp3'), }, { icon: , id: 'cows', label: 'Cows', + dataI18n: 'sounds.animals.cows', src: getAssetPath('/sounds/animals/cows.mp3'), }, { icon: , id: 'sheep', label: 'Sheep', + dataI18n: 'sounds.animals.sheep', src: getAssetPath('/sounds/animals/sheep.mp3'), }, ], title: 'Animals', + dataI18n: 'categories.animals', }; diff --git a/src/data/sounds/nature.tsx b/src/data/sounds/nature.tsx index e3d5b8d..20eebf6 100644 --- a/src/data/sounds/nature.tsx +++ b/src/data/sounds/nature.tsx @@ -21,74 +21,87 @@ export const nature: Category = { icon: , id: 'river', label: 'River', + dataI18n: 'sounds.nature.river', src: getAssetPath('/sounds/nature/river.mp3'), }, { icon: , id: 'waves', label: 'Waves', + dataI18n: 'sounds.nature.waves', src: getAssetPath('/sounds/nature/waves.mp3'), }, { icon: , id: 'campfire', label: 'Campfire', + dataI18n: 'sounds.nature.campfire', src: getAssetPath('/sounds/nature/campfire.mp3'), }, { icon: , id: 'wind', label: 'Wind', + dataI18n: 'sounds.nature.wind', src: getAssetPath('/sounds/nature/wind.mp3'), }, { icon: , id: 'howling-wind', label: 'Howling Wind', + dataI18n: 'sounds.nature.howlingWind', src: getAssetPath('/sounds/nature/howling-wind.mp3'), }, { icon: , id: 'wind-in-trees', label: 'Wind in Trees', + dataI18n: 'sounds.nature.windInTrees', src: getAssetPath('/sounds/nature/wind-in-trees.mp3'), }, { icon: , id: 'waterfall', label: 'Waterfall', + dataI18n: 'sounds.nature.waterfall', src: getAssetPath('/sounds/nature/waterfall.mp3'), }, { icon: , id: 'walk-in-snow', label: 'Walk in Snow', + dataI18n: 'sounds.nature.walkInSnow', src: getAssetPath('/sounds/nature/walk-in-snow.mp3'), }, { icon: , id: 'walk-on-leaves', label: 'Walk on Leaves', + dataI18n: 'sounds.nature.walkOnLeaves', src: getAssetPath('/sounds/nature/walk-on-leaves.mp3'), }, { icon: , id: 'walk-on-gravel', label: 'Walk on Gravel', + dataI18n: 'sounds.nature.walkOnGravel', src: getAssetPath('/sounds/nature/walk-on-gravel.mp3'), }, { icon: , id: 'droplets', label: 'Droplets', + dataI18n: 'sounds.nature.droplets', src: getAssetPath('/sounds/nature/droplets.mp3'), }, { icon: , id: 'jungle', label: 'Jungle', + dataI18n: 'sounds.nature.jungle', src: getAssetPath('/sounds/nature/jungle.mp3'), }, ], title: 'Nature', + dataI18n: 'categories.nature', }; diff --git a/src/data/sounds/noise.tsx b/src/data/sounds/noise.tsx index e0e7f39..d91f9ab 100644 --- a/src/data/sounds/noise.tsx +++ b/src/data/sounds/noise.tsx @@ -13,20 +13,24 @@ export const noise: Category = { icon: , id: 'white-noise', label: 'White Noise', + dataI18n: 'sounds.noise.whiteNoise', src: getAssetPath('/sounds/noise/white-noise.wav'), }, { icon: , id: 'pink-noise', label: 'Pink Noise', + dataI18n: 'sounds.noise.pinkNoise', src: getAssetPath('/sounds/noise/pink-noise.wav'), }, { icon: , id: 'brown-noise', label: 'Brown Noise', + dataI18n: 'sounds.noise.brownNoise', src: getAssetPath('/sounds/noise/brown-noise.wav'), }, ], title: 'Noise', + dataI18n: 'categories.noise', }; diff --git a/src/data/sounds/places.tsx b/src/data/sounds/places.tsx index c63ed2b..ff462d3 100644 --- a/src/data/sounds/places.tsx +++ b/src/data/sounds/places.tsx @@ -28,98 +28,115 @@ export const places: Category = { icon: , id: 'cafe', label: 'Cafe', + dataI18n: 'sounds.places.cafe', src: getAssetPath('/sounds/places/cafe.mp3'), }, { icon: , id: 'airport', label: 'Airport', + dataI18n: 'sounds.places.airport', src: getAssetPath('/sounds/places/airport.mp3'), }, { icon: , id: 'church', label: 'Church', + dataI18n: 'sounds.places.church', src: getAssetPath('/sounds/places/church.mp3'), }, { icon: , id: 'temple', label: 'Temple', + dataI18n: 'sounds.places.temple', src: getAssetPath('/sounds/places/temple.mp3'), }, { icon: , id: 'construction-site', label: 'Construction Site', + dataI18n: 'sounds.places.constructionSite', src: getAssetPath('/sounds/places/construction-site.mp3'), }, { icon: , id: 'underwater', label: 'Underwater', + dataI18n: 'sounds.places.underwater', src: getAssetPath('/sounds/places/underwater.mp3'), }, { icon: , id: 'crowded-bar', label: 'Crowded Bar', + dataI18n: 'sounds.places.crowdedBar', src: getAssetPath('/sounds/places/crowded-bar.mp3'), }, { icon: , id: 'night-village', label: 'Night Village', + dataI18n: 'sounds.places.nightVillage', src: getAssetPath('/sounds/places/night-village.mp3'), }, { icon: , id: 'subway-station', label: 'Subway Station', + dataI18n: 'sounds.places.subwayStation', src: getAssetPath('/sounds/places/subway-station.mp3'), }, { icon: , id: 'office', label: 'Office', + dataI18n: 'sounds.places.office', src: getAssetPath('/sounds/places/office.mp3'), }, { icon: , id: 'supermarket', label: 'Supermarket', + dataI18n: 'sounds.places.supermarket', src: getAssetPath('/sounds/places/supermarket.mp3'), }, { icon: , id: 'carousel', label: 'Carousel', + dataI18n: 'sounds.places.carousel', src: getAssetPath('/sounds/places/carousel.mp3'), }, { icon: , id: 'laboratory', label: 'Laboratory', + dataI18n: 'sounds.places.laboratory', src: getAssetPath('/sounds/places/laboratory.mp3'), }, { icon: , id: 'laundry-room', label: 'Laundry Room', + dataI18n: 'sounds.places.laundryRoom', src: getAssetPath('/sounds/places/laundry-room.mp3'), }, { icon: , id: 'restaurant', label: 'Restaurant', + dataI18n: 'sounds.places.restaurant', src: getAssetPath('/sounds/places/restaurant.mp3'), }, { icon: , id: 'library', label: 'Library', + dataI18n: 'sounds.places.library', src: getAssetPath('/sounds/places/library.mp3'), }, ], title: 'Places', + dataI18n: 'categories.places', }; diff --git a/src/data/sounds/rain.tsx b/src/data/sounds/rain.tsx index 1276ef6..6704a5a 100644 --- a/src/data/sounds/rain.tsx +++ b/src/data/sounds/rain.tsx @@ -20,50 +20,73 @@ export const rain: Category = { icon: , id: 'light-rain', label: 'Light Rain', + dataI18n: 'sounds.rain.lightRain', src: getAssetPath('/sounds/rain/light-rain.mp3'), }, + { + icon: , + id: 'moderate-rain', + label: 'Moderate Rain', + dataI18n: 'sounds.rain.moderateRain', + src: getAssetPath('/sounds/rain/moderate-rain.mp3'), + }, { icon: , id: 'heavy-rain', label: 'Heavy Rain', + dataI18n: 'sounds.rain.heavyRain', src: getAssetPath('/sounds/rain/heavy-rain.mp3'), }, + { + icon: , + id: 'storm', + label: 'Storm', + dataI18n: 'sounds.rain.storm', + src: getAssetPath('/sounds/rain/storm.mp3'), + }, { icon: , id: 'thunder', label: 'Thunder', + dataI18n: 'sounds.rain.thunder', src: getAssetPath('/sounds/rain/thunder.mp3'), }, { icon: , id: 'rain-on-window', label: 'Rain on Window', + dataI18n: 'sounds.rain.rainOnWindow', src: getAssetPath('/sounds/rain/rain-on-window.mp3'), }, { icon: , id: 'rain-on-car-roof', label: 'Rain on Car Roof', + dataI18n: 'sounds.rain.carRain', src: getAssetPath('/sounds/rain/rain-on-car-roof.mp3'), }, { icon: , id: 'rain-on-umbrella', label: 'Rain on Umbrella', + dataI18n: 'sounds.rain.rainOnUmbrella', src: getAssetPath('/sounds/rain/rain-on-umbrella.mp3'), }, { icon: , id: 'rain-on-tent', label: 'Rain on Tent', + dataI18n: 'sounds.rain.rainOnTent', src: getAssetPath('/sounds/rain/rain-on-tent.mp3'), }, { icon: , id: 'rain-on-leaves', label: 'Rain on Leaves', + dataI18n: 'sounds.rain.rainOnLeaves', src: getAssetPath('/sounds/rain/rain-on-leaves.mp3'), }, ], title: 'Rain', + dataI18n: 'categories.rain', }; diff --git a/src/data/sounds/things.tsx b/src/data/sounds/things.tsx index 3b58b2f..3f9ba4c 100644 --- a/src/data/sounds/things.tsx +++ b/src/data/sounds/things.tsx @@ -24,98 +24,115 @@ export const things: Category = { icon: , id: 'keyboard', label: 'Keyboard', + dataI18n: 'sounds.things.keyboard', src: getAssetPath('/sounds/things/keyboard.mp3'), }, { icon: , id: 'typewriter', label: 'Typewriter', + dataI18n: 'sounds.things.typewriter', src: getAssetPath('/sounds/things/typewriter.mp3'), }, { icon: , id: 'paper', label: 'Paper', + dataI18n: 'sounds.things.paper', src: getAssetPath('/sounds/things/paper.mp3'), }, { icon: , id: 'clock', label: 'Clock', + dataI18n: 'sounds.things.clock', src: getAssetPath('/sounds/things/clock.mp3'), }, { icon: , id: 'wind-chimes', label: 'Wind Chimes', + dataI18n: 'sounds.things.windChimes', src: getAssetPath('/sounds/things/wind-chimes.mp3'), }, { icon: , id: 'singing-bowl', label: 'Singing Bowl', + dataI18n: 'sounds.things.singingBowl', src: getAssetPath('/sounds/things/singing-bowl.mp3'), }, { icon: , id: 'ceiling-fan', label: 'Ceiling Fan', + dataI18n: 'sounds.things.ceilingFan', src: getAssetPath('/sounds/things/ceiling-fan.mp3'), }, { icon: , id: 'dryer', label: 'Dryer', + dataI18n: 'sounds.things.dryer', src: getAssetPath('/sounds/things/dryer.mp3'), }, { icon: , id: 'slide-projector', label: 'Slide Projector', + dataI18n: 'sounds.things.slideProjector', src: getAssetPath('/sounds/things/slide-projector.mp3'), }, { icon: , id: 'boiling-water', label: 'Boiling Water', + dataI18n: 'sounds.things.boilingWater', src: getAssetPath('/sounds/things/boiling-water.mp3'), }, { icon: , id: 'bubbles', label: 'Bubbles', + dataI18n: 'sounds.things.bubbles', src: getAssetPath('/sounds/things/bubbles.mp3'), }, { icon: , id: 'tuning-radio', label: 'Tuning Radio', + dataI18n: 'sounds.things.tuningRadio', src: getAssetPath('/sounds/things/tuning-radio.mp3'), }, { icon: , id: 'morse-code', label: 'Morse Code', + dataI18n: 'sounds.things.morseCode', src: getAssetPath('/sounds/things/morse-code.mp3'), }, { icon: , id: 'washing-machine', label: 'Washing Machine', + dataI18n: 'sounds.things.washingMachine', src: getAssetPath('/sounds/things/washing-machine.mp3'), }, { icon: , id: 'vinyl-effect', label: 'Vinyl Effect', + dataI18n: 'sounds.things.vinylEffect', src: getAssetPath('/sounds/things/vinyl-effect.mp3'), }, { icon: , id: 'windshield-wipers', label: 'Windshield Wipers', + dataI18n: 'sounds.things.windshieldWipers', src: getAssetPath('/sounds/things/windshield-wipers.mp3'), }, ], title: 'Things', + dataI18n: 'categories.things', }; diff --git a/src/data/sounds/transport.tsx b/src/data/sounds/transport.tsx index 62d2579..d74b302 100644 --- a/src/data/sounds/transport.tsx +++ b/src/data/sounds/transport.tsx @@ -15,38 +15,45 @@ export const transport: Category = { icon: , id: 'train', label: 'Train', + dataI18n: 'sounds.transport.train', src: getAssetPath('/sounds/transport/train.mp3'), }, { icon: , id: 'inside-a-train', label: 'Inside a Train', + dataI18n: 'sounds.transport.insideTrain', src: getAssetPath('/sounds/transport/inside-a-train.mp3'), }, { icon: , id: 'airplane', label: 'Airplane', + dataI18n: 'sounds.transport.airplane', src: getAssetPath('/sounds/transport/airplane.mp3'), }, { icon: , id: 'submarine', label: 'Submarine', + dataI18n: 'sounds.transport.submarine', src: getAssetPath('/sounds/transport/submarine.mp3'), }, { icon: , id: 'sailboat', label: 'Sailboat', + dataI18n: 'sounds.transport.sailboat', src: getAssetPath('/sounds/transport/sailboat.mp3'), }, { icon: , id: 'rowing-boat', label: 'Rowing Boat', + dataI18n: 'sounds.transport.rowingBoat', src: getAssetPath('/sounds/transport/rowing-boat.mp3'), }, ], title: 'Transport', + dataI18n: 'categories.transport', }; diff --git a/src/data/sounds/urban.tsx b/src/data/sounds/urban.tsx index 142745e..edfb861 100644 --- a/src/data/sounds/urban.tsx +++ b/src/data/sounds/urban.tsx @@ -16,44 +16,52 @@ export const urban: Category = { icon: , id: 'highway', label: 'Highway', + dataI18n: 'sounds.urban.highway', src: getAssetPath('/sounds/urban/highway.mp3'), }, { icon: , id: 'road', label: 'Road', + dataI18n: 'sounds.urban.road', src: getAssetPath('/sounds/urban/road.mp3'), }, { icon: , id: 'ambulance-siren', label: 'Ambulance Siren', + dataI18n: 'sounds.urban.ambulanceSiren', src: getAssetPath('/sounds/urban/ambulance-siren.mp3'), }, { icon: , id: 'busy-street', label: 'Busy Street', + dataI18n: 'sounds.urban.busyStreet', src: getAssetPath('/sounds/urban/busy-street.mp3'), }, { icon: , id: 'crowd', label: 'Crowd', + dataI18n: 'sounds.urban.crowd', src: getAssetPath('/sounds/urban/crowd.mp3'), }, { icon: , id: 'traffic', label: 'Traffic', + dataI18n: 'sounds.urban.traffic', src: getAssetPath('/sounds/urban/traffic.mp3'), }, { icon: , id: 'fireworks', label: 'Fireworks', + dataI18n: 'sounds.urban.fireworks', src: getAssetPath('/sounds/urban/fireworks.mp3'), }, ], title: 'Urban', + dataI18n: 'categories.urban', }; diff --git a/src/data/types.d.ts b/src/data/types.d.ts index 515d516..47091da 100644 --- a/src/data/types.d.ts +++ b/src/data/types.d.ts @@ -3,6 +3,7 @@ export interface Sound { id: string; label: string; src: string; + dataI18n?: string; } export type Sounds = Array; @@ -12,6 +13,7 @@ export interface Category { id: string; sounds: Sounds; title: string; + dataI18n?: string; } export type Categories = Array; diff --git a/src/helpers/translation.ts b/src/helpers/translation.ts new file mode 100644 index 0000000..fdcf088 --- /dev/null +++ b/src/helpers/translation.ts @@ -0,0 +1,18 @@ +import { useTranslation } from '@/hooks/useTranslation'; + +export function useTranslatedSounds() { + const { t } = useTranslation(); + + const translateCategory = (category: string) => { + return t(`categories.${category.toLowerCase()}`); + }; + + const translateSound = (category: string, soundId: string) => { + return t(`sounds.${category.toLowerCase()}.${soundId}`); + }; + + return { + translateCategory, + translateSound, + }; +} \ No newline at end of file diff --git a/src/hooks/use-sound.ts b/src/hooks/use-sound.ts index 934092d..ed8f247 100644 --- a/src/hooks/use-sound.ts +++ b/src/hooks/use-sound.ts @@ -17,6 +17,7 @@ import { FADE_OUT } from '@/constants/events'; * @param {Object} [options] - Options for sound playback. * @param {boolean} [options.loop=false] - Whether the sound should loop. * @param {number} [options.volume=0.5] - The initial volume of the sound, ranging from 0.0 to 1.0. + * @param {number} [options.speed=1.0] - The initial playback speed of the sound, ranging from 0.5 to 2.0. * @returns {{ play: () => void, stop: () => void, pause: () => void, fadeOut: (duration: number) => void, isLoading: boolean }} An object containing control functions for the sound: * - play: Function to play the sound. * - stop: Function to stop the sound. @@ -26,7 +27,7 @@ import { FADE_OUT } from '@/constants/events'; */ export function useSound( src: string, - options: { loop?: boolean; preload?: boolean; volume?: number } = {}, + options: { loop?: boolean; preload?: boolean; volume?: number; speed?: number } = {}, html5: boolean = false, ) { const [hasLoaded, setHasLoaded] = useState(false); @@ -37,7 +38,7 @@ export function useSound( const sound = useMemo(() => { let sound: Howl | null = null; - if (isBrowser) { + if (isBrowser && src) { sound = new Howl({ html5, onload: () => { @@ -45,7 +46,7 @@ export function useSound( setHasLoaded(true); }, preload: options.preload ?? false, - src: src, + src: [src], // Howler.js 期望 src 是数组格式 }); } @@ -62,6 +63,10 @@ export function useSound( if (sound) sound.volume(options.volume ?? 0.5); }, [sound, options.volume]); + useEffect(() => { + if (sound) sound.rate(options.speed ?? 1.0); + }, [sound, options.speed]); + const play = useCallback( (cb?: () => void) => { if (sound) { diff --git a/src/hooks/useLanguage.ts b/src/hooks/useLanguage.ts new file mode 100644 index 0000000..1d047ee --- /dev/null +++ b/src/hooks/useLanguage.ts @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react'; + +export function useLanguage() { + const [currentLang, setCurrentLang] = useState('en'); + + useEffect(() => { + // Detect language from URL path first + const pathname = window.location.pathname; + const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/'); + + // Then check URL parameter + const urlParams = new URLSearchParams(window.location.search); + const urlLang = urlParams.get('lang'); + + // Finally check saved preference + const savedLang = localStorage.getItem('moodist-language'); + + const lang = urlLang || (isZhPage ? 'zh-CN' : savedLang) || 'en'; + setCurrentLang(lang); + }, []); + + const changeLanguage = (lang: string) => { + setCurrentLang(lang); + localStorage.setItem('moodist-language', lang); + + // Update URL and navigate if needed + const url = new URL(window.location.href); + + if (lang === 'zh-CN') { + // Navigate to Chinese page + window.location.href = `${window.location.origin}/zh`; + } else { + // Navigate to English page + window.location.href = `${window.location.origin}/`; + } + }; + + return { + currentLang, + changeLanguage, + isChinese: currentLang === 'zh-CN' + }; +} \ No newline at end of file diff --git a/src/hooks/useLocalizedSounds.ts b/src/hooks/useLocalizedSounds.ts new file mode 100644 index 0000000..bcc2b5b --- /dev/null +++ b/src/hooks/useLocalizedSounds.ts @@ -0,0 +1,35 @@ +import { useMemo } from 'react'; +import { sounds } from '@/data/sounds'; +import type { Category, Sound } from '@/data/types'; +import { useTranslation } from './useTranslation'; + +export function useLocalizedSounds() { + const { t } = useTranslation(); + + const localizedSounds = useMemo(() => { + return sounds.categories.map((category: Category): Category => { + // Translate category title + const categoryKey = `categories.${category.id}`; + const translatedTitle = t(categoryKey as any); + + // Translate sounds + const translatedSounds = category.sounds.map((sound: Sound): Sound => { + if (sound.dataI18n) { + return { + ...sound, + label: t(sound.dataI18n as any) || sound.label + }; + } + return sound; + }); + + return { + ...category, + title: translatedTitle, + sounds: translatedSounds + }; + }); + }, [t]); + + return localizedSounds; +} \ No newline at end of file diff --git a/src/hooks/useNotification.ts b/src/hooks/useNotification.ts new file mode 100644 index 0000000..d0f884b --- /dev/null +++ b/src/hooks/useNotification.ts @@ -0,0 +1,38 @@ +import { useState } from 'react'; + +interface NotificationState { + showNotification: boolean; + notificationMessage: string; + notificationType: 'success' | 'error'; +} + +export function useNotification() { + const [state, setState] = useState({ + showNotification: false, + notificationMessage: '', + notificationType: 'success' + }); + + const showNotificationMessage = (message: string, type: 'success' | 'error') => { + setState({ + showNotification: true, + notificationMessage: message, + notificationType: type + }); + + // 3秒后自动关闭 + setTimeout(() => { + setState(prev => ({ ...prev, showNotification: false })); + }, 3000); + }; + + const hideNotification = () => { + setState(prev => ({ ...prev, showNotification: false })); + }; + + return { + ...state, + showNotificationMessage, + hideNotification + }; +} \ No newline at end of file diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts new file mode 100644 index 0000000..901008c --- /dev/null +++ b/src/hooks/useTranslation.ts @@ -0,0 +1,43 @@ +import { useLanguage } from './useLanguage'; +import { getTranslation } from '@/data/i18n'; +import type { Translations } from '@/data/i18n'; + +export function useTranslation() { + const { currentLang, isChinese, changeLanguage } = useLanguage(); + const translations: Translations = getTranslation(currentLang); + + const t = (key: keyof Translations, params?: Record) => { + let translation = getNestedValue(translations, key) as string; + + if (typeof translation !== 'string') { + // Fallback to English if translation is missing + const englishTranslations = getTranslation('en'); + translation = getNestedValue(englishTranslations, key) as string; + } + + if (typeof translation !== 'string') { + return key; // Return key if no translation found + } + + // Replace parameters like {{count}} + if (params) { + return translation.replace(/\{\{(\w+)\}\}/g, (match, param) => { + return params[param]?.toString() || match; + }); + } + + return translation; + }; + + return { + t, + currentLang, + isChinese, + changeLanguage, + translations + }; +} + +function getNestedValue(obj: any, path: string) { + return path.split('.').reduce((current, key) => current?.[key], obj); +} \ No newline at end of file diff --git a/src/layouts/layout.astro b/src/layouts/layout.astro index bbc64e6..2c9d413 100644 --- a/src/layouts/layout.astro +++ b/src/layouts/layout.astro @@ -2,6 +2,7 @@ import { pwaInfo } from 'virtual:pwa-info'; // eslint-disable-line import { Reload } from '@/components/reload'; +import { LanguageSwitcher } from '@/components/language-switcher'; import { count } from '@/lib/sounds'; @@ -16,10 +17,15 @@ const title = Astro.props.title || 'Moodist: Ambient Sounds for Focus and Calm'; const description = Astro.props.description || `Moodist is a free and open-source ambient sound generator featuring ${count()} carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.`; + +// Get language from URL parameter +const url = Astro.url; +const langParam = url.searchParams.get('lang') || 'en'; +const lang = ['en', 'zh-CN'].includes(langParam) ? langParam : 'en'; --- - + @@ -40,11 +46,20 @@ const description = + + + + + {pwaInfo && } + + + diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts new file mode 100644 index 0000000..ff461f1 --- /dev/null +++ b/src/lib/api-client.ts @@ -0,0 +1,135 @@ +import { useAuthStore } from '@/stores/auth'; + +/** + * API客户端辅助函数,自动添加JWT Authorization头 + */ +export class ApiClient { + /** + * 发起API请求 + * @param url - API URL + * @param options - fetch options + * @returns Promise + */ + static async fetch(url: string, options: RequestInit = {}): Promise { + // 获取token - 尝试多种方式获取Zustand store + let token = null; + + try { + // 方法1: 通过useAuthStore.getState()获取 + token = useAuthStore.getState().getToken(); + console.log('🔐 方法1获取token结果:', token ? '成功' : '失败'); + } catch (e) { + console.warn('无法通过useAuthStore.getState()获取token:', e); + } + + // 如果方法1失败,尝试方法2: 从localStorage直接获取 + if (!token) { + try { + const authStorage = localStorage.getItem('auth-storage'); + console.log('🔐 localStorage auth-storage:', authStorage ? '存在' : '不存在'); + if (authStorage) { + const parsed = JSON.parse(authStorage); + token = parsed.state?.token; + console.log('🔐 方法2获取token结果:', token ? '成功' : '失败'); + } + } catch (e) { + console.warn('无法从localStorage获取token:', e); + } + } + + // 创建新的headers对象 + const headers = new Headers(options.headers || {}); + + // 添加Content-Type(如果没有的话) + if (!headers.has('Content-Type') && (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH')) { + headers.set('Content-Type', 'application/json'); + } + + // 添加Authorization头(如果有token) + if (token) { + headers.set('Authorization', `Bearer ${token}`); + console.log('🔑 已添加Authorization头,URL:', url); + } else { + console.warn('⚠️ 没有找到token,请求URL:', url); + } + + // 发起请求 + const response = await fetch(url, { + ...options, + headers, + }); + + console.log('📡 API响应:', url, response.status); + return response; + } + + /** + * 发起POST请求 + * @param url - API URL + * @param data - 请求数据 + * @returns Promise + */ + static async post(url: string, data?: any): Promise { + return this.fetch(url, { + method: 'POST', + body: data ? JSON.stringify(data) : undefined, + }); + } + + /** + * 发起GET请求 + * @param url - API URL + * @returns Promise + */ + static async get(url: string): Promise { + return this.fetch(url, { + method: 'GET', + }); + } + + /** + * 发起PUT请求 + * @param url - API URL + * @param data - 请求数据 + * @returns Promise + */ + static async put(url: string, data?: any): Promise { + return this.fetch(url, { + method: 'PUT', + body: data ? JSON.stringify(data) : undefined, + }); + } + + /** + * 发起DELETE请求 + * @param url - API URL + * @returns Promise + */ + static async delete(url: string): Promise { + return this.fetch(url, { + method: 'DELETE', + }); + } +} + +/** + * 简化的API调用函数 + * @param url - API URL + * @param data - 请求数据 + * @param method - HTTP方法 + * @returns Promise + */ +export async function apiCall(url: string, data?: any, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'POST'): Promise { + const response = await ApiClient.fetch(url, { + method, + body: data ? JSON.stringify(data) : undefined, + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || 'API 调用失败'); + } + + return result; +} \ No newline at end of file diff --git a/src/lib/auth-middleware.ts b/src/lib/auth-middleware.ts new file mode 100644 index 0000000..d2ecc95 --- /dev/null +++ b/src/lib/auth-middleware.ts @@ -0,0 +1,166 @@ +import type { APIRoute } from 'astro'; +import { authenticateUser } from '@/lib/database'; + +/** + * 认证中间件 - 统一处理用户身份验证 + * @param request - Astro请求对象 + * @returns 认证结果对象 + */ +export interface AuthResult { + success: boolean; + user?: { + id: number; + username: string; + }; + error?: { + message: string; + status: number; + }; + data?: any; +} + +/** + * 验证请求并解析JSON数据 + * @param request - Astro请求对象 + * @param requiredFields - 必需的字段数组 + * @returns 认证结果 + */ +export async function authenticateRequest( + request: Request, + requiredFields: string[] = ['username', 'password'] +): Promise { + try { + // 验证请求体 + const body = await request.text(); + if (!body.trim()) { + return { + success: false, + error: { + message: '请求体不能为空', + status: 400 + } + }; + } + + // 解析JSON + let data; + try { + data = JSON.parse(body); + } catch (parseError) { + return { + success: false, + error: { + message: '请求格式错误,请检查JSON格式', + status: 400 + } + }; + } + + // 验证必需字段 + const missingFields = requiredFields.filter(field => { + // 对于密码字段,我们允许空字符串,但不允许undefined/null + if (field === 'password') { + return data[field] === undefined || data[field] === null; + } + return !data[field]; + }); + if (missingFields.length > 0) { + return { + success: false, + error: { + message: `缺少必需字段: ${missingFields.join(', ')}`, + status: 400 + } + }; + } + + // 验证用户身份 + const user = authenticateUser(data.username, data.password); + if (!user) { + return { + success: false, + error: { + message: '用户认证失败,请检查用户名和密码', + status: 401 + } + }; + } + + return { + success: true, + user: { + id: user.id, + username: user.username + }, + data + }; + + } catch (error) { + console.error('认证过程出错:', error); + return { + success: false, + error: { + message: '服务器内部错误', + status: 500 + } + }; + } +} + +/** + * 创建标准化的API响应 + * @param success - 是否成功 + * @param data - 响应数据 + * @param message - 响应消息 + * @param status - HTTP状态码 + * @returns Response对象 + */ +export function createApiResponse( + success: boolean, + data?: any, + message?: string, + status: number = 200 +): Response { + const responseBody = { + success, + ...(message && { message }), + ...(data && data) + }; + + return new Response(JSON.stringify(responseBody), { + status, + headers: { 'Content-Type': 'application/json' } + }); +} + +/** + * 创建错误响应 + * @param message - 错误消息 + * @param status - HTTP状态码 + * @returns Response对象 + */ +export function createErrorResponse(message: string, status: number = 500): Response { + return createApiResponse(false, undefined, message, status); +} + +/** + * 处理API错误的统一函数 + * @param error - 错误对象 + * @param operation - 操作描述 + * @returns Response对象 + */ +export function handleApiError(error: unknown, operation: string): Response { + console.error(`${operation}错误:`, error); + + let errorMessage = `${operation}失败,请稍后再试`; + let status = 500; + + if (error instanceof SyntaxError && error.message.includes('JSON')) { + errorMessage = '请求格式错误'; + status = 400; + } else if (error instanceof Error) { + errorMessage = error.message; + } + + return createErrorResponse(errorMessage, status); +} \ No newline at end of file diff --git a/src/lib/database.ts b/src/lib/database.ts new file mode 100644 index 0000000..50df099 --- /dev/null +++ b/src/lib/database.ts @@ -0,0 +1,213 @@ +import Database from 'better-sqlite3'; +import bcrypt from 'bcryptjs'; +import path from 'path'; +import fs from 'fs'; + +let db: Database.Database | null = null; + +export interface User { + id: number; + username: string; + password: string; + created_at: string; +} + +export interface CreateUserData { + username: string; + password: string; +} + +export interface SavedMusic { + id: number; + user_id: number; + name: string; + sounds: string; // JSON string of sound IDs + volume: string; // JSON string of volume settings + speed: string; // JSON string of speed settings + rate: string; // JSON string of rate settings + random_effects: string; // JSON string of random effects settings + created_at: string; + updated_at: string; +} + +export interface CreateMusicData { + user_id: number; + name: string; + sounds: string[]; + volume: Record; + speed: Record; + rate: Record; + random_effects: Record; +} + +export function getDatabase(): Database.Database { + if (!db) { + // 创建数据库文件路径 + const dbPath = path.join(process.cwd(), 'data', 'users.db'); + + // 确保目录存在 + const dbDir = path.dirname(dbPath); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + + // 以读写模式打开数据库,启用WAL模式提高并发性能 + db = new Database(dbPath, { + readonly: false, + fileMustExist: false, + verbose: console.log // 添加SQL执行日志 + }); + + // 启用WAL模式,提高并发写入性能 + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + db.pragma('cache_size = 1000'); + + // 创建用户表 + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // 创建音乐保存表 + db.exec(` + CREATE TABLE IF NOT EXISTS saved_music ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + sounds TEXT NOT NULL, + volume TEXT NOT NULL, + speed TEXT NOT NULL, + rate TEXT NOT NULL, + random_effects TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ) + `); + } + + return db; +} + +export async function createUser(userData: CreateUserData): Promise { + const database = getDatabase(); + + // 检查用户名是否已存在 + const existingUser = database.prepare('SELECT id FROM users WHERE username = ?').get(userData.username); + if (existingUser) { + throw new Error('用户名已存在'); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(userData.password, 10); + + // 创建用户 + const stmt = database.prepare('INSERT INTO users (username, password) VALUES (?, ?)'); + const result = stmt.run(userData.username, hashedPassword); + + // 返回创建的用户 + const user = database.prepare('SELECT id, username, created_at FROM users WHERE id = ?').get(result.lastInsertRowid) as User; + + return user; +} + +export function authenticateUser(username: string, password: string): User | null { + const database = getDatabase(); + + // 查找用户 + const user = database.prepare('SELECT id, username, password, created_at FROM users WHERE username = ?').get(username) as User & { password: string } | null; + + if (!user) { + return null; + } + + // 验证密码 + const isValidPassword = bcrypt.compareSync(password, user.password); + + if (!isValidPassword) { + return null; + } + + // 返回用户信息(不包含密码) + return { + id: user.id, + username: user.username, + created_at: user.created_at + }; +} + +export function getUserById(id: number): User | null { + const database = getDatabase(); + + const user = database.prepare('SELECT id, username, created_at FROM users WHERE id = ?').get(id) as User | null; + + return user; +} + +export function getUserByUsername(username: string): User | null { + const database = getDatabase(); + + const user = database.prepare('SELECT id, username, created_at FROM users WHERE username = ?').get(username) as User | null; + + return user; +} + +// 音乐保存相关函数 +export async function createMusic(musicData: CreateMusicData): Promise { + const database = getDatabase(); + + const stmt = database.prepare(` + INSERT INTO saved_music (user_id, name, sounds, volume, speed, rate, random_effects) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + + const result = stmt.run( + musicData.user_id, + musicData.name, + JSON.stringify(musicData.sounds), + JSON.stringify(musicData.volume), + JSON.stringify(musicData.speed), + JSON.stringify(musicData.rate), + JSON.stringify(musicData.random_effects) + ); + + const music = database.prepare('SELECT * FROM saved_music WHERE id = ?').get(result.lastInsertRowid) as SavedMusic; + + return music; +} + +export function getUserMusic(userId: number): SavedMusic[] { + const database = getDatabase(); + + const musicList = database.prepare('SELECT * FROM saved_music WHERE user_id = ? ORDER BY created_at DESC').all(userId) as SavedMusic[]; + + return musicList; +} + +export function updateMusicName(musicId: number, name: string, userId: number): boolean { + const database = getDatabase(); + + const stmt = database.prepare(` + UPDATE saved_music + SET name = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND user_id = ? + `); + + const result = stmt.run(name, musicId, userId); + + return result.changes > 0; +} + +export function deleteMusic(musicId: number, userId: number): boolean { + const database = getDatabase(); + + const stmt = database.prepare('DELETE FROM saved_music WHERE id = ? AND user_id = ?'); + const result = stmt.run(musicId, userId); + + return result.changes > 0; +} \ No newline at end of file diff --git a/src/lib/jwt-auth-middleware.ts b/src/lib/jwt-auth-middleware.ts new file mode 100644 index 0000000..68a531f --- /dev/null +++ b/src/lib/jwt-auth-middleware.ts @@ -0,0 +1,195 @@ +import type { APIRoute } from 'astro'; +import { verifyJWT, extractTokenFromHeader } from '@/lib/jwt'; + +export interface JWTAuthResult { + success: boolean; + user?: { + id: number; + username: string; + }; + error?: { + message: string; + status: number; + }; + data?: any; +} + +/** + * JWT认证中间件 - 验证请求中的JWT Token + * @param request - Astro请求对象 + * @returns 认证结果对象 + */ +export async function authenticateJWTRequest(request: Request): Promise { + try { + // 从Authorization头中提取token + const authHeader = request.headers.get('Authorization'); + const token = extractTokenFromHeader(authHeader); + + if (!token) { + return { + success: false, + error: { + message: '缺少授权令牌,请先登录', + status: 401 + } + }; + } + + // 验证JWT token + const payload = verifyJWT(token); + if (!payload) { + return { + success: false, + error: { + message: '授权令牌无效或已过期,请重新登录', + status: 401 + } + }; + } + + return { + success: true, + user: { + id: payload.userId, + username: payload.username + } + }; + + } catch (error) { + console.error('JWT认证过程出错:', error); + return { + success: false, + error: { + message: '认证服务异常', + status: 500 + } + }; + } +} + +/** + * 验证请求体并解析JSON数据 + * @param request - Astro请求对象 + * @param requiredFields - 必需的字段数组 + * @returns 解析结果 + */ +export async function parseRequestBody( + request: Request, + requiredFields: string[] = [] +): Promise<{ success: boolean; data?: any; error?: { message: string; status: number } }> { + try { + // 验证请求体 + const body = await request.text(); + if (!body.trim()) { + return { + success: false, + error: { + message: '请求体不能为空', + status: 400 + } + }; + } + + // 解析JSON + let data; + try { + data = JSON.parse(body); + } catch (parseError) { + return { + success: false, + error: { + message: '请求格式错误,请检查JSON格式', + status: 400 + } + }; + } + + // 验证必需字段 + const missingFields = requiredFields.filter(field => !data[field]); + if (missingFields.length > 0) { + return { + success: false, + error: { + message: `缺少必需字段: ${missingFields.join(', ')}`, + status: 400 + } + }; + } + + return { + success: true, + data + }; + + } catch (error) { + console.error('请求体解析出错:', error); + return { + success: false, + error: { + message: '服务器内部错误', + status: 500 + } + }; + } +} + +/** + * 创建标准化的API响应 + * @param success - 是否成功 + * @param data - 响应数据 + * @param message - 响应消息 + * @param status - HTTP状态码 + * @returns Response对象 + */ +export function createApiResponse( + success: boolean, + data?: any, + message?: string, + status: number = 200 +): Response { + const responseBody = { + success, + ...(message && { message }), + ...(data && data) + }; + + return new Response(JSON.stringify(responseBody), { + status, + headers: { 'Content-Type': 'application/json' } + }); +} + +/** + * 创建错误响应 + * @param message - 错误消息 + * @param status - HTTP状态码 + * @returns Response对象 + */ +export function createErrorResponse(message: string, status: number = 500): Response { + return new Response(JSON.stringify({ error: message }), { + status, + headers: { 'Content-Type': 'application/json' } + }); +} + +/** + * 处理API错误的统一函数 + * @param error - 错误对象 + * @param operation - 操作描述 + * @returns Response对象 + */ +export function handleApiError(error: unknown, operation: string): Response { + console.error(`${operation}错误:`, error); + + let errorMessage = `${operation}失败,请稍后再试`; + let status = 500; + + if (error instanceof SyntaxError && error.message.includes('JSON')) { + errorMessage = '请求格式错误'; + status = 400; + } else if (error instanceof Error) { + errorMessage = error.message; + } + + return createErrorResponse(errorMessage, status); +} \ No newline at end of file diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts new file mode 100644 index 0000000..bbaa48e --- /dev/null +++ b/src/lib/jwt.ts @@ -0,0 +1,118 @@ +import crypto from 'crypto'; + +// JWT密钥 - 在生产环境中应该使用环境变量 +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +const JWT_ALGORITHM = 'HS256'; +const JWT_EXPIRES_IN = 7 * 24 * 60 * 60; // 7天过期 + +export interface JWTPayload { + userId: number; + username: string; + iat?: number; + exp?: number; +} + +/** + * 创建JWT Token + */ +export function createJWT(payload: Omit): string { + const header = { + alg: JWT_ALGORITHM, + typ: 'JWT' + }; + + const now = Math.floor(Date.now() / 1000); + const jwtPayload: JWTPayload = { + ...payload, + iat: now, + exp: now + JWT_EXPIRES_IN + }; + + // Base64URL编码 + const encodedHeader = base64UrlEncode(JSON.stringify(header)); + const encodedPayload = base64UrlEncode(JSON.stringify(jwtPayload)); + + // 创建签名 + const signatureInput = `${encodedHeader}.${encodedPayload}`; + const signature = crypto + .createHmac('sha256', JWT_SECRET) + .update(signatureInput) + .digest('base64url'); + + return `${signatureInput}.${signature}`; +} + +/** + * 验证JWT Token + */ +export function verifyJWT(token: string): JWTPayload | null { + try { + const parts = token.split('.'); + if (parts.length !== 3) { + return null; + } + + const [encodedHeader, encodedPayload, signature] = parts; + + // 验证签名 + const signatureInput = `${encodedHeader}.${encodedPayload}`; + const expectedSignature = crypto + .createHmac('sha256', JWT_SECRET) + .update(signatureInput) + .digest('base64url'); + + if (signature !== expectedSignature) { + return null; + } + + // 解析payload + const payload = JSON.parse(base64UrlDecode(encodedPayload)) as JWTPayload; + + // 检查过期时间 + if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) { + return null; + } + + return payload; + } catch (error) { + console.error('JWT验证错误:', error); + return null; + } +} + +/** + * 从Authorization头中提取Token + */ +export function extractTokenFromHeader(authHeader: string | null): string | null { + if (!authHeader) { + return null; + } + + // 支持 "Bearer token" 格式 + if (authHeader.startsWith('Bearer ')) { + return authHeader.substring(7); + } + + // 直接返回token + return authHeader; +} + +/** + * Base64URL编码 + */ +function base64UrlEncode(str: string): string { + return Buffer.from(str) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +/** + * Base64URL解码 + */ +function base64UrlDecode(str: string): string { + // 添加填充字符 + str += '='.repeat((4 - str.length % 4) % 4); + return Buffer.from(str.replace(/\-/g, '+').replace(/_/g, '/'), 'base64').toString(); +} \ No newline at end of file diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts new file mode 100644 index 0000000..777536a --- /dev/null +++ b/src/pages/api/auth/login.ts @@ -0,0 +1,72 @@ +import type { APIRoute } from 'astro'; +import { authenticateUser } from '@/lib/database'; +import { createJWT } from '@/lib/jwt'; + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.text(); + + if (!body.trim()) { + return new Response(JSON.stringify({ error: '请求体不能为空' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const { username, password } = JSON.parse(body); + + // 验证输入 + if (!username || !password) { + return new Response(JSON.stringify({ error: '用户名和密码不能为空' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 验证用户 + const user = authenticateUser(username, password); + + if (!user) { + return new Response(JSON.stringify({ error: '用户名或密码错误' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 创建JWT token + const token = createJWT({ + userId: user.id, + username: user.username + }); + + return new Response(JSON.stringify({ + success: true, + user: { + id: user.id, + username: user.username, + created_at: user.created_at + }, + token, + expiresIn: 7 * 24 * 60 * 60 // 7天,单位:秒 + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error) { + console.error('登录错误:', error); + + let errorMessage = '登录失败,请稍后再试'; + + if (error instanceof SyntaxError && error.message.includes('JSON')) { + errorMessage = '请求格式错误'; + } else if (error instanceof Error) { + errorMessage = error.message; + } + + return new Response(JSON.stringify({ error: errorMessage }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +}; \ No newline at end of file diff --git a/src/pages/api/auth/music/delete.ts b/src/pages/api/auth/music/delete.ts new file mode 100644 index 0000000..61e676c --- /dev/null +++ b/src/pages/api/auth/music/delete.ts @@ -0,0 +1,62 @@ +import type { APIRoute } from 'astro'; +import { deleteMusic } from '@/lib/database'; +import { authenticateJWTRequest, parseRequestBody, handleApiError } from '@/lib/jwt-auth-middleware'; + +export const POST: APIRoute = async ({ request }) => { + // 首先进行JWT认证 + const authResult = await authenticateJWTRequest(request); + if (!authResult.success) { + return new Response(JSON.stringify({ error: authResult.error!.message }), { + status: authResult.error!.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 解析请求体 + const bodyResult = await parseRequestBody(request, ['musicId']); + if (!bodyResult.success) { + return new Response(JSON.stringify({ error: bodyResult.error!.message }), { + status: bodyResult.error!.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + const { user } = authResult; + const { data } = bodyResult; + const { musicId } = data; + + // 验证音乐ID + if (!musicId || (typeof musicId !== 'string' && typeof musicId !== 'number')) { + return new Response(JSON.stringify({ + error: '音乐ID不能为空且必须是有效的标识符' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 删除音乐记录 + const success = deleteMusic(musicId.toString(), user!.id); + + if (!success) { + return new Response(JSON.stringify({ + error: '音乐不存在或无权限删除' + }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }); + } + + return new Response(JSON.stringify({ + success: true, + message: '音乐删除成功' + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + return handleApiError(error, '删除音乐'); + } +}; \ No newline at end of file diff --git a/src/pages/api/auth/music/list.ts b/src/pages/api/auth/music/list.ts new file mode 100644 index 0000000..8a842d8 --- /dev/null +++ b/src/pages/api/auth/music/list.ts @@ -0,0 +1,63 @@ +import type { APIRoute } from 'astro'; +import { getUserMusic } from '@/lib/database'; +import { authenticateJWTRequest, handleApiError } from '@/lib/jwt-auth-middleware'; + +export const POST: APIRoute = async ({ request }) => { + // 使用JWT认证中间件 + const authResult = await authenticateJWTRequest(request); + if (!authResult.success) { + return new Response(JSON.stringify({ error: authResult.error!.message }), { + status: authResult.error!.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + const { user } = authResult; + + // 获取用户音乐列表 + const musicList = getUserMusic(user!.id); + + // 解析JSON字段并格式化数据 + const formattedMusicList = musicList.map(music => { + try { + return { + id: music.id, + name: music.name, + sounds: JSON.parse(music.sounds), + volume: JSON.parse(music.volume), + speed: JSON.parse(music.speed), + rate: JSON.parse(music.rate), + random_effects: JSON.parse(music.random_effects), + created_at: music.created_at, + updated_at: music.updated_at + }; + } catch (parseError) { + console.error(`解析音乐数据失败 (ID: ${music.id}):`, parseError); + // 返回安全的默认值 + return { + id: music.id, + name: music.name, + sounds: [], + volume: {}, + speed: {}, + rate: {}, + random_effects: {}, + created_at: music.created_at, + updated_at: music.updated_at + }; + } + }); + + return new Response(JSON.stringify({ + success: true, + musicList: formattedMusicList + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + return handleApiError(error, '获取音乐列表'); + } +}; \ No newline at end of file diff --git a/src/pages/api/auth/music/rename.ts b/src/pages/api/auth/music/rename.ts new file mode 100644 index 0000000..ba1f973 --- /dev/null +++ b/src/pages/api/auth/music/rename.ts @@ -0,0 +1,81 @@ +import type { APIRoute } from 'astro'; +import { updateMusicName } from '@/lib/database'; +import { authenticateJWTRequest, parseRequestBody, handleApiError } from '@/lib/jwt-auth-middleware'; + +export const POST: APIRoute = async ({ request }) => { + // 首先进行JWT认证 + const authResult = await authenticateJWTRequest(request); + if (!authResult.success) { + return new Response(JSON.stringify({ error: authResult.error!.message }), { + status: authResult.error!.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 解析请求体 + const bodyResult = await parseRequestBody(request, ['musicId', 'name']); + if (!bodyResult.success) { + return new Response(JSON.stringify({ error: bodyResult.error!.message }), { + status: bodyResult.error!.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + const { user } = authResult; + const { data } = bodyResult; + const { musicId, name } = data; + + // 验证输入 + if (!musicId || (typeof musicId !== 'string' && typeof musicId !== 'number')) { + return new Response(JSON.stringify({ + error: '音乐ID不能为空且必须是有效的标识符' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + if (!name || typeof name !== 'string' || name.trim().length === 0) { + return new Response(JSON.stringify({ + error: '音乐名称不能为空且必须是有效的字符串' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 验证名称长度 + if (name.trim().length > 100) { + return new Response(JSON.stringify({ + error: '音乐名称长度不能超过100个字符' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 更新音乐名称 + const success = updateMusicName(musicId.toString(), name.trim(), user!.id); + + if (!success) { + return new Response(JSON.stringify({ + error: '音乐不存在或无权限修改' + }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }); + } + + return new Response(JSON.stringify({ + success: true, + message: '音乐名称更新成功' + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + return handleApiError(error, '重命名音乐'); + } +}; \ No newline at end of file diff --git a/src/pages/api/auth/music/save.ts b/src/pages/api/auth/music/save.ts new file mode 100644 index 0000000..2d93e9c --- /dev/null +++ b/src/pages/api/auth/music/save.ts @@ -0,0 +1,82 @@ +import type { APIRoute } from 'astro'; +import { createMusic } from '@/lib/database'; +import { authenticateJWTRequest, parseRequestBody, handleApiError } from '@/lib/jwt-auth-middleware'; + +export const POST: APIRoute = async ({ request }) => { + // 首先进行JWT认证 + const authResult = await authenticateJWTRequest(request); + if (!authResult.success) { + return new Response(JSON.stringify({ error: authResult.error!.message }), { + status: authResult.error!.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 解析请求体 + const bodyResult = await parseRequestBody(request, ['name', 'sounds']); + if (!bodyResult.success) { + return new Response(JSON.stringify({ error: bodyResult.error!.message }), { + status: bodyResult.error!.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + const { user } = authResult; + const { data } = bodyResult; + const { name, sounds, volume, speed, rate, random_effects } = data; + + console.log('🎵 保存音乐请求:', { userId: user?.id, name, soundsCount: sounds?.length }); + + // 验证输入 + if (!name || !sounds || !Array.isArray(sounds)) { + return new Response(JSON.stringify({ + error: '音乐名称和声音配置不能为空,声音必须是数组' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // 创建音乐记录 + const music = await createMusic({ + user_id: user!.id, + name, + sounds, + volume: volume || {}, + speed: speed || {}, + rate: rate || {}, + random_effects: random_effects || {}, + }); + + console.log('✅ 音乐保存成功:', { id: music.id, name: music.name }); + + return new Response(JSON.stringify({ + success: true, + message: '音乐保存成功', + music: { + id: music.id, + name: music.name, + created_at: music.created_at + } + }), { + status: 201, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + console.error('❌ 保存音乐API错误:', error); + + // 处理特定的数据库错误 + if (error instanceof Error && error.message.includes('readonly')) { + return new Response(JSON.stringify({ + error: '数据库权限错误,请检查文件权限' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + + return handleApiError(error, '保存音乐'); + } +}; \ No newline at end of file diff --git a/src/pages/api/auth/register.ts b/src/pages/api/auth/register.ts new file mode 100644 index 0000000..5b4fa61 --- /dev/null +++ b/src/pages/api/auth/register.ts @@ -0,0 +1,85 @@ +import type { APIRoute } from 'astro'; +import { createUser } from '@/lib/database'; +import { createJWT } from '@/lib/jwt'; + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.text(); + + if (!body.trim()) { + return new Response(JSON.stringify({ error: '请求体不能为空' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const { username, password } = JSON.parse(body); + + // 验证输入 + if (!username || !password) { + return new Response(JSON.stringify({ error: '用户名和密码不能为空' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (username.length < 3) { + return new Response(JSON.stringify({ error: '用户名至少需要3个字符' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (password.length < 6) { + return new Response(JSON.stringify({ error: '密码至少需要6个字符' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 创建用户 + const user = await createUser({ username, password }); + + // 创建JWT token + const token = createJWT({ + userId: user.id, + username: user.username + }); + + return new Response(JSON.stringify({ + success: true, + user: { + id: user.id, + username: user.username, + created_at: user.created_at + }, + token, + expiresIn: 7 * 24 * 60 * 60 // 7天,单位:秒 + }), { + status: 201, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error) { + console.error('注册错误:', error); + + let errorMessage = '注册失败,请稍后再试'; + + if (error instanceof SyntaxError && error.message.includes('JSON')) { + errorMessage = '请求格式错误'; + } else if (error instanceof Error && error.message === '用户名已存在') { + errorMessage = '用户名已存在'; + return new Response(JSON.stringify({ error: errorMessage }), { + status: 409, + headers: { 'Content-Type': 'application/json' }, + }); + } else if (error instanceof Error) { + errorMessage = error.message; + } + + return new Response(JSON.stringify({ error: errorMessage }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +}; \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index 94e2adc..ef603d1 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,20 +1,26 @@ --- import Layout from '@/layouts/layout.astro'; -import Donate from '@/components/donate.astro'; import Hero from '@/components/hero.astro'; -import About from '@/components/about.astro'; -import Source from '@/components/source.astro'; -import Footer from '@/components/footer.astro'; +import About from '@/components/about-unified.astro'; import { App } from '@/components/app'; +import { getTranslation } from '@/data/i18n'; + +// Get language from URL path +const url = Astro.url; +const pathname = url.pathname; +const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/'); +const lang = isZhPage ? 'zh-CN' : 'en'; +const t = getTranslation(lang); +const pageTitle = lang === 'zh-CN' ? 'Moodist:专注与放松的环境音' : 'Moodist: Ambient Sounds for Focus and Calm'; +const pageDesc = lang === 'zh-CN' + ? 'Moodist 是一个免费开源的环境音生成器,提供精心挑选的声音,帮您创造放松、专注或创意的理想氛围。' + : 'Moodist is a free and open-source ambient sound generator featuring carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.'; --- - - + - -