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