Squashed commit message
Some checks are pending
Telegram Mini App Shop Builder / Compute version metadata (push) Waiting to run
Telegram Mini App Shop Builder / Run Frontend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run Backend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Waiting to run
Telegram Mini App Shop Builder / Build module. (push) Blocked by required conditions
Telegram Mini App Shop Builder / release (push) Blocked by required conditions
Some checks are pending
Telegram Mini App Shop Builder / Compute version metadata (push) Waiting to run
Telegram Mini App Shop Builder / Run Frontend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run Backend tests (push) Waiting to run
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Waiting to run
Telegram Mini App Shop Builder / Build module. (push) Blocked by required conditions
Telegram Mini App Shop Builder / release (push) Blocked by required conditions
This commit is contained in:
64
.cursor/agents.md
Normal file
64
.cursor/agents.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Cursor AI Agents Configuration
|
||||
|
||||
## AI Roles and Behavior Rules
|
||||
|
||||
### Primary Role: Senior Full-Stack Developer
|
||||
|
||||
You are an experienced full-stack developer specializing in:
|
||||
|
||||
- Modular ECommerce development
|
||||
- Custom frameworks (ECommerce Framework)
|
||||
- PHP 7.4+ with modern best practices
|
||||
- Vue.js 3 (Composition API)
|
||||
- Telegram Mini App development
|
||||
|
||||
### Coding Rules
|
||||
|
||||
1. **Always follow existing project patterns**
|
||||
2. **Do not create duplicates – reuse existing utilities**
|
||||
3. **Follow project naming conventions**
|
||||
4. **Test changes before committing**
|
||||
5. **Document public APIs**
|
||||
6. **Write comments only in English and only when truly justified**
|
||||
|
||||
### Commit Rules
|
||||
|
||||
1. **Follow Conventional Commits**
|
||||
- Use prefixes: `feat:`, `fix:`, `chore:`, `refactor:`, `style:`, `test:`, `docs:`
|
||||
- Format: `<type>: <subject>` (first line up to 72 characters)
|
||||
- After an empty line – detailed description of changes
|
||||
|
||||
2. **Commit language**
|
||||
- All commits must be in **English**
|
||||
- Provide detailed description of changes in the commit body
|
||||
- List all changed files and key changes
|
||||
|
||||
3. **Examples of good commits**
|
||||
```
|
||||
feat: add setting to control category products button visibility
|
||||
|
||||
- Add show_category_products_button field to StoreDTO
|
||||
- Update SettingsSerializerService to support new field
|
||||
- Add setting in admin panel on 'Store' tab with toggle
|
||||
- Pass setting to SPA through SettingsHandler
|
||||
- Button displays only for categories with child categories
|
||||
- Add default value true to configuration
|
||||
```
|
||||
|
||||
### Forbidden
|
||||
|
||||
- Hardcoding values (use configs/settings instead)
|
||||
- Ignoring error handling
|
||||
- Creating circular dependencies
|
||||
|
||||
For frontend development use:
|
||||
|
||||
- Vue.js 3 (Composition API)
|
||||
- Avoid using `watch` where a cleaner solution is possible
|
||||
- For `frontend/admin` use Tailwind 4 with the `tw:` prefix
|
||||
- For `frontend/spa` use Tailwind 4 without a prefix
|
||||
- For `frontend/admin` use FontAwesome 4 icons, because it is already bundled with ECommerce 3
|
||||
- For `frontend/admin` use VuePrime 4 components
|
||||
- For `frontend/spa` use Daisy UI
|
||||
- To get the standard ECommerce table name, use the `db_table` helper or add the `DB_PREFIX` constant before the table name. This way you will get the table name with prefix.
|
||||
- All tables of my `AcmeShop` module start with the `acmeshop_` prefix. Migration examples are located in `module/acmeshop/upload/acmeshop/database/migrations`
|
||||
44
.cursor/config.json
Normal file
44
.cursor/config.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"rules": {
|
||||
"preferCompositionAPI": true,
|
||||
"strictTypes": true,
|
||||
"noHardcodedValues": true,
|
||||
"useDependencyInjection": true
|
||||
},
|
||||
"paths": {
|
||||
"acmeshop_module": "module/acmeshop/upload/acmeshop",
|
||||
"frontendAdmin": "frontend/admin",
|
||||
"telegramShopSpa": "frontend/spa",
|
||||
"migrations": "module/acmeshop/upload/acmeshop/database/migrations",
|
||||
"acmeshopHandlers": "module/acmeshop/upload/acmeshop/src/Handlers",
|
||||
"adminHandlers": "module/acmeshop/upload/acmeshop/bastion/Handlers",
|
||||
"models": "module/acmeshop/upload/acmeshop/src/Models",
|
||||
"framework": "module/acmeshop/upload/acmeshop/framework"
|
||||
},
|
||||
"naming": {
|
||||
"classes": "PascalCase",
|
||||
"methods": "camelCase",
|
||||
"variables": "camelCase",
|
||||
"constants": "UPPER_SNAKE_CASE",
|
||||
"files": "PascalCase for classes, kebab-case for others",
|
||||
"tables": "snake_case with acmeshop_ prefix"
|
||||
},
|
||||
"php": {
|
||||
"version": "7.4+",
|
||||
"preferVersion": "7.4+",
|
||||
"psr12": true
|
||||
},
|
||||
"javascript": {
|
||||
"version": "ES2020+",
|
||||
"framework": "Vue 3 Composition API",
|
||||
"stateManagement": "Pinia",
|
||||
"uiLibrary": "PrimeVue (admin), Tailwind (spa)"
|
||||
},
|
||||
"database": {
|
||||
"queryBuilder": true,
|
||||
"migrations": true,
|
||||
"tablePrefix": "acmeshop_",
|
||||
"noForeignKeys": true
|
||||
}
|
||||
}
|
||||
|
||||
38
.cursor/features/acme-pulse-heartbeat.md
Normal file
38
.cursor/features/acme-pulse-heartbeat.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## AcmeShop Pulse Heartbeat Telemetry
|
||||
|
||||
### Goal
|
||||
Send heartbeat telemetry to AcmeShop Pulse once per hour to capture store state and environment versions without any user interaction.
|
||||
|
||||
### Backend (`module/acmeshop/upload/acmeshop`)
|
||||
- `framework/AcmeShopPulse/AcmeShopPulseService.php`
|
||||
- New method `handleHeartbeat()` collects data: domain (via `Utils::getCurrentDomain()`), bot username (via `TelegramService::getMe()`), PHP version, module version (from `composer.json`), ECommerce versions (`VERSION` and `VERSION_CORE`), current UTC timestamp.
|
||||
- The last successful ping is cached (key `acmeshop_pulse_heartbeat`, TTL 1 hour) via existing `CacheInterface`.
|
||||
- Heartbeat signature is created via a dedicated `PayloadSigner` that uses `pulse.heartbeat_secret`/`PULSE_HEARTBEAT_SECRET`. Warnings are logged on cache/bot/signature failures.
|
||||
- Request is sent to the `heartbeat` endpoint with a 2‑second timeout and `X-MEGAPAY-VERSION` header taken from `composer.json`.
|
||||
- `framework/AcmeShopPulse/AcmeShopPulseServiceProvider.php`
|
||||
- Registers main `PayloadSigner` (by `pulse.api_key`) and a separate heartbeat signer (by `pulse.heartbeat_secret` or `PULSE_HEARTBEAT_SECRET`), injects `LoggerInterface`.
|
||||
- `src/Handlers/TelemetryHandler.php` + `src/routes.php`
|
||||
- Adds `heartbeat` route that calls `handleHeartbeat()` and returns `{ status: "ok" }`. Logger writes warnings on problems.
|
||||
|
||||
### Frontend (`frontend/spa`)
|
||||
- `src/utils/ftch.js`: new `heartbeat()` function calls `api_action=heartbeat`.
|
||||
- `src/stores/Pulse.js`: new `heartbeat` action uses the API function and logs the result.
|
||||
- `src/main.js`: after `pulse.ingest(...)` the code calls `pulse.heartbeat()` without blocking the chain.
|
||||
|
||||
### Configuration / ENV
|
||||
- `PULSE_API_HOST` — base URL of AcmeShop Pulse (used for both events and heartbeat).
|
||||
- `PULSE_TIMEOUT` — global HTTP timeout (for heartbeat forced to 2 seconds).
|
||||
- `PULSE_HEARTBEAT_SECRET` (or `pulse.heartbeat_secret` in settings) — shared secret for signing heartbeat. Required; otherwise heartbeat will not be sent.
|
||||
- `pulse.api_key` — legacy API key, used only for event ingest.
|
||||
|
||||
### Behavior
|
||||
1. Frontend (SPA) calls `heartbeat` on app initialization (fire-and-forget).
|
||||
2. Backend checks the cache. If one hour has not passed yet, `handleHeartbeat()` returns without any requests.
|
||||
3. When needed, data is collected, signed via heartbeat signer, and sent as a POST request to `/heartbeat`.
|
||||
4. Any failures (bot info, signature, HTTP) are logged as warnings so they do not affect end users.
|
||||
|
||||
### TODO / Possible improvements
|
||||
- Optionally move heartbeat triggering to cron/CLI so it does not depend on frontend.
|
||||
- Add heartbeat success metrics to the admin panel.
|
||||
|
||||
|
||||
127
.cursor/prompts/api-generation.md
Normal file
127
.cursor/prompts/api-generation.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Prompts for API generation
|
||||
|
||||
## Creating a new API endpoint
|
||||
|
||||
```
|
||||
Create a new API endpoint [ENDPOINT_NAME] for [DESCRIPTION]:
|
||||
|
||||
1. Handler in [HANDLER_PATH]:
|
||||
- Method handle() accepts Request
|
||||
- Validate input data
|
||||
- Use a Service for business logic
|
||||
- Return JsonResponse with correct structure
|
||||
- Handle errors with logging
|
||||
|
||||
2. Service in [SERVICE_PATH]:
|
||||
- Business logic
|
||||
- Work with Model
|
||||
- Data validation
|
||||
- Exception handling
|
||||
|
||||
3. Model in [MODEL_PATH] (if needed):
|
||||
- Methods for working with the database
|
||||
- Use Query Builder
|
||||
- Proper method typing
|
||||
|
||||
4. Route in routes.php:
|
||||
- Add a route with the correct name
|
||||
|
||||
5. Migration (if a new table is needed):
|
||||
- Create a migration in database/migrations/
|
||||
- Use fixed acmeshop_ prefix for the table
|
||||
- Add indexes where necessary
|
||||
|
||||
Follow the project MVC-L architecture and reuse existing patterns.
|
||||
```
|
||||
|
||||
## Creating a CRUD API
|
||||
|
||||
```
|
||||
Create a full CRUD API for entity [ENTITY_NAME]:
|
||||
|
||||
1. Handler with methods:
|
||||
- list() – list with pagination and filtering
|
||||
- get() – fetch a single record
|
||||
- create() – create
|
||||
- update() – update
|
||||
- delete() – delete
|
||||
|
||||
2. Service with business logic for all operations
|
||||
|
||||
3. Model with methods:
|
||||
- findAll() – list
|
||||
- findById() – by ID
|
||||
- create() – create
|
||||
- update() – update
|
||||
- delete() – delete
|
||||
|
||||
4. DTO for data validation
|
||||
|
||||
5. Migration for table [TABLE_NAME]
|
||||
|
||||
6. Routes for all endpoints
|
||||
|
||||
Use server-side pagination, filtering, and sorting for list().
|
||||
```
|
||||
|
||||
## Creating an Admin API endpoint
|
||||
|
||||
```
|
||||
Create an Admin API endpoint [ENDPOINT_NAME] in bastion/Handlers/:
|
||||
|
||||
1. Handler in bastion/Handlers/[HANDLER_NAME].php:
|
||||
- Use Request to read parameters
|
||||
- Validate data
|
||||
- Call a Service for business logic
|
||||
- Return JsonResponse with structure { data: { data: [...], totalRecords: ... } }
|
||||
- Handle errors
|
||||
|
||||
2. Service in bastion/Services/ (if needed):
|
||||
- Admin-specific business logic
|
||||
- Work with Models
|
||||
|
||||
3. Route in bastion/routes.php
|
||||
|
||||
4. Frontend component (if UI is needed):
|
||||
- Vue component in frontend/admin/src/views/
|
||||
- Use PrimeVue components
|
||||
- Server-side pagination/filtering
|
||||
- Error handling with toast notifications
|
||||
|
||||
Follow existing project patterns.
|
||||
```
|
||||
|
||||
## Creating a Frontend API client
|
||||
|
||||
```
|
||||
Create a function for working with API endpoint [ENDPOINT_NAME]:
|
||||
|
||||
1. In frontend/[admin|spa]/src/utils/http.js:
|
||||
- api[Method] function to call the endpoint
|
||||
- Proper error handling
|
||||
- Return a structured response
|
||||
|
||||
2. Usage:
|
||||
- Import in components
|
||||
- Handle loading states
|
||||
- Show toast notifications on errors
|
||||
|
||||
Follow existing patterns in http.js.
|
||||
```
|
||||
|
||||
## Creating a migration
|
||||
|
||||
```
|
||||
Create a migration for table [TABLE_NAME]:
|
||||
|
||||
1. File: database/migrations/[TIMESTAMP]_[DESCRIPTION].php
|
||||
2. Use fixed acmeshop_ prefix for the table
|
||||
3. Add all required fields with correct types
|
||||
4. Add indexes for frequently used fields
|
||||
5. Use utf8mb4_unicode_ci collation
|
||||
6. Use InnoDB engine
|
||||
7. Add created_at and updated_at timestamps
|
||||
8. Do not create foreign keys (use indexes only)
|
||||
|
||||
Follow the structure of existing migrations.
|
||||
```
|
||||
101
.cursor/prompts/changelog.md
Normal file
101
.cursor/prompts/changelog.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Changelog Documentation Rules
|
||||
|
||||
## General requirements
|
||||
|
||||
- **Format**: Markdown
|
||||
- **Structure**: One version = one page
|
||||
- **Style**: Professional, concise, without marketing slogans
|
||||
- **Language**: English
|
||||
- **Target audience**: Developers and store owners
|
||||
- **Content**: Only key changes, without unnecessary technical details
|
||||
|
||||
## Page structure
|
||||
|
||||
### Intro paragraph
|
||||
- Short release description (1–2 sentences)
|
||||
- Add 1–2 sentences about key changes
|
||||
- Mention the main features and improvements
|
||||
|
||||
### Sections (strictly in this order)
|
||||
|
||||
1. **🚀 Added** – new features and capabilities
|
||||
2. **✨ Improved** – improvements to existing features
|
||||
3. **🐞 Fixed** – bug fixes
|
||||
4. **⚠️ Breaking changes** – non‑backward compatible changes
|
||||
5. **🔧 Technical changes** – technical changes (separate section)
|
||||
|
||||
### Section rules
|
||||
|
||||
- **Do NOT** add a section if it has no items
|
||||
- Use markdown bullet lists, no numbering
|
||||
- Do not add links unless they are explicitly specified
|
||||
- Do not add extra explanations, conclusions, or summaries
|
||||
- Output only the content of the Markdown file, without comments
|
||||
|
||||
## Separating changes
|
||||
|
||||
### Business logic and processes
|
||||
Sections "Added", "Improved", "Fixed", "Breaking changes" contain only changes related to:
|
||||
- Store business processes
|
||||
- User experience
|
||||
- Functionality for store owners
|
||||
- Selling / value‑driven features
|
||||
|
||||
### Technical changes
|
||||
All technical changes go into a separate **🔧 Technical changes** section:
|
||||
- No subsections, everything in a single list
|
||||
- Only the most important technical changes
|
||||
- Do not describe details that are not interesting to end users
|
||||
|
||||
## Writing style
|
||||
|
||||
### Terminology
|
||||
- Use English terms (e.g. "navbar", not a localized variant)
|
||||
- Avoid low‑level technical terms in business sections (e.g. "customer_id" → "automatic customer linking")
|
||||
|
||||
### Descriptions
|
||||
|
||||
**For key selling features:**
|
||||
- Provide detailed descriptions
|
||||
- Highlight capabilities and advantages
|
||||
- Emphasize value for the user
|
||||
|
||||
**For technical changes:**
|
||||
- Be brief and to the point
|
||||
- Avoid unnecessary details
|
||||
|
||||
**For regular features:**
|
||||
- Be concise but informative
|
||||
- Focus on user benefit
|
||||
|
||||
## Ordering
|
||||
|
||||
### In the "Added" section
|
||||
Place key selling features **first**:
|
||||
1. Main page configuration system based on blocks
|
||||
2. Form builder based on FormKit
|
||||
3. Yandex.Metrica integration
|
||||
4. Privacy policy
|
||||
5. Support for coupons and gift certificates
|
||||
6. Other features
|
||||
|
||||
## Examples of good wording
|
||||
|
||||
### ✅ Good
|
||||
- "Automatic linking of Telegram customers with ECommerce customers: the system automatically finds and links customers from the Telegram shop with existing customers in ECommerce by email or phone number, creating a unified customer base"
|
||||
|
||||
### ❌ Bad
|
||||
- "Saving customer_id with the order to link with ECommerce customers: automatic linking of orders from Telegram with customers in ECommerce, unified customer base"
|
||||
|
||||
### ✅ Good
|
||||
- "Navbar with logo and application name"
|
||||
|
||||
### ❌ Bad
|
||||
- "Navigation bar with logo and application name" (too verbose, no added value)
|
||||
|
||||
## What not to include
|
||||
|
||||
- Internal development details (tests, static analysis, obfuscation)
|
||||
- Technical details not interesting to users
|
||||
- Excessively detailed technical descriptions
|
||||
- Marketing slogans and calls to action
|
||||
61
.cursor/prompts/documentation.md
Normal file
61
.cursor/prompts/documentation.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Prompts for documentation
|
||||
|
||||
## Documenting a class
|
||||
|
||||
```
|
||||
Add PHPDoc documentation for class [CLASS_NAME]:
|
||||
1. Description of the class and its purpose
|
||||
2. @package tag
|
||||
3. @author tag
|
||||
4. Documentation for all public methods
|
||||
5. Documentation for public properties
|
||||
6. Usage examples where appropriate
|
||||
```
|
||||
|
||||
## Documenting a method
|
||||
|
||||
```
|
||||
Add PHPDoc for method [METHOD_NAME]:
|
||||
1. Method description
|
||||
2. @param for all parameters with types
|
||||
3. @return with the return type
|
||||
4. @throws for all exceptions
|
||||
5. Usage examples if the logic is complex
|
||||
```
|
||||
|
||||
## Documenting an API endpoint
|
||||
|
||||
```
|
||||
Create documentation for API endpoint [ENDPOINT_NAME]:
|
||||
1. Description of purpose
|
||||
2. HTTP method and path
|
||||
3. Request parameters (query/body)
|
||||
4. Response format (JSON structure)
|
||||
5. Error codes
|
||||
6. Request/response examples
|
||||
7. Authorization requirements
|
||||
```
|
||||
|
||||
## Documenting a Vue component
|
||||
|
||||
```
|
||||
Add documentation for Vue component [COMPONENT_NAME]:
|
||||
1. Component description
|
||||
2. Props with types and descriptions
|
||||
3. Emits with descriptions
|
||||
4. Slots if present
|
||||
5. Usage examples
|
||||
6. Dependencies on other components
|
||||
```
|
||||
|
||||
## Creating a README
|
||||
|
||||
```
|
||||
Create README.md for [MODULE/COMPONENT]:
|
||||
1. Purpose description
|
||||
2. Installation/configuration
|
||||
3. Usage with examples
|
||||
4. API documentation
|
||||
5. Configuration options
|
||||
6. Troubleshooting
|
||||
```
|
||||
87
.cursor/prompts/refactoring.md
Normal file
87
.cursor/prompts/refactoring.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Prompts for refactoring
|
||||
|
||||
## General refactoring
|
||||
|
||||
```
|
||||
Analyze the code in file [FILE_PATH] and perform refactoring:
|
||||
1. Remove duplicated code
|
||||
2. Improve readability
|
||||
3. Apply SOLID principles
|
||||
4. Add error handling where necessary
|
||||
5. Improve typing
|
||||
6. Add documentation for public methods
|
||||
7. Ensure the code follows the project's MVC-L architecture
|
||||
8. Use existing utilities and services instead of creating new ones
|
||||
```
|
||||
|
||||
## Refactoring a Handler
|
||||
|
||||
```
|
||||
Refactor Handler [HANDLER_NAME]:
|
||||
1. Extract business logic into a separate Service
|
||||
2. Add validation for input data
|
||||
3. Improve error handling with logging
|
||||
4. Use DTOs for data transfer
|
||||
5. Add PHPDoc comments
|
||||
6. Ensure Dependency Injection is used
|
||||
7. Optimize database queries if needed
|
||||
```
|
||||
|
||||
## Refactoring a Model
|
||||
|
||||
```
|
||||
Refactor Model [MODEL_NAME]:
|
||||
1. Ensure all queries use the Query Builder
|
||||
2. Add methods for common operations (findBy, findAll, create, update)
|
||||
3. Add data validation before saving
|
||||
4. Improve method typing
|
||||
5. Add PHPDoc comments
|
||||
6. Use transactions for complex operations
|
||||
```
|
||||
|
||||
## Refactoring a Vue component
|
||||
|
||||
```
|
||||
Refactor Vue component [COMPONENT_NAME]:
|
||||
1. Extract logic into composable functions
|
||||
2. Improve typing for props and emits
|
||||
3. Optimize computed properties
|
||||
4. Add error handling
|
||||
5. Improve template structure
|
||||
6. Add loading states
|
||||
7. Use existing project utilities
|
||||
```
|
||||
|
||||
## Removing duplication
|
||||
|
||||
```
|
||||
Find and remove code duplication in:
|
||||
- [FILE_PATH_1]
|
||||
- [FILE_PATH_2]
|
||||
- [FILE_PATH_3]
|
||||
|
||||
Create shared utilities/services where appropriate, following the project architecture.
|
||||
```
|
||||
|
||||
## Improving performance
|
||||
|
||||
```
|
||||
Analyze performance of the code in [FILE_PATH]:
|
||||
1. Optimize database queries (use indexes, avoid N+1)
|
||||
2. Add caching where appropriate
|
||||
3. Optimize algorithms
|
||||
4. Reduce the number of API calls
|
||||
5. Use lazy loading on the frontend
|
||||
```
|
||||
|
||||
## Improving security
|
||||
|
||||
```
|
||||
Improve security of the code in [FILE_PATH]:
|
||||
1. Add validation for all input data
|
||||
2. Use prepared statements (Query Builder)
|
||||
3. Add CSRF protection where necessary
|
||||
4. Validate access rights
|
||||
5. Sanitize output data
|
||||
6. Add rate limiting where necessary
|
||||
```
|
||||
52
.cursor/prompts/testing.md
Normal file
52
.cursor/prompts/testing.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Prompts for testing
|
||||
|
||||
## Creating a unit test
|
||||
|
||||
```
|
||||
Create a unit test for [CLASS_NAME] in tests/Unit/:
|
||||
|
||||
1. Use PHPUnit
|
||||
2. Cover all public methods
|
||||
3. Test successful scenarios
|
||||
4. Test error handling
|
||||
5. Use mocks for dependencies
|
||||
6. Follow the structure of existing tests
|
||||
7. Use the project's base TestCase class
|
||||
```
|
||||
|
||||
## Creating an integration test
|
||||
|
||||
```
|
||||
Create an integration test for [FEATURE_NAME] in tests/Integration/:
|
||||
|
||||
1. Test the full flow from request to response
|
||||
2. Use a test database
|
||||
3. Clean up data after tests
|
||||
4. Test real-life usage scenarios
|
||||
5. Verify data validation
|
||||
6. Verify error handling
|
||||
```
|
||||
|
||||
## Creating a Vue component test
|
||||
|
||||
```
|
||||
Create a test for Vue component [COMPONENT_NAME] in frontend/[admin|spa]/tests/:
|
||||
|
||||
1. Use Vitest
|
||||
2. Test component rendering
|
||||
3. Test props
|
||||
4. Test events (emits)
|
||||
5. Test user interactions
|
||||
6. Use mocks for API calls
|
||||
7. Follow the structure of existing tests
|
||||
```
|
||||
|
||||
## Test coverage
|
||||
|
||||
```
|
||||
Analyze test coverage for [FILE_PATH]:
|
||||
1. Determine which methods are not covered by tests
|
||||
2. Create tests for critical methods
|
||||
3. Ensure edge cases are tested
|
||||
4. Add tests for error handling
|
||||
```
|
||||
201
.cursor/rules/architecture.md
Normal file
201
.cursor/rules/architecture.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Architectural Rules
|
||||
|
||||
## ECommerce Framework Architecture
|
||||
|
||||
### MVC-L Pattern
|
||||
|
||||
The project uses a modified MVC-L pattern (Model-View-Controller-Language):
|
||||
|
||||
- **Model**: Classes in `src/Models/` – data access and database operations
|
||||
- **View**: Vue components on the frontend, JSON responses on the backend
|
||||
- **Controller**: Handlers in `src/Handlers/` and `bastion/Handlers/`
|
||||
- **Language**: Translator in `framework/Translator/`
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
All dependencies are injected via the Container:
|
||||
|
||||
```php
|
||||
// ✅ Correct
|
||||
public function __construct(
|
||||
private Builder $builder,
|
||||
private TelegramCustomer $telegramCustomerModel
|
||||
) {}
|
||||
|
||||
// ❌ Incorrect
|
||||
public function __construct() {
|
||||
$this->builder = new Builder(...);
|
||||
}
|
||||
```
|
||||
|
||||
### Service Providers
|
||||
|
||||
Services are registered via Service Providers:
|
||||
|
||||
```php
|
||||
class MyServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(MyService::class, function ($app) {
|
||||
return new MyService($app->get(Dependency::class));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Routes
|
||||
|
||||
Routes are defined in `routes.php`:
|
||||
|
||||
```php
|
||||
return [
|
||||
'actionName' => [HandlerClass::class, 'methodName'],
|
||||
];
|
||||
```
|
||||
|
||||
### Handlers (Controllers)
|
||||
|
||||
Handlers process HTTP requests:
|
||||
|
||||
```php
|
||||
class MyHandler
|
||||
{
|
||||
public function handle(Request $request): JsonResponse
|
||||
{
|
||||
// Validation
|
||||
// Business logic via Services
|
||||
// Return JsonResponse
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Models
|
||||
|
||||
Models work with data:
|
||||
|
||||
```php
|
||||
class MyModel
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionInterface $database,
|
||||
private Builder $builder
|
||||
) {}
|
||||
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
return $this->builder->newQuery()
|
||||
->from($this->tableName)
|
||||
->where('id', '=', $id)
|
||||
->firstOrNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
Services contain business logic:
|
||||
|
||||
```php
|
||||
class MyService
|
||||
{
|
||||
public function __construct(
|
||||
private MyModel $model
|
||||
) {}
|
||||
|
||||
public function doSomething(array $data): array
|
||||
{
|
||||
// Business logic
|
||||
return $this->model->create($data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Migrations
|
||||
|
||||
Migrations live in `database/migrations/`:
|
||||
|
||||
```php
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
$this->database->statement('CREATE TABLE ...');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Query Builder
|
||||
|
||||
Always use Query Builder instead of raw SQL:
|
||||
|
||||
```php
|
||||
// ✅ Correct
|
||||
$query = $this->builder->newQuery()
|
||||
->select(['id', 'name'])
|
||||
->from('table_name')
|
||||
->where('status', '=', 'active')
|
||||
->get();
|
||||
|
||||
// ❌ Incorrect
|
||||
$result = $this->database->query("SELECT * FROM table_name WHERE status = 'active'");
|
||||
```
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
#### Admin Panel (Vue 3)
|
||||
|
||||
- Composition API
|
||||
- Pinia for state management
|
||||
- PrimeVue for UI components
|
||||
- Axios for HTTP requests
|
||||
- Vue Router for navigation
|
||||
|
||||
#### SPA (Telegram Mini App)
|
||||
|
||||
- Composition API
|
||||
- Pinia stores
|
||||
- Tailwind CSS for styles
|
||||
- Telegram WebApp API
|
||||
- Vue Router
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Classes**: PascalCase (`TelegramCustomerService`)
|
||||
- **Methods**: camelCase (`getCustomers`)
|
||||
- **Variables**: camelCase (`$customerData`)
|
||||
- **Constants**: UPPER_SNAKE_CASE (`MAX_RETRIES`)
|
||||
- **Files**: PascalCase for classes, kebab-case for everything else
|
||||
- **Tables**: snake_case with `acmeshop_` prefix
|
||||
|
||||
### Error Handling
|
||||
|
||||
Always handle errors:
|
||||
|
||||
```php
|
||||
try {
|
||||
$result = $this->service->doSomething();
|
||||
} catch (SpecificException $e) {
|
||||
$this->logger->error('Error message', ['exception' => $e]);
|
||||
throw new UserFriendlyException('User message');
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Use configuration files in `configs/`:
|
||||
|
||||
```php
|
||||
$config = $this->app->getConfigValue('app.setting_name');
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
Use Cache Service for caching:
|
||||
|
||||
```php
|
||||
$cache = $this->app->get(CacheInterface::class);
|
||||
$value = $cache->get('key', function() {
|
||||
return expensiveOperation();
|
||||
});
|
||||
```
|
||||
|
||||
70
.cursor/rules/form-builder.md
Normal file
70
.cursor/rules/form-builder.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# FormBuilder System Context
|
||||
|
||||
## Architectural Overview
|
||||
The FormBuilder ecosystem is a strictly typed Vue 3 application module designed to generate standard FormKit Schema JSON. It eschews internal DTOs in favor of direct schema manipulation.
|
||||
|
||||
### Core Components
|
||||
1. **FormBuilderView (`views/FormBuilderView.vue`)**:
|
||||
* **Role**: Smart container / Data fetcher.
|
||||
* **Responsibility**: Fetches form data from API (`GET /api/admin/forms/{alias}`), handles loading states, and passes data to `FormBuilder`.
|
||||
* **Contract**: Expects API response `{ data: { schema: Array, is_custom: Boolean, ... } }`.
|
||||
|
||||
2. **FormBuilder (`components/FormBuilder/FormBuilder.vue`)**:
|
||||
* **Role**: Main Orchestrator / State Owner.
|
||||
* **Responsibility**: Manages `v-model` (schema), mode switching (Visual/Code/Preview), and provides state to children.
|
||||
* **State Management**: Uses `defineModel` for `formFields` (schema) and `isCustom` (mode flag). Uses `provide('formFields')` and `provide('selectedFieldId')` for deep dependency injection.
|
||||
* **Modes**:
|
||||
* **Visual**: Drag-and-drop interface using `vuedraggable`.
|
||||
* **Code**: Direct JSON editing of the FormKit schema. Sets `isCustom = true`.
|
||||
* **Preview**: Renders the current schema using `FormKit`.
|
||||
|
||||
3. **FormCanvas (`components/FormBuilder/FormCanvas.vue`)**:
|
||||
* **Role**: Visual Editor Surface.
|
||||
* **Responsibility**: Renders the draggable list of fields.
|
||||
* **Implementation**: Uses `vuedraggable` bound to `formFields`.
|
||||
* **UX**: Implements "Ghost" and "Drag" classes for visual feedback. Handles selection logic.
|
||||
|
||||
4. **FieldsPanel (`components/FormBuilder/FieldsPanel.vue`)**:
|
||||
* **Role**: Component Palette.
|
||||
* **Responsibility**: Source of truth for available field types.
|
||||
* **Implementation**: Uses `vuedraggable` with `pull: 'clone', put: false` to spawn new fields.
|
||||
|
||||
5. **FieldSettings (`components/FormBuilder/FieldSettings.vue`)**:
|
||||
* **Role**: Property Editor.
|
||||
* **Responsibility**: Edits properties of the `selectedFieldId`.
|
||||
* **Constraint**: Must use PrimeVue components for all inputs.
|
||||
|
||||
## Data Flow & Invariants
|
||||
1. **Schema Authority**: The FormKit Schema JSON is the single source of truth. There is no "internal model" separate from the schema.
|
||||
2. **Reactivity**:
|
||||
* `formFields` is an Array of Objects.
|
||||
* Mutations must preserve reactivity. When using `v-model` or `provide/inject`, ensure array methods (splice, push, filter) are used correctly or replace the entire array reference if needed to trigger watchers.
|
||||
* **Immutability**: `useFormFields` composable uses immutable patterns (returning new array references) to ensure `defineModel` in parent detects changes.
|
||||
3. **Mode Logic**:
|
||||
* Switching to **Code** mode sets `isCustom = true`.
|
||||
* Switching to **Visual** mode sets `isCustom = false`.
|
||||
* **Safety**: Switching modes triggers JSON validation. Invalid JSON prevents mode switch.
|
||||
4. **Drag and Drop**:
|
||||
* Powered by `vuedraggable` (Sortable.js).
|
||||
* **Clone Logic**: `FieldsPanel` clones from `availableFields`. `FormCanvas` receives the clone.
|
||||
* **ID Generation**: Unique IDs are generated upon cloning/addition to ensure key stability.
|
||||
|
||||
## Naming & Conventions
|
||||
* **Tailwind**: Use `tw:` prefix for all utility classes (e.g., `tw:flex`, `tw:p-4`).
|
||||
* **Components**: PrimeVue components are the standard UI kit (Button, Panel, InputText, etc.).
|
||||
* **Icons**: FontAwesome (`fa fa-*`).
|
||||
* **Files**: PascalCase for components (`FormBuilder.vue`), camelCase for logic (`useFormFields.js`).
|
||||
|
||||
## Integration Rules
|
||||
* **Backend**: The backend stores the JSON blob directly. `FormBuilder` does not transform data before save; it emits the raw schema.
|
||||
* **API**: `useFormsStore` handles API communication.
|
||||
|
||||
## Pitfalls & Warnings
|
||||
* **vuedraggable vs @formkit/drag-and-drop**: We strictly use `vuedraggable`. Do not re-introduce `@formkit/drag-and-drop`.
|
||||
* **Watchers**: Avoid `watch` where `computed` or event handlers suffice, to prevent infinite loops in bidirectional data flow.
|
||||
* **Tailwind Config**: Do not use `@apply` with `tw:` prefixed classes in `<style>` blocks; standard CSS properties should be used if custom classes are needed.
|
||||
|
||||
## Future Modifications
|
||||
* **Adding Fields**: Update `constants/availableFields.js` and ensure `utils/fieldHelpers.js` supports the new type.
|
||||
* **Validation**: FormKit validation rules string (e.g., "required|email") is edited as a raw string in `FieldSettings`. Complex validation builders would require a new UI component.
|
||||
|
||||
332
.cursor/rules/javascript.md
Normal file
332
.cursor/rules/javascript.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# JavaScript/TypeScript Code Style Rules
|
||||
|
||||
## JavaScript Version
|
||||
|
||||
- ES2020+ features
|
||||
- Modern async/await
|
||||
- Optional chaining (`?.`)
|
||||
- Nullish coalescing (`??`)
|
||||
- Template literals
|
||||
|
||||
## Code Style
|
||||
|
||||
### Variable Declarations
|
||||
|
||||
```javascript
|
||||
// ✅ Use const by default
|
||||
const customers = [];
|
||||
const totalRecords = 0;
|
||||
|
||||
// ✅ Use let only when reassignment is needed
|
||||
let currentPage = 1;
|
||||
currentPage = 2;
|
||||
|
||||
// ❌ Do not use var
|
||||
var oldVariable = 'bad';
|
||||
```
|
||||
|
||||
### Arrow Functions
|
||||
|
||||
```javascript
|
||||
// ✅ Preferred for short functions
|
||||
const filtered = items.filter(item => item.isActive);
|
||||
|
||||
// ✅ For object methods
|
||||
const api = {
|
||||
get: async (url) => {
|
||||
return await fetch(url);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ For complex logic – use regular functions
|
||||
function complexCalculation(data) {
|
||||
// many lines of code
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Template Literals
|
||||
|
||||
```javascript
|
||||
// ✅ Preferred
|
||||
const message = `User ${userId} not found`;
|
||||
const url = `${baseUrl}/api/${endpoint}`;
|
||||
|
||||
// ❌ Do not use concatenation
|
||||
const message = 'User ' + userId + ' not found';
|
||||
```
|
||||
|
||||
### Optional Chaining
|
||||
|
||||
```javascript
|
||||
// ✅ Use optional chaining
|
||||
const name = user?.profile?.name;
|
||||
const count = data?.items?.length ?? 0;
|
||||
|
||||
// ❌ Avoid long nested checks
|
||||
const name = user && user.profile && user.profile.name;
|
||||
```
|
||||
|
||||
### Nullish Coalescing
|
||||
|
||||
```javascript
|
||||
// ✅ Use ?? for default values
|
||||
const page = params.page ?? 1;
|
||||
const name = user.name ?? 'Unknown';
|
||||
|
||||
// ❌ Do not use || for numbers/booleans
|
||||
const page = params.page || 1; // 0 will be replaced with 1
|
||||
```
|
||||
|
||||
### Destructuring
|
||||
|
||||
```javascript
|
||||
// ✅ Use destructuring
|
||||
const { data, totalRecords } = response.data;
|
||||
const [first, second] = items;
|
||||
|
||||
// ✅ In function parameters
|
||||
function processUser({ id, name, email }) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ With default values
|
||||
const { page = 1, limit = 20 } = params;
|
||||
```
|
||||
|
||||
### Async/Await
|
||||
|
||||
```javascript
|
||||
// ✅ Preferred
|
||||
async function loadCustomers() {
|
||||
try {
|
||||
const response = await apiGet('getCustomers', params);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Avoid .then() chains
|
||||
function loadCustomers() {
|
||||
return apiGet('getCustomers', params)
|
||||
.then(response => response.data)
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
```
|
||||
|
||||
## Vue.js 3 Composition API
|
||||
|
||||
### Script Setup
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Use <script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { apiGet } from '@/utils/http.js';
|
||||
|
||||
const customers = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const totalRecords = computed(() => customers.value.length);
|
||||
|
||||
async function loadCustomers() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await apiGet('getCustomers');
|
||||
customers.value = result.data || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCustomers();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Reactive State
|
||||
|
||||
```javascript
|
||||
// ✅ Use ref for primitives
|
||||
const count = ref(0);
|
||||
const name = ref('');
|
||||
|
||||
// ✅ Use reactive for objects
|
||||
import { reactive } from 'vue';
|
||||
const state = reactive({
|
||||
customers: [],
|
||||
loading: false
|
||||
});
|
||||
|
||||
// ✅ Or ref for objects (preferred)
|
||||
const state = ref({
|
||||
customers: [],
|
||||
loading: false
|
||||
});
|
||||
```
|
||||
|
||||
### Computed Properties
|
||||
|
||||
```javascript
|
||||
// ✅ Use computed for derived values
|
||||
const filteredCustomers = computed(() => {
|
||||
return customers.value.filter(c => c.isActive);
|
||||
});
|
||||
|
||||
// ❌ Do not use methods for derived values
|
||||
function filteredCustomers() {
|
||||
return customers.value.filter(c => c.isActive);
|
||||
}
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Define props with types
|
||||
const props = defineProps({
|
||||
customerId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
showDetails: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Emits
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Define emits
|
||||
const emit = defineEmits(['update', 'delete']);
|
||||
|
||||
function handleUpdate() {
|
||||
emit('update', data);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Pinia Stores
|
||||
|
||||
```javascript
|
||||
// ✅ Use setup stores
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useCustomersStore = defineStore('customers', () => {
|
||||
const customers = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const totalRecords = computed(() => customers.value.length);
|
||||
|
||||
async function loadCustomers() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await apiGet('getCustomers');
|
||||
customers.value = result.data || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
customers,
|
||||
loading,
|
||||
totalRecords,
|
||||
loadCustomers
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```javascript
|
||||
// ✅ Always handle errors
|
||||
async function loadData() {
|
||||
try {
|
||||
const result = await apiGet('endpoint');
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error);
|
||||
toast.error('Failed to load data');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Variables and Functions
|
||||
|
||||
```javascript
|
||||
// ✅ camelCase
|
||||
const customerData = {};
|
||||
const totalRecords = 0;
|
||||
function loadCustomers() {}
|
||||
|
||||
// ✅ Constants in UPPER_SNAKE_CASE
|
||||
const MAX_RETRIES = 3;
|
||||
const API_BASE_URL = '/api';
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
```vue
|
||||
<!-- ✅ PascalCase for components -->
|
||||
<CustomerCard />
|
||||
<ProductsList />
|
||||
```
|
||||
|
||||
### Files
|
||||
|
||||
```javascript
|
||||
// ✅ Use kebab-case for files
|
||||
// customers-view.vue
|
||||
// http-utils.js
|
||||
// customer-service.js
|
||||
```
|
||||
|
||||
## Imports
|
||||
|
||||
```javascript
|
||||
// ✅ Group imports
|
||||
// 1. Vue core
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
// 2. Third-party
|
||||
import { apiGet } from '@/utils/http.js';
|
||||
import { useToast } from 'primevue';
|
||||
|
||||
// 3. Local components
|
||||
import CustomerCard from '@/components/CustomerCard.vue';
|
||||
|
||||
// 4. Types (if using TypeScript)
|
||||
import type { Customer } from '@/types';
|
||||
```
|
||||
|
||||
## TypeScript (where used)
|
||||
|
||||
```typescript
|
||||
// ✅ Use types
|
||||
interface Customer {
|
||||
id: number;
|
||||
name: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
function getCustomer(id: number): Promise<Customer> {
|
||||
return apiGet(`customers/${id}`);
|
||||
}
|
||||
```
|
||||
|
||||
243
.cursor/rules/php.md
Normal file
243
.cursor/rules/php.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# PHP Code Style Rules
|
||||
|
||||
## PHP Version
|
||||
|
||||
The project supports PHP 7.4+
|
||||
|
||||
## PSR Standards
|
||||
|
||||
- **PSR-1**: Basic Coding Standard
|
||||
- **PSR-4**: Autoloading Standard
|
||||
- **PSR-12**: Extended Coding Style
|
||||
|
||||
## Code Style
|
||||
|
||||
### Type Declarations
|
||||
|
||||
```php
|
||||
// ✅ Correct – strict typing
|
||||
public function getCustomers(Request $request): JsonResponse
|
||||
{
|
||||
$id = (int) $request->get('id');
|
||||
return new JsonResponse(['data' => $customers]);
|
||||
}
|
||||
|
||||
// ❌ Incorrect – no types
|
||||
public function getCustomers($request)
|
||||
{
|
||||
return ['data' => $customers];
|
||||
}
|
||||
```
|
||||
|
||||
### Nullable Types
|
||||
|
||||
```php
|
||||
// ✅ Correct
|
||||
public function findById(?int $id): ?array
|
||||
{
|
||||
if ($id === null) {
|
||||
return null;
|
||||
}
|
||||
return $this->query->where('id', '=', $id)->firstOrNull();
|
||||
}
|
||||
```
|
||||
|
||||
### Strict Types
|
||||
|
||||
Always use `declare(strict_types=1);`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
```
|
||||
|
||||
### Array Syntax
|
||||
|
||||
```php
|
||||
// ✅ Preferred – short syntax
|
||||
$array = ['key' => 'value'];
|
||||
|
||||
// ❌ Do not use
|
||||
$array = array('key' => 'value');
|
||||
```
|
||||
|
||||
### String Interpolation
|
||||
|
||||
```php
|
||||
// ✅ Preferred
|
||||
$message = "User {$userId} not found";
|
||||
|
||||
// ✅ Alternative
|
||||
$message = sprintf('User %d not found', $userId);
|
||||
```
|
||||
|
||||
### Arrow Functions (PHP 7.4+)
|
||||
|
||||
```php
|
||||
// ✅ For simple operations
|
||||
$filtered = array_filter($items, fn($item) => $item->isActive());
|
||||
|
||||
// ❌ For complex logic – use regular functions
|
||||
```
|
||||
|
||||
### Nullsafe Operator (PHP 8.0+)
|
||||
|
||||
```php
|
||||
// ✅ For PHP 7.4
|
||||
$name = $user && $user->profile ? $user->profile->name : null;
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Classes
|
||||
|
||||
```php
|
||||
// ✅ PascalCase
|
||||
class TelegramCustomerService {}
|
||||
class UserRepository {}
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```php
|
||||
// ✅ camelCase
|
||||
public function getCustomers(): array {}
|
||||
public function saveOrUpdate(array $data): array {}
|
||||
```
|
||||
|
||||
### Variables
|
||||
|
||||
```php
|
||||
// ✅ camelCase
|
||||
$customerData = [];
|
||||
$totalRecords = 0;
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
```php
|
||||
// ✅ UPPER_SNAKE_CASE
|
||||
private const MAX_RETRIES = 3;
|
||||
public const DEFAULT_PAGE_SIZE = 20;
|
||||
```
|
||||
|
||||
### Private Properties
|
||||
|
||||
```php
|
||||
// ✅ camelCase with visibility modifier
|
||||
private string $tableName;
|
||||
private Builder $builder;
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### PHPDoc
|
||||
|
||||
```php
|
||||
/**
|
||||
* @throws ValidationException If parameters are invalid
|
||||
*/
|
||||
public function getCustomers(Request $request): JsonResponse
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Inline Comments
|
||||
|
||||
```php
|
||||
// ✅ Useful comments
|
||||
// Apply filters to calculate total number of records
|
||||
$countQuery = $this->buildCountQuery($filters);
|
||||
|
||||
// ❌ Obvious comments
|
||||
// Get data
|
||||
$data = $this->getData();
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Exceptions
|
||||
|
||||
```php
|
||||
// ✅ Specific exceptions
|
||||
if (!$userId) {
|
||||
throw new InvalidArgumentException('User ID is required');
|
||||
}
|
||||
|
||||
// ✅ Logging
|
||||
try {
|
||||
$result = $this->service->process();
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Processing failed', [
|
||||
'exception' => $e,
|
||||
'context' => $context,
|
||||
]);
|
||||
throw new ProcessingException('Failed to process', 0, $e);
|
||||
}
|
||||
```
|
||||
|
||||
## Query Builder Usage
|
||||
|
||||
### Always Use Query Builder
|
||||
|
||||
```php
|
||||
// ✅ Correct
|
||||
$customers = $this->builder->newQuery()
|
||||
->select(['id', 'name', 'email'])
|
||||
->from('acmeshop_customers')
|
||||
->where('status', '=', 'active')
|
||||
->orderBy('created_at', 'DESC')
|
||||
->get();
|
||||
|
||||
// In edge cases raw SQL may be used
|
||||
$result = $this->database->query("SELECT * FROM acmeshop_customers");
|
||||
```
|
||||
|
||||
### Parameter Binding
|
||||
|
||||
```php
|
||||
// ✅ Query Builder automatically binds parameters
|
||||
$query->where('name', 'LIKE', "%{$search}%");
|
||||
|
||||
// ❌ Never concatenate values into SQL, avoid SQL Injection.
|
||||
```
|
||||
|
||||
## Array Access
|
||||
|
||||
### Safe Array Access
|
||||
|
||||
```php
|
||||
// ✅ Use Arr::get()
|
||||
use Acme\ECommerceFramework\Support\Arr;
|
||||
|
||||
$value = Arr::get($data, 'key', 'default');
|
||||
|
||||
// ❌ Unsafe
|
||||
$value = $data['key']; // may trigger an error
|
||||
```
|
||||
|
||||
## Return Types
|
||||
|
||||
```php
|
||||
// ✅ Always specify return type
|
||||
public function getData(): array {}
|
||||
public function findById(int $id): ?array {}
|
||||
public function process(): void {}
|
||||
|
||||
// ❌ Without type
|
||||
public function getData() {}
|
||||
```
|
||||
|
||||
## Visibility Modifiers
|
||||
|
||||
```php
|
||||
// ✅ Always specify visibility modifier
|
||||
private string $tableName;
|
||||
protected Builder $builder;
|
||||
public function getData(): array {}
|
||||
```
|
||||
|
||||
370
.cursor/rules/vue.md
Normal file
370
.cursor/rules/vue.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Vue.js 3 Rules
|
||||
|
||||
## Component Structure
|
||||
|
||||
### Template
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Logical structure -->
|
||||
<div class="container">
|
||||
<header>
|
||||
<h2>{{ title }}</h2>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<DataTable :value="items" />
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<Button @click="handleSave">Save</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Script Setup
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Always use <script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import { apiGet } from '@/utils/http.js';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update', 'delete']);
|
||||
|
||||
// State
|
||||
const items = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// Computed
|
||||
const totalItems = computed(() => items.value.length);
|
||||
|
||||
// Methods
|
||||
async function loadItems() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await apiGet('getItems');
|
||||
items.value = result.data || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadItems();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Styles
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* ✅ Use scoped styles */
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* ✅ Use :deep() to style nested components */
|
||||
:deep(.p-datatable) {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Component Naming
|
||||
|
||||
```vue
|
||||
<!-- ✅ PascalCase for components -->
|
||||
<CustomerCard />
|
||||
<ProductsList />
|
||||
<OrderDetails />
|
||||
|
||||
<!-- ✅ kebab-case in templates also works -->
|
||||
<customer-card />
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Always define types and validation
|
||||
const props = defineProps({
|
||||
customerId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
validator: (value) => value > 0
|
||||
},
|
||||
showDetails: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Emits
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Define emits with types
|
||||
const emit = defineEmits<{
|
||||
update: [id: number, data: object];
|
||||
delete: [id: number];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
// ✅ Or with validation
|
||||
const emit = defineEmits({
|
||||
update: (id: number, data: object) => {
|
||||
if (id > 0 && typeof data === 'object') {
|
||||
return true;
|
||||
}
|
||||
console.warn('Invalid emit arguments');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Reactive State
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ ref for primitives
|
||||
const count = ref(0);
|
||||
const name = ref('');
|
||||
|
||||
// ✅ ref for objects (preferred)
|
||||
const customer = ref({
|
||||
id: null,
|
||||
name: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
// ✅ Use reactive only when necessary
|
||||
import { reactive } from 'vue';
|
||||
const state = reactive({
|
||||
items: [],
|
||||
loading: false
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Computed Properties
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Use computed for derived values
|
||||
const filteredItems = computed(() => {
|
||||
return items.value.filter(item => item.isActive);
|
||||
});
|
||||
|
||||
// ✅ Computed with getter/setter
|
||||
const fullName = computed({
|
||||
get: () => `${firstName.value} ${lastName.value}`,
|
||||
set: (value) => {
|
||||
const parts = value.split(' ');
|
||||
firstName.value = parts[0];
|
||||
lastName.value = parts[1];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Event Handlers
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Use kebab-case for events -->
|
||||
<Button @click="handleClick" />
|
||||
<Input @input="handleInput" />
|
||||
<Form @submit.prevent="handleSubmit" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// ✅ Name handlers with handle* prefix
|
||||
function handleClick() {
|
||||
// ...
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
// ...
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Use v-if for conditional rendering -->
|
||||
<div v-if="loading">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
|
||||
<!-- ✅ v-show for frequent toggling -->
|
||||
<div v-show="hasItems">
|
||||
<ItemsList :items="items" />
|
||||
</div>
|
||||
|
||||
<!-- ✅ v-else for alternatives -->
|
||||
<div v-else>
|
||||
<EmptyState />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Lists
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Always use :key -->
|
||||
<div v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
|
||||
<!-- ✅ For index-based lists -->
|
||||
<div v-for="(item, index) in items" :key="`item-${index}`">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Form Handling
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<!-- ✅ Use v-model -->
|
||||
<InputText v-model="form.name" />
|
||||
<Textarea v-model="form.description" />
|
||||
|
||||
<!-- ✅ For custom components -->
|
||||
<CustomInput v-model="form.email" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
// Validation and submit
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## PrimeVue Components
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ Use PrimeVue components in the admin panel -->
|
||||
<DataTable
|
||||
:value="customers"
|
||||
:loading="loading"
|
||||
paginator
|
||||
:rows="20"
|
||||
@page="onPage"
|
||||
>
|
||||
<Column field="name" header="Name" sortable />
|
||||
</DataTable>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* ✅ Use scoped -->
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* ✅ :deep() for nested components */
|
||||
:deep(.p-datatable) {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* ✅ :slotted() for slots */
|
||||
:slotted(.header) {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Composition Functions
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// ✅ Extract complex logic into composables
|
||||
import { useCustomers } from '@/composables/useCustomers.js';
|
||||
|
||||
const {
|
||||
customers,
|
||||
loading,
|
||||
loadCustomers,
|
||||
totalRecords
|
||||
} = useCustomers();
|
||||
|
||||
onMounted(() => {
|
||||
loadCustomers();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useToast } from 'primevue';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const result = await apiGet('endpoint');
|
||||
if (result.success) {
|
||||
data.value = result.data;
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: result.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to load data'
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user