update deploy
This commit is contained in:
167
.claude/skills/pest-testing/SKILL.md
Normal file
167
.claude/skills/pest-testing/SKILL.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: pest-testing
|
||||
description: "Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating new tests (unit, feature, or browser)
|
||||
- Modifying existing tests
|
||||
- Debugging test failures
|
||||
- Working with browser testing or smoke testing
|
||||
- Writing architecture tests or visual regression tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<!-- Basic Pest Test Example -->
|
||||
```php
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<!-- Pest Response Assertion -->
|
||||
```php
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
```
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<!-- Pest Dataset Example -->
|
||||
```php
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
```
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<!-- Pest Browser Test Example -->
|
||||
```php
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<!-- Pest Smoke Testing Example -->
|
||||
```php
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<!-- Architecture Test Example -->
|
||||
```php
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
20
.cursor/mcp.json
Normal file
20
.cursor/mcp.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "php",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
},
|
||||
"herd": {
|
||||
"command": "php",
|
||||
"args": [
|
||||
"/Applications/Herd.app/Contents/Resources/herd-mcp.phar"
|
||||
],
|
||||
"env": {
|
||||
"SITE_PATH": "/Users/bulutkuru/Herd/bogazici-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
.cursor/skills/pest-testing/SKILL.md
Normal file
167
.cursor/skills/pest-testing/SKILL.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: pest-testing
|
||||
description: "Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating new tests (unit, feature, or browser)
|
||||
- Modifying existing tests
|
||||
- Debugging test failures
|
||||
- Working with browser testing or smoke testing
|
||||
- Writing architecture tests or visual regression tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<!-- Basic Pest Test Example -->
|
||||
```php
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<!-- Pest Response Assertion -->
|
||||
```php
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
```
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<!-- Pest Dataset Example -->
|
||||
```php
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
```
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<!-- Pest Browser Test Example -->
|
||||
```php
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<!-- Pest Smoke Testing Example -->
|
||||
```php
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<!-- Architecture Test Example -->
|
||||
```php
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
57
.drone.yml
Normal file
57
.drone.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- develop
|
||||
- test
|
||||
- main
|
||||
|
||||
steps:
|
||||
- name: laravel-check
|
||||
image: php:8.4-cli
|
||||
when:
|
||||
branch:
|
||||
- develop
|
||||
commands:
|
||||
- apt-get update && apt-get install -y git unzip curl libzip-dev default-mysql-client
|
||||
- docker-php-ext-install pdo_mysql zip
|
||||
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
- cp .env.example .env || true
|
||||
- composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
- php artisan key:generate --force || true
|
||||
- php artisan config:clear || true
|
||||
- php artisan test || true
|
||||
|
||||
- name: deploy-test
|
||||
image: appleboy/drone-ssh
|
||||
when:
|
||||
branch:
|
||||
- test
|
||||
settings:
|
||||
host:
|
||||
from_secret: server_host
|
||||
username:
|
||||
from_secret: server_user
|
||||
key:
|
||||
from_secret: server_ssh_key
|
||||
script:
|
||||
- cd /opt/projects/bogazici/corporate-api/test/api
|
||||
- bash scripts/deploy-test.sh
|
||||
|
||||
- name: deploy-prod
|
||||
image: appleboy/drone-ssh
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
settings:
|
||||
host:
|
||||
from_secret: server_host
|
||||
username:
|
||||
from_secret: server_user
|
||||
key:
|
||||
from_secret: server_ssh_key
|
||||
script:
|
||||
- cd /opt/projects/bogazici/corporate-api/prod/api
|
||||
- bash scripts/deploy-prod.sh
|
||||
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
65
.env.example
Normal file
65
.env.example
Normal file
@@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=bogazici_api
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/uploads/images/*
|
||||
/public/uploads/videos/*
|
||||
/public/uploads/hero-slides/*
|
||||
/public/uploads/settings/*
|
||||
/public/uploads/pages/*
|
||||
/public/uploads/courses/*
|
||||
/public/uploads/announcements/*
|
||||
/public/uploads/categories/*
|
||||
!/public/uploads/*/.gitkeep
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
238
.junie/guidelines.md
Normal file
238
.junie/guidelines.md
Normal file
@@ -0,0 +1,238 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4.18
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v4
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
|
||||
## Conventions
|
||||
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
|
||||
|
||||
## URLs
|
||||
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
|
||||
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- `public function __construct(public GitHub $github) { }`
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
|
||||
=== herd rules ===
|
||||
|
||||
# Laravel Herd
|
||||
|
||||
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
|
||||
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
## Database
|
||||
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## Controllers & Validation
|
||||
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
## Laravel 12 Structure
|
||||
|
||||
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
## Database
|
||||
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
|
||||
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
20
.junie/mcp/mcp.json
Normal file
20
.junie/mcp/mcp.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "/Users/bulutkuru/Library/Application Support/Herd/bin/php84",
|
||||
"args": [
|
||||
"/Users/bulutkuru/Herd/bogazici-api/artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
},
|
||||
"herd": {
|
||||
"command": "/Users/bulutkuru/Library/Application Support/Herd/bin/php84",
|
||||
"args": [
|
||||
"/Applications/Herd.app/Contents/Resources/herd-mcp.phar"
|
||||
],
|
||||
"env": {
|
||||
"SITE_PATH": "/Users/bulutkuru/Herd/bogazici-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
.junie/skills/pest-testing/SKILL.md
Normal file
167
.junie/skills/pest-testing/SKILL.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: pest-testing
|
||||
description: "Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating new tests (unit, feature, or browser)
|
||||
- Modifying existing tests
|
||||
- Debugging test failures
|
||||
- Working with browser testing or smoke testing
|
||||
- Writing architecture tests or visual regression tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<!-- Basic Pest Test Example -->
|
||||
```php
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<!-- Pest Response Assertion -->
|
||||
```php
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
```
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<!-- Pest Dataset Example -->
|
||||
```php
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
```
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<!-- Pest Browser Test Example -->
|
||||
```php
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<!-- Pest Smoke Testing Example -->
|
||||
```php
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<!-- Architecture Test Example -->
|
||||
```php
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
20
.mcp.json
Normal file
20
.mcp.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "php",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
},
|
||||
"herd": {
|
||||
"command": "php",
|
||||
"args": [
|
||||
"/Applications/Herd.app/Contents/Resources/herd-mcp.phar"
|
||||
],
|
||||
"env": {
|
||||
"SITE_PATH": "/Users/bulutkuru/Herd/bogazici-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
242
AGENTS.md
Normal file
242
AGENTS.md
Normal file
@@ -0,0 +1,242 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4.19
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v4
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
|
||||
## Conventions
|
||||
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan Commands
|
||||
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
|
||||
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
|
||||
## URLs
|
||||
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Debugging
|
||||
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
|
||||
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
|
||||
- To inspect routes, run `php artisan route:list` directly.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
|
||||
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- `public function __construct(public GitHub $github) { }`
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
|
||||
=== herd rules ===
|
||||
|
||||
# Laravel Herd
|
||||
|
||||
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
|
||||
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
## Database
|
||||
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## Controllers & Validation
|
||||
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
## Laravel 12 Structure
|
||||
|
||||
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
## Database
|
||||
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
|
||||
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
242
CLAUDE.md
Normal file
242
CLAUDE.md
Normal file
@@ -0,0 +1,242 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4.19
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v4
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
|
||||
## Conventions
|
||||
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan Commands
|
||||
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
|
||||
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
|
||||
## URLs
|
||||
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Debugging
|
||||
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
|
||||
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
|
||||
- To inspect routes, run `php artisan route:list` directly.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
|
||||
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- `public function __construct(public GitHub $github) { }`
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
|
||||
=== herd rules ===
|
||||
|
||||
# Laravel Herd
|
||||
|
||||
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
|
||||
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
## Database
|
||||
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## Controllers & Validation
|
||||
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
## Laravel 12 Structure
|
||||
|
||||
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
## Database
|
||||
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
|
||||
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
54
Dockerfile
Normal file
54
Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
||||
FROM php:8.4-apache
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libpng-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libfreetype6-dev \
|
||||
locales \
|
||||
zip \
|
||||
jpegoptim \
|
||||
optipng \
|
||||
pngquant \
|
||||
gifsicle \
|
||||
vim \
|
||||
nano \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
libicu-dev \
|
||||
git \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
libmagickwand-dev \
|
||||
--no-install-recommends \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN printf "\n" | pecl install imagick \
|
||||
&& docker-php-ext-enable imagick
|
||||
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) gd pdo_mysql exif zip bcmath intl
|
||||
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# PHP upload limits
|
||||
RUN echo "upload_max_filesize=110M" > /usr/local/etc/php/conf.d/uploads.ini \
|
||||
&& echo "post_max_size=120M" >> /usr/local/etc/php/conf.d/uploads.ini \
|
||||
&& echo "memory_limit=256M" >> /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
|
||||
COPY . /var/www/html
|
||||
COPY docker/apache/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh \
|
||||
&& mkdir -p storage bootstrap/cache \
|
||||
&& chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 775 storage bootstrap/cache
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
242
GEMINI.md
Normal file
242
GEMINI.md
Normal file
@@ -0,0 +1,242 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4.19
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v4
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
|
||||
## Conventions
|
||||
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan Commands
|
||||
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
|
||||
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
|
||||
## URLs
|
||||
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Debugging
|
||||
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
|
||||
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
|
||||
- To inspect routes, run `php artisan route:list` directly.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
|
||||
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- `public function __construct(public GitHub $github) { }`
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
|
||||
=== herd rules ===
|
||||
|
||||
# Laravel Herd
|
||||
|
||||
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
|
||||
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
## Database
|
||||
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## Controllers & Validation
|
||||
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
## Laravel 12 Structure
|
||||
|
||||
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
## Database
|
||||
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
|
||||
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
25
TODO.md
Normal file
25
TODO.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Boğaziçi Platform - Entegrasyon Düzeltmeleri
|
||||
|
||||
## 🔴 KRİTİK
|
||||
|
||||
- [x] **1. `.env` dosyasını düzelt** — `.env.example`'dan yeniden oluşturuldu, APP_KEY generate edildi
|
||||
- [x] **2. Web form endpoint'ini düzelt** — Next.js API route `/api/basvuru/route.ts` oluşturuldu, FormData → JSON dönüşümü yapıp backend'e proxy'liyor
|
||||
- [x] **3. Web form alan adlarını düzelt** — Tüm formlarda: `ad` → `name`, `course` → `target_course`, `education` → `education_level`, `source` hidden field + `kvkk_consent` checkbox eklendi
|
||||
- [x] **4. LeadResource alan hatalarını düzelt** — `utm` JSON parse, `consent_kvkk` mapping, `notes` mapping, `email` alanı eklendi
|
||||
|
||||
## 🟠 YÜKSEK ÖNCELİK
|
||||
|
||||
- [x] **5. Factory'lerde `order` → `order_index` düzelt** — FaqFactory.php, HeroSlideFactory.php
|
||||
- [x] **6. Seeder'larda `order` → `order_index` düzelt** — FaqSeeder.php, HeroSlideSeeder.php, FaqContentSeeder.php
|
||||
- [x] **7. CourseScheduleResource eksik alanları** — Migration, Model, Resource güncellendi: `instructor`, `enrolled_count`, `price_override`, `status`, `notes` eklendi
|
||||
- [x] **8. Web'de kullanılmayan API endpoint'lerini entegre et** — `api.ts`'e `getGuideCards()`, `getComments()`, `getSitemapData()` fonksiyonları eklendi
|
||||
|
||||
## 🟡 DÜŞÜK ÖNCELİK
|
||||
|
||||
- [x] **9. Admin'den çağrılmayan endpoint'ler** — `apiResource` otomatik endpoint'leri, zararsız, atlandı
|
||||
|
||||
## ✅ DOĞRULAMA
|
||||
|
||||
- [x] **10. Testleri çalıştır ve geçir** — 2 test geçti
|
||||
- [x] **11. Seed çalıştır ve doğrula** — Tüm seeder'lar sorunsuz çalıştı
|
||||
- [x] **12. Web formlarını uçtan uca test et** — Lead API'ye curl ile tam form gönderildi, DB'ye doğru düştüğü ve LeadResource'un doğru döndüğü doğrulandı
|
||||
22
app/Actions/Announcement/CreateAnnouncementAction.php
Normal file
22
app/Actions/Announcement/CreateAnnouncementAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Announcement;
|
||||
|
||||
use App\DTOs\AnnouncementData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Announcement;
|
||||
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
|
||||
|
||||
final class CreateAnnouncementAction
|
||||
{
|
||||
public function __construct(private AnnouncementRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(AnnouncementData $data): Announcement
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Announcement::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Announcement/DeleteAnnouncementAction.php
Normal file
21
app/Actions/Announcement/DeleteAnnouncementAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Announcement;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Announcement;
|
||||
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
|
||||
|
||||
final class DeleteAnnouncementAction
|
||||
{
|
||||
public function __construct(private AnnouncementRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Announcement $announcement): bool
|
||||
{
|
||||
$this->repository->delete($announcement);
|
||||
|
||||
ModelChanged::dispatch(Announcement::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Announcement/UpdateAnnouncementAction.php
Normal file
22
app/Actions/Announcement/UpdateAnnouncementAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Announcement;
|
||||
|
||||
use App\DTOs\AnnouncementData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Announcement;
|
||||
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
|
||||
|
||||
final class UpdateAnnouncementAction
|
||||
{
|
||||
public function __construct(private AnnouncementRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Announcement $announcement, AnnouncementData $data): Announcement
|
||||
{
|
||||
$result = $this->repository->update($announcement, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Announcement::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
23
app/Actions/Category/CreateCategoryAction.php
Normal file
23
app/Actions/Category/CreateCategoryAction.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Category;
|
||||
|
||||
use App\DTOs\CategoryData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Category;
|
||||
use App\Repositories\Contracts\CategoryRepositoryInterface;
|
||||
|
||||
class CreateCategoryAction
|
||||
{
|
||||
public function __construct(private CategoryRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(CategoryData $data): Category
|
||||
{
|
||||
/** @var Category */
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Category::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Category/DeleteCategoryAction.php
Normal file
21
app/Actions/Category/DeleteCategoryAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Category;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Category;
|
||||
use App\Repositories\Contracts\CategoryRepositoryInterface;
|
||||
|
||||
class DeleteCategoryAction
|
||||
{
|
||||
public function __construct(private CategoryRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Category $category): bool
|
||||
{
|
||||
$this->repository->delete($category);
|
||||
|
||||
ModelChanged::dispatch(Category::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
23
app/Actions/Category/UpdateCategoryAction.php
Normal file
23
app/Actions/Category/UpdateCategoryAction.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Category;
|
||||
|
||||
use App\DTOs\CategoryData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Category;
|
||||
use App\Repositories\Contracts\CategoryRepositoryInterface;
|
||||
|
||||
class UpdateCategoryAction
|
||||
{
|
||||
public function __construct(private CategoryRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Category $category, CategoryData $data): Category
|
||||
{
|
||||
/** @var Category */
|
||||
$result = $this->repository->update($category, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Category::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Comment/CreateCommentAction.php
Normal file
22
app/Actions/Comment/CreateCommentAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Comment;
|
||||
|
||||
use App\DTOs\CommentData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Comment;
|
||||
use App\Repositories\Contracts\CommentRepositoryInterface;
|
||||
|
||||
final class CreateCommentAction
|
||||
{
|
||||
public function __construct(private CommentRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(CommentData $data): Comment
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Comment::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Comment/DeleteCommentAction.php
Normal file
21
app/Actions/Comment/DeleteCommentAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Comment;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Comment;
|
||||
use App\Repositories\Contracts\CommentRepositoryInterface;
|
||||
|
||||
final class DeleteCommentAction
|
||||
{
|
||||
public function __construct(private CommentRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Comment $comment): bool
|
||||
{
|
||||
$this->repository->delete($comment);
|
||||
|
||||
ModelChanged::dispatch(Comment::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
24
app/Actions/Comment/UpdateCommentAction.php
Normal file
24
app/Actions/Comment/UpdateCommentAction.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Comment;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Comment;
|
||||
use App\Repositories\Contracts\CommentRepositoryInterface;
|
||||
|
||||
final class UpdateCommentAction
|
||||
{
|
||||
public function __construct(private CommentRepositoryInterface $repository) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function execute(Comment $comment, array $data): Comment
|
||||
{
|
||||
$result = $this->repository->update($comment, $data);
|
||||
|
||||
ModelChanged::dispatch(Comment::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
23
app/Actions/Course/CreateCourseAction.php
Normal file
23
app/Actions/Course/CreateCourseAction.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Course;
|
||||
|
||||
use App\DTOs\CourseData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Course;
|
||||
use App\Repositories\Contracts\CourseRepositoryInterface;
|
||||
|
||||
class CreateCourseAction
|
||||
{
|
||||
public function __construct(private CourseRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(CourseData $data): Course
|
||||
{
|
||||
/** @var Course */
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Course::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Course/DeleteCourseAction.php
Normal file
21
app/Actions/Course/DeleteCourseAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Course;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Course;
|
||||
use App\Repositories\Contracts\CourseRepositoryInterface;
|
||||
|
||||
class DeleteCourseAction
|
||||
{
|
||||
public function __construct(private CourseRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Course $course): bool
|
||||
{
|
||||
$this->repository->delete($course);
|
||||
|
||||
ModelChanged::dispatch(Course::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
23
app/Actions/Course/UpdateCourseAction.php
Normal file
23
app/Actions/Course/UpdateCourseAction.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Course;
|
||||
|
||||
use App\DTOs\CourseData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Course;
|
||||
use App\Repositories\Contracts\CourseRepositoryInterface;
|
||||
|
||||
class UpdateCourseAction
|
||||
{
|
||||
public function __construct(private CourseRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Course $course, CourseData $data): Course
|
||||
{
|
||||
/** @var Course */
|
||||
$result = $this->repository->update($course, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Course::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Faq/CreateFaqAction.php
Normal file
22
app/Actions/Faq/CreateFaqAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Faq;
|
||||
|
||||
use App\DTOs\FaqData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Faq;
|
||||
use App\Repositories\Contracts\FaqRepositoryInterface;
|
||||
|
||||
final class CreateFaqAction
|
||||
{
|
||||
public function __construct(private FaqRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(FaqData $data): Faq
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Faq::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Faq/DeleteFaqAction.php
Normal file
21
app/Actions/Faq/DeleteFaqAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Faq;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Faq;
|
||||
use App\Repositories\Contracts\FaqRepositoryInterface;
|
||||
|
||||
final class DeleteFaqAction
|
||||
{
|
||||
public function __construct(private FaqRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Faq $faq): bool
|
||||
{
|
||||
$this->repository->delete($faq);
|
||||
|
||||
ModelChanged::dispatch(Faq::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Faq/UpdateFaqAction.php
Normal file
22
app/Actions/Faq/UpdateFaqAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Faq;
|
||||
|
||||
use App\DTOs\FaqData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Faq;
|
||||
use App\Repositories\Contracts\FaqRepositoryInterface;
|
||||
|
||||
final class UpdateFaqAction
|
||||
{
|
||||
public function __construct(private FaqRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Faq $faq, FaqData $data): Faq
|
||||
{
|
||||
$result = $this->repository->update($faq, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Faq::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/GuideCard/CreateGuideCardAction.php
Normal file
22
app/Actions/GuideCard/CreateGuideCardAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\GuideCard;
|
||||
|
||||
use App\DTOs\GuideCardData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\GuideCard;
|
||||
use App\Repositories\Contracts\GuideCardRepositoryInterface;
|
||||
|
||||
final class CreateGuideCardAction
|
||||
{
|
||||
public function __construct(private GuideCardRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(GuideCardData $data): GuideCard
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(GuideCard::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/GuideCard/DeleteGuideCardAction.php
Normal file
21
app/Actions/GuideCard/DeleteGuideCardAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\GuideCard;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\GuideCard;
|
||||
use App\Repositories\Contracts\GuideCardRepositoryInterface;
|
||||
|
||||
final class DeleteGuideCardAction
|
||||
{
|
||||
public function __construct(private GuideCardRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(GuideCard $guideCard): bool
|
||||
{
|
||||
$this->repository->delete($guideCard);
|
||||
|
||||
ModelChanged::dispatch(GuideCard::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/GuideCard/UpdateGuideCardAction.php
Normal file
22
app/Actions/GuideCard/UpdateGuideCardAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\GuideCard;
|
||||
|
||||
use App\DTOs\GuideCardData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\GuideCard;
|
||||
use App\Repositories\Contracts\GuideCardRepositoryInterface;
|
||||
|
||||
final class UpdateGuideCardAction
|
||||
{
|
||||
public function __construct(private GuideCardRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(GuideCard $guideCard, GuideCardData $data): GuideCard
|
||||
{
|
||||
$result = $this->repository->update($guideCard, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(GuideCard::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/HeroSlide/CreateHeroSlideAction.php
Normal file
22
app/Actions/HeroSlide/CreateHeroSlideAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\HeroSlide;
|
||||
|
||||
use App\DTOs\HeroSlideData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\HeroSlide;
|
||||
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
|
||||
|
||||
final class CreateHeroSlideAction
|
||||
{
|
||||
public function __construct(private HeroSlideRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(HeroSlideData $data): HeroSlide
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(HeroSlide::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/HeroSlide/DeleteHeroSlideAction.php
Normal file
21
app/Actions/HeroSlide/DeleteHeroSlideAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\HeroSlide;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\HeroSlide;
|
||||
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
|
||||
|
||||
final class DeleteHeroSlideAction
|
||||
{
|
||||
public function __construct(private HeroSlideRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(HeroSlide $heroSlide): bool
|
||||
{
|
||||
$this->repository->delete($heroSlide);
|
||||
|
||||
ModelChanged::dispatch(HeroSlide::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/HeroSlide/UpdateHeroSlideAction.php
Normal file
22
app/Actions/HeroSlide/UpdateHeroSlideAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\HeroSlide;
|
||||
|
||||
use App\DTOs\HeroSlideData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\HeroSlide;
|
||||
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
|
||||
|
||||
final class UpdateHeroSlideAction
|
||||
{
|
||||
public function __construct(private HeroSlideRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(HeroSlide $heroSlide, HeroSlideData $data): HeroSlide
|
||||
{
|
||||
$result = $this->repository->update($heroSlide, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(HeroSlide::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Lead/CreateLeadAction.php
Normal file
22
app/Actions/Lead/CreateLeadAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Lead;
|
||||
|
||||
use App\DTOs\LeadData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Lead;
|
||||
use App\Repositories\Contracts\LeadRepositoryInterface;
|
||||
|
||||
final class CreateLeadAction
|
||||
{
|
||||
public function __construct(private LeadRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(LeadData $data): Lead
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Lead::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Lead/DeleteLeadAction.php
Normal file
21
app/Actions/Lead/DeleteLeadAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Lead;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Lead;
|
||||
use App\Repositories\Contracts\LeadRepositoryInterface;
|
||||
|
||||
final class DeleteLeadAction
|
||||
{
|
||||
public function __construct(private LeadRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Lead $lead): bool
|
||||
{
|
||||
$this->repository->delete($lead);
|
||||
|
||||
ModelChanged::dispatch(Lead::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Lead/UpdateLeadAction.php
Normal file
22
app/Actions/Lead/UpdateLeadAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Lead;
|
||||
|
||||
use App\DTOs\LeadData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Lead;
|
||||
use App\Repositories\Contracts\LeadRepositoryInterface;
|
||||
|
||||
final class UpdateLeadAction
|
||||
{
|
||||
public function __construct(private LeadRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Lead $lead, LeadData $data): Lead
|
||||
{
|
||||
$result = $this->repository->update($lead, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Lead::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Menu/CreateMenuAction.php
Normal file
22
app/Actions/Menu/CreateMenuAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Menu;
|
||||
|
||||
use App\DTOs\MenuData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Menu;
|
||||
use App\Repositories\Contracts\MenuRepositoryInterface;
|
||||
|
||||
final class CreateMenuAction
|
||||
{
|
||||
public function __construct(private MenuRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(MenuData $data): Menu
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Menu::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Menu/DeleteMenuAction.php
Normal file
21
app/Actions/Menu/DeleteMenuAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Menu;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Menu;
|
||||
use App\Repositories\Contracts\MenuRepositoryInterface;
|
||||
|
||||
final class DeleteMenuAction
|
||||
{
|
||||
public function __construct(private MenuRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Menu $menu): bool
|
||||
{
|
||||
$this->repository->delete($menu);
|
||||
|
||||
ModelChanged::dispatch(Menu::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Menu/UpdateMenuAction.php
Normal file
22
app/Actions/Menu/UpdateMenuAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Menu;
|
||||
|
||||
use App\DTOs\MenuData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Menu;
|
||||
use App\Repositories\Contracts\MenuRepositoryInterface;
|
||||
|
||||
final class UpdateMenuAction
|
||||
{
|
||||
public function __construct(private MenuRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Menu $menu, MenuData $data): Menu
|
||||
{
|
||||
$result = $this->repository->update($menu, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Menu::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Page/CreatePageAction.php
Normal file
22
app/Actions/Page/CreatePageAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Page;
|
||||
|
||||
use App\DTOs\PageData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Page;
|
||||
use App\Repositories\Contracts\PageRepositoryInterface;
|
||||
|
||||
final class CreatePageAction
|
||||
{
|
||||
public function __construct(private PageRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(PageData $data): Page
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Page::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Page/DeletePageAction.php
Normal file
21
app/Actions/Page/DeletePageAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Page;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Page;
|
||||
use App\Repositories\Contracts\PageRepositoryInterface;
|
||||
|
||||
final class DeletePageAction
|
||||
{
|
||||
public function __construct(private PageRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Page $page): bool
|
||||
{
|
||||
$this->repository->delete($page);
|
||||
|
||||
ModelChanged::dispatch(Page::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Page/UpdatePageAction.php
Normal file
22
app/Actions/Page/UpdatePageAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Page;
|
||||
|
||||
use App\DTOs\PageData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Page;
|
||||
use App\Repositories\Contracts\PageRepositoryInterface;
|
||||
|
||||
final class UpdatePageAction
|
||||
{
|
||||
public function __construct(private PageRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(Page $page, PageData $data): Page
|
||||
{
|
||||
$result = $this->repository->update($page, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(Page::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Schedule/CreateScheduleAction.php
Normal file
22
app/Actions/Schedule/CreateScheduleAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Schedule;
|
||||
|
||||
use App\DTOs\ScheduleData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\CourseSchedule;
|
||||
use App\Repositories\Contracts\ScheduleRepositoryInterface;
|
||||
|
||||
final class CreateScheduleAction
|
||||
{
|
||||
public function __construct(private ScheduleRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(ScheduleData $data): CourseSchedule
|
||||
{
|
||||
$result = $this->repository->create($data->toArray());
|
||||
|
||||
ModelChanged::dispatch(CourseSchedule::class, 'created');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Schedule/DeleteScheduleAction.php
Normal file
21
app/Actions/Schedule/DeleteScheduleAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Schedule;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\CourseSchedule;
|
||||
use App\Repositories\Contracts\ScheduleRepositoryInterface;
|
||||
|
||||
final class DeleteScheduleAction
|
||||
{
|
||||
public function __construct(private ScheduleRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(CourseSchedule $schedule): bool
|
||||
{
|
||||
$this->repository->delete($schedule);
|
||||
|
||||
ModelChanged::dispatch(CourseSchedule::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Schedule/UpdateScheduleAction.php
Normal file
22
app/Actions/Schedule/UpdateScheduleAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Schedule;
|
||||
|
||||
use App\DTOs\ScheduleData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\CourseSchedule;
|
||||
use App\Repositories\Contracts\ScheduleRepositoryInterface;
|
||||
|
||||
final class UpdateScheduleAction
|
||||
{
|
||||
public function __construct(private ScheduleRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(CourseSchedule $schedule, ScheduleData $data): CourseSchedule
|
||||
{
|
||||
$result = $this->repository->update($schedule, $data->toArray());
|
||||
|
||||
ModelChanged::dispatch(CourseSchedule::class, 'updated');
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Setting/UpdateSettingsAction.php
Normal file
22
app/Actions/Setting/UpdateSettingsAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Setting;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\Setting;
|
||||
use App\Repositories\Contracts\SettingRepositoryInterface;
|
||||
|
||||
final class UpdateSettingsAction
|
||||
{
|
||||
public function __construct(private SettingRepositoryInterface $repository) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
*/
|
||||
public function execute(array $settings): void
|
||||
{
|
||||
$this->repository->bulkUpdate($settings);
|
||||
|
||||
ModelChanged::dispatch(Setting::class, 'updated');
|
||||
}
|
||||
}
|
||||
29
app/Actions/User/CreateUserAction.php
Normal file
29
app/Actions/User/CreateUserAction.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\User;
|
||||
|
||||
use App\DTOs\UserData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Contracts\UserRepositoryInterface;
|
||||
|
||||
class CreateUserAction
|
||||
{
|
||||
public function __construct(private UserRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(UserData $data): User
|
||||
{
|
||||
/** @var User */
|
||||
$user = $this->repository->create($data->toArray());
|
||||
|
||||
if ($data->role) {
|
||||
$user->syncRoles([$data->role]);
|
||||
}
|
||||
|
||||
$user->load('roles');
|
||||
|
||||
ModelChanged::dispatch(User::class, 'created');
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
21
app/Actions/User/DeleteUserAction.php
Normal file
21
app/Actions/User/DeleteUserAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\User;
|
||||
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Contracts\UserRepositoryInterface;
|
||||
|
||||
class DeleteUserAction
|
||||
{
|
||||
public function __construct(private UserRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(User $user): bool
|
||||
{
|
||||
$this->repository->delete($user);
|
||||
|
||||
ModelChanged::dispatch(User::class, 'deleted');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
29
app/Actions/User/UpdateUserAction.php
Normal file
29
app/Actions/User/UpdateUserAction.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\User;
|
||||
|
||||
use App\DTOs\UserData;
|
||||
use App\Events\ModelChanged;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Contracts\UserRepositoryInterface;
|
||||
|
||||
class UpdateUserAction
|
||||
{
|
||||
public function __construct(private UserRepositoryInterface $repository) {}
|
||||
|
||||
public function execute(User $user, UserData $data): User
|
||||
{
|
||||
/** @var User */
|
||||
$user = $this->repository->update($user, $data->toArray());
|
||||
|
||||
if ($data->role !== null) {
|
||||
$user->syncRoles([$data->role]);
|
||||
}
|
||||
|
||||
$user->load('roles');
|
||||
|
||||
ModelChanged::dispatch(User::class, 'updated');
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
57
app/DTOs/AnnouncementData.php
Normal file
57
app/DTOs/AnnouncementData.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
class AnnouncementData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $slug,
|
||||
public readonly string $title,
|
||||
public readonly string $category,
|
||||
public readonly string $excerpt,
|
||||
public readonly string $content,
|
||||
public readonly ?string $image = null,
|
||||
public readonly bool $isFeatured = false,
|
||||
public readonly ?string $metaTitle = null,
|
||||
public readonly ?string $metaDescription = null,
|
||||
public readonly ?string $publishedAt = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
slug: $data['slug'],
|
||||
title: $data['title'],
|
||||
category: $data['category'],
|
||||
excerpt: $data['excerpt'],
|
||||
content: $data['content'],
|
||||
image: $data['image'] ?? null,
|
||||
isFeatured: $data['is_featured'] ?? false,
|
||||
metaTitle: $data['meta_title'] ?? null,
|
||||
metaDescription: $data['meta_description'] ?? null,
|
||||
publishedAt: $data['published_at'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'slug' => $this->slug,
|
||||
'title' => $this->title,
|
||||
'category' => $this->category,
|
||||
'excerpt' => $this->excerpt,
|
||||
'content' => $this->content,
|
||||
'image' => $this->image,
|
||||
'is_featured' => $this->isFeatured,
|
||||
'meta_title' => $this->metaTitle,
|
||||
'meta_description' => $this->metaDescription,
|
||||
'published_at' => $this->publishedAt,
|
||||
];
|
||||
}
|
||||
}
|
||||
45
app/DTOs/CategoryData.php
Normal file
45
app/DTOs/CategoryData.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
class CategoryData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $slug,
|
||||
public readonly string $label,
|
||||
public readonly ?string $desc = null,
|
||||
public readonly ?string $image = null,
|
||||
public readonly ?string $metaTitle = null,
|
||||
public readonly ?string $metaDescription = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
slug: $data['slug'],
|
||||
label: $data['label'],
|
||||
desc: $data['desc'] ?? null,
|
||||
image: $data['image'] ?? null,
|
||||
metaTitle: $data['meta_title'] ?? null,
|
||||
metaDescription: $data['meta_description'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'slug' => $this->slug,
|
||||
'label' => $this->label,
|
||||
'desc' => $this->desc,
|
||||
'image' => $this->image,
|
||||
'meta_title' => $this->metaTitle,
|
||||
'meta_description' => $this->metaDescription,
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/DTOs/CommentData.php
Normal file
48
app/DTOs/CommentData.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
final readonly class CommentData
|
||||
{
|
||||
public function __construct(
|
||||
public string $commentableType,
|
||||
public int $commentableId,
|
||||
public string $authorName,
|
||||
public string $content,
|
||||
public ?int $rating,
|
||||
public bool $isApproved = false,
|
||||
public ?string $adminReply = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
commentableType: $data['commentable_type'],
|
||||
commentableId: $data['commentable_id'],
|
||||
content: $data['content'],
|
||||
authorName: $data['author_name'],
|
||||
rating: $data['rating'] ?? null,
|
||||
isApproved: $data['is_approved'] ?? false,
|
||||
adminReply: $data['admin_reply'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'commentable_type' => $this->commentableType,
|
||||
'commentable_id' => $this->commentableId,
|
||||
'author_name' => $this->authorName,
|
||||
'content' => $this->content,
|
||||
'rating' => $this->rating,
|
||||
'is_approved' => $this->isApproved,
|
||||
'admin_reply' => $this->adminReply,
|
||||
];
|
||||
}
|
||||
}
|
||||
92
app/DTOs/CourseData.php
Normal file
92
app/DTOs/CourseData.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
class CourseData
|
||||
{
|
||||
/**
|
||||
* @param list<string>|null $includes
|
||||
* @param list<string>|null $requirements
|
||||
* @param list<string>|null $scope
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $categoryId,
|
||||
public readonly string $slug,
|
||||
public readonly string $title,
|
||||
public readonly string $desc,
|
||||
public readonly string $longDesc,
|
||||
public readonly string $duration,
|
||||
public readonly ?string $sub = null,
|
||||
public readonly int $students = 0,
|
||||
public readonly float $rating = 5.0,
|
||||
public readonly ?string $badge = null,
|
||||
public readonly ?string $image = null,
|
||||
public readonly ?string $price = null,
|
||||
public readonly ?array $includes = null,
|
||||
public readonly ?array $requirements = null,
|
||||
public readonly ?string $metaTitle = null,
|
||||
public readonly ?string $metaDescription = null,
|
||||
public readonly ?array $scope = null,
|
||||
public readonly ?string $standard = null,
|
||||
public readonly ?string $language = null,
|
||||
public readonly ?string $location = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
categoryId: $data['category_id'],
|
||||
slug: $data['slug'],
|
||||
title: $data['title'],
|
||||
desc: $data['desc'],
|
||||
longDesc: $data['long_desc'],
|
||||
duration: $data['duration'],
|
||||
sub: $data['sub'] ?? null,
|
||||
students: $data['students'] ?? 0,
|
||||
rating: $data['rating'] ?? 5.0,
|
||||
badge: $data['badge'] ?? null,
|
||||
image: $data['image'] ?? null,
|
||||
price: $data['price'] ?? null,
|
||||
includes: $data['includes'] ?? null,
|
||||
requirements: $data['requirements'] ?? null,
|
||||
metaTitle: $data['meta_title'] ?? null,
|
||||
metaDescription: $data['meta_description'] ?? null,
|
||||
scope: $data['scope'] ?? null,
|
||||
standard: $data['standard'] ?? null,
|
||||
language: $data['language'] ?? null,
|
||||
location: $data['location'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'category_id' => $this->categoryId,
|
||||
'slug' => $this->slug,
|
||||
'title' => $this->title,
|
||||
'sub' => $this->sub,
|
||||
'desc' => $this->desc,
|
||||
'long_desc' => $this->longDesc,
|
||||
'duration' => $this->duration,
|
||||
'students' => $this->students,
|
||||
'rating' => $this->rating,
|
||||
'badge' => $this->badge,
|
||||
'image' => $this->image,
|
||||
'price' => $this->price,
|
||||
'includes' => $this->includes,
|
||||
'requirements' => $this->requirements,
|
||||
'meta_title' => $this->metaTitle,
|
||||
'meta_description' => $this->metaDescription,
|
||||
'scope' => $this->scope,
|
||||
'standard' => $this->standard,
|
||||
'language' => $this->language,
|
||||
'location' => $this->location,
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/DTOs/FaqData.php
Normal file
44
app/DTOs/FaqData.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
use App\Enums\FaqCategory;
|
||||
|
||||
final readonly class FaqData
|
||||
{
|
||||
public function __construct(
|
||||
public string $question,
|
||||
public string $answer,
|
||||
public FaqCategory $category,
|
||||
public int $orderIndex = 0,
|
||||
public bool $isActive = true,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
question: $data['question'],
|
||||
answer: $data['answer'],
|
||||
category: FaqCategory::from($data['category']),
|
||||
orderIndex: $data['order_index'] ?? 0,
|
||||
isActive: $data['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'question' => $this->question,
|
||||
'answer' => $this->answer,
|
||||
'category' => $this->category->value,
|
||||
'order_index' => $this->orderIndex,
|
||||
'is_active' => $this->isActive,
|
||||
];
|
||||
}
|
||||
}
|
||||
42
app/DTOs/GuideCardData.php
Normal file
42
app/DTOs/GuideCardData.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
final readonly class GuideCardData
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public string $description,
|
||||
public ?string $icon,
|
||||
public int $orderIndex = 0,
|
||||
public bool $isActive = true,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
title: $data['title'],
|
||||
description: $data['description'],
|
||||
icon: $data['icon'] ?? null,
|
||||
orderIndex: $data['order_index'] ?? 0,
|
||||
isActive: $data['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'icon' => $this->icon,
|
||||
'order_index' => $this->orderIndex,
|
||||
'is_active' => $this->isActive,
|
||||
];
|
||||
}
|
||||
}
|
||||
63
app/DTOs/HeroSlideData.php
Normal file
63
app/DTOs/HeroSlideData.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
final readonly class HeroSlideData
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public ?string $label = null,
|
||||
public ?string $description = null,
|
||||
public string $mediaType = 'image',
|
||||
public ?string $image = null,
|
||||
public ?string $videoUrl = null,
|
||||
public ?string $mobileVideoUrl = null,
|
||||
public ?string $mobileImage = null,
|
||||
public ?string $buttonText = null,
|
||||
public ?string $buttonUrl = null,
|
||||
public int $orderIndex = 0,
|
||||
public bool $isActive = true,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
title: $data['title'],
|
||||
label: $data['label'] ?? null,
|
||||
description: $data['description'] ?? null,
|
||||
mediaType: $data['media_type'] ?? 'image',
|
||||
image: $data['image'] ?? null,
|
||||
videoUrl: $data['video_url'] ?? null,
|
||||
mobileVideoUrl: $data['mobile_video_url'] ?? null,
|
||||
mobileImage: $data['mobile_image'] ?? null,
|
||||
buttonText: $data['button_text'] ?? null,
|
||||
buttonUrl: $data['button_url'] ?? null,
|
||||
orderIndex: $data['order_index'] ?? 0,
|
||||
isActive: $data['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'label' => $this->label,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'media_type' => $this->mediaType,
|
||||
'image' => $this->image,
|
||||
'video_url' => $this->videoUrl,
|
||||
'mobile_video_url' => $this->mobileVideoUrl,
|
||||
'mobile_image' => $this->mobileImage,
|
||||
'button_text' => $this->buttonText,
|
||||
'button_url' => $this->buttonUrl,
|
||||
'order_index' => $this->orderIndex,
|
||||
'is_active' => $this->isActive,
|
||||
];
|
||||
}
|
||||
}
|
||||
69
app/DTOs/LeadData.php
Normal file
69
app/DTOs/LeadData.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
class LeadData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly string $phone,
|
||||
public readonly string $source,
|
||||
public readonly ?string $email = null,
|
||||
public readonly ?string $targetCourse = null,
|
||||
public readonly ?string $educationLevel = null,
|
||||
public readonly ?string $subject = null,
|
||||
public readonly ?string $message = null,
|
||||
public readonly ?array $utm = null,
|
||||
public readonly bool $consentKvkk = false,
|
||||
public readonly bool $marketingConsent = false,
|
||||
public readonly ?string $consentTextVersion = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$utm = array_filter([
|
||||
'utm_source' => $data['utm_source'] ?? null,
|
||||
'utm_medium' => $data['utm_medium'] ?? null,
|
||||
'utm_campaign' => $data['utm_campaign'] ?? null,
|
||||
]);
|
||||
|
||||
return new self(
|
||||
name: $data['name'],
|
||||
phone: $data['phone'],
|
||||
source: $data['source'],
|
||||
email: $data['email'] ?? null,
|
||||
targetCourse: $data['target_course'] ?? null,
|
||||
educationLevel: $data['education_level'] ?? null,
|
||||
subject: $data['subject'] ?? null,
|
||||
message: $data['message'] ?? null,
|
||||
utm: $utm ?: null,
|
||||
consentKvkk: (bool) ($data['kvkk_consent'] ?? false),
|
||||
marketingConsent: (bool) ($data['marketing_consent'] ?? false),
|
||||
consentTextVersion: $data['consent_text_version'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'phone' => $this->phone,
|
||||
'email' => $this->email,
|
||||
'source' => $this->source,
|
||||
'target_course' => $this->targetCourse,
|
||||
'education_level' => $this->educationLevel,
|
||||
'subject' => $this->subject,
|
||||
'message' => $this->message,
|
||||
'utm' => $this->utm,
|
||||
'consent_kvkk' => $this->consentKvkk,
|
||||
'marketing_consent' => $this->marketingConsent,
|
||||
'consent_text_version' => $this->consentTextVersion,
|
||||
];
|
||||
}
|
||||
}
|
||||
51
app/DTOs/MenuData.php
Normal file
51
app/DTOs/MenuData.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
use App\Enums\MenuLocation;
|
||||
use App\Enums\MenuType;
|
||||
|
||||
final readonly class MenuData
|
||||
{
|
||||
public function __construct(
|
||||
public string $label,
|
||||
public string $url,
|
||||
public MenuLocation $location,
|
||||
public MenuType $type,
|
||||
public ?int $parentId,
|
||||
public int $orderIndex = 0,
|
||||
public bool $isActive = true,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
label: $data['label'],
|
||||
url: $data['url'],
|
||||
location: MenuLocation::from($data['location']),
|
||||
type: MenuType::from($data['type']),
|
||||
parentId: $data['parent_id'] ?? null,
|
||||
orderIndex: $data['order_index'] ?? 0,
|
||||
isActive: $data['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'label' => $this->label,
|
||||
'url' => $this->url,
|
||||
'location' => $this->location->value,
|
||||
'type' => $this->type->value,
|
||||
'parent_id' => $this->parentId,
|
||||
'order_index' => $this->orderIndex,
|
||||
'is_active' => $this->isActive,
|
||||
];
|
||||
}
|
||||
}
|
||||
45
app/DTOs/PageData.php
Normal file
45
app/DTOs/PageData.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
final readonly class PageData
|
||||
{
|
||||
public function __construct(
|
||||
public string $slug,
|
||||
public string $title,
|
||||
public ?string $content,
|
||||
public ?string $metaTitle,
|
||||
public ?string $metaDescription,
|
||||
public bool $isActive = true,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
slug: $data['slug'],
|
||||
title: $data['title'],
|
||||
content: $data['content'] ?? null,
|
||||
metaTitle: $data['meta_title'] ?? null,
|
||||
metaDescription: $data['meta_description'] ?? null,
|
||||
isActive: $data['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'slug' => $this->slug,
|
||||
'title' => $this->title,
|
||||
'content' => $this->content,
|
||||
'meta_title' => $this->metaTitle,
|
||||
'meta_description' => $this->metaDescription,
|
||||
'is_active' => $this->isActive,
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/DTOs/ScheduleData.php
Normal file
48
app/DTOs/ScheduleData.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
class ScheduleData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $courseId,
|
||||
public readonly string $startDate,
|
||||
public readonly string $endDate,
|
||||
public readonly string $location,
|
||||
public readonly int $quota,
|
||||
public readonly int $availableSeats,
|
||||
public readonly bool $isUrgent = false,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
courseId: $data['course_id'],
|
||||
startDate: $data['start_date'],
|
||||
endDate: $data['end_date'],
|
||||
location: $data['location'],
|
||||
quota: $data['quota'],
|
||||
availableSeats: $data['available_seats'],
|
||||
isUrgent: $data['is_urgent'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'course_id' => $this->courseId,
|
||||
'start_date' => $this->startDate,
|
||||
'end_date' => $this->endDate,
|
||||
'location' => $this->location,
|
||||
'quota' => $this->quota,
|
||||
'available_seats' => $this->availableSeats,
|
||||
'is_urgent' => $this->isUrgent,
|
||||
];
|
||||
}
|
||||
}
|
||||
43
app/DTOs/UserData.php
Normal file
43
app/DTOs/UserData.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
class UserData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly string $email,
|
||||
public readonly ?string $password = null,
|
||||
public readonly ?string $role = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
name: $data['name'],
|
||||
email: $data['email'],
|
||||
password: $data['password'] ?? null,
|
||||
role: $data['role'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
];
|
||||
|
||||
if ($this->password !== null) {
|
||||
$result['password'] = $this->password;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
10
app/Enums/AnnouncementCategory.php
Normal file
10
app/Enums/AnnouncementCategory.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum AnnouncementCategory: string
|
||||
{
|
||||
case Announcement = 'announcement';
|
||||
case News = 'news';
|
||||
case Event = 'event';
|
||||
}
|
||||
23
app/Enums/CourseBadge.php
Normal file
23
app/Enums/CourseBadge.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum CourseBadge: string
|
||||
{
|
||||
case Popular = 'popular';
|
||||
case MostPreferred = 'most_preferred';
|
||||
case New = 'new';
|
||||
case Recommended = 'recommended';
|
||||
case Limited = 'limited';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Popular => 'Popüler',
|
||||
self::MostPreferred => 'En Çok Tercih Edilen',
|
||||
self::New => 'Yeni',
|
||||
self::Recommended => 'Önerilen',
|
||||
self::Limited => 'Sınırlı Kontenjan',
|
||||
};
|
||||
}
|
||||
}
|
||||
15
app/Enums/FaqCategory.php
Normal file
15
app/Enums/FaqCategory.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum FaqCategory: string
|
||||
{
|
||||
case Egitimler = 'egitimler';
|
||||
case Stcw = 'stcw';
|
||||
case Makine = 'makine';
|
||||
case YatKaptanligi = 'yat-kaptanligi';
|
||||
case Yenileme = 'yenileme';
|
||||
case Guvenlik = 'guvenlik';
|
||||
case Kayit = 'kayit';
|
||||
case Iletisim = 'iletisim';
|
||||
}
|
||||
13
app/Enums/LeadSource.php
Normal file
13
app/Enums/LeadSource.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum LeadSource: string
|
||||
{
|
||||
case KursKayit = 'kurs_kayit';
|
||||
case Danismanlik = 'danismanlik';
|
||||
case Duyuru = 'duyuru';
|
||||
case Iletisim = 'iletisim';
|
||||
case HeroForm = 'hero_form';
|
||||
case WhatsappWidget = 'whatsapp_widget';
|
||||
}
|
||||
11
app/Enums/LeadStatus.php
Normal file
11
app/Enums/LeadStatus.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum LeadStatus: string
|
||||
{
|
||||
case New = 'new';
|
||||
case Contacted = 'contacted';
|
||||
case Enrolled = 'enrolled';
|
||||
case Cancelled = 'cancelled';
|
||||
}
|
||||
11
app/Enums/MenuLocation.php
Normal file
11
app/Enums/MenuLocation.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum MenuLocation: string
|
||||
{
|
||||
case HeaderMain = 'header_main';
|
||||
case FooterCorporate = 'footer_corporate';
|
||||
case FooterEducation = 'footer_education';
|
||||
case FooterQuicklinks = 'footer_quicklinks';
|
||||
}
|
||||
10
app/Enums/MenuType.php
Normal file
10
app/Enums/MenuType.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum MenuType: string
|
||||
{
|
||||
case Link = 'link';
|
||||
case MegaMenuEducation = 'mega_menu_education';
|
||||
case MegaMenuCalendar = 'mega_menu_calendar';
|
||||
}
|
||||
17
app/Enums/SettingGroup.php
Normal file
17
app/Enums/SettingGroup.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SettingGroup: string
|
||||
{
|
||||
case General = 'general';
|
||||
case Contact = 'contact';
|
||||
case Maps = 'maps';
|
||||
case Social = 'social';
|
||||
case Seo = 'seo';
|
||||
case Analytics = 'analytics';
|
||||
case Header = 'header';
|
||||
case Footer = 'footer';
|
||||
case Integrations = 'integrations';
|
||||
case InfoSections = 'info_sections';
|
||||
}
|
||||
15
app/Enums/SettingType.php
Normal file
15
app/Enums/SettingType.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SettingType: string
|
||||
{
|
||||
case Text = 'text';
|
||||
case Textarea = 'textarea';
|
||||
case Image = 'image';
|
||||
case Boolean = 'boolean';
|
||||
case Json = 'json';
|
||||
case Richtext = 'richtext';
|
||||
case Url = 'url';
|
||||
case Color = 'color';
|
||||
}
|
||||
22
app/Events/ModelChanged.php
Normal file
22
app/Events/ModelChanged.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ModelChanged
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param string $modelClass The fully qualified class name of the changed model.
|
||||
* @param string $action The action performed (created, updated, deleted).
|
||||
*/
|
||||
public function __construct(
|
||||
public string $modelClass,
|
||||
public string $action = 'updated',
|
||||
) {}
|
||||
}
|
||||
140
app/Http/Controllers/Api/Admin/AnnouncementController.php
Normal file
140
app/Http/Controllers/Api/Admin/AnnouncementController.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Announcement\CreateAnnouncementAction;
|
||||
use App\Actions\Announcement\DeleteAnnouncementAction;
|
||||
use App\Actions\Announcement\UpdateAnnouncementAction;
|
||||
use App\DTOs\AnnouncementData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Announcement\StoreAnnouncementRequest;
|
||||
use App\Http\Requests\Announcement\UpdateAnnouncementRequest;
|
||||
use App\Http\Resources\AnnouncementResource;
|
||||
use App\Models\Announcement;
|
||||
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class AnnouncementController extends Controller
|
||||
{
|
||||
public function __construct(private AnnouncementRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/announcements',
|
||||
summary: 'Duyuruları listele (Admin)',
|
||||
tags: ['Admin - Announcements'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'featured', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Duyuru listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$announcements = $this->repository->paginate(
|
||||
$request->only(['category', 'featured', 'search', 'sort']),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return AnnouncementResource::collection($announcements);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/announcements',
|
||||
summary: 'Yeni duyuru oluştur',
|
||||
tags: ['Admin - Announcements'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['title', 'slug', 'category', 'content'],
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'slug', type: 'string'),
|
||||
new OA\Property(property: 'category', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'string'),
|
||||
new OA\Property(property: 'excerpt', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string'),
|
||||
new OA\Property(property: 'is_featured', type: 'boolean'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'published_at', type: 'string', format: 'date-time'),
|
||||
new OA\Property(property: 'meta_title', type: 'string'),
|
||||
new OA\Property(property: 'meta_description', type: 'string'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Duyuru oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreAnnouncementRequest $request, CreateAnnouncementAction $action): JsonResponse
|
||||
{
|
||||
$dto = AnnouncementData::fromArray($request->validated());
|
||||
$announcement = $action->execute($dto);
|
||||
|
||||
return response()->json(new AnnouncementResource($announcement), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/announcements/{announcement}',
|
||||
summary: 'Duyuru detayı (Admin)',
|
||||
tags: ['Admin - Announcements'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'announcement', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Duyuru detayı')],
|
||||
)]
|
||||
public function show(Announcement $announcement): JsonResponse
|
||||
{
|
||||
return response()->json(new AnnouncementResource($announcement));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/announcements/{announcement}',
|
||||
summary: 'Duyuru güncelle',
|
||||
tags: ['Admin - Announcements'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'announcement', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'slug', type: 'string'),
|
||||
new OA\Property(property: 'category', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'string'),
|
||||
new OA\Property(property: 'excerpt', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string'),
|
||||
new OA\Property(property: 'is_featured', type: 'boolean'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Duyuru güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateAnnouncementRequest $request, Announcement $announcement, UpdateAnnouncementAction $action): JsonResponse
|
||||
{
|
||||
$dto = AnnouncementData::fromArray(array_merge($announcement->toArray(), $request->validated()));
|
||||
$announcement = $action->execute($announcement, $dto);
|
||||
|
||||
return response()->json(new AnnouncementResource($announcement));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/announcements/{announcement}',
|
||||
summary: 'Duyuru sil',
|
||||
tags: ['Admin - Announcements'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'announcement', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Duyuru silindi')],
|
||||
)]
|
||||
public function destroy(Announcement $announcement, DeleteAnnouncementAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($announcement);
|
||||
|
||||
return response()->json(['message' => 'Duyuru silindi.']);
|
||||
}
|
||||
}
|
||||
118
app/Http/Controllers/Api/Admin/AuthController.php
Normal file
118
app/Http/Controllers/Api/Admin/AuthController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
#[OA\Post(
|
||||
path: '/api/admin/login',
|
||||
summary: 'Admin girişi',
|
||||
description: 'E-posta ve şifre ile giriş yaparak Sanctum token alır.',
|
||||
tags: ['Auth'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['email', 'password'],
|
||||
properties: [
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'admin@bogazicidenizcilik.com.tr'),
|
||||
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password'),
|
||||
],
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Başarılı giriş', content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'data', type: 'object', properties: [
|
||||
new OA\Property(property: 'token', type: 'string'),
|
||||
new OA\Property(property: 'user', type: 'object'),
|
||||
]),
|
||||
],
|
||||
)),
|
||||
new OA\Response(response: 401, description: 'Geçersiz kimlik bilgileri'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function login(LoginRequest $request): JsonResponse
|
||||
{
|
||||
if (! Auth::attempt($request->only('email', 'password'))) {
|
||||
return response()->json([
|
||||
'message' => 'Geçersiz e-posta veya şifre.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$token = $user->createToken('admin-token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'roles' => $user->getRoleNames(),
|
||||
'permissions' => $user->getAllPermissions()->pluck('name'),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/me',
|
||||
summary: 'Mevcut kullanıcı bilgileri',
|
||||
description: 'Oturum açmış kullanıcının bilgilerini, rollerini ve izinlerini döndürür.',
|
||||
security: [['sanctum' => []]],
|
||||
tags: ['Auth'],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kullanıcı bilgileri'),
|
||||
new OA\Response(response: 401, description: 'Yetkisiz erişim'),
|
||||
],
|
||||
)]
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'roles' => $user->getRoleNames(),
|
||||
'permissions' => $user->getAllPermissions()->pluck('name'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/logout',
|
||||
summary: 'Çıkış yap',
|
||||
description: 'Mevcut token\'ı iptal eder.',
|
||||
security: [['sanctum' => []]],
|
||||
tags: ['Auth'],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Başarıyla çıkış yapıldı'),
|
||||
new OA\Response(response: 401, description: 'Yetkisiz erişim'),
|
||||
],
|
||||
)]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
$user->currentAccessToken()->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Başarıyla çıkış yapıldı.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
172
app/Http/Controllers/Api/Admin/BlockController.php
Normal file
172
app/Http/Controllers/Api/Admin/BlockController.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\PageBlockResource;
|
||||
use App\Models\Page;
|
||||
use App\Models\PageBlock;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class BlockController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/admin/pages/{page}/blocks',
|
||||
summary: 'Sayfa bloklarını listele',
|
||||
tags: ['Admin - Page Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Blok listesi')],
|
||||
)]
|
||||
public function index(Page $page): AnonymousResourceCollection
|
||||
{
|
||||
return PageBlockResource::collection(
|
||||
$page->blocks()->orderBy('order_index')->get()
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/pages/{page}/blocks',
|
||||
summary: 'Yeni blok oluştur',
|
||||
tags: ['Admin - Page Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['type', 'content'],
|
||||
properties: [
|
||||
new OA\Property(property: 'type', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'object'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Blok oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(Request $request, Page $page): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'type' => ['required', 'string', 'max:50'],
|
||||
'content' => ['present', 'array'],
|
||||
'order_index' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$validated['order_index'] ??= $page->blocks()->max('order_index') + 1;
|
||||
|
||||
$block = $page->blocks()->create($validated);
|
||||
|
||||
return response()->json(new PageBlockResource($block), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/pages/{page}/blocks/{block}',
|
||||
summary: 'Blok detayı',
|
||||
tags: ['Admin - Page Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Blok detayı')],
|
||||
)]
|
||||
public function show(Page $page, PageBlock $block): JsonResponse
|
||||
{
|
||||
return response()->json(new PageBlockResource($block));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/pages/{page}/blocks/{block}',
|
||||
summary: 'Blok güncelle',
|
||||
tags: ['Admin - Page Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'type', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'object'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Blok güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(Request $request, Page $page, PageBlock $block): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'type' => ['sometimes', 'string', 'max:50'],
|
||||
'content' => ['sometimes', 'array'],
|
||||
'order_index' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$block->update($validated);
|
||||
|
||||
return response()->json(new PageBlockResource($block->fresh()));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/pages/{page}/blocks/{block}',
|
||||
summary: 'Blok sil',
|
||||
tags: ['Admin - Page Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Blok silindi')],
|
||||
)]
|
||||
public function destroy(Page $page, PageBlock $block): JsonResponse
|
||||
{
|
||||
$block->delete();
|
||||
|
||||
return response()->json(['message' => 'Blok silindi.']);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/pages/{page}/blocks/reorder',
|
||||
summary: 'Blok sıralamasını güncelle',
|
||||
tags: ['Admin - Page Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['items'],
|
||||
properties: [
|
||||
new OA\Property(property: 'items', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
responses: [new OA\Response(response: 200, description: 'Sıralama güncellendi')],
|
||||
)]
|
||||
public function reorder(Request $request, Page $page): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.id' => ['required', 'integer', 'exists:page_blocks,id'],
|
||||
'items.*.order_index' => ['required', 'integer', 'min:0'],
|
||||
]);
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
$page->blocks()
|
||||
->where('id', $item['id'])
|
||||
->update(['order_index' => $item['order_index']]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Blok sıralaması güncellendi.']);
|
||||
}
|
||||
}
|
||||
147
app/Http/Controllers/Api/Admin/CategoryController.php
Normal file
147
app/Http/Controllers/Api/Admin/CategoryController.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Category\CreateCategoryAction;
|
||||
use App\Actions\Category\DeleteCategoryAction;
|
||||
use App\Actions\Category\UpdateCategoryAction;
|
||||
use App\DTOs\CategoryData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Category\StoreCategoryRequest;
|
||||
use App\Http\Requests\Category\UpdateCategoryRequest;
|
||||
use App\Http\Resources\CategoryResource;
|
||||
use App\Models\Category;
|
||||
use App\Repositories\Contracts\CategoryRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function __construct(private CategoryRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/categories',
|
||||
summary: 'Kategorileri listele (Admin)',
|
||||
tags: ['Admin - Categories'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Kategori listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$categories = $this->repository->paginate(
|
||||
filters: $request->only('search'),
|
||||
perPage: $request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return CategoryResource::collection($categories);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/categories',
|
||||
summary: 'Yeni kategori oluştur',
|
||||
tags: ['Admin - Categories'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['name'],
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'slug', type: 'string'),
|
||||
new OA\Property(property: 'description', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string'),
|
||||
new OA\Property(property: 'icon', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
new OA\Property(property: 'meta_title', type: 'string'),
|
||||
new OA\Property(property: 'meta_description', type: 'string'),
|
||||
],
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Kategori oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreCategoryRequest $request, CreateCategoryAction $action): JsonResponse
|
||||
{
|
||||
$dto = CategoryData::fromArray($request->validated());
|
||||
$category = $action->execute($dto);
|
||||
|
||||
return (new CategoryResource($category))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/categories/{category}',
|
||||
summary: 'Kategori detayı (Admin)',
|
||||
tags: ['Admin - Categories'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'category', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kategori detayı'),
|
||||
new OA\Response(response: 404, description: 'Bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function show(Category $category): CategoryResource
|
||||
{
|
||||
return new CategoryResource($category);
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/categories/{category}',
|
||||
summary: 'Kategori güncelle',
|
||||
tags: ['Admin - Categories'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'category', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'slug', type: 'string'),
|
||||
new OA\Property(property: 'description', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string'),
|
||||
new OA\Property(property: 'icon', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
new OA\Property(property: 'meta_title', type: 'string'),
|
||||
new OA\Property(property: 'meta_description', type: 'string'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kategori güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateCategoryRequest $request, Category $category, UpdateCategoryAction $action): CategoryResource
|
||||
{
|
||||
$dto = CategoryData::fromArray($request->validated());
|
||||
$category = $action->execute($category, $dto);
|
||||
|
||||
return new CategoryResource($category);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/categories/{category}',
|
||||
summary: 'Kategori sil',
|
||||
tags: ['Admin - Categories'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'category', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kategori silindi'),
|
||||
new OA\Response(response: 404, description: 'Bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function destroy(Category $category, DeleteCategoryAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($category);
|
||||
|
||||
return response()->json(['message' => 'Kategori başarıyla silindi.']);
|
||||
}
|
||||
}
|
||||
94
app/Http/Controllers/Api/Admin/CommentController.php
Normal file
94
app/Http/Controllers/Api/Admin/CommentController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Comment\DeleteCommentAction;
|
||||
use App\Actions\Comment\UpdateCommentAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Comment\UpdateCommentRequest;
|
||||
use App\Http\Resources\CommentResource;
|
||||
use App\Models\Comment;
|
||||
use App\Repositories\Contracts\CommentRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
public function __construct(private CommentRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/comments',
|
||||
summary: 'Yorumları listele (Admin)',
|
||||
tags: ['Admin - Comments'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'is_approved', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
|
||||
new OA\Parameter(name: 'commentable_type', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Yorum listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$comments = $this->repository->paginate(
|
||||
$request->only(['is_approved', 'commentable_type', 'search']),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return CommentResource::collection($comments);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/comments/{comment}',
|
||||
summary: 'Yorum detayı',
|
||||
tags: ['Admin - Comments'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'comment', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Yorum detayı')],
|
||||
)]
|
||||
public function show(Comment $comment): JsonResponse
|
||||
{
|
||||
return response()->json(new CommentResource($comment));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/comments/{comment}',
|
||||
summary: 'Yorum güncelle (onayla/reddet)',
|
||||
tags: ['Admin - Comments'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'comment', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'is_approved', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Yorum güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateCommentRequest $request, Comment $comment, UpdateCommentAction $action): JsonResponse
|
||||
{
|
||||
$comment = $action->execute($comment, $request->validated());
|
||||
|
||||
return response()->json(new CommentResource($comment));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/comments/{comment}',
|
||||
summary: 'Yorum sil',
|
||||
tags: ['Admin - Comments'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'comment', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Yorum silindi')],
|
||||
)]
|
||||
public function destroy(Comment $comment, DeleteCommentAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($comment);
|
||||
|
||||
return response()->json(['message' => 'Yorum silindi.']);
|
||||
}
|
||||
}
|
||||
172
app/Http/Controllers/Api/Admin/CourseBlockController.php
Normal file
172
app/Http/Controllers/Api/Admin/CourseBlockController.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CourseBlockResource;
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseBlock;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class CourseBlockController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/admin/courses/{course}/blocks',
|
||||
summary: 'Eğitim bloklarını listele',
|
||||
tags: ['Admin - Course Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Blok listesi')],
|
||||
)]
|
||||
public function index(Course $course): AnonymousResourceCollection
|
||||
{
|
||||
return CourseBlockResource::collection(
|
||||
$course->blocks()->orderBy('order_index')->get()
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/courses/{course}/blocks',
|
||||
summary: 'Yeni eğitim bloğu oluştur',
|
||||
tags: ['Admin - Course Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['type', 'content'],
|
||||
properties: [
|
||||
new OA\Property(property: 'type', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'object'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Blok oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(Request $request, Course $course): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'type' => ['required', 'string', 'max:50'],
|
||||
'content' => ['present', 'array'],
|
||||
'order_index' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$validated['order_index'] ??= $course->blocks()->max('order_index') + 1;
|
||||
|
||||
$block = $course->blocks()->create($validated);
|
||||
|
||||
return response()->json(new CourseBlockResource($block), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/courses/{course}/blocks/{block}',
|
||||
summary: 'Eğitim blok detayı',
|
||||
tags: ['Admin - Course Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Blok detayı')],
|
||||
)]
|
||||
public function show(Course $course, CourseBlock $block): JsonResponse
|
||||
{
|
||||
return response()->json(new CourseBlockResource($block));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/courses/{course}/blocks/{block}',
|
||||
summary: 'Eğitim bloğu güncelle',
|
||||
tags: ['Admin - Course Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'type', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'object'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Blok güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(Request $request, Course $course, CourseBlock $block): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'type' => ['sometimes', 'string', 'max:50'],
|
||||
'content' => ['sometimes', 'array'],
|
||||
'order_index' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$block->update($validated);
|
||||
|
||||
return response()->json(new CourseBlockResource($block->fresh()));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/courses/{course}/blocks/{block}',
|
||||
summary: 'Eğitim bloğu sil',
|
||||
tags: ['Admin - Course Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'block', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Blok silindi')],
|
||||
)]
|
||||
public function destroy(Course $course, CourseBlock $block): JsonResponse
|
||||
{
|
||||
$block->delete();
|
||||
|
||||
return response()->json(['message' => 'Blok silindi.']);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/courses/{course}/blocks/reorder',
|
||||
summary: 'Eğitim blok sıralamasını güncelle',
|
||||
tags: ['Admin - Course Blocks'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['items'],
|
||||
properties: [
|
||||
new OA\Property(property: 'items', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
responses: [new OA\Response(response: 200, description: 'Sıralama güncellendi')],
|
||||
)]
|
||||
public function reorder(Request $request, Course $course): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.id' => ['required', 'integer', 'exists:course_blocks,id'],
|
||||
'items.*.order_index' => ['required', 'integer', 'min:0'],
|
||||
]);
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
$course->blocks()
|
||||
->where('id', $item['id'])
|
||||
->update(['order_index' => $item['order_index']]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Blok sıralaması güncellendi.']);
|
||||
}
|
||||
}
|
||||
163
app/Http/Controllers/Api/Admin/CourseController.php
Normal file
163
app/Http/Controllers/Api/Admin/CourseController.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Course\CreateCourseAction;
|
||||
use App\Actions\Course\DeleteCourseAction;
|
||||
use App\Actions\Course\UpdateCourseAction;
|
||||
use App\DTOs\CourseData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Course\StoreCourseRequest;
|
||||
use App\Http\Requests\Course\UpdateCourseRequest;
|
||||
use App\Http\Resources\CourseResource;
|
||||
use App\Models\Course;
|
||||
use App\Repositories\Contracts\CourseRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class CourseController extends Controller
|
||||
{
|
||||
public function __construct(private CourseRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/courses',
|
||||
summary: 'Eğitimleri listele (Admin)',
|
||||
tags: ['Admin - Courses'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Eğitim listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$courses = $this->repository->paginate(
|
||||
filters: $request->only('category', 'search', 'sort'),
|
||||
perPage: $request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return CourseResource::collection($courses);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/courses',
|
||||
summary: 'Yeni eğitim oluştur',
|
||||
tags: ['Admin - Courses'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['category_id', 'title', 'slug', 'desc', 'long_desc', 'duration'],
|
||||
properties: [
|
||||
new OA\Property(property: 'category_id', type: 'integer', description: 'Kategori ID'),
|
||||
new OA\Property(property: 'slug', type: 'string', description: 'URL slug (unique)'),
|
||||
new OA\Property(property: 'title', type: 'string', description: 'Eğitim başlığı'),
|
||||
new OA\Property(property: 'sub', type: 'string', nullable: true, description: 'Alt başlık. Örn: STCW II/1'),
|
||||
new OA\Property(property: 'desc', type: 'string', description: 'Kısa açıklama'),
|
||||
new OA\Property(property: 'long_desc', type: 'string', description: 'Detaylı açıklama'),
|
||||
new OA\Property(property: 'duration', type: 'string', description: 'Süre. Örn: 5 Gün'),
|
||||
new OA\Property(property: 'students', type: 'integer', description: 'Öğrenci sayısı'),
|
||||
new OA\Property(property: 'rating', type: 'number', format: 'float', description: 'Puan (0-5)'),
|
||||
new OA\Property(property: 'badge', type: 'string', nullable: true, description: 'Rozet. Örn: Simülatör'),
|
||||
new OA\Property(property: 'image', type: 'string', nullable: true, description: 'Görsel path'),
|
||||
new OA\Property(property: 'price', type: 'string', nullable: true, description: 'Fiyat. Örn: 5.000 TL'),
|
||||
new OA\Property(property: 'includes', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Fiyata dahil olanlar'),
|
||||
new OA\Property(property: 'requirements', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Katılım koşulları'),
|
||||
new OA\Property(property: 'meta_title', type: 'string', nullable: true, description: 'SEO Title'),
|
||||
new OA\Property(property: 'meta_description', type: 'string', nullable: true, description: 'SEO Description'),
|
||||
new OA\Property(property: 'scope', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Eğitim kapsamı konu başlıkları'),
|
||||
new OA\Property(property: 'standard', type: 'string', nullable: true, description: 'Uyum standardı. Örn: STCW / IMO Uyumlu'),
|
||||
new OA\Property(property: 'language', type: 'string', nullable: true, description: 'Eğitim dili. Varsayılan: Türkçe'),
|
||||
new OA\Property(property: 'location', type: 'string', nullable: true, description: 'Varsayılan lokasyon. Örn: Kadıköy, İstanbul'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Eğitim oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreCourseRequest $request, CreateCourseAction $action): JsonResponse
|
||||
{
|
||||
$dto = CourseData::fromArray($request->validated());
|
||||
$course = $action->execute($dto);
|
||||
|
||||
return (new CourseResource($course->load('category')))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/courses/{course}',
|
||||
summary: 'Eğitim detayı (Admin)',
|
||||
tags: ['Admin - Courses'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Eğitim detayı')],
|
||||
)]
|
||||
public function show(Course $course): CourseResource
|
||||
{
|
||||
return new CourseResource($course->load(['category', 'schedules', 'blocks']));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/courses/{course}',
|
||||
summary: 'Eğitim güncelle',
|
||||
tags: ['Admin - Courses'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['category_id', 'title', 'slug', 'desc', 'long_desc', 'duration'],
|
||||
properties: [
|
||||
new OA\Property(property: 'category_id', type: 'integer', description: 'Kategori ID'),
|
||||
new OA\Property(property: 'slug', type: 'string', description: 'URL slug (unique)'),
|
||||
new OA\Property(property: 'title', type: 'string', description: 'Eğitim başlığı'),
|
||||
new OA\Property(property: 'sub', type: 'string', nullable: true, description: 'Alt başlık'),
|
||||
new OA\Property(property: 'desc', type: 'string', description: 'Kısa açıklama'),
|
||||
new OA\Property(property: 'long_desc', type: 'string', description: 'Detaylı açıklama'),
|
||||
new OA\Property(property: 'duration', type: 'string', description: 'Süre. Örn: 5 Gün'),
|
||||
new OA\Property(property: 'students', type: 'integer', description: 'Öğrenci sayısı'),
|
||||
new OA\Property(property: 'rating', type: 'number', format: 'float', description: 'Puan (0-5)'),
|
||||
new OA\Property(property: 'badge', type: 'string', nullable: true, description: 'Rozet'),
|
||||
new OA\Property(property: 'image', type: 'string', nullable: true, description: 'Görsel path'),
|
||||
new OA\Property(property: 'price', type: 'string', nullable: true, description: 'Fiyat'),
|
||||
new OA\Property(property: 'includes', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Fiyata dahil olanlar'),
|
||||
new OA\Property(property: 'requirements', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Katılım koşulları'),
|
||||
new OA\Property(property: 'meta_title', type: 'string', nullable: true, description: 'SEO Title'),
|
||||
new OA\Property(property: 'meta_description', type: 'string', nullable: true, description: 'SEO Description'),
|
||||
new OA\Property(property: 'scope', type: 'array', items: new OA\Items(type: 'string'), nullable: true, description: 'Eğitim kapsamı konu başlıkları'),
|
||||
new OA\Property(property: 'standard', type: 'string', nullable: true, description: 'Uyum standardı'),
|
||||
new OA\Property(property: 'language', type: 'string', nullable: true, description: 'Eğitim dili'),
|
||||
new OA\Property(property: 'location', type: 'string', nullable: true, description: 'Varsayılan lokasyon'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Eğitim güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateCourseRequest $request, Course $course, UpdateCourseAction $action): CourseResource
|
||||
{
|
||||
$dto = CourseData::fromArray($request->validated());
|
||||
$course = $action->execute($course, $dto);
|
||||
|
||||
return new CourseResource($course->load('category'));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/courses/{course}',
|
||||
summary: 'Eğitim sil',
|
||||
tags: ['Admin - Courses'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'course', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Eğitim silindi')],
|
||||
)]
|
||||
public function destroy(Course $course, DeleteCourseAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($course);
|
||||
|
||||
return response()->json(['message' => 'Eğitim başarıyla silindi.']);
|
||||
}
|
||||
}
|
||||
128
app/Http/Controllers/Api/Admin/FaqController.php
Normal file
128
app/Http/Controllers/Api/Admin/FaqController.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Faq\CreateFaqAction;
|
||||
use App\Actions\Faq\DeleteFaqAction;
|
||||
use App\Actions\Faq\UpdateFaqAction;
|
||||
use App\DTOs\FaqData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Faq\StoreFaqRequest;
|
||||
use App\Http\Requests\Faq\UpdateFaqRequest;
|
||||
use App\Http\Resources\FaqResource;
|
||||
use App\Models\Faq;
|
||||
use App\Repositories\Contracts\FaqRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class FaqController extends Controller
|
||||
{
|
||||
public function __construct(private FaqRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/faqs',
|
||||
summary: 'SSS listele (Admin)',
|
||||
tags: ['Admin - FAQs'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 50)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'SSS listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$faqs = $this->repository->paginate(
|
||||
$request->only(['category']),
|
||||
$request->integer('per_page', 50),
|
||||
);
|
||||
|
||||
return FaqResource::collection($faqs);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/faqs',
|
||||
summary: 'Yeni SSS oluştur',
|
||||
tags: ['Admin - FAQs'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['question', 'answer', 'category'],
|
||||
properties: [
|
||||
new OA\Property(property: 'question', type: 'string'),
|
||||
new OA\Property(property: 'answer', type: 'string'),
|
||||
new OA\Property(property: 'category', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'SSS oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreFaqRequest $request, CreateFaqAction $action): JsonResponse
|
||||
{
|
||||
$dto = FaqData::fromArray($request->validated());
|
||||
$faq = $action->execute($dto);
|
||||
|
||||
return response()->json(new FaqResource($faq), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/faqs/{faq}',
|
||||
summary: 'SSS detayı',
|
||||
tags: ['Admin - FAQs'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'faq', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'SSS detayı')],
|
||||
)]
|
||||
public function show(Faq $faq): JsonResponse
|
||||
{
|
||||
return response()->json(new FaqResource($faq));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/faqs/{faq}',
|
||||
summary: 'SSS güncelle',
|
||||
tags: ['Admin - FAQs'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'faq', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'question', type: 'string'),
|
||||
new OA\Property(property: 'answer', type: 'string'),
|
||||
new OA\Property(property: 'category', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'SSS güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateFaqRequest $request, Faq $faq, UpdateFaqAction $action): JsonResponse
|
||||
{
|
||||
$dto = FaqData::fromArray(array_merge($faq->toArray(), $request->validated()));
|
||||
$faq = $action->execute($faq, $dto);
|
||||
|
||||
return response()->json(new FaqResource($faq));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/faqs/{faq}',
|
||||
summary: 'SSS sil',
|
||||
tags: ['Admin - FAQs'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'faq', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'SSS silindi')],
|
||||
)]
|
||||
public function destroy(Faq $faq, DeleteFaqAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($faq);
|
||||
|
||||
return response()->json(['message' => 'SSS silindi.']);
|
||||
}
|
||||
}
|
||||
127
app/Http/Controllers/Api/Admin/GuideCardController.php
Normal file
127
app/Http/Controllers/Api/Admin/GuideCardController.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\GuideCard\CreateGuideCardAction;
|
||||
use App\Actions\GuideCard\DeleteGuideCardAction;
|
||||
use App\Actions\GuideCard\UpdateGuideCardAction;
|
||||
use App\DTOs\GuideCardData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\GuideCard\StoreGuideCardRequest;
|
||||
use App\Http\Requests\GuideCard\UpdateGuideCardRequest;
|
||||
use App\Http\Resources\GuideCardResource;
|
||||
use App\Models\GuideCard;
|
||||
use App\Repositories\Contracts\GuideCardRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class GuideCardController extends Controller
|
||||
{
|
||||
public function __construct(private GuideCardRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/guide-cards',
|
||||
summary: 'Rehber kartları listele (Admin)',
|
||||
tags: ['Admin - Guide Cards'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15))],
|
||||
responses: [new OA\Response(response: 200, description: 'Kart listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$cards = $this->repository->paginate(
|
||||
$request->only([]),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return GuideCardResource::collection($cards);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/guide-cards',
|
||||
summary: 'Yeni rehber kartı oluştur',
|
||||
tags: ['Admin - Guide Cards'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['title', 'description', 'icon'],
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'description', type: 'string'),
|
||||
new OA\Property(property: 'icon', type: 'string'),
|
||||
new OA\Property(property: 'url', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Kart oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreGuideCardRequest $request, CreateGuideCardAction $action): JsonResponse
|
||||
{
|
||||
$dto = GuideCardData::fromArray($request->validated());
|
||||
$card = $action->execute($dto);
|
||||
|
||||
return response()->json(new GuideCardResource($card), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/guide-cards/{guideCard}',
|
||||
summary: 'Rehber kart detayı',
|
||||
tags: ['Admin - Guide Cards'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'guideCard', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Kart detayı')],
|
||||
)]
|
||||
public function show(GuideCard $guideCard): JsonResponse
|
||||
{
|
||||
return response()->json(new GuideCardResource($guideCard));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/guide-cards/{guideCard}',
|
||||
summary: 'Rehber kart güncelle',
|
||||
tags: ['Admin - Guide Cards'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'guideCard', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'description', type: 'string'),
|
||||
new OA\Property(property: 'icon', type: 'string'),
|
||||
new OA\Property(property: 'url', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kart güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateGuideCardRequest $request, GuideCard $guideCard, UpdateGuideCardAction $action): JsonResponse
|
||||
{
|
||||
$dto = GuideCardData::fromArray(array_merge($guideCard->toArray(), $request->validated()));
|
||||
$guideCard = $action->execute($guideCard, $dto);
|
||||
|
||||
return response()->json(new GuideCardResource($guideCard));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/guide-cards/{guideCard}',
|
||||
summary: 'Rehber kart sil',
|
||||
tags: ['Admin - Guide Cards'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'guideCard', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Kart silindi')],
|
||||
)]
|
||||
public function destroy(GuideCard $guideCard, DeleteGuideCardAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($guideCard);
|
||||
|
||||
return response()->json(['message' => 'Rehber kartı silindi.']);
|
||||
}
|
||||
}
|
||||
130
app/Http/Controllers/Api/Admin/HeroSlideController.php
Normal file
130
app/Http/Controllers/Api/Admin/HeroSlideController.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\HeroSlide\CreateHeroSlideAction;
|
||||
use App\Actions\HeroSlide\DeleteHeroSlideAction;
|
||||
use App\Actions\HeroSlide\UpdateHeroSlideAction;
|
||||
use App\DTOs\HeroSlideData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\HeroSlide\StoreHeroSlideRequest;
|
||||
use App\Http\Requests\HeroSlide\UpdateHeroSlideRequest;
|
||||
use App\Http\Resources\HeroSlideResource;
|
||||
use App\Models\HeroSlide;
|
||||
use App\Repositories\Contracts\HeroSlideRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class HeroSlideController extends Controller
|
||||
{
|
||||
public function __construct(private HeroSlideRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/hero-slides',
|
||||
summary: 'Hero slide listele (Admin)',
|
||||
tags: ['Admin - Hero Slides'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15))],
|
||||
responses: [new OA\Response(response: 200, description: 'Slide listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$slides = $this->repository->paginate(
|
||||
$request->only([]),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return HeroSlideResource::collection($slides);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/hero-slides',
|
||||
summary: 'Yeni hero slide oluştur',
|
||||
tags: ['Admin - Hero Slides'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['title', 'image'],
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'subtitle', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string'),
|
||||
new OA\Property(property: 'mobile_image', type: 'string'),
|
||||
new OA\Property(property: 'button_text', type: 'string'),
|
||||
new OA\Property(property: 'button_url', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Slide oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreHeroSlideRequest $request, CreateHeroSlideAction $action): JsonResponse
|
||||
{
|
||||
$dto = HeroSlideData::fromArray($request->validated());
|
||||
$slide = $action->execute($dto);
|
||||
|
||||
return response()->json(new HeroSlideResource($slide), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/hero-slides/{heroSlide}',
|
||||
summary: 'Hero slide detayı',
|
||||
tags: ['Admin - Hero Slides'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'heroSlide', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Slide detayı')],
|
||||
)]
|
||||
public function show(HeroSlide $heroSlide): JsonResponse
|
||||
{
|
||||
return response()->json(new HeroSlideResource($heroSlide));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/hero-slides/{heroSlide}',
|
||||
summary: 'Hero slide güncelle',
|
||||
tags: ['Admin - Hero Slides'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'heroSlide', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'subtitle', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string'),
|
||||
new OA\Property(property: 'button_text', type: 'string'),
|
||||
new OA\Property(property: 'button_url', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Slide güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateHeroSlideRequest $request, HeroSlide $heroSlide, UpdateHeroSlideAction $action): JsonResponse
|
||||
{
|
||||
$dto = HeroSlideData::fromArray(array_merge($heroSlide->toArray(), $request->validated()));
|
||||
$heroSlide = $action->execute($heroSlide, $dto);
|
||||
|
||||
return response()->json(new HeroSlideResource($heroSlide));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/hero-slides/{heroSlide}',
|
||||
summary: 'Hero slide sil',
|
||||
tags: ['Admin - Hero Slides'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'heroSlide', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Slide silindi')],
|
||||
)]
|
||||
public function destroy(HeroSlide $heroSlide, DeleteHeroSlideAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($heroSlide);
|
||||
|
||||
return response()->json(['message' => 'Hero slide silindi.']);
|
||||
}
|
||||
}
|
||||
103
app/Http/Controllers/Api/Admin/LeadController.php
Normal file
103
app/Http/Controllers/Api/Admin/LeadController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Lead\DeleteLeadAction;
|
||||
use App\Actions\Lead\UpdateLeadAction;
|
||||
use App\DTOs\LeadData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Lead\UpdateLeadRequest;
|
||||
use App\Http\Resources\LeadResource;
|
||||
use App\Models\Lead;
|
||||
use App\Repositories\Contracts\LeadRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class LeadController extends Controller
|
||||
{
|
||||
public function __construct(private LeadRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/leads',
|
||||
summary: 'Başvuruları listele (Admin)',
|
||||
tags: ['Admin - Leads'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'source', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'is_read', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Başvuru listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$leads = $this->repository->paginate(
|
||||
$request->only(['status', 'source', 'is_read', 'search']),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return LeadResource::collection($leads);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/leads/{lead}',
|
||||
summary: 'Başvuru detayı',
|
||||
tags: ['Admin - Leads'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'lead', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Başvuru detayı')],
|
||||
)]
|
||||
public function show(Lead $lead): JsonResponse
|
||||
{
|
||||
if (! $lead->is_read) {
|
||||
$lead->update(['is_read' => true]);
|
||||
}
|
||||
|
||||
return response()->json(new LeadResource($lead));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/leads/{lead}',
|
||||
summary: 'Başvuru güncelle',
|
||||
tags: ['Admin - Leads'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'lead', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'status', type: 'string'),
|
||||
new OA\Property(property: 'is_read', type: 'boolean'),
|
||||
new OA\Property(property: 'admin_notes', type: 'string'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Başvuru güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateLeadRequest $request, Lead $lead, UpdateLeadAction $action): JsonResponse
|
||||
{
|
||||
$dto = LeadData::fromArray(array_merge($lead->toArray(), $request->validated()));
|
||||
$lead = $action->execute($lead, $dto);
|
||||
|
||||
return response()->json(new LeadResource($lead));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/leads/{lead}',
|
||||
summary: 'Başvuru sil',
|
||||
tags: ['Admin - Leads'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'lead', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Başvuru silindi')],
|
||||
)]
|
||||
public function destroy(Lead $lead, DeleteLeadAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($lead);
|
||||
|
||||
return response()->json(['message' => 'Talep silindi.']);
|
||||
}
|
||||
}
|
||||
159
app/Http/Controllers/Api/Admin/MenuController.php
Normal file
159
app/Http/Controllers/Api/Admin/MenuController.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Menu\CreateMenuAction;
|
||||
use App\Actions\Menu\DeleteMenuAction;
|
||||
use App\Actions\Menu\UpdateMenuAction;
|
||||
use App\DTOs\MenuData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Menu\ReorderMenuRequest;
|
||||
use App\Http\Requests\Menu\StoreMenuRequest;
|
||||
use App\Http\Requests\Menu\UpdateMenuRequest;
|
||||
use App\Http\Resources\MenuResource;
|
||||
use App\Models\Menu;
|
||||
use App\Repositories\Contracts\MenuRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class MenuController extends Controller
|
||||
{
|
||||
public function __construct(private MenuRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/menus',
|
||||
summary: 'Menü öğelerini listele (Admin)',
|
||||
tags: ['Admin - Menus'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'location', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 50)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Menü listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$menus = $this->repository->paginate(
|
||||
$request->only(['location']),
|
||||
$request->integer('per_page', 50),
|
||||
);
|
||||
|
||||
return MenuResource::collection($menus);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/menus',
|
||||
summary: 'Yeni menü öğesi oluştur',
|
||||
tags: ['Admin - Menus'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['title', 'url', 'location'],
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'url', type: 'string'),
|
||||
new OA\Property(property: 'location', type: 'string'),
|
||||
new OA\Property(property: 'parent_id', type: 'integer'),
|
||||
new OA\Property(property: 'target', type: 'string'),
|
||||
new OA\Property(property: 'icon', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Menü oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreMenuRequest $request, CreateMenuAction $action): JsonResponse
|
||||
{
|
||||
$dto = MenuData::fromArray($request->validated());
|
||||
$menu = $action->execute($dto);
|
||||
|
||||
return response()->json(new MenuResource($menu), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/menus/{menu}',
|
||||
summary: 'Menü detayı',
|
||||
tags: ['Admin - Menus'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'menu', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Menü detayı')],
|
||||
)]
|
||||
public function show(Menu $menu): JsonResponse
|
||||
{
|
||||
return response()->json(new MenuResource($menu->load('children')));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/menus/{menu}',
|
||||
summary: 'Menü güncelle',
|
||||
tags: ['Admin - Menus'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'menu', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'url', type: 'string'),
|
||||
new OA\Property(property: 'location', type: 'string'),
|
||||
new OA\Property(property: 'parent_id', type: 'integer'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Menü güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateMenuRequest $request, Menu $menu, UpdateMenuAction $action): JsonResponse
|
||||
{
|
||||
$dto = MenuData::fromArray(array_merge($menu->toArray(), $request->validated()));
|
||||
$menu = $action->execute($menu, $dto);
|
||||
|
||||
return response()->json(new MenuResource($menu->load('children')));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/menus/{menu}',
|
||||
summary: 'Menü sil',
|
||||
tags: ['Admin - Menus'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'menu', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Menü silindi')],
|
||||
)]
|
||||
public function destroy(Menu $menu, DeleteMenuAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($menu);
|
||||
|
||||
return response()->json(['message' => 'Menü silindi.']);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/menus/reorder',
|
||||
summary: 'Menü sıralamasını güncelle',
|
||||
tags: ['Admin - Menus'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['items'],
|
||||
properties: [
|
||||
new OA\Property(property: 'items', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
new OA\Property(property: 'parent_id', type: 'integer', nullable: true),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
responses: [new OA\Response(response: 200, description: 'Sıralama güncellendi')],
|
||||
)]
|
||||
public function reorder(ReorderMenuRequest $request): JsonResponse
|
||||
{
|
||||
$this->repository->reorder($request->validated('items'));
|
||||
|
||||
return response()->json(['message' => 'Menü sıralaması güncellendi.']);
|
||||
}
|
||||
}
|
||||
179
app/Http/Controllers/Api/Admin/PageController.php
Normal file
179
app/Http/Controllers/Api/Admin/PageController.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Page\CreatePageAction;
|
||||
use App\Actions\Page\DeletePageAction;
|
||||
use App\Actions\Page\UpdatePageAction;
|
||||
use App\DTOs\PageData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Page\StorePageRequest;
|
||||
use App\Http\Requests\Page\UpdatePageRequest;
|
||||
use App\Http\Resources\PageResource;
|
||||
use App\Models\Page;
|
||||
use App\Repositories\Contracts\PageRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
public function __construct(private PageRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/pages',
|
||||
summary: 'Sayfaları listele (Admin)',
|
||||
tags: ['Admin - Pages'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Sayfa listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$pages = $this->repository->paginate(
|
||||
$request->only(['search']),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return PageResource::collection($pages);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/pages',
|
||||
summary: 'Yeni sayfa oluştur',
|
||||
tags: ['Admin - Pages'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['title', 'slug'],
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'slug', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'string'),
|
||||
new OA\Property(property: 'template', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'meta_title', type: 'string'),
|
||||
new OA\Property(property: 'meta_description', type: 'string'),
|
||||
new OA\Property(property: 'blocks', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'type', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'object'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Sayfa oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StorePageRequest $request, CreatePageAction $action): JsonResponse
|
||||
{
|
||||
return DB::transaction(function () use ($request, $action) {
|
||||
$validated = $request->validated();
|
||||
$blocks = $validated['blocks'] ?? [];
|
||||
unset($validated['blocks']);
|
||||
|
||||
$dto = PageData::fromArray($validated);
|
||||
$page = $action->execute($dto);
|
||||
|
||||
foreach ($blocks as $index => $block) {
|
||||
$page->blocks()->create([
|
||||
'type' => $block['type'],
|
||||
'content' => $block['content'],
|
||||
'order_index' => $block['order_index'] ?? $index,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(new PageResource($page->load('blocks')), 201);
|
||||
});
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/pages/{page}',
|
||||
summary: 'Sayfa detayı (Admin)',
|
||||
tags: ['Admin - Pages'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Sayfa detayı')],
|
||||
)]
|
||||
public function show(Page $page): JsonResponse
|
||||
{
|
||||
return response()->json(new PageResource($page->load('blocks')));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/pages/{page}',
|
||||
summary: 'Sayfa güncelle',
|
||||
tags: ['Admin - Pages'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'slug', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'string'),
|
||||
new OA\Property(property: 'template', type: 'string'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
new OA\Property(property: 'meta_title', type: 'string'),
|
||||
new OA\Property(property: 'meta_description', type: 'string'),
|
||||
new OA\Property(property: 'blocks', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'type', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'object'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Sayfa güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdatePageRequest $request, Page $page, UpdatePageAction $action): JsonResponse
|
||||
{
|
||||
return DB::transaction(function () use ($request, $page, $action) {
|
||||
$validated = $request->validated();
|
||||
$blocks = $validated['blocks'] ?? null;
|
||||
unset($validated['blocks']);
|
||||
|
||||
$dto = PageData::fromArray(array_merge($page->toArray(), $validated));
|
||||
$page = $action->execute($page, $dto);
|
||||
|
||||
if ($blocks !== null) {
|
||||
$page->blocks()->delete();
|
||||
foreach ($blocks as $index => $block) {
|
||||
$page->blocks()->create([
|
||||
'type' => $block['type'],
|
||||
'content' => $block['content'],
|
||||
'order_index' => $block['order_index'] ?? $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(new PageResource($page->load('blocks')));
|
||||
});
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/pages/{page}',
|
||||
summary: 'Sayfa sil',
|
||||
tags: ['Admin - Pages'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'page', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Sayfa silindi')],
|
||||
)]
|
||||
public function destroy(Page $page, DeletePageAction $action): JsonResponse
|
||||
{
|
||||
$page->blocks()->delete();
|
||||
$action->execute($page);
|
||||
|
||||
return response()->json(['message' => 'Sayfa silindi.']);
|
||||
}
|
||||
}
|
||||
92
app/Http/Controllers/Api/Admin/PreviewController.php
Normal file
92
app/Http/Controllers/Api/Admin/PreviewController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Preview\StorePreviewRequest;
|
||||
use App\Models\Page;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class PreviewController extends Controller
|
||||
{
|
||||
private const CACHE_TTL = 600; // 10 minutes
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/preview',
|
||||
summary: 'Önizleme oluştur',
|
||||
tags: ['Admin - Preview'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['page_id', 'blocks'],
|
||||
properties: [
|
||||
new OA\Property(property: 'page_id', type: 'integer'),
|
||||
new OA\Property(property: 'blocks', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'type', type: 'string'),
|
||||
new OA\Property(property: 'content', type: 'object'),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Önizleme oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StorePreviewRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$page = Page::findOrFail($validated['page_id']);
|
||||
$token = (string) Str::uuid();
|
||||
|
||||
$blocks = collect($validated['blocks'])
|
||||
->sortBy('order_index')
|
||||
->values()
|
||||
->map(fn (array $block, int $index) => [
|
||||
'id' => $index + 1,
|
||||
'type' => $block['type'],
|
||||
'content' => $block['content'],
|
||||
'order_index' => $block['order_index'],
|
||||
])
|
||||
->all();
|
||||
|
||||
Cache::put("preview_{$token}", [
|
||||
'id' => $page->id,
|
||||
'slug' => $page->slug,
|
||||
'title' => $page->title,
|
||||
'meta_title' => $page->meta_title,
|
||||
'meta_description' => $page->meta_description,
|
||||
'is_active' => $page->is_active,
|
||||
'blocks' => $blocks,
|
||||
'created_at' => $page->created_at?->toISOString(),
|
||||
'updated_at' => now()->toISOString(),
|
||||
], self::CACHE_TTL);
|
||||
|
||||
$previewUrl = config('app.frontend_url')."/api/preview?token={$token}&slug={$page->slug}";
|
||||
|
||||
return response()->json([
|
||||
'token' => $token,
|
||||
'preview_url' => $previewUrl,
|
||||
'expires_in' => self::CACHE_TTL,
|
||||
], 201);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/preview/{token}',
|
||||
summary: 'Önizlemeyi sil',
|
||||
tags: ['Admin - Preview'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'token', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Önizleme silindi')],
|
||||
)]
|
||||
public function destroy(string $token): JsonResponse
|
||||
{
|
||||
Cache::forget("preview_{$token}");
|
||||
|
||||
return response()->json(['message' => 'Önizleme silindi.']);
|
||||
}
|
||||
}
|
||||
180
app/Http/Controllers/Api/Admin/RoleController.php
Normal file
180
app/Http/Controllers/Api/Admin/RoleController.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Role\StoreRoleRequest;
|
||||
use App\Http\Requests\Role\UpdateRoleRequest;
|
||||
use App\Http\Resources\RoleResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/admin/roles',
|
||||
summary: 'Rolleri listele',
|
||||
tags: ['Admin - Roles'],
|
||||
security: [['sanctum' => []]],
|
||||
responses: [new OA\Response(response: 200, description: 'Rol listesi')],
|
||||
)]
|
||||
public function index(): AnonymousResourceCollection
|
||||
{
|
||||
$roles = Role::query()
|
||||
->with('permissions')
|
||||
->get()
|
||||
->loadCount('users');
|
||||
|
||||
return RoleResource::collection($roles);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/roles',
|
||||
summary: 'Yeni rol oluştur',
|
||||
tags: ['Admin - Roles'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['name', 'permissions'],
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string', example: 'moderator'),
|
||||
new OA\Property(property: 'permissions', type: 'array', items: new OA\Items(type: 'string'), example: ['view-category', 'view-course']),
|
||||
],
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Rol oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreRoleRequest $request): JsonResponse
|
||||
{
|
||||
$role = Role::create([
|
||||
'name' => $request->validated('name'),
|
||||
'guard_name' => 'web',
|
||||
]);
|
||||
|
||||
$role->syncPermissions($request->validated('permissions'));
|
||||
$role->load('permissions');
|
||||
|
||||
return (new RoleResource($role))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/roles/{role}',
|
||||
summary: 'Rol detayı',
|
||||
tags: ['Admin - Roles'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'role', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Rol detayı'),
|
||||
new OA\Response(response: 404, description: 'Bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function show(Role $role): RoleResource
|
||||
{
|
||||
$role->load('permissions');
|
||||
$role->loadCount('users');
|
||||
|
||||
return new RoleResource($role);
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/roles/{role}',
|
||||
summary: 'Rol güncelle',
|
||||
tags: ['Admin - Roles'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'role', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string', example: 'moderator'),
|
||||
new OA\Property(property: 'permissions', type: 'array', items: new OA\Items(type: 'string')),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Rol güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateRoleRequest $request, Role $role): RoleResource
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
if (isset($validated['name'])) {
|
||||
$role->update(['name' => $validated['name']]);
|
||||
}
|
||||
|
||||
if (isset($validated['permissions'])) {
|
||||
$role->syncPermissions($validated['permissions']);
|
||||
}
|
||||
|
||||
$role->load('permissions');
|
||||
|
||||
return new RoleResource($role);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/roles/{role}',
|
||||
summary: 'Rol sil',
|
||||
tags: ['Admin - Roles'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'role', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Rol silindi'),
|
||||
new OA\Response(response: 403, description: 'Varsayılan roller silinemez'),
|
||||
new OA\Response(response: 404, description: 'Bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function destroy(Role $role): JsonResponse
|
||||
{
|
||||
if (in_array($role->name, ['super-admin', 'editor'])) {
|
||||
return response()->json(['message' => 'Varsayılan roller silinemez.'], 403);
|
||||
}
|
||||
|
||||
if ($role->users()->count() > 0) {
|
||||
return response()->json(['message' => 'Bu role atanmış kullanıcılar var. Önce kullanıcıların rollerini değiştirin.'], 422);
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
|
||||
return response()->json(['message' => 'Rol başarıyla silindi.']);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/permissions',
|
||||
summary: 'Tüm yetkileri listele',
|
||||
description: 'Rol oluştururken/düzenlerken kullanılacak tüm mevcut yetkileri modül bazlı gruplandırarak döner.',
|
||||
tags: ['Admin - Roles'],
|
||||
security: [['sanctum' => []]],
|
||||
responses: [new OA\Response(response: 200, description: 'Yetki listesi')],
|
||||
)]
|
||||
public function permissions(): JsonResponse
|
||||
{
|
||||
$permissions = Permission::query()
|
||||
->where('guard_name', 'web')
|
||||
->orderBy('name')
|
||||
->pluck('name');
|
||||
|
||||
// Modül bazlı gruplandırma
|
||||
$grouped = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$parts = explode('-', $permission, 2);
|
||||
if (count($parts) === 2) {
|
||||
$grouped[$parts[1]][] = $permission;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'all' => $permissions,
|
||||
'grouped' => $grouped,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
135
app/Http/Controllers/Api/Admin/ScheduleController.php
Normal file
135
app/Http/Controllers/Api/Admin/ScheduleController.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Schedule\CreateScheduleAction;
|
||||
use App\Actions\Schedule\DeleteScheduleAction;
|
||||
use App\Actions\Schedule\UpdateScheduleAction;
|
||||
use App\DTOs\ScheduleData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Schedule\StoreScheduleRequest;
|
||||
use App\Http\Requests\Schedule\UpdateScheduleRequest;
|
||||
use App\Http\Resources\CourseScheduleResource;
|
||||
use App\Models\CourseSchedule;
|
||||
use App\Repositories\Contracts\ScheduleRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ScheduleController extends Controller
|
||||
{
|
||||
public function __construct(private ScheduleRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/schedules',
|
||||
summary: 'Takvimleri listele (Admin)',
|
||||
tags: ['Admin - Schedules'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'course_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Takvim listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$schedules = $this->repository->paginate(
|
||||
$request->only(['course_id']),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return CourseScheduleResource::collection($schedules);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/schedules',
|
||||
summary: 'Yeni takvim oluştur',
|
||||
tags: ['Admin - Schedules'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['course_id', 'start_date', 'location', 'quota'],
|
||||
properties: [
|
||||
new OA\Property(property: 'course_id', type: 'integer'),
|
||||
new OA\Property(property: 'start_date', type: 'string', format: 'date'),
|
||||
new OA\Property(property: 'end_date', type: 'string', format: 'date'),
|
||||
new OA\Property(property: 'location', type: 'string'),
|
||||
new OA\Property(property: 'instructor', type: 'string'),
|
||||
new OA\Property(property: 'quota', type: 'integer'),
|
||||
new OA\Property(property: 'enrolled_count', type: 'integer'),
|
||||
new OA\Property(property: 'price_override', type: 'number'),
|
||||
new OA\Property(property: 'status', type: 'string'),
|
||||
new OA\Property(property: 'notes', type: 'string'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Takvim oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreScheduleRequest $request, CreateScheduleAction $action): JsonResponse
|
||||
{
|
||||
$dto = ScheduleData::fromArray($request->validated());
|
||||
$schedule = $action->execute($dto);
|
||||
|
||||
return response()->json(new CourseScheduleResource($schedule->load('course')), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/schedules/{schedule}',
|
||||
summary: 'Takvim detayı (Admin)',
|
||||
tags: ['Admin - Schedules'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'schedule', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Takvim detayı')],
|
||||
)]
|
||||
public function show(CourseSchedule $schedule): JsonResponse
|
||||
{
|
||||
return response()->json(new CourseScheduleResource($schedule->load('course')));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/schedules/{schedule}',
|
||||
summary: 'Takvim güncelle',
|
||||
tags: ['Admin - Schedules'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'schedule', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'course_id', type: 'integer'),
|
||||
new OA\Property(property: 'start_date', type: 'string', format: 'date'),
|
||||
new OA\Property(property: 'end_date', type: 'string', format: 'date'),
|
||||
new OA\Property(property: 'location', type: 'string'),
|
||||
new OA\Property(property: 'instructor', type: 'string'),
|
||||
new OA\Property(property: 'quota', type: 'integer'),
|
||||
new OA\Property(property: 'status', type: 'string'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Takvim güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateScheduleRequest $request, CourseSchedule $schedule, UpdateScheduleAction $action): JsonResponse
|
||||
{
|
||||
$dto = ScheduleData::fromArray(array_merge($schedule->toArray(), $request->validated()));
|
||||
$schedule = $action->execute($schedule, $dto);
|
||||
|
||||
return response()->json(new CourseScheduleResource($schedule->load('course')));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/schedules/{schedule}',
|
||||
summary: 'Takvim sil',
|
||||
tags: ['Admin - Schedules'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'schedule', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Takvim silindi')],
|
||||
)]
|
||||
public function destroy(CourseSchedule $schedule, DeleteScheduleAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($schedule);
|
||||
|
||||
return response()->json(['message' => 'Takvim silindi.']);
|
||||
}
|
||||
}
|
||||
89
app/Http/Controllers/Api/Admin/SettingController.php
Normal file
89
app/Http/Controllers/Api/Admin/SettingController.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\Setting\UpdateSettingsAction;
|
||||
use App\Enums\SettingGroup;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Setting\UpdateSettingsRequest;
|
||||
use App\Http\Resources\SettingResource;
|
||||
use App\Repositories\Contracts\SettingRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
public function __construct(private SettingRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/settings',
|
||||
summary: 'Tüm ayarları listele (Admin)',
|
||||
tags: ['Admin - Settings'],
|
||||
security: [['sanctum' => []]],
|
||||
responses: [new OA\Response(response: 200, description: 'Ayar listesi')],
|
||||
)]
|
||||
public function index(): AnonymousResourceCollection
|
||||
{
|
||||
return SettingResource::collection($this->repository->all());
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/settings/group/{group}',
|
||||
summary: 'Gruba göre ayarları getir',
|
||||
tags: ['Admin - Settings'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'group', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Grup ayarları'),
|
||||
new OA\Response(response: 404, description: 'Grup bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function group(string $group): AnonymousResourceCollection
|
||||
{
|
||||
$settingGroup = SettingGroup::tryFrom($group);
|
||||
|
||||
if (! $settingGroup) {
|
||||
abort(404, 'Ayar grubu bulunamadı.');
|
||||
}
|
||||
|
||||
return SettingResource::collection($this->repository->getByGroup($settingGroup));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/settings',
|
||||
summary: 'Ayarları toplu güncelle (dot notation: general.site_name)',
|
||||
tags: ['Admin - Settings'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['settings'],
|
||||
properties: [
|
||||
new OA\Property(property: 'settings', type: 'object', example: '{"general.site_name": "Yeni Ad", "contact.phone_primary": "+90 ..."}'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Ayarlar güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateSettingsRequest $request, UpdateSettingsAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($request->validated('settings'));
|
||||
|
||||
return response()->json(['message' => 'Ayarlar güncellendi.']);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/settings/clear-cache',
|
||||
summary: 'Ayar cache temizle',
|
||||
tags: ['Admin - Settings'],
|
||||
security: [['sanctum' => []]],
|
||||
responses: [new OA\Response(response: 200, description: 'Cache temizlendi')],
|
||||
)]
|
||||
public function clearCache(): JsonResponse
|
||||
{
|
||||
$this->repository->clearCache();
|
||||
|
||||
return response()->json(['message' => 'Ayar cache temizlendi.']);
|
||||
}
|
||||
}
|
||||
138
app/Http/Controllers/Api/Admin/StoryController.php
Normal file
138
app/Http/Controllers/Api/Admin/StoryController.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\StoryResource;
|
||||
use App\Models\Story;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class StoryController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/admin/stories',
|
||||
summary: 'Tüm hikayeleri listele (Admin)',
|
||||
tags: ['Admin - Stories'],
|
||||
security: [['sanctum' => []]],
|
||||
responses: [new OA\Response(response: 200, description: 'Hikaye listesi')],
|
||||
)]
|
||||
public function index(): AnonymousResourceCollection
|
||||
{
|
||||
return StoryResource::collection(
|
||||
Story::query()->orderBy('order_index')->get()
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/stories',
|
||||
summary: 'Yeni hikaye oluştur',
|
||||
tags: ['Admin - Stories'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['title', 'content'],
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'badge', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'content', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'cta_text', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'cta_url', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Hikaye oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'badge' => ['nullable', 'string', 'max:100'],
|
||||
'content' => ['required', 'string'],
|
||||
'image' => ['nullable', 'string', 'max:255'],
|
||||
'cta_text' => ['nullable', 'string', 'max:100'],
|
||||
'cta_url' => ['nullable', 'string', 'max:255'],
|
||||
'order_index' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$story = Story::create($validated);
|
||||
|
||||
return response()->json(new StoryResource($story), 201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/stories/{story}',
|
||||
summary: 'Hikaye detayı',
|
||||
tags: ['Admin - Stories'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'story', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Hikaye detayı')],
|
||||
)]
|
||||
public function show(Story $story): JsonResponse
|
||||
{
|
||||
return response()->json(new StoryResource($story));
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/stories/{story}',
|
||||
summary: 'Hikaye güncelle',
|
||||
tags: ['Admin - Stories'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'story', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'title', type: 'string'),
|
||||
new OA\Property(property: 'badge', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'content', type: 'string'),
|
||||
new OA\Property(property: 'image', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'cta_text', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'cta_url', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'order_index', type: 'integer'),
|
||||
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Hikaye güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(Request $request, Story $story): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => ['sometimes', 'string', 'max:255'],
|
||||
'badge' => ['nullable', 'string', 'max:100'],
|
||||
'content' => ['sometimes', 'string'],
|
||||
'image' => ['nullable', 'string', 'max:255'],
|
||||
'cta_text' => ['nullable', 'string', 'max:100'],
|
||||
'cta_url' => ['nullable', 'string', 'max:255'],
|
||||
'order_index' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$story->update($validated);
|
||||
|
||||
return response()->json(new StoryResource($story->fresh()));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/stories/{story}',
|
||||
summary: 'Hikaye sil',
|
||||
tags: ['Admin - Stories'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'story', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [new OA\Response(response: 200, description: 'Hikaye silindi')],
|
||||
)]
|
||||
public function destroy(Story $story): JsonResponse
|
||||
{
|
||||
$story->delete();
|
||||
|
||||
return response()->json(['message' => 'Hikaye silindi.']);
|
||||
}
|
||||
}
|
||||
138
app/Http/Controllers/Api/Admin/UploadController.php
Normal file
138
app/Http/Controllers/Api/Admin/UploadController.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class UploadController extends Controller
|
||||
{
|
||||
#[OA\Post(
|
||||
path: '/api/admin/upload',
|
||||
summary: 'Dosya yükle',
|
||||
description: 'Görsel veya video dosyası yükler. Görseller max 5MB, videolar max 100MB. Modül bazlı klasörleme destekler.',
|
||||
security: [['sanctum' => []]],
|
||||
tags: ['Upload'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'multipart/form-data',
|
||||
schema: new OA\Schema(
|
||||
required: ['file'],
|
||||
properties: [
|
||||
new OA\Property(property: 'file', type: 'string', format: 'binary', description: 'Görsel veya video dosyası'),
|
||||
new OA\Property(property: 'type', type: 'string', enum: ['image', 'video'], description: 'Dosya tipi'),
|
||||
new OA\Property(property: 'folder', type: 'string', description: 'Modül klasörü (hero-slides, courses, vb.)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Dosya yüklendi', content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'data', type: 'object', properties: [
|
||||
new OA\Property(property: 'path', type: 'string'),
|
||||
new OA\Property(property: 'url', type: 'string'),
|
||||
]),
|
||||
],
|
||||
)),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->input('type', 'image');
|
||||
|
||||
if ($type === 'video') {
|
||||
return $this->storeVideo($request);
|
||||
}
|
||||
|
||||
return $this->storeImage($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private const ALLOWED_FOLDERS = [
|
||||
'images',
|
||||
'videos',
|
||||
'hero-slides',
|
||||
'settings',
|
||||
'pages',
|
||||
'courses',
|
||||
'announcements',
|
||||
'categories',
|
||||
];
|
||||
|
||||
private function resolveFolder(Request $request, string $default): string
|
||||
{
|
||||
$folder = $request->input('folder', $default);
|
||||
|
||||
if (! in_array($folder, self::ALLOWED_FOLDERS, true)) {
|
||||
$folder = $default;
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
|
||||
private function storeImage(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => ['required', 'file', 'image', 'max:5120'],
|
||||
'folder' => ['sometimes', 'string'],
|
||||
], [
|
||||
'file.required' => 'Dosya zorunludur.',
|
||||
'file.image' => 'Dosya bir görsel (jpg, png, gif, svg, webp) olmalıdır.',
|
||||
'file.max' => 'Dosya boyutu en fazla 5MB olabilir.',
|
||||
]);
|
||||
|
||||
$folder = $this->resolveFolder($request, 'images');
|
||||
|
||||
$file = $request->file('file');
|
||||
$filename = Str::uuid().'.'.$file->getClientOriginalExtension();
|
||||
|
||||
$directory = public_path('uploads/'.$folder);
|
||||
$file->move($directory, $filename);
|
||||
|
||||
$relativePath = 'uploads/'.$folder.'/'.$filename;
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'path' => $relativePath,
|
||||
'url' => url($relativePath),
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
private function storeVideo(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => ['required', 'file', 'mimes:mp4,webm,mov,avi,mkv', 'max:102400'],
|
||||
'folder' => ['sometimes', 'string'],
|
||||
], [
|
||||
'file.required' => 'Dosya zorunludur.',
|
||||
'file.mimes' => 'Dosya bir video (mp4, webm, mov, avi, mkv) olmalıdır.',
|
||||
'file.max' => 'Video boyutu en fazla 100MB olabilir.',
|
||||
]);
|
||||
|
||||
$folder = $this->resolveFolder($request, 'videos');
|
||||
|
||||
$file = $request->file('file');
|
||||
$filename = Str::uuid().'.'.$file->getClientOriginalExtension();
|
||||
|
||||
$directory = public_path('uploads/'.$folder);
|
||||
$file->move($directory, $filename);
|
||||
|
||||
$relativePath = 'uploads/'.$folder.'/'.$filename;
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'path' => $relativePath,
|
||||
'url' => url($relativePath),
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
147
app/Http/Controllers/Api/Admin/UserController.php
Normal file
147
app/Http/Controllers/Api/Admin/UserController.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Actions\User\CreateUserAction;
|
||||
use App\Actions\User\DeleteUserAction;
|
||||
use App\Actions\User\UpdateUserAction;
|
||||
use App\DTOs\UserData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\StoreUserRequest;
|
||||
use App\Http\Requests\User\UpdateUserRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Contracts\UserRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct(private UserRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/users',
|
||||
summary: 'Admin kullanıcılarını listele',
|
||||
tags: ['Admin - Users'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'role', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Kullanıcı listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$users = $this->repository->paginate(
|
||||
filters: $request->only('search', 'role'),
|
||||
perPage: $request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return UserResource::collection($users);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/admin/users',
|
||||
summary: 'Yeni admin kullanıcı oluştur',
|
||||
tags: ['Admin - Users'],
|
||||
security: [['sanctum' => []]],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['name', 'email', 'password', 'password_confirmation', 'role'],
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string', example: 'Editör Kullanıcı'),
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'editor@bogazici.com'),
|
||||
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password123'),
|
||||
new OA\Property(property: 'password_confirmation', type: 'string', format: 'password', example: 'password123'),
|
||||
new OA\Property(property: 'role', type: 'string', example: 'editor'),
|
||||
],
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Kullanıcı oluşturuldu'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreUserRequest $request, CreateUserAction $action): JsonResponse
|
||||
{
|
||||
$dto = UserData::fromArray($request->validated());
|
||||
$user = $action->execute($dto);
|
||||
|
||||
return (new UserResource($user))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/admin/users/{user}',
|
||||
summary: 'Kullanıcı detayı',
|
||||
tags: ['Admin - Users'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kullanıcı detayı'),
|
||||
new OA\Response(response: 404, description: 'Bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function show(User $user): UserResource
|
||||
{
|
||||
$user->load('roles');
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/api/admin/users/{user}',
|
||||
summary: 'Kullanıcı güncelle',
|
||||
tags: ['Admin - Users'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email'),
|
||||
new OA\Property(property: 'password', type: 'string', format: 'password'),
|
||||
new OA\Property(property: 'password_confirmation', type: 'string', format: 'password'),
|
||||
new OA\Property(property: 'role', type: 'string', example: 'editor'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kullanıcı güncellendi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function update(UpdateUserRequest $request, User $user, UpdateUserAction $action): UserResource
|
||||
{
|
||||
$dto = UserData::fromArray($request->validated());
|
||||
$user = $action->execute($user, $dto);
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/api/admin/users/{user}',
|
||||
summary: 'Kullanıcı sil (soft delete)',
|
||||
tags: ['Admin - Users'],
|
||||
security: [['sanctum' => []]],
|
||||
parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kullanıcı silindi'),
|
||||
new OA\Response(response: 403, description: 'Kendini silemezsin'),
|
||||
new OA\Response(response: 404, description: 'Bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function destroy(User $user, DeleteUserAction $action): JsonResponse
|
||||
{
|
||||
if ($user->id === auth()->id()) {
|
||||
return response()->json(['message' => 'Kendinizi silemezsiniz.'], 403);
|
||||
}
|
||||
|
||||
$action->execute($user);
|
||||
|
||||
return response()->json(['message' => 'Kullanıcı başarıyla silindi.']);
|
||||
}
|
||||
}
|
||||
62
app/Http/Controllers/Api/V1/AnnouncementController.php
Normal file
62
app/Http/Controllers/Api/V1/AnnouncementController.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\AnnouncementResource;
|
||||
use App\Repositories\Contracts\AnnouncementRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class AnnouncementController extends Controller
|
||||
{
|
||||
public function __construct(private AnnouncementRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/announcements',
|
||||
summary: 'Duyuruları listele',
|
||||
tags: ['Announcements'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'featured', in: 'query', required: false, schema: new OA\Schema(type: 'boolean')),
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'Duyuru listesi')],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$announcements = $this->repository->paginate(
|
||||
$request->only(['category', 'featured', 'search', 'sort']),
|
||||
$request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return AnnouncementResource::collection($announcements);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/announcements/{slug}',
|
||||
summary: 'Duyuru detayı',
|
||||
tags: ['Announcements'],
|
||||
parameters: [new OA\Parameter(name: 'slug', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Duyuru detayı'),
|
||||
new OA\Response(response: 404, description: 'Duyuru bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function show(string $slug): JsonResponse
|
||||
{
|
||||
$announcement = $this->repository->findBySlug($slug);
|
||||
|
||||
if (! $announcement) {
|
||||
return response()->json(['message' => 'Duyuru bulunamadı.'], 404);
|
||||
}
|
||||
|
||||
$announcement->load(['comments' => fn ($q) => $q->where('is_approved', true)->latest()]);
|
||||
|
||||
return response()->json(new AnnouncementResource($announcement));
|
||||
}
|
||||
}
|
||||
60
app/Http/Controllers/Api/V1/CategoryController.php
Normal file
60
app/Http/Controllers/Api/V1/CategoryController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CategoryResource;
|
||||
use App\Repositories\Contracts\CategoryRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function __construct(private CategoryRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/categories',
|
||||
summary: 'Kategorileri listele',
|
||||
description: 'Tüm aktif kategorileri sayfalanmış olarak döndürür.',
|
||||
tags: ['Categories'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kategori listesi'),
|
||||
],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$categories = $this->repository->paginate(
|
||||
filters: $request->only('search'),
|
||||
perPage: $request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return CategoryResource::collection($categories);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/categories/{slug}',
|
||||
summary: 'Kategori detayı',
|
||||
description: 'Slug ile kategori detayını döndürür.',
|
||||
tags: ['Categories'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'slug', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Kategori detayı'),
|
||||
new OA\Response(response: 404, description: 'Kategori bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function show(string $slug): CategoryResource
|
||||
{
|
||||
$category = $this->repository->findBySlug($slug);
|
||||
|
||||
abort_if(! $category, 404, 'Kategori bulunamadı.');
|
||||
|
||||
return new CategoryResource($category);
|
||||
}
|
||||
}
|
||||
92
app/Http/Controllers/Api/V1/CommentController.php
Normal file
92
app/Http/Controllers/Api/V1/CommentController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\Comment\CreateCommentAction;
|
||||
use App\DTOs\CommentData;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Comment\StoreCommentRequest;
|
||||
use App\Http\Resources\CommentResource;
|
||||
use App\Models\Announcement;
|
||||
use App\Models\Category;
|
||||
use App\Models\Course;
|
||||
use App\Repositories\Contracts\CommentRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
/**
|
||||
* Morph map for commentable types from the request.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private const COMMENTABLE_MAP = [
|
||||
'course' => Course::class,
|
||||
'category' => Category::class,
|
||||
'announcement' => Announcement::class,
|
||||
];
|
||||
|
||||
public function __construct(private CommentRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/comments/{type}/{id}',
|
||||
summary: 'Onaylı yorumları getir',
|
||||
tags: ['Comments'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'type', in: 'path', required: true, schema: new OA\Schema(type: 'string', enum: ['course', 'category', 'announcement'])),
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Onaylı yorum listesi'),
|
||||
new OA\Response(response: 404, description: 'Geçersiz yorum tipi'),
|
||||
],
|
||||
)]
|
||||
public function index(string $type, int $id): AnonymousResourceCollection|JsonResponse
|
||||
{
|
||||
$commentableType = self::COMMENTABLE_MAP[$type] ?? null;
|
||||
|
||||
if ($commentableType === null) {
|
||||
return response()->json(['message' => 'Geçersiz yorum tipi.'], 404);
|
||||
}
|
||||
|
||||
$comments = $this->repository->getApprovedByCommentable($commentableType, $id);
|
||||
|
||||
return CommentResource::collection($comments);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/comments',
|
||||
summary: 'Yorum gönder',
|
||||
tags: ['Comments'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['body', 'author_name', 'author_email', 'commentable_type', 'commentable_id'],
|
||||
properties: [
|
||||
new OA\Property(property: 'body', type: 'string'),
|
||||
new OA\Property(property: 'author_name', type: 'string'),
|
||||
new OA\Property(property: 'author_email', type: 'string', format: 'email'),
|
||||
new OA\Property(property: 'commentable_type', type: 'string', enum: ['course', 'category', 'announcement']),
|
||||
new OA\Property(property: 'commentable_id', type: 'integer'),
|
||||
new OA\Property(property: 'rating', type: 'integer', minimum: 1, maximum: 5),
|
||||
],
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Yorum gönderildi'),
|
||||
new OA\Response(response: 422, description: 'Validasyon hatası'),
|
||||
],
|
||||
)]
|
||||
public function store(StoreCommentRequest $request, CreateCommentAction $action): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$validated['commentable_type'] = self::COMMENTABLE_MAP[$validated['commentable_type']] ?? $validated['commentable_type'];
|
||||
|
||||
$dto = CommentData::fromArray($validated);
|
||||
$action->execute($dto);
|
||||
|
||||
return response()->json(['message' => 'Yorumunuz incelenmek üzere gönderildi.'], 201);
|
||||
}
|
||||
}
|
||||
62
app/Http/Controllers/Api/V1/CourseController.php
Normal file
62
app/Http/Controllers/Api/V1/CourseController.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CourseResource;
|
||||
use App\Repositories\Contracts\CourseRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class CourseController extends Controller
|
||||
{
|
||||
public function __construct(private CourseRepositoryInterface $repository) {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/courses',
|
||||
summary: 'Eğitimleri listele',
|
||||
description: 'Tüm eğitimleri sayfalanmış olarak döndürür. Kategori, arama ve sıralama filtresi destekler.',
|
||||
tags: ['Courses'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'category', in: 'query', required: false, schema: new OA\Schema(type: 'string'), description: 'Kategori slug filtresi'),
|
||||
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'sort', in: 'query', required: false, schema: new OA\Schema(type: 'string'), description: 'Örn: -created_at, title'),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Eğitim listesi'),
|
||||
],
|
||||
)]
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$courses = $this->repository->paginate(
|
||||
filters: $request->only('category', 'search', 'sort'),
|
||||
perPage: $request->integer('per_page', 15),
|
||||
);
|
||||
|
||||
return CourseResource::collection($courses);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/courses/{slug}',
|
||||
summary: 'Eğitim detayı',
|
||||
description: 'Slug ile eğitim detayını döndürür.',
|
||||
tags: ['Courses'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'slug', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Eğitim detayı'),
|
||||
new OA\Response(response: 404, description: 'Eğitim bulunamadı'),
|
||||
],
|
||||
)]
|
||||
public function show(string $slug): CourseResource
|
||||
{
|
||||
$course = $this->repository->findBySlug($slug);
|
||||
|
||||
abort_if(! $course, 404, 'Eğitim bulunamadı.');
|
||||
|
||||
return new CourseResource($course);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user