diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md new file mode 100644 index 0000000..5619861 --- /dev/null +++ b/.claude/skills/pest-testing/SKILL.md @@ -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 + + +```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()`: + + +```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.): + + +```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. + + +```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: + + +```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): + + +```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 \ No newline at end of file diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..5845f55 --- /dev/null +++ b/.cursor/mcp.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/.cursor/skills/pest-testing/SKILL.md b/.cursor/skills/pest-testing/SKILL.md new file mode 100644 index 0000000..5619861 --- /dev/null +++ b/.cursor/skills/pest-testing/SKILL.md @@ -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 + + +```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()`: + + +```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.): + + +```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. + + +```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: + + +```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): + + +```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 \ No newline at end of file diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..31eec26 --- /dev/null +++ b/.drone.yml @@ -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 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5f8fbce --- /dev/null +++ b/.env.example @@ -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}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33cb24b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 0000000..9ce7946 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,238 @@ + +=== 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. + + +```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. + + diff --git a/.junie/mcp/mcp.json b/.junie/mcp/mcp.json new file mode 100644 index 0000000..ea8f2f8 --- /dev/null +++ b/.junie/mcp/mcp.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/.junie/skills/pest-testing/SKILL.md b/.junie/skills/pest-testing/SKILL.md new file mode 100644 index 0000000..5619861 --- /dev/null +++ b/.junie/skills/pest-testing/SKILL.md @@ -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 + + +```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()`: + + +```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.): + + +```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. + + +```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: + + +```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): + + +```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 \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..5845f55 --- /dev/null +++ b/.mcp.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..70acd96 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,242 @@ + +=== 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. + + +```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. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..70acd96 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,242 @@ + +=== 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. + + +```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. + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a2df24f --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..70acd96 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,242 @@ + +=== 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. + + +```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. + + diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e0f28bd --- /dev/null +++ b/TODO.md @@ -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ı diff --git a/app/Actions/Announcement/CreateAnnouncementAction.php b/app/Actions/Announcement/CreateAnnouncementAction.php new file mode 100644 index 0000000..8a12983 --- /dev/null +++ b/app/Actions/Announcement/CreateAnnouncementAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Announcement::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Announcement/DeleteAnnouncementAction.php b/app/Actions/Announcement/DeleteAnnouncementAction.php new file mode 100644 index 0000000..47754a1 --- /dev/null +++ b/app/Actions/Announcement/DeleteAnnouncementAction.php @@ -0,0 +1,21 @@ +repository->delete($announcement); + + ModelChanged::dispatch(Announcement::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Announcement/UpdateAnnouncementAction.php b/app/Actions/Announcement/UpdateAnnouncementAction.php new file mode 100644 index 0000000..077b25e --- /dev/null +++ b/app/Actions/Announcement/UpdateAnnouncementAction.php @@ -0,0 +1,22 @@ +repository->update($announcement, $data->toArray()); + + ModelChanged::dispatch(Announcement::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Category/CreateCategoryAction.php b/app/Actions/Category/CreateCategoryAction.php new file mode 100644 index 0000000..76ed1e3 --- /dev/null +++ b/app/Actions/Category/CreateCategoryAction.php @@ -0,0 +1,23 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Category::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Category/DeleteCategoryAction.php b/app/Actions/Category/DeleteCategoryAction.php new file mode 100644 index 0000000..3b6e74a --- /dev/null +++ b/app/Actions/Category/DeleteCategoryAction.php @@ -0,0 +1,21 @@ +repository->delete($category); + + ModelChanged::dispatch(Category::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Category/UpdateCategoryAction.php b/app/Actions/Category/UpdateCategoryAction.php new file mode 100644 index 0000000..2dd017f --- /dev/null +++ b/app/Actions/Category/UpdateCategoryAction.php @@ -0,0 +1,23 @@ +repository->update($category, $data->toArray()); + + ModelChanged::dispatch(Category::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Comment/CreateCommentAction.php b/app/Actions/Comment/CreateCommentAction.php new file mode 100644 index 0000000..7679881 --- /dev/null +++ b/app/Actions/Comment/CreateCommentAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Comment::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Comment/DeleteCommentAction.php b/app/Actions/Comment/DeleteCommentAction.php new file mode 100644 index 0000000..16318d8 --- /dev/null +++ b/app/Actions/Comment/DeleteCommentAction.php @@ -0,0 +1,21 @@ +repository->delete($comment); + + ModelChanged::dispatch(Comment::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Comment/UpdateCommentAction.php b/app/Actions/Comment/UpdateCommentAction.php new file mode 100644 index 0000000..30d999f --- /dev/null +++ b/app/Actions/Comment/UpdateCommentAction.php @@ -0,0 +1,24 @@ + $data + */ + public function execute(Comment $comment, array $data): Comment + { + $result = $this->repository->update($comment, $data); + + ModelChanged::dispatch(Comment::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Course/CreateCourseAction.php b/app/Actions/Course/CreateCourseAction.php new file mode 100644 index 0000000..3ec9648 --- /dev/null +++ b/app/Actions/Course/CreateCourseAction.php @@ -0,0 +1,23 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Course::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Course/DeleteCourseAction.php b/app/Actions/Course/DeleteCourseAction.php new file mode 100644 index 0000000..cd3b4ff --- /dev/null +++ b/app/Actions/Course/DeleteCourseAction.php @@ -0,0 +1,21 @@ +repository->delete($course); + + ModelChanged::dispatch(Course::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Course/UpdateCourseAction.php b/app/Actions/Course/UpdateCourseAction.php new file mode 100644 index 0000000..1b38e6e --- /dev/null +++ b/app/Actions/Course/UpdateCourseAction.php @@ -0,0 +1,23 @@ +repository->update($course, $data->toArray()); + + ModelChanged::dispatch(Course::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Faq/CreateFaqAction.php b/app/Actions/Faq/CreateFaqAction.php new file mode 100644 index 0000000..5a78aac --- /dev/null +++ b/app/Actions/Faq/CreateFaqAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Faq::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Faq/DeleteFaqAction.php b/app/Actions/Faq/DeleteFaqAction.php new file mode 100644 index 0000000..8233cda --- /dev/null +++ b/app/Actions/Faq/DeleteFaqAction.php @@ -0,0 +1,21 @@ +repository->delete($faq); + + ModelChanged::dispatch(Faq::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Faq/UpdateFaqAction.php b/app/Actions/Faq/UpdateFaqAction.php new file mode 100644 index 0000000..f2a5bd0 --- /dev/null +++ b/app/Actions/Faq/UpdateFaqAction.php @@ -0,0 +1,22 @@ +repository->update($faq, $data->toArray()); + + ModelChanged::dispatch(Faq::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/GuideCard/CreateGuideCardAction.php b/app/Actions/GuideCard/CreateGuideCardAction.php new file mode 100644 index 0000000..e1cab8c --- /dev/null +++ b/app/Actions/GuideCard/CreateGuideCardAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(GuideCard::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/GuideCard/DeleteGuideCardAction.php b/app/Actions/GuideCard/DeleteGuideCardAction.php new file mode 100644 index 0000000..8661901 --- /dev/null +++ b/app/Actions/GuideCard/DeleteGuideCardAction.php @@ -0,0 +1,21 @@ +repository->delete($guideCard); + + ModelChanged::dispatch(GuideCard::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/GuideCard/UpdateGuideCardAction.php b/app/Actions/GuideCard/UpdateGuideCardAction.php new file mode 100644 index 0000000..58c3121 --- /dev/null +++ b/app/Actions/GuideCard/UpdateGuideCardAction.php @@ -0,0 +1,22 @@ +repository->update($guideCard, $data->toArray()); + + ModelChanged::dispatch(GuideCard::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/HeroSlide/CreateHeroSlideAction.php b/app/Actions/HeroSlide/CreateHeroSlideAction.php new file mode 100644 index 0000000..1e23c2e --- /dev/null +++ b/app/Actions/HeroSlide/CreateHeroSlideAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(HeroSlide::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/HeroSlide/DeleteHeroSlideAction.php b/app/Actions/HeroSlide/DeleteHeroSlideAction.php new file mode 100644 index 0000000..7393a15 --- /dev/null +++ b/app/Actions/HeroSlide/DeleteHeroSlideAction.php @@ -0,0 +1,21 @@ +repository->delete($heroSlide); + + ModelChanged::dispatch(HeroSlide::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/HeroSlide/UpdateHeroSlideAction.php b/app/Actions/HeroSlide/UpdateHeroSlideAction.php new file mode 100644 index 0000000..1633476 --- /dev/null +++ b/app/Actions/HeroSlide/UpdateHeroSlideAction.php @@ -0,0 +1,22 @@ +repository->update($heroSlide, $data->toArray()); + + ModelChanged::dispatch(HeroSlide::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Lead/CreateLeadAction.php b/app/Actions/Lead/CreateLeadAction.php new file mode 100644 index 0000000..b6ab4c3 --- /dev/null +++ b/app/Actions/Lead/CreateLeadAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Lead::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Lead/DeleteLeadAction.php b/app/Actions/Lead/DeleteLeadAction.php new file mode 100644 index 0000000..b984f38 --- /dev/null +++ b/app/Actions/Lead/DeleteLeadAction.php @@ -0,0 +1,21 @@ +repository->delete($lead); + + ModelChanged::dispatch(Lead::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Lead/UpdateLeadAction.php b/app/Actions/Lead/UpdateLeadAction.php new file mode 100644 index 0000000..29df119 --- /dev/null +++ b/app/Actions/Lead/UpdateLeadAction.php @@ -0,0 +1,22 @@ +repository->update($lead, $data->toArray()); + + ModelChanged::dispatch(Lead::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Menu/CreateMenuAction.php b/app/Actions/Menu/CreateMenuAction.php new file mode 100644 index 0000000..7bb1d34 --- /dev/null +++ b/app/Actions/Menu/CreateMenuAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Menu::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Menu/DeleteMenuAction.php b/app/Actions/Menu/DeleteMenuAction.php new file mode 100644 index 0000000..a805ed6 --- /dev/null +++ b/app/Actions/Menu/DeleteMenuAction.php @@ -0,0 +1,21 @@ +repository->delete($menu); + + ModelChanged::dispatch(Menu::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Menu/UpdateMenuAction.php b/app/Actions/Menu/UpdateMenuAction.php new file mode 100644 index 0000000..5bc7bb4 --- /dev/null +++ b/app/Actions/Menu/UpdateMenuAction.php @@ -0,0 +1,22 @@ +repository->update($menu, $data->toArray()); + + ModelChanged::dispatch(Menu::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Page/CreatePageAction.php b/app/Actions/Page/CreatePageAction.php new file mode 100644 index 0000000..26ea410 --- /dev/null +++ b/app/Actions/Page/CreatePageAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(Page::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Page/DeletePageAction.php b/app/Actions/Page/DeletePageAction.php new file mode 100644 index 0000000..14d64a0 --- /dev/null +++ b/app/Actions/Page/DeletePageAction.php @@ -0,0 +1,21 @@ +repository->delete($page); + + ModelChanged::dispatch(Page::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Page/UpdatePageAction.php b/app/Actions/Page/UpdatePageAction.php new file mode 100644 index 0000000..943e7fd --- /dev/null +++ b/app/Actions/Page/UpdatePageAction.php @@ -0,0 +1,22 @@ +repository->update($page, $data->toArray()); + + ModelChanged::dispatch(Page::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Schedule/CreateScheduleAction.php b/app/Actions/Schedule/CreateScheduleAction.php new file mode 100644 index 0000000..9504a91 --- /dev/null +++ b/app/Actions/Schedule/CreateScheduleAction.php @@ -0,0 +1,22 @@ +repository->create($data->toArray()); + + ModelChanged::dispatch(CourseSchedule::class, 'created'); + + return $result; + } +} diff --git a/app/Actions/Schedule/DeleteScheduleAction.php b/app/Actions/Schedule/DeleteScheduleAction.php new file mode 100644 index 0000000..9cbdfe3 --- /dev/null +++ b/app/Actions/Schedule/DeleteScheduleAction.php @@ -0,0 +1,21 @@ +repository->delete($schedule); + + ModelChanged::dispatch(CourseSchedule::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/Schedule/UpdateScheduleAction.php b/app/Actions/Schedule/UpdateScheduleAction.php new file mode 100644 index 0000000..187a634 --- /dev/null +++ b/app/Actions/Schedule/UpdateScheduleAction.php @@ -0,0 +1,22 @@ +repository->update($schedule, $data->toArray()); + + ModelChanged::dispatch(CourseSchedule::class, 'updated'); + + return $result; + } +} diff --git a/app/Actions/Setting/UpdateSettingsAction.php b/app/Actions/Setting/UpdateSettingsAction.php new file mode 100644 index 0000000..06abf69 --- /dev/null +++ b/app/Actions/Setting/UpdateSettingsAction.php @@ -0,0 +1,22 @@ + $settings + */ + public function execute(array $settings): void + { + $this->repository->bulkUpdate($settings); + + ModelChanged::dispatch(Setting::class, 'updated'); + } +} diff --git a/app/Actions/User/CreateUserAction.php b/app/Actions/User/CreateUserAction.php new file mode 100644 index 0000000..d9b3322 --- /dev/null +++ b/app/Actions/User/CreateUserAction.php @@ -0,0 +1,29 @@ +repository->create($data->toArray()); + + if ($data->role) { + $user->syncRoles([$data->role]); + } + + $user->load('roles'); + + ModelChanged::dispatch(User::class, 'created'); + + return $user; + } +} diff --git a/app/Actions/User/DeleteUserAction.php b/app/Actions/User/DeleteUserAction.php new file mode 100644 index 0000000..a12b23d --- /dev/null +++ b/app/Actions/User/DeleteUserAction.php @@ -0,0 +1,21 @@ +repository->delete($user); + + ModelChanged::dispatch(User::class, 'deleted'); + + return true; + } +} diff --git a/app/Actions/User/UpdateUserAction.php b/app/Actions/User/UpdateUserAction.php new file mode 100644 index 0000000..776aa84 --- /dev/null +++ b/app/Actions/User/UpdateUserAction.php @@ -0,0 +1,29 @@ +repository->update($user, $data->toArray()); + + if ($data->role !== null) { + $user->syncRoles([$data->role]); + } + + $user->load('roles'); + + ModelChanged::dispatch(User::class, 'updated'); + + return $user; + } +} diff --git a/app/DTOs/AnnouncementData.php b/app/DTOs/AnnouncementData.php new file mode 100644 index 0000000..9fcde16 --- /dev/null +++ b/app/DTOs/AnnouncementData.php @@ -0,0 +1,57 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/CategoryData.php b/app/DTOs/CategoryData.php new file mode 100644 index 0000000..b9c7185 --- /dev/null +++ b/app/DTOs/CategoryData.php @@ -0,0 +1,45 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/CommentData.php b/app/DTOs/CommentData.php new file mode 100644 index 0000000..3357c83 --- /dev/null +++ b/app/DTOs/CommentData.php @@ -0,0 +1,48 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/CourseData.php b/app/DTOs/CourseData.php new file mode 100644 index 0000000..4c1104a --- /dev/null +++ b/app/DTOs/CourseData.php @@ -0,0 +1,92 @@ +|null $includes + * @param list|null $requirements + * @param list|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 $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/FaqData.php b/app/DTOs/FaqData.php new file mode 100644 index 0000000..d97ef16 --- /dev/null +++ b/app/DTOs/FaqData.php @@ -0,0 +1,44 @@ + $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 + */ + public function toArray(): array + { + return [ + 'question' => $this->question, + 'answer' => $this->answer, + 'category' => $this->category->value, + 'order_index' => $this->orderIndex, + 'is_active' => $this->isActive, + ]; + } +} diff --git a/app/DTOs/GuideCardData.php b/app/DTOs/GuideCardData.php new file mode 100644 index 0000000..f82b647 --- /dev/null +++ b/app/DTOs/GuideCardData.php @@ -0,0 +1,42 @@ + $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 + */ + public function toArray(): array + { + return [ + 'title' => $this->title, + 'description' => $this->description, + 'icon' => $this->icon, + 'order_index' => $this->orderIndex, + 'is_active' => $this->isActive, + ]; + } +} diff --git a/app/DTOs/HeroSlideData.php b/app/DTOs/HeroSlideData.php new file mode 100644 index 0000000..330af60 --- /dev/null +++ b/app/DTOs/HeroSlideData.php @@ -0,0 +1,63 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/LeadData.php b/app/DTOs/LeadData.php new file mode 100644 index 0000000..e05f060 --- /dev/null +++ b/app/DTOs/LeadData.php @@ -0,0 +1,69 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/MenuData.php b/app/DTOs/MenuData.php new file mode 100644 index 0000000..f4edc62 --- /dev/null +++ b/app/DTOs/MenuData.php @@ -0,0 +1,51 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/PageData.php b/app/DTOs/PageData.php new file mode 100644 index 0000000..db42f04 --- /dev/null +++ b/app/DTOs/PageData.php @@ -0,0 +1,45 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/ScheduleData.php b/app/DTOs/ScheduleData.php new file mode 100644 index 0000000..f4f2e22 --- /dev/null +++ b/app/DTOs/ScheduleData.php @@ -0,0 +1,48 @@ + $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 + */ + 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, + ]; + } +} diff --git a/app/DTOs/UserData.php b/app/DTOs/UserData.php new file mode 100644 index 0000000..937b533 --- /dev/null +++ b/app/DTOs/UserData.php @@ -0,0 +1,43 @@ + $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 + */ + public function toArray(): array + { + $result = [ + 'name' => $this->name, + 'email' => $this->email, + ]; + + if ($this->password !== null) { + $result['password'] = $this->password; + } + + return $result; + } +} diff --git a/app/Enums/AnnouncementCategory.php b/app/Enums/AnnouncementCategory.php new file mode 100644 index 0000000..5b7d7b0 --- /dev/null +++ b/app/Enums/AnnouncementCategory.php @@ -0,0 +1,10 @@ + 'Popüler', + self::MostPreferred => 'En Çok Tercih Edilen', + self::New => 'Yeni', + self::Recommended => 'Önerilen', + self::Limited => 'Sınırlı Kontenjan', + }; + } +} diff --git a/app/Enums/FaqCategory.php b/app/Enums/FaqCategory.php new file mode 100644 index 0000000..502e789 --- /dev/null +++ b/app/Enums/FaqCategory.php @@ -0,0 +1,15 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/AuthController.php b/app/Http/Controllers/Api/Admin/AuthController.php new file mode 100644 index 0000000..a6988b6 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/AuthController.php @@ -0,0 +1,118 @@ +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ı.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/BlockController.php b/app/Http/Controllers/Api/Admin/BlockController.php new file mode 100644 index 0000000..6ce1577 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/BlockController.php @@ -0,0 +1,172 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/CategoryController.php b/app/Http/Controllers/Api/Admin/CategoryController.php new file mode 100644 index 0000000..8fca236 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/CategoryController.php @@ -0,0 +1,147 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/CommentController.php b/app/Http/Controllers/Api/Admin/CommentController.php new file mode 100644 index 0000000..542d653 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/CommentController.php @@ -0,0 +1,94 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/CourseBlockController.php b/app/Http/Controllers/Api/Admin/CourseBlockController.php new file mode 100644 index 0000000..960fd17 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/CourseBlockController.php @@ -0,0 +1,172 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/CourseController.php b/app/Http/Controllers/Api/Admin/CourseController.php new file mode 100644 index 0000000..c434cb2 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/CourseController.php @@ -0,0 +1,163 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/FaqController.php b/app/Http/Controllers/Api/Admin/FaqController.php new file mode 100644 index 0000000..dd272e2 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/FaqController.php @@ -0,0 +1,128 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/GuideCardController.php b/app/Http/Controllers/Api/Admin/GuideCardController.php new file mode 100644 index 0000000..4320196 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/GuideCardController.php @@ -0,0 +1,127 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/HeroSlideController.php b/app/Http/Controllers/Api/Admin/HeroSlideController.php new file mode 100644 index 0000000..331ceb3 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/HeroSlideController.php @@ -0,0 +1,130 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/LeadController.php b/app/Http/Controllers/Api/Admin/LeadController.php new file mode 100644 index 0000000..255700d --- /dev/null +++ b/app/Http/Controllers/Api/Admin/LeadController.php @@ -0,0 +1,103 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/MenuController.php b/app/Http/Controllers/Api/Admin/MenuController.php new file mode 100644 index 0000000..dabeb34 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/MenuController.php @@ -0,0 +1,159 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/PageController.php b/app/Http/Controllers/Api/Admin/PageController.php new file mode 100644 index 0000000..0e8f923 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/PageController.php @@ -0,0 +1,179 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/PreviewController.php b/app/Http/Controllers/Api/Admin/PreviewController.php new file mode 100644 index 0000000..649c96e --- /dev/null +++ b/app/Http/Controllers/Api/Admin/PreviewController.php @@ -0,0 +1,92 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/RoleController.php b/app/Http/Controllers/Api/Admin/RoleController.php new file mode 100644 index 0000000..4517ebd --- /dev/null +++ b/app/Http/Controllers/Api/Admin/RoleController.php @@ -0,0 +1,180 @@ + []]], + 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, + ], + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/ScheduleController.php b/app/Http/Controllers/Api/Admin/ScheduleController.php new file mode 100644 index 0000000..3516bcd --- /dev/null +++ b/app/Http/Controllers/Api/Admin/ScheduleController.php @@ -0,0 +1,135 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/SettingController.php b/app/Http/Controllers/Api/Admin/SettingController.php new file mode 100644 index 0000000..a56a34d --- /dev/null +++ b/app/Http/Controllers/Api/Admin/SettingController.php @@ -0,0 +1,89 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/StoryController.php b/app/Http/Controllers/Api/Admin/StoryController.php new file mode 100644 index 0000000..3e85fbe --- /dev/null +++ b/app/Http/Controllers/Api/Admin/StoryController.php @@ -0,0 +1,138 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/Admin/UploadController.php b/app/Http/Controllers/Api/Admin/UploadController.php new file mode 100644 index 0000000..a5617ea --- /dev/null +++ b/app/Http/Controllers/Api/Admin/UploadController.php @@ -0,0 +1,138 @@ + []]], + 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 + */ + 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); + } +} diff --git a/app/Http/Controllers/Api/Admin/UserController.php b/app/Http/Controllers/Api/Admin/UserController.php new file mode 100644 index 0000000..b79b177 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/UserController.php @@ -0,0 +1,147 @@ + []]], + 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.']); + } +} diff --git a/app/Http/Controllers/Api/V1/AnnouncementController.php b/app/Http/Controllers/Api/V1/AnnouncementController.php new file mode 100644 index 0000000..75a6f71 --- /dev/null +++ b/app/Http/Controllers/Api/V1/AnnouncementController.php @@ -0,0 +1,62 @@ +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)); + } +} diff --git a/app/Http/Controllers/Api/V1/CategoryController.php b/app/Http/Controllers/Api/V1/CategoryController.php new file mode 100644 index 0000000..3ab3aac --- /dev/null +++ b/app/Http/Controllers/Api/V1/CategoryController.php @@ -0,0 +1,60 @@ +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); + } +} diff --git a/app/Http/Controllers/Api/V1/CommentController.php b/app/Http/Controllers/Api/V1/CommentController.php new file mode 100644 index 0000000..bf14be1 --- /dev/null +++ b/app/Http/Controllers/Api/V1/CommentController.php @@ -0,0 +1,92 @@ + + */ + 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); + } +} diff --git a/app/Http/Controllers/Api/V1/CourseController.php b/app/Http/Controllers/Api/V1/CourseController.php new file mode 100644 index 0000000..fe915db --- /dev/null +++ b/app/Http/Controllers/Api/V1/CourseController.php @@ -0,0 +1,62 @@ +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); + } +} diff --git a/app/Http/Controllers/Api/V1/FaqController.php b/app/Http/Controllers/Api/V1/FaqController.php new file mode 100644 index 0000000..c299a99 --- /dev/null +++ b/app/Http/Controllers/Api/V1/FaqController.php @@ -0,0 +1,55 @@ +integer('limit', 0); + + if ($category) { + $faqCategory = FaqCategory::tryFrom($category); + + if (! $faqCategory) { + abort(404, 'FAQ kategorisi bulunamadı.'); + } + + $faqs = $this->repository->getByCategory($faqCategory); + + if ($limit > 0) { + $faqs = $faqs->take($limit); + } + + return FaqResource::collection($faqs); + } + + $perPage = $limit > 0 ? $limit : 100; + + return FaqResource::collection($this->repository->paginate([], $perPage)); + } +} diff --git a/app/Http/Controllers/Api/V1/GuideCardController.php b/app/Http/Controllers/Api/V1/GuideCardController.php new file mode 100644 index 0000000..247f01e --- /dev/null +++ b/app/Http/Controllers/Api/V1/GuideCardController.php @@ -0,0 +1,25 @@ +repository->active()); + } +} diff --git a/app/Http/Controllers/Api/V1/HeroSlideController.php b/app/Http/Controllers/Api/V1/HeroSlideController.php new file mode 100644 index 0000000..84471be --- /dev/null +++ b/app/Http/Controllers/Api/V1/HeroSlideController.php @@ -0,0 +1,25 @@ +repository->active()); + } +} diff --git a/app/Http/Controllers/Api/V1/LeadController.php b/app/Http/Controllers/Api/V1/LeadController.php new file mode 100644 index 0000000..3fd96e1 --- /dev/null +++ b/app/Http/Controllers/Api/V1/LeadController.php @@ -0,0 +1,54 @@ +validated()); + $action->execute($dto); + + return response()->json([ + 'success' => true, + 'message' => 'Talebiniz alınmıştır. En kısa sürede sizinle iletişime geçeceğiz.', + ], 201); + } +} diff --git a/app/Http/Controllers/Api/V1/MenuController.php b/app/Http/Controllers/Api/V1/MenuController.php new file mode 100644 index 0000000..7de4a04 --- /dev/null +++ b/app/Http/Controllers/Api/V1/MenuController.php @@ -0,0 +1,36 @@ +repository->getByLocation($menuLocation)); + } +} diff --git a/app/Http/Controllers/Api/V1/PageController.php b/app/Http/Controllers/Api/V1/PageController.php new file mode 100644 index 0000000..41ce606 --- /dev/null +++ b/app/Http/Controllers/Api/V1/PageController.php @@ -0,0 +1,35 @@ +repository->findBySlug($slug); + + if (! $page || ! $page->is_active) { + return response()->json(['message' => 'Sayfa bulunamadı.'], 404); + } + + return response()->json(new PageResource($page)); + } +} diff --git a/app/Http/Controllers/Api/V1/PreviewController.php b/app/Http/Controllers/Api/V1/PreviewController.php new file mode 100644 index 0000000..14f4cb1 --- /dev/null +++ b/app/Http/Controllers/Api/V1/PreviewController.php @@ -0,0 +1,32 @@ +json(['message' => 'Önizleme bulunamadı veya süresi dolmuş.'], 404); + } + + return response()->json(['data' => $data]); + } +} diff --git a/app/Http/Controllers/Api/V1/ScheduleController.php b/app/Http/Controllers/Api/V1/ScheduleController.php new file mode 100644 index 0000000..4cbdaaa --- /dev/null +++ b/app/Http/Controllers/Api/V1/ScheduleController.php @@ -0,0 +1,72 @@ +repository->paginate( + $request->only(['course_id']), + $request->integer('per_page', 15), + ); + + return CourseScheduleResource::collection($schedules); + } + + #[OA\Get( + path: '/api/v1/schedules/upcoming', + summary: 'Yaklaşan eğitimleri listele', + tags: ['Schedules'], + responses: [new OA\Response(response: 200, description: 'Yaklaşan eğitimler')], + )] + public function upcoming(): AnonymousResourceCollection + { + $schedules = $this->repository->upcoming(20); + + return CourseScheduleResource::collection($schedules); + } + + #[OA\Get( + path: '/api/v1/schedules/{id}', + summary: 'Takvim detayı', + tags: ['Schedules'], + parameters: [new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], + responses: [ + new OA\Response(response: 200, description: 'Takvim detayı'), + new OA\Response(response: 404, description: 'Takvim bulunamadı'), + ], + )] + public function show(int $id): JsonResponse + { + $schedule = $this->repository->findById($id); + + if (! $schedule) { + return response()->json(['message' => 'Takvim bulunamadı.'], 404); + } + + $schedule->load('course.category'); + + return response()->json(new CourseScheduleResource($schedule)); + } +} diff --git a/app/Http/Controllers/Api/V1/SettingController.php b/app/Http/Controllers/Api/V1/SettingController.php new file mode 100644 index 0000000..d11b540 --- /dev/null +++ b/app/Http/Controllers/Api/V1/SettingController.php @@ -0,0 +1,52 @@ +json($this->repository->publicGrouped()); + } + + /** + * Return public settings for a single group. + */ + #[OA\Get( + path: '/api/v1/settings/{group}', + summary: 'Tek grup ayarlarını getir', + tags: ['Settings'], + 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 show(string $group): JsonResponse + { + $settingGroup = SettingGroup::tryFrom($group); + + if (! $settingGroup) { + abort(404, 'Ayar grubu bulunamadı.'); + } + + return response()->json($this->repository->publicByGroup($settingGroup)); + } +} diff --git a/app/Http/Controllers/Api/V1/SitemapController.php b/app/Http/Controllers/Api/V1/SitemapController.php new file mode 100644 index 0000000..172ebbc --- /dev/null +++ b/app/Http/Controllers/Api/V1/SitemapController.php @@ -0,0 +1,54 @@ +select('slug', 'updated_at') + ->get() + ->map(fn (Course $c) => [ + 'loc' => '/courses/'.$c->slug, + 'lastmod' => $c->updated_at?->toISOString(), + ]); + + $announcements = Announcement::query() + ->select('slug', 'updated_at') + ->get() + ->map(fn (Announcement $a) => [ + 'loc' => '/announcements/'.$a->slug, + 'lastmod' => $a->updated_at?->toISOString(), + ]); + + $pages = Page::query() + ->where('is_active', true) + ->select('slug', 'updated_at') + ->get() + ->map(fn (Page $p) => [ + 'loc' => '/'.$p->slug, + 'lastmod' => $p->updated_at?->toISOString(), + ]); + + return response()->json([ + 'courses' => $courses, + 'announcements' => $announcements, + 'pages' => $pages, + ]); + } +} diff --git a/app/Http/Controllers/Api/V1/StoryController.php b/app/Http/Controllers/Api/V1/StoryController.php new file mode 100644 index 0000000..a6d64c8 --- /dev/null +++ b/app/Http/Controllers/Api/V1/StoryController.php @@ -0,0 +1,28 @@ +where('is_active', true) + ->orderBy('order_index') + ->get(); + + return StoryResource::collection($stories); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +> + */ + public function rules(): array + { + return [ + 'slug' => ['required', 'string', 'max:255', 'unique:announcements,slug'], + 'title' => ['required', 'string', 'max:255'], + 'category' => ['required', 'string', Rule::enum(AnnouncementCategory::class)], + 'excerpt' => ['nullable', 'string', 'max:500'], + 'content' => ['required', 'string'], + 'image' => ['nullable', 'string', 'max:255'], + 'is_featured' => ['sometimes', 'boolean'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string', 'max:255'], + 'published_at' => ['nullable', 'date'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'slug.required' => 'URL slug alanı zorunludur.', + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + 'title.required' => 'Başlık zorunludur.', + 'category.required' => 'Kategori zorunludur.', + 'content.required' => 'İçerik zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Announcement/UpdateAnnouncementRequest.php b/app/Http/Requests/Announcement/UpdateAnnouncementRequest.php new file mode 100644 index 0000000..243cb1a --- /dev/null +++ b/app/Http/Requests/Announcement/UpdateAnnouncementRequest.php @@ -0,0 +1,44 @@ +> + */ + public function rules(): array + { + return [ + 'slug' => ['sometimes', 'string', 'max:255', Rule::unique('announcements', 'slug')->ignore($this->route('announcement'))], + 'title' => ['sometimes', 'string', 'max:255'], + 'category' => ['sometimes', 'string', Rule::enum(AnnouncementCategory::class)], + 'excerpt' => ['nullable', 'string', 'max:500'], + 'content' => ['sometimes', 'string'], + 'image' => ['nullable', 'string', 'max:255'], + 'is_featured' => ['sometimes', 'boolean'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string', 'max:255'], + 'published_at' => ['nullable', 'date'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + ]; + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..f05a263 --- /dev/null +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,36 @@ +> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'email.required' => 'E-posta adresi zorunludur.', + 'email.email' => 'Geçerli bir e-posta adresi giriniz.', + 'password.required' => 'Şifre zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Category/StoreCategoryRequest.php b/app/Http/Requests/Category/StoreCategoryRequest.php new file mode 100644 index 0000000..631b3a7 --- /dev/null +++ b/app/Http/Requests/Category/StoreCategoryRequest.php @@ -0,0 +1,40 @@ +> + */ + public function rules(): array + { + return [ + 'slug' => ['required', 'string', 'max:255', 'unique:categories,slug'], + 'label' => ['required', 'string', 'max:255'], + 'desc' => ['nullable', 'string'], + 'image' => ['nullable', 'string', 'max:255'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'slug.required' => 'Slug alanı zorunludur.', + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + 'label.required' => 'Kategori adı zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Category/UpdateCategoryRequest.php b/app/Http/Requests/Category/UpdateCategoryRequest.php new file mode 100644 index 0000000..ef61bab --- /dev/null +++ b/app/Http/Requests/Category/UpdateCategoryRequest.php @@ -0,0 +1,41 @@ +> + */ + public function rules(): array + { + return [ + 'slug' => ['required', 'string', 'max:255', Rule::unique('categories', 'slug')->ignore($this->route('category'))], + 'label' => ['required', 'string', 'max:255'], + 'desc' => ['nullable', 'string'], + 'image' => ['nullable', 'string', 'max:255'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'slug.required' => 'Slug alanı zorunludur.', + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + 'label.required' => 'Kategori adı zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Comment/StoreCommentRequest.php b/app/Http/Requests/Comment/StoreCommentRequest.php new file mode 100644 index 0000000..1ec502b --- /dev/null +++ b/app/Http/Requests/Comment/StoreCommentRequest.php @@ -0,0 +1,44 @@ +> + */ + public function rules(): array + { + return [ + 'commentable_type' => ['required', 'string', 'in:course,category,announcement'], + 'commentable_id' => ['required', 'integer'], + 'author_name' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string', 'max:1000'], + 'rating' => ['nullable', 'integer', 'min:1', 'max:5'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'commentable_type.required' => 'Yorum tipi zorunludur.', + 'commentable_type.in' => 'Geçersiz yorum tipi.', + 'commentable_id.required' => 'Yorum hedefi zorunludur.', + 'author_name.required' => 'İsim zorunludur.', + 'content.required' => 'Yorum içeriği zorunludur.', + 'content.max' => 'Yorum en fazla 1000 karakter olabilir.', + 'rating.min' => 'Puan en az 1 olabilir.', + 'rating.max' => 'Puan en fazla 5 olabilir.', + ]; + } +} diff --git a/app/Http/Requests/Comment/UpdateCommentRequest.php b/app/Http/Requests/Comment/UpdateCommentRequest.php new file mode 100644 index 0000000..7162607 --- /dev/null +++ b/app/Http/Requests/Comment/UpdateCommentRequest.php @@ -0,0 +1,34 @@ +> + */ + public function rules(): array + { + return [ + 'is_approved' => ['sometimes', 'boolean'], + 'admin_reply' => ['nullable', 'string', 'max:1000'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'admin_reply.max' => 'Admin yanıtı en fazla 1000 karakter olabilir.', + ]; + } +} diff --git a/app/Http/Requests/Course/StoreCourseRequest.php b/app/Http/Requests/Course/StoreCourseRequest.php new file mode 100644 index 0000000..85580cf --- /dev/null +++ b/app/Http/Requests/Course/StoreCourseRequest.php @@ -0,0 +1,64 @@ +> + */ + public function rules(): array + { + return [ + 'category_id' => ['required', 'integer', 'exists:categories,id'], + 'slug' => ['required', 'string', 'max:255', 'unique:courses,slug'], + 'title' => ['required', 'string', 'max:255'], + 'sub' => ['nullable', 'string', 'max:255'], + 'desc' => ['required', 'string'], + 'long_desc' => ['required', 'string'], + 'duration' => ['required', 'string', 'max:255'], + 'students' => ['nullable', 'integer', 'min:0'], + 'rating' => ['nullable', 'numeric', 'min:0', 'max:5'], + 'badge' => ['nullable', 'string', Rule::enum(CourseBadge::class)], + 'image' => ['nullable', 'string', 'max:255'], + 'price' => ['nullable', 'string', 'max:255'], + 'includes' => ['nullable', 'array'], + 'includes.*' => ['string'], + 'requirements' => ['nullable', 'array'], + 'requirements.*' => ['string'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string'], + 'scope' => ['nullable', 'array'], + 'scope.*' => ['string'], + 'standard' => ['nullable', 'string', 'max:255'], + 'language' => ['nullable', 'string', 'max:255'], + 'location' => ['nullable', 'string', 'max:255'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'category_id.required' => 'Kategori seçimi zorunludur.', + 'category_id.exists' => 'Seçilen kategori bulunamadı.', + 'slug.required' => 'Slug alanı zorunludur.', + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + 'title.required' => 'Eğitim başlığı zorunludur.', + 'desc.required' => 'Kısa açıklama zorunludur.', + 'long_desc.required' => 'Detaylı açıklama zorunludur.', + 'duration.required' => 'Süre alanı zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Course/UpdateCourseRequest.php b/app/Http/Requests/Course/UpdateCourseRequest.php new file mode 100644 index 0000000..b5b6aa3 --- /dev/null +++ b/app/Http/Requests/Course/UpdateCourseRequest.php @@ -0,0 +1,61 @@ +> + */ + public function rules(): array + { + return [ + 'category_id' => ['required', 'integer', 'exists:categories,id'], + 'slug' => ['required', 'string', 'max:255', Rule::unique('courses', 'slug')->ignore($this->route('course'))], + 'title' => ['required', 'string', 'max:255'], + 'sub' => ['nullable', 'string', 'max:255'], + 'desc' => ['required', 'string'], + 'long_desc' => ['required', 'string'], + 'duration' => ['required', 'string', 'max:255'], + 'students' => ['nullable', 'integer', 'min:0'], + 'rating' => ['nullable', 'numeric', 'min:0', 'max:5'], + 'badge' => ['nullable', 'string', Rule::enum(CourseBadge::class)], + 'image' => ['nullable', 'string', 'max:255'], + 'price' => ['nullable', 'string', 'max:255'], + 'includes' => ['nullable', 'array'], + 'includes.*' => ['string'], + 'requirements' => ['nullable', 'array'], + 'requirements.*' => ['string'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string'], + 'scope' => ['nullable', 'array'], + 'scope.*' => ['string'], + 'standard' => ['nullable', 'string', 'max:255'], + 'language' => ['nullable', 'string', 'max:255'], + 'location' => ['nullable', 'string', 'max:255'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'category_id.required' => 'Kategori seçimi zorunludur.', + 'category_id.exists' => 'Seçilen kategori bulunamadı.', + 'slug.required' => 'Slug alanı zorunludur.', + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + 'title.required' => 'Eğitim başlığı zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Faq/StoreFaqRequest.php b/app/Http/Requests/Faq/StoreFaqRequest.php new file mode 100644 index 0000000..fbd2306 --- /dev/null +++ b/app/Http/Requests/Faq/StoreFaqRequest.php @@ -0,0 +1,41 @@ +> + */ + public function rules(): array + { + return [ + 'question' => ['required', 'string', 'max:500'], + 'answer' => ['required', 'string'], + 'category' => ['required', 'string', Rule::enum(FaqCategory::class)], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'question.required' => 'Soru zorunludur.', + 'answer.required' => 'Cevap zorunludur.', + 'category.required' => 'Kategori zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Faq/UpdateFaqRequest.php b/app/Http/Requests/Faq/UpdateFaqRequest.php new file mode 100644 index 0000000..f53b2ec --- /dev/null +++ b/app/Http/Requests/Faq/UpdateFaqRequest.php @@ -0,0 +1,29 @@ +> + */ + public function rules(): array + { + return [ + 'question' => ['sometimes', 'string', 'max:500'], + 'answer' => ['sometimes', 'string'], + 'category' => ['sometimes', 'string', Rule::enum(FaqCategory::class)], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/GuideCard/StoreGuideCardRequest.php b/app/Http/Requests/GuideCard/StoreGuideCardRequest.php new file mode 100644 index 0000000..676ae59 --- /dev/null +++ b/app/Http/Requests/GuideCard/StoreGuideCardRequest.php @@ -0,0 +1,38 @@ +> + */ + public function rules(): array + { + return [ + 'title' => ['required', 'string', 'max:255'], + 'description' => ['required', 'string', 'max:500'], + 'icon' => ['nullable', 'string', 'max:100'], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'title.required' => 'Başlık zorunludur.', + 'description.required' => 'Açıklama zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/GuideCard/UpdateGuideCardRequest.php b/app/Http/Requests/GuideCard/UpdateGuideCardRequest.php new file mode 100644 index 0000000..d3151ed --- /dev/null +++ b/app/Http/Requests/GuideCard/UpdateGuideCardRequest.php @@ -0,0 +1,27 @@ +> + */ + public function rules(): array + { + return [ + 'title' => ['sometimes', 'string', 'max:255'], + 'description' => ['sometimes', 'string', 'max:500'], + 'icon' => ['nullable', 'string', 'max:100'], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/HeroSlide/StoreHeroSlideRequest.php b/app/Http/Requests/HeroSlide/StoreHeroSlideRequest.php new file mode 100644 index 0000000..9a9f143 --- /dev/null +++ b/app/Http/Requests/HeroSlide/StoreHeroSlideRequest.php @@ -0,0 +1,44 @@ +> + */ + public function rules(): array + { + return [ + 'label' => ['nullable', 'string', 'max:255'], + 'title' => ['required', 'string', 'max:255'], + 'description' => ['nullable', 'string'], + 'media_type' => ['sometimes', 'string', 'in:image,video'], + 'image' => ['nullable', 'string', 'max:255'], + 'video_url' => ['nullable', 'string', 'max:255'], + 'mobile_video_url' => ['nullable', 'string', 'max:255'], + 'mobile_image' => ['nullable', 'string', 'max:255'], + 'button_text' => ['nullable', 'string', 'max:100'], + 'button_url' => ['nullable', 'string', 'max:255'], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'title.required' => 'Başlık zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/HeroSlide/UpdateHeroSlideRequest.php b/app/Http/Requests/HeroSlide/UpdateHeroSlideRequest.php new file mode 100644 index 0000000..e68247d --- /dev/null +++ b/app/Http/Requests/HeroSlide/UpdateHeroSlideRequest.php @@ -0,0 +1,34 @@ +> + */ + public function rules(): array + { + return [ + 'label' => ['nullable', 'string', 'max:255'], + 'title' => ['sometimes', 'string', 'max:255'], + 'description' => ['nullable', 'string'], + 'media_type' => ['sometimes', 'string', 'in:image,video'], + 'image' => ['nullable', 'string', 'max:255'], + 'video_url' => ['nullable', 'string', 'max:255'], + 'mobile_video_url' => ['nullable', 'string', 'max:255'], + 'mobile_image' => ['nullable', 'string', 'max:255'], + 'button_text' => ['nullable', 'string', 'max:100'], + 'button_url' => ['nullable', 'string', 'max:255'], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/Lead/StoreLeadRequest.php b/app/Http/Requests/Lead/StoreLeadRequest.php new file mode 100644 index 0000000..1fc545d --- /dev/null +++ b/app/Http/Requests/Lead/StoreLeadRequest.php @@ -0,0 +1,51 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'phone' => ['required', 'string', 'max:20'], + 'email' => ['nullable', 'email', 'max:255'], + 'source' => ['required', 'string', Rule::enum(LeadSource::class)], + 'target_course' => ['nullable', 'string', 'max:255'], + 'education_level' => ['nullable', 'string', 'max:255'], + 'subject' => ['nullable', 'string', 'max:255'], + 'message' => ['nullable', 'string'], + 'kvkk_consent' => ['required', 'accepted'], + 'marketing_consent' => ['sometimes', 'boolean'], + 'utm_source' => ['nullable', 'string', 'max:255'], + 'utm_medium' => ['nullable', 'string', 'max:255'], + 'utm_campaign' => ['nullable', 'string', 'max:255'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Ad Soyad zorunludur.', + 'phone.required' => 'Telefon numarası zorunludur.', + 'source.required' => 'Kaynak bilgisi zorunludur.', + 'kvkk_consent.required' => 'KVKK onayı zorunludur.', + 'kvkk_consent.accepted' => 'KVKK metnini onaylamanız gerekmektedir.', + ]; + } +} diff --git a/app/Http/Requests/Lead/UpdateLeadRequest.php b/app/Http/Requests/Lead/UpdateLeadRequest.php new file mode 100644 index 0000000..5b2b594 --- /dev/null +++ b/app/Http/Requests/Lead/UpdateLeadRequest.php @@ -0,0 +1,37 @@ +> + */ + public function rules(): array + { + return [ + 'status' => ['sometimes', 'string', Rule::enum(LeadStatus::class)], + 'is_read' => ['sometimes', 'boolean'], + 'admin_note' => ['nullable', 'string'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'status.in' => 'Geçersiz durum değeri.', + ]; + } +} diff --git a/app/Http/Requests/Menu/ReorderMenuRequest.php b/app/Http/Requests/Menu/ReorderMenuRequest.php new file mode 100644 index 0000000..4ea1d4c --- /dev/null +++ b/app/Http/Requests/Menu/ReorderMenuRequest.php @@ -0,0 +1,39 @@ +> + */ + public function rules(): array + { + return [ + 'items' => ['required', 'array', 'min:1'], + 'items.*.id' => ['required', 'integer', 'exists:menus,id'], + 'items.*.order_index' => ['required', 'integer', 'min:0'], + 'items.*.parent_id' => ['nullable', 'integer', 'exists:menus,id'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'items.required' => 'Sıralama verileri zorunludur.', + 'items.*.id.required' => 'Menü ID zorunludur.', + 'items.*.id.exists' => 'Menü bulunamadı.', + 'items.*.order_index.required' => 'Sıra numarası zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Menu/StoreMenuRequest.php b/app/Http/Requests/Menu/StoreMenuRequest.php new file mode 100644 index 0000000..0a00c90 --- /dev/null +++ b/app/Http/Requests/Menu/StoreMenuRequest.php @@ -0,0 +1,46 @@ +> + */ + public function rules(): array + { + return [ + 'label' => ['required', 'string', 'max:255'], + 'url' => ['required', 'string', 'max:255'], + 'location' => ['required', 'string', Rule::enum(MenuLocation::class)], + 'type' => ['required', 'string', Rule::enum(MenuType::class)], + 'parent_id' => ['nullable', 'integer', 'exists:menus,id'], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'label.required' => 'Menü etiketi zorunludur.', + 'url.required' => 'URL zorunludur.', + 'location.required' => 'Menü konumu zorunludur.', + 'type.required' => 'Menü tipi zorunludur.', + 'parent_id.exists' => 'Üst menü bulunamadı.', + ]; + } +} diff --git a/app/Http/Requests/Menu/UpdateMenuRequest.php b/app/Http/Requests/Menu/UpdateMenuRequest.php new file mode 100644 index 0000000..3e933c1 --- /dev/null +++ b/app/Http/Requests/Menu/UpdateMenuRequest.php @@ -0,0 +1,42 @@ +> + */ + public function rules(): array + { + return [ + 'label' => ['sometimes', 'string', 'max:255'], + 'url' => ['sometimes', 'string', 'max:255'], + 'location' => ['sometimes', 'string', Rule::enum(MenuLocation::class)], + 'type' => ['sometimes', 'string', Rule::enum(MenuType::class)], + 'parent_id' => ['nullable', 'integer', 'exists:menus,id'], + 'order_index' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'parent_id.exists' => 'Üst menü bulunamadı.', + ]; + } +} diff --git a/app/Http/Requests/Page/StorePageRequest.php b/app/Http/Requests/Page/StorePageRequest.php new file mode 100644 index 0000000..5b03196 --- /dev/null +++ b/app/Http/Requests/Page/StorePageRequest.php @@ -0,0 +1,44 @@ +> + */ + public function rules(): array + { + return [ + 'slug' => ['required', 'string', 'max:255', 'unique:pages,slug'], + 'title' => ['required', 'string', 'max:255'], + 'content' => ['nullable', 'string'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string', 'max:255'], + 'is_active' => ['sometimes', 'boolean'], + 'blocks' => ['sometimes', 'array'], + 'blocks.*.type' => ['required_with:blocks', 'string', 'max:50'], + 'blocks.*.content' => ['required_with:blocks', 'array'], + 'blocks.*.order_index' => ['sometimes', 'integer', 'min:0'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'slug.required' => 'URL slug alanı zorunludur.', + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + 'title.required' => 'Başlık zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Page/UpdatePageRequest.php b/app/Http/Requests/Page/UpdatePageRequest.php new file mode 100644 index 0000000..cab2a75 --- /dev/null +++ b/app/Http/Requests/Page/UpdatePageRequest.php @@ -0,0 +1,43 @@ +> + */ + public function rules(): array + { + return [ + 'slug' => ['sometimes', 'string', 'max:255', Rule::unique('pages', 'slug')->ignore($this->route('page'))], + 'title' => ['sometimes', 'string', 'max:255'], + 'content' => ['nullable', 'string'], + 'meta_title' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['nullable', 'string', 'max:255'], + 'is_active' => ['sometimes', 'boolean'], + 'blocks' => ['sometimes', 'array'], + 'blocks.*.type' => ['required_with:blocks', 'string', 'max:50'], + 'blocks.*.content' => ['required_with:blocks', 'array'], + 'blocks.*.order_index' => ['sometimes', 'integer', 'min:0'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'slug.unique' => 'Bu slug zaten kullanılıyor.', + ]; + } +} diff --git a/app/Http/Requests/Preview/StorePreviewRequest.php b/app/Http/Requests/Preview/StorePreviewRequest.php new file mode 100644 index 0000000..30b4868 --- /dev/null +++ b/app/Http/Requests/Preview/StorePreviewRequest.php @@ -0,0 +1,41 @@ +> + */ + public function rules(): array + { + return [ + 'page_id' => ['required', 'integer', 'exists:pages,id'], + 'blocks' => ['present', 'array'], + 'blocks.*.type' => ['required', 'string', 'max:50'], + 'blocks.*.content' => ['present', 'array'], + 'blocks.*.order_index' => ['required', 'integer', 'min:0'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'page_id.required' => 'Sayfa ID zorunludur.', + 'page_id.exists' => 'Geçersiz sayfa.', + 'blocks.present' => 'Bloklar alanı gereklidir.', + 'blocks.*.type.required' => 'Blok tipi zorunludur.', + 'blocks.*.order_index.required' => 'Blok sırası zorunludur.', + ]; + } +} diff --git a/app/Http/Requests/Role/StoreRoleRequest.php b/app/Http/Requests/Role/StoreRoleRequest.php new file mode 100644 index 0000000..81064d7 --- /dev/null +++ b/app/Http/Requests/Role/StoreRoleRequest.php @@ -0,0 +1,39 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255', 'unique:roles,name'], + 'permissions' => ['required', 'array', 'min:1'], + 'permissions.*' => ['string', 'exists:permissions,name'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Rol adı zorunludur.', + 'name.unique' => 'Bu rol adı zaten kullanılıyor.', + 'permissions.required' => 'En az bir yetki seçilmelidir.', + 'permissions.min' => 'En az bir yetki seçilmelidir.', + 'permissions.*.exists' => 'Geçersiz yetki.', + ]; + } +} diff --git a/app/Http/Requests/Role/UpdateRoleRequest.php b/app/Http/Requests/Role/UpdateRoleRequest.php new file mode 100644 index 0000000..290b10e --- /dev/null +++ b/app/Http/Requests/Role/UpdateRoleRequest.php @@ -0,0 +1,39 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('roles', 'name')->ignore($this->route('role'))], + 'permissions' => ['sometimes', 'required', 'array', 'min:1'], + 'permissions.*' => ['string', 'exists:permissions,name'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Rol adı zorunludur.', + 'name.unique' => 'Bu rol adı zaten kullanılıyor.', + 'permissions.min' => 'En az bir yetki seçilmelidir.', + 'permissions.*.exists' => 'Geçersiz yetki.', + ]; + } +} diff --git a/app/Http/Requests/Schedule/StoreScheduleRequest.php b/app/Http/Requests/Schedule/StoreScheduleRequest.php new file mode 100644 index 0000000..b527d92 --- /dev/null +++ b/app/Http/Requests/Schedule/StoreScheduleRequest.php @@ -0,0 +1,49 @@ +> + */ + public function rules(): array + { + return [ + 'course_id' => ['required', 'integer', 'exists:courses,id'], + 'start_date' => ['required', 'date', 'after_or_equal:today'], + 'end_date' => ['required', 'date', 'after:start_date'], + 'location' => ['required', 'string', 'max:255'], + 'quota' => ['required', 'integer', 'min:1'], + 'available_seats' => ['required', 'integer', 'min:0', 'lte:quota'], + 'is_urgent' => ['sometimes', 'boolean'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'course_id.required' => 'Eğitim seçimi zorunludur.', + 'course_id.exists' => 'Seçilen eğitim bulunamadı.', + 'start_date.required' => 'Başlangıç tarihi zorunludur.', + 'start_date.after_or_equal' => 'Başlangıç tarihi bugün veya sonrası olmalıdır.', + 'end_date.required' => 'Bitiş tarihi zorunludur.', + 'end_date.after' => 'Bitiş tarihi başlangıçtan sonra olmalıdır.', + 'location.required' => 'Konum zorunludur.', + 'quota.required' => 'Kontenjan zorunludur.', + 'quota.min' => 'Kontenjan en az 1 olmalıdır.', + 'available_seats.required' => 'Müsait koltuk sayısı zorunludur.', + 'available_seats.lte' => 'Müsait koltuk sayısı kontenjandan fazla olamaz.', + ]; + } +} diff --git a/app/Http/Requests/Schedule/UpdateScheduleRequest.php b/app/Http/Requests/Schedule/UpdateScheduleRequest.php new file mode 100644 index 0000000..60666d1 --- /dev/null +++ b/app/Http/Requests/Schedule/UpdateScheduleRequest.php @@ -0,0 +1,42 @@ +> + */ + public function rules(): array + { + return [ + 'course_id' => ['sometimes', 'integer', 'exists:courses,id'], + 'start_date' => ['sometimes', 'date'], + 'end_date' => ['sometimes', 'date', 'after:start_date'], + 'location' => ['sometimes', 'string', 'max:255'], + 'quota' => ['sometimes', 'integer', 'min:1'], + 'available_seats' => ['sometimes', 'integer', 'min:0', 'lte:quota'], + 'is_urgent' => ['sometimes', 'boolean'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'course_id.exists' => 'Seçilen eğitim bulunamadı.', + 'end_date.after' => 'Bitiş tarihi başlangıçtan sonra olmalıdır.', + 'quota.min' => 'Kontenjan en az 1 olmalıdır.', + 'available_seats.lte' => 'Müsait koltuk sayısı kontenjandan fazla olamaz.', + ]; + } +} diff --git a/app/Http/Requests/Setting/UpdateSettingsRequest.php b/app/Http/Requests/Setting/UpdateSettingsRequest.php new file mode 100644 index 0000000..40f333e --- /dev/null +++ b/app/Http/Requests/Setting/UpdateSettingsRequest.php @@ -0,0 +1,35 @@ +> + */ + public function rules(): array + { + return [ + 'settings' => ['required', 'array'], + 'settings.*' => ['nullable', 'string'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'settings.required' => 'Ayarlar zorunludur.', + 'settings.array' => 'Ayarlar bir dizi olmalıdır.', + ]; + } +} diff --git a/app/Http/Requests/User/StoreUserRequest.php b/app/Http/Requests/User/StoreUserRequest.php new file mode 100644 index 0000000..3633b5c --- /dev/null +++ b/app/Http/Requests/User/StoreUserRequest.php @@ -0,0 +1,43 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + 'role' => ['required', 'string', 'exists:roles,name'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'İsim alanı zorunludur.', + 'email.required' => 'E-posta alanı zorunludur.', + 'email.unique' => 'Bu e-posta adresi zaten kullanılıyor.', + 'password.required' => 'Şifre alanı zorunludur.', + 'password.min' => 'Şifre en az 8 karakter olmalıdır.', + 'password.confirmed' => 'Şifre tekrarı eşleşmiyor.', + 'role.required' => 'Rol alanı zorunludur.', + 'role.exists' => 'Geçersiz rol.', + ]; + } +} diff --git a/app/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php new file mode 100644 index 0000000..99fa1b6 --- /dev/null +++ b/app/Http/Requests/User/UpdateUserRequest.php @@ -0,0 +1,42 @@ +> + */ + public function rules(): array + { + return [ + 'name' => ['sometimes', 'required', 'string', 'max:255'], + 'email' => ['sometimes', 'required', 'string', 'email', 'max:255', Rule::unique('users', 'email')->ignore($this->route('user'))], + 'password' => ['nullable', 'string', 'min:8', 'confirmed'], + 'role' => ['sometimes', 'required', 'string', 'exists:roles,name'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'İsim alanı zorunludur.', + 'email.required' => 'E-posta alanı zorunludur.', + 'email.unique' => 'Bu e-posta adresi zaten kullanılıyor.', + 'password.min' => 'Şifre en az 8 karakter olmalıdır.', + 'password.confirmed' => 'Şifre tekrarı eşleşmiyor.', + 'role.exists' => 'Geçersiz rol.', + ]; + } +} diff --git a/app/Http/Resources/AnnouncementResource.php b/app/Http/Resources/AnnouncementResource.php new file mode 100644 index 0000000..b02c806 --- /dev/null +++ b/app/Http/Resources/AnnouncementResource.php @@ -0,0 +1,36 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'slug' => $this->slug, + 'title' => $this->title, + 'category' => $this->category?->value, + 'excerpt' => $this->excerpt, + 'content' => $this->content, + 'image' => $this->image, + 'is_featured' => $this->is_featured, + 'meta_title' => $this->meta_title, + 'meta_description' => $this->meta_description, + 'published_at' => $this->published_at?->toISOString(), + 'comments' => CommentResource::collection($this->whenLoaded('comments')), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/CategoryResource.php b/app/Http/Resources/CategoryResource.php new file mode 100644 index 0000000..ba4f290 --- /dev/null +++ b/app/Http/Resources/CategoryResource.php @@ -0,0 +1,38 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'slug' => $this->slug, + 'label' => $this->label, + 'desc' => $this->desc, + 'image' => $this->image, + 'meta_title' => $this->meta_title, + 'meta_description' => $this->meta_description, + 'courses_count' => $this->whenCounted('courses'), + // Mega menu kartında gösterilecek kurslar (menu_order 1-3) + 'menu_courses' => $this->whenLoaded('menuCourses', fn () => $this->menuCourses->map(fn ($c) => [ + 'title' => $c->title, + 'slug' => $c->slug, + ]) + ), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/CommentResource.php b/app/Http/Resources/CommentResource.php new file mode 100644 index 0000000..18739b7 --- /dev/null +++ b/app/Http/Resources/CommentResource.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'commentable_type' => $this->commentable_type, + 'commentable_id' => $this->commentable_id, + 'author_name' => $this->author_name, + 'content' => $this->content, + 'rating' => $this->rating, + 'is_approved' => $this->is_approved, + 'admin_reply' => $this->admin_reply, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/CourseBlockResource.php b/app/Http/Resources/CourseBlockResource.php new file mode 100644 index 0000000..65363c3 --- /dev/null +++ b/app/Http/Resources/CourseBlockResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'type' => $this->type, + 'content' => $this->content, + 'order_index' => $this->order_index, + ]; + } +} diff --git a/app/Http/Resources/CourseResource.php b/app/Http/Resources/CourseResource.php new file mode 100644 index 0000000..e449947 --- /dev/null +++ b/app/Http/Resources/CourseResource.php @@ -0,0 +1,49 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'category_id' => $this->category_id, + 'category' => new CategoryResource($this->whenLoaded('category')), + 'slug' => $this->slug, + 'title' => $this->title, + 'sub' => $this->sub, + 'desc' => $this->desc, + 'long_desc' => $this->long_desc, + 'duration' => $this->duration, + 'students' => $this->students, + 'rating' => $this->rating, + 'badge' => $this->badge?->value, + 'badge_label' => $this->badge?->label(), + 'image' => $this->image, + 'price' => $this->price, + 'includes' => $this->includes, + 'requirements' => $this->requirements, + 'meta_title' => $this->meta_title, + 'meta_description' => $this->meta_description, + 'scope' => $this->scope, + 'standard' => $this->standard, + 'language' => $this->language, + 'location' => $this->location, + 'blocks' => CourseBlockResource::collection($this->whenLoaded('blocks')), + 'schedules' => CourseScheduleResource::collection($this->whenLoaded('schedules')), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/CourseScheduleResource.php b/app/Http/Resources/CourseScheduleResource.php new file mode 100644 index 0000000..1098810 --- /dev/null +++ b/app/Http/Resources/CourseScheduleResource.php @@ -0,0 +1,38 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'course_id' => $this->course_id, + 'course' => new CourseResource($this->whenLoaded('course')), + 'start_date' => $this->start_date?->toDateString(), + 'end_date' => $this->end_date?->toDateString(), + 'location' => $this->location, + 'instructor' => $this->instructor, + 'quota' => $this->quota, + 'available_seats' => $this->available_seats, + 'enrolled_count' => $this->enrolled_count, + 'price_override' => $this->price_override, + 'status' => $this->status, + 'is_urgent' => $this->is_urgent, + 'notes' => $this->notes, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/FaqResource.php b/app/Http/Resources/FaqResource.php new file mode 100644 index 0000000..e3541e4 --- /dev/null +++ b/app/Http/Resources/FaqResource.php @@ -0,0 +1,30 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'question' => $this->question, + 'answer' => $this->answer, + 'category' => $this->category?->value, + 'order_index' => $this->order_index, + 'is_active' => $this->is_active, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/GuideCardResource.php b/app/Http/Resources/GuideCardResource.php new file mode 100644 index 0000000..88fea83 --- /dev/null +++ b/app/Http/Resources/GuideCardResource.php @@ -0,0 +1,30 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'icon' => $this->icon, + 'order_index' => $this->order_index, + 'is_active' => $this->is_active, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/HeroSlideResource.php b/app/Http/Resources/HeroSlideResource.php new file mode 100644 index 0000000..6853189 --- /dev/null +++ b/app/Http/Resources/HeroSlideResource.php @@ -0,0 +1,37 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'label' => $this->label, + 'title' => $this->title, + 'description' => $this->description, + 'media_type' => $this->media_type ?? 'image', + 'image' => $this->image, + 'video_url' => $this->video_url, + 'mobile_video_url' => $this->mobile_video_url, + 'mobile_image' => $this->mobile_image, + 'button_text' => $this->button_text, + 'button_url' => $this->button_url, + 'order_index' => $this->order_index, + 'is_active' => $this->is_active, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/LeadResource.php b/app/Http/Resources/LeadResource.php new file mode 100644 index 0000000..efe616f --- /dev/null +++ b/app/Http/Resources/LeadResource.php @@ -0,0 +1,43 @@ + + */ + public function toArray(Request $request): array + { + $utm = $this->utm; + + return [ + 'id' => $this->id, + 'name' => $this->name, + 'phone' => $this->phone, + 'email' => $this->email, + 'source' => $this->source?->value, + 'status' => $this->status?->value, + 'target_course' => $this->target_course, + 'education_level' => $this->education_level, + 'subject' => $this->subject, + 'message' => $this->message, + 'utm_source' => $utm['utm_source'] ?? null, + 'utm_medium' => $utm['utm_medium'] ?? null, + 'utm_campaign' => $utm['utm_campaign'] ?? null, + 'kvkk_consent' => $this->consent_kvkk, + 'marketing_consent' => $this->marketing_consent, + 'is_read' => $this->is_read, + 'admin_note' => $this->notes, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/MenuResource.php b/app/Http/Resources/MenuResource.php new file mode 100644 index 0000000..bd833d7 --- /dev/null +++ b/app/Http/Resources/MenuResource.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'label' => $this->label, + 'url' => $this->url, + 'location' => $this->location?->value, + 'type' => $this->type?->value, + 'parent_id' => $this->parent_id, + 'order_index' => $this->order_index, + 'is_active' => $this->is_active, + 'children' => self::collection($this->whenLoaded('children')), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/PageBlockResource.php b/app/Http/Resources/PageBlockResource.php new file mode 100644 index 0000000..c31c540 --- /dev/null +++ b/app/Http/Resources/PageBlockResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'type' => $this->type, + 'content' => $this->content, + 'order_index' => $this->order_index, + ]; + } +} diff --git a/app/Http/Resources/PageResource.php b/app/Http/Resources/PageResource.php new file mode 100644 index 0000000..5000367 --- /dev/null +++ b/app/Http/Resources/PageResource.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'slug' => $this->slug, + 'title' => $this->title, + 'content' => $this->content, + 'meta_title' => $this->meta_title, + 'meta_description' => $this->meta_description, + 'is_active' => $this->is_active, + 'blocks' => PageBlockResource::collection($this->whenLoaded('blocks')), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/RoleResource.php b/app/Http/Resources/RoleResource.php new file mode 100644 index 0000000..08bd976 --- /dev/null +++ b/app/Http/Resources/RoleResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'guard_name' => $this->guard_name, + 'permissions' => $this->whenLoaded('permissions', fn () => $this->permissions->pluck('name')), + 'users_count' => $this->whenCounted('users'), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/SettingResource.php b/app/Http/Resources/SettingResource.php new file mode 100644 index 0000000..7aaf3c3 --- /dev/null +++ b/app/Http/Resources/SettingResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'key' => $this->key, + 'value' => $this->value, + 'group' => $this->group?->value, + 'type' => $this->type?->value, + 'label' => $this->label, + 'order_index' => $this->order_index, + ]; + } +} diff --git a/app/Http/Resources/StoryResource.php b/app/Http/Resources/StoryResource.php new file mode 100644 index 0000000..18d097c --- /dev/null +++ b/app/Http/Resources/StoryResource.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'badge' => $this->badge, + 'content' => $this->content, + 'image' => $this->image, + 'cta_text' => $this->cta_text, + 'cta_url' => $this->cta_url, + 'order_index' => $this->order_index, + 'is_active' => $this->is_active, + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..0d6e1c9 --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'roles' => $this->whenLoaded('roles', fn () => $this->getRoleNames()), + 'permissions' => $this->whenLoaded('roles', fn () => $this->getAllPermissions()->pluck('name')), + 'email_verified_at' => $this->email_verified_at?->toISOString(), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + 'deleted_at' => $this->deleted_at?->toISOString(), + ]; + } +} diff --git a/app/Listeners/ClearModelCache.php b/app/Listeners/ClearModelCache.php new file mode 100644 index 0000000..8497577 --- /dev/null +++ b/app/Listeners/ClearModelCache.php @@ -0,0 +1,66 @@ +> + */ + private const CACHE_KEYS = [ + Category::class => ['categories'], + Course::class => ['courses', 'categories'], + CourseSchedule::class => ['schedules', 'courses'], + Announcement::class => ['announcements'], + HeroSlide::class => ['hero_slides'], + Menu::class => ['menus'], + Comment::class => ['comments'], + Faq::class => ['faqs'], + GuideCard::class => ['guide_cards'], + Setting::class => ['settings'], + Page::class => ['pages'], + ]; + + /** + * Handle the event. + */ + public function handle(ModelChanged $event): void + { + $keys = self::CACHE_KEYS[$event->modelClass] ?? []; + + foreach ($keys as $prefix) { + Cache::forget($prefix); + + // Clear sub-keyed caches (e.g., menus.header_main, faqs.egitimler) + if ($prefix === 'menus') { + foreach (MenuLocation::cases() as $location) { + Cache::forget("menus.{$location->value}"); + } + } + + if ($prefix === 'faqs') { + foreach (FaqCategory::cases() as $category) { + Cache::forget("faqs.{$category->value}"); + } + } + } + } +} diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php new file mode 100644 index 0000000..fad7c6f --- /dev/null +++ b/app/Models/Announcement.php @@ -0,0 +1,60 @@ + */ + use Concerns\HasTurkishSlug, HasFactory, LogsActivity; + + /** + * @var list + */ + protected $fillable = [ + 'slug', + 'title', + 'category', + 'excerpt', + 'content', + 'image', + 'is_featured', + 'meta_title', + 'meta_description', + 'published_at', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'category' => AnnouncementCategory::class, + 'is_featured' => 'boolean', + 'published_at' => 'datetime', + ]; + } + + /** + * @return MorphMany + */ + public function comments(): MorphMany + { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->logOnlyDirty(); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..b549cfc --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,70 @@ + */ + use Concerns\HasTurkishSlug, HasFactory, LogsActivity, SoftDeletes; + + protected function slugSourceField(): string + { + return 'label'; + } + + /** + * @var list + */ + protected $fillable = [ + 'slug', + 'label', + 'desc', + 'image', + 'meta_title', + 'meta_description', + ]; + + /** + * @return HasMany + */ + public function courses(): HasMany + { + return $this->hasMany(Course::class); + } + + /** + * Mega menu'de gösterilecek kurslar (menu_order = 1, 2, 3). + * + * @return HasMany + */ + public function menuCourses(): HasMany + { + return $this->hasMany(Course::class) + ->whereNotNull('menu_order') + ->orderBy('menu_order'); + } + + /** + * @return MorphMany + */ + public function comments(): MorphMany + { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->logOnlyDirty(); + } +} diff --git a/app/Models/Comment.php b/app/Models/Comment.php new file mode 100644 index 0000000..3b72a0e --- /dev/null +++ b/app/Models/Comment.php @@ -0,0 +1,46 @@ + */ + use HasFactory, SoftDeletes; + + /** + * @var list + */ + protected $fillable = [ + 'commentable_id', + 'commentable_type', + 'name_surname', + 'phone', + 'body', + 'admin_reply', + 'is_approved', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'is_approved' => 'boolean', + ]; + } + + /** + * @return MorphTo + */ + public function commentable(): MorphTo + { + return $this->morphTo(); + } +} diff --git a/app/Models/Concerns/HasTurkishSlug.php b/app/Models/Concerns/HasTurkishSlug.php new file mode 100644 index 0000000..57ad323 --- /dev/null +++ b/app/Models/Concerns/HasTurkishSlug.php @@ -0,0 +1,24 @@ +slug) && $model->{$model->slugSourceField()}) { + $model->slug = Str::turkishSlug($model->{$model->slugSourceField()}); + } else { + $model->slug = Str::turkishSlug($model->slug); + } + }); + } +} diff --git a/app/Models/Course.php b/app/Models/Course.php new file mode 100644 index 0000000..7464aea --- /dev/null +++ b/app/Models/Course.php @@ -0,0 +1,101 @@ + */ + use Concerns\HasTurkishSlug, HasFactory, LogsActivity, SoftDeletes; + + /** + * @var list + */ + protected $fillable = [ + 'category_id', + 'slug', + 'title', + 'sub', + 'desc', + 'long_desc', + 'duration', + 'students', + 'rating', + 'badge', + 'menu_order', + 'image', + 'price', + 'includes', + 'requirements', + 'meta_title', + 'meta_description', + 'scope', + 'standard', + 'language', + 'location', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'badge' => CourseBadge::class, + 'includes' => 'array', + 'requirements' => 'array', + 'scope' => 'array', + 'students' => 'integer', + 'rating' => 'decimal:1', + ]; + } + + /** + * @return BelongsTo + */ + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } + + /** + * @return HasMany + */ + public function schedules(): HasMany + { + return $this->hasMany(CourseSchedule::class); + } + + /** + * @return HasMany + */ + public function blocks(): HasMany + { + return $this->hasMany(CourseBlock::class)->orderBy('order_index'); + } + + /** + * @return MorphMany + */ + public function comments(): MorphMany + { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->logOnlyDirty(); + } +} diff --git a/app/Models/CourseBlock.php b/app/Models/CourseBlock.php new file mode 100644 index 0000000..65d0faf --- /dev/null +++ b/app/Models/CourseBlock.php @@ -0,0 +1,45 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'course_id', + 'type', + 'content', + 'order_index', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'content' => 'array', + 'order_index' => 'integer', + 'is_active' => 'boolean', + ]; + } + + /** + * @return BelongsTo + */ + public function course(): BelongsTo + { + return $this->belongsTo(Course::class); + } +} diff --git a/app/Models/CourseSchedule.php b/app/Models/CourseSchedule.php new file mode 100644 index 0000000..2b0c95d --- /dev/null +++ b/app/Models/CourseSchedule.php @@ -0,0 +1,56 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'course_id', + 'start_date', + 'end_date', + 'location', + 'instructor', + 'quota', + 'available_seats', + 'enrolled_count', + 'price_override', + 'status', + 'is_urgent', + 'notes', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'start_date' => 'date', + 'end_date' => 'date', + 'quota' => 'integer', + 'available_seats' => 'integer', + 'enrolled_count' => 'integer', + 'price_override' => 'decimal:2', + 'is_urgent' => 'boolean', + ]; + } + + /** + * @return BelongsTo + */ + public function course(): BelongsTo + { + return $this->belongsTo(Course::class); + } +} diff --git a/app/Models/Faq.php b/app/Models/Faq.php new file mode 100644 index 0000000..699cdd0 --- /dev/null +++ b/app/Models/Faq.php @@ -0,0 +1,37 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'category', + 'question', + 'answer', + 'order_index', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'category' => FaqCategory::class, + 'order_index' => 'integer', + 'is_active' => 'boolean', + ]; + } +} diff --git a/app/Models/GuideCard.php b/app/Models/GuideCard.php new file mode 100644 index 0000000..d90a498 --- /dev/null +++ b/app/Models/GuideCard.php @@ -0,0 +1,37 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'title', + 'description', + 'icon', + 'color', + 'link', + 'order', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'order' => 'integer', + 'is_active' => 'boolean', + ]; + } +} diff --git a/app/Models/HeroSlide.php b/app/Models/HeroSlide.php new file mode 100644 index 0000000..977dfc3 --- /dev/null +++ b/app/Models/HeroSlide.php @@ -0,0 +1,42 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'label', + 'title', + 'description', + 'media_type', + 'image', + 'video_url', + 'mobile_video_url', + 'mobile_image', + 'button_text', + 'button_url', + 'order_index', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'order_index' => 'integer', + 'is_active' => 'boolean', + ]; + } +} diff --git a/app/Models/Lead.php b/app/Models/Lead.php new file mode 100644 index 0000000..23398c5 --- /dev/null +++ b/app/Models/Lead.php @@ -0,0 +1,61 @@ + */ + use HasFactory, LogsActivity, SoftDeletes; + + /** + * @var list + */ + protected $fillable = [ + 'name', + 'phone', + 'email', + 'target_course', + 'education_level', + 'subject', + 'message', + 'status', + 'notes', + 'is_read', + 'source', + 'utm', + 'consent_kvkk', + 'marketing_consent', + 'consent_text_version', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'status' => LeadStatus::class, + 'source' => LeadSource::class, + 'is_read' => 'boolean', + 'utm' => 'array', + 'consent_kvkk' => 'boolean', + 'marketing_consent' => 'boolean', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->logOnlyDirty(); + } +} diff --git a/app/Models/Menu.php b/app/Models/Menu.php new file mode 100644 index 0000000..840dfcc --- /dev/null +++ b/app/Models/Menu.php @@ -0,0 +1,59 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'location', + 'label', + 'url', + 'type', + 'parent_id', + 'order', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'location' => MenuLocation::class, + 'type' => MenuType::class, + 'order' => 'integer', + 'is_active' => 'boolean', + ]; + } + + /** + * @return BelongsTo + */ + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id'); + } + + /** + * @return HasMany + */ + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id')->orderBy('order'); + } +} diff --git a/app/Models/Page.php b/app/Models/Page.php new file mode 100644 index 0000000..8a8e1a6 --- /dev/null +++ b/app/Models/Page.php @@ -0,0 +1,43 @@ + */ + use Concerns\HasTurkishSlug, HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'slug', + 'title', + 'meta_title', + 'meta_description', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'is_active' => 'boolean', + ]; + } + + /** + * @return HasMany + */ + public function blocks(): HasMany + { + return $this->hasMany(PageBlock::class)->orderBy('order_index'); + } +} diff --git a/app/Models/PageBlock.php b/app/Models/PageBlock.php new file mode 100644 index 0000000..53dfa74 --- /dev/null +++ b/app/Models/PageBlock.php @@ -0,0 +1,45 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'page_id', + 'type', + 'content', + 'order_index', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'content' => 'array', + 'order_index' => 'integer', + 'is_active' => 'boolean', + ]; + } + + /** + * @return BelongsTo + */ + public function page(): BelongsTo + { + return $this->belongsTo(Page::class); + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..864e046 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,54 @@ + + */ + public const SENSITIVE_KEY_PATTERNS = ['_key', '_secret', '_password']; + + /** + * @var list + */ + protected $fillable = [ + 'group', + 'key', + 'value', + 'type', + 'label', + 'order_index', + 'is_public', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'group' => SettingGroup::class, + 'type' => SettingType::class, + 'is_public' => 'boolean', + 'order_index' => 'integer', + ]; + } + + public function isSensitive(): bool + { + foreach (self::SENSITIVE_KEY_PATTERNS as $pattern) { + if (str_contains($this->key, $pattern)) { + return true; + } + } + + return false; + } +} diff --git a/app/Models/Story.php b/app/Models/Story.php new file mode 100644 index 0000000..ac8af08 --- /dev/null +++ b/app/Models/Story.php @@ -0,0 +1,38 @@ + */ + use HasFactory; + + /** + * @var list + */ + protected $fillable = [ + 'title', + 'badge', + 'content', + 'image', + 'cta_text', + 'cta_url', + 'order_index', + 'is_active', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'order_index' => 'integer', + 'is_active' => 'boolean', + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..c0a2e21 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,54 @@ + */ + use HasApiTokens, HasFactory, HasRoles, LogsActivity, Notifiable, SoftDeletes; + + /** + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->logOnlyDirty(); + } +} diff --git a/app/Policies/AnnouncementPolicy.php b/app/Policies/AnnouncementPolicy.php new file mode 100644 index 0000000..bde1ae5 --- /dev/null +++ b/app/Policies/AnnouncementPolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-announcement'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Announcement $announcement): bool + { + return $user->hasPermissionTo('view-announcement'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-announcement'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Announcement $announcement): bool + { + return $user->hasPermissionTo('update-announcement'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Announcement $announcement): bool + { + return $user->hasPermissionTo('delete-announcement'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Announcement $announcement): bool + { + return $user->hasPermissionTo('delete-announcement'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Announcement $announcement): bool + { + return $user->hasPermissionTo('delete-announcement'); + } +} diff --git a/app/Policies/CategoryPolicy.php b/app/Policies/CategoryPolicy.php new file mode 100644 index 0000000..6df6173 --- /dev/null +++ b/app/Policies/CategoryPolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-category'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Category $category): bool + { + return $user->hasPermissionTo('view-category'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-category'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Category $category): bool + { + return $user->hasPermissionTo('update-category'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Category $category): bool + { + return $user->hasPermissionTo('delete-category'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Category $category): bool + { + return $user->hasPermissionTo('delete-category'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Category $category): bool + { + return $user->hasPermissionTo('delete-category'); + } +} diff --git a/app/Policies/CommentPolicy.php b/app/Policies/CommentPolicy.php new file mode 100644 index 0000000..9024679 --- /dev/null +++ b/app/Policies/CommentPolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-comment'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Comment $comment): bool + { + return $user->hasPermissionTo('view-comment'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-comment'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Comment $comment): bool + { + return $user->hasPermissionTo('update-comment'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Comment $comment): bool + { + return $user->hasPermissionTo('delete-comment'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Comment $comment): bool + { + return $user->hasPermissionTo('delete-comment'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Comment $comment): bool + { + return $user->hasPermissionTo('delete-comment'); + } +} diff --git a/app/Policies/CoursePolicy.php b/app/Policies/CoursePolicy.php new file mode 100644 index 0000000..f115f6d --- /dev/null +++ b/app/Policies/CoursePolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-course'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Course $course): bool + { + return $user->hasPermissionTo('view-course'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-course'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Course $course): bool + { + return $user->hasPermissionTo('update-course'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Course $course): bool + { + return $user->hasPermissionTo('delete-course'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Course $course): bool + { + return $user->hasPermissionTo('delete-course'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Course $course): bool + { + return $user->hasPermissionTo('delete-course'); + } +} diff --git a/app/Policies/CourseSchedulePolicy.php b/app/Policies/CourseSchedulePolicy.php new file mode 100644 index 0000000..283fb63 --- /dev/null +++ b/app/Policies/CourseSchedulePolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-schedule'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, CourseSchedule $courseSchedule): bool + { + return $user->hasPermissionTo('view-schedule'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-schedule'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, CourseSchedule $courseSchedule): bool + { + return $user->hasPermissionTo('update-schedule'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, CourseSchedule $courseSchedule): bool + { + return $user->hasPermissionTo('delete-schedule'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, CourseSchedule $courseSchedule): bool + { + return $user->hasPermissionTo('delete-schedule'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, CourseSchedule $courseSchedule): bool + { + return $user->hasPermissionTo('delete-schedule'); + } +} diff --git a/app/Policies/FaqPolicy.php b/app/Policies/FaqPolicy.php new file mode 100644 index 0000000..23c03a1 --- /dev/null +++ b/app/Policies/FaqPolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-faq'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Faq $faq): bool + { + return $user->hasPermissionTo('view-faq'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-faq'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Faq $faq): bool + { + return $user->hasPermissionTo('update-faq'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Faq $faq): bool + { + return $user->hasPermissionTo('delete-faq'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Faq $faq): bool + { + return $user->hasPermissionTo('delete-faq'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Faq $faq): bool + { + return $user->hasPermissionTo('delete-faq'); + } +} diff --git a/app/Policies/GuideCardPolicy.php b/app/Policies/GuideCardPolicy.php new file mode 100644 index 0000000..2d1190e --- /dev/null +++ b/app/Policies/GuideCardPolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-guide-card'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, GuideCard $guideCard): bool + { + return $user->hasPermissionTo('view-guide-card'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-guide-card'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, GuideCard $guideCard): bool + { + return $user->hasPermissionTo('update-guide-card'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, GuideCard $guideCard): bool + { + return $user->hasPermissionTo('delete-guide-card'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, GuideCard $guideCard): bool + { + return $user->hasPermissionTo('delete-guide-card'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, GuideCard $guideCard): bool + { + return $user->hasPermissionTo('delete-guide-card'); + } +} diff --git a/app/Policies/HeroSlidePolicy.php b/app/Policies/HeroSlidePolicy.php new file mode 100644 index 0000000..bd7027a --- /dev/null +++ b/app/Policies/HeroSlidePolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-hero-slide'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, HeroSlide $heroSlide): bool + { + return $user->hasPermissionTo('view-hero-slide'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-hero-slide'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, HeroSlide $heroSlide): bool + { + return $user->hasPermissionTo('update-hero-slide'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, HeroSlide $heroSlide): bool + { + return $user->hasPermissionTo('delete-hero-slide'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, HeroSlide $heroSlide): bool + { + return $user->hasPermissionTo('delete-hero-slide'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, HeroSlide $heroSlide): bool + { + return $user->hasPermissionTo('delete-hero-slide'); + } +} diff --git a/app/Policies/LeadPolicy.php b/app/Policies/LeadPolicy.php new file mode 100644 index 0000000..ad53ee4 --- /dev/null +++ b/app/Policies/LeadPolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-lead'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Lead $lead): bool + { + return $user->hasPermissionTo('view-lead'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-lead'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Lead $lead): bool + { + return $user->hasPermissionTo('update-lead'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Lead $lead): bool + { + return $user->hasPermissionTo('delete-lead'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Lead $lead): bool + { + return $user->hasPermissionTo('delete-lead'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Lead $lead): bool + { + return $user->hasPermissionTo('delete-lead'); + } +} diff --git a/app/Policies/MenuPolicy.php b/app/Policies/MenuPolicy.php new file mode 100644 index 0000000..d12579e --- /dev/null +++ b/app/Policies/MenuPolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-menu'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Menu $menu): bool + { + return $user->hasPermissionTo('view-menu'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-menu'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Menu $menu): bool + { + return $user->hasPermissionTo('update-menu'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Menu $menu): bool + { + return $user->hasPermissionTo('delete-menu'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Menu $menu): bool + { + return $user->hasPermissionTo('delete-menu'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Menu $menu): bool + { + return $user->hasPermissionTo('delete-menu'); + } +} diff --git a/app/Policies/PagePolicy.php b/app/Policies/PagePolicy.php new file mode 100644 index 0000000..70bada9 --- /dev/null +++ b/app/Policies/PagePolicy.php @@ -0,0 +1,65 @@ +hasPermissionTo('view-page'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Page $page): bool + { + return $user->hasPermissionTo('view-page'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-page'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Page $page): bool + { + return $user->hasPermissionTo('update-page'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Page $page): bool + { + return $user->hasPermissionTo('delete-page'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Page $page): bool + { + return $user->hasPermissionTo('delete-page'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Page $page): bool + { + return $user->hasPermissionTo('delete-page'); + } +} diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000..2aeef26 --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,69 @@ +hasPermissionTo('view-user'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, User $model): bool + { + return $user->hasPermissionTo('view-user'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasPermissionTo('create-user'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, User $model): bool + { + return $user->hasPermissionTo('update-user'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, User $model): bool + { + // Kendini silemez + if ($user->id === $model->id) { + return false; + } + + return $user->hasPermissionTo('delete-user'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, User $model): bool + { + return $user->hasPermissionTo('delete-user'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, User $model): bool + { + return $user->hasPermissionTo('delete-user'); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..1ec5462 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,100 @@ + + */ + public array $bindings = [ + CategoryRepositoryInterface::class => CategoryRepository::class, + CourseRepositoryInterface::class => CourseRepository::class, + ScheduleRepositoryInterface::class => ScheduleRepository::class, + AnnouncementRepositoryInterface::class => AnnouncementRepository::class, + HeroSlideRepositoryInterface::class => HeroSlideRepository::class, + LeadRepositoryInterface::class => LeadRepository::class, + MenuRepositoryInterface::class => MenuRepository::class, + CommentRepositoryInterface::class => CommentRepository::class, + FaqRepositoryInterface::class => FaqRepository::class, + GuideCardRepositoryInterface::class => GuideCardRepository::class, + SettingRepositoryInterface::class => SettingRepository::class, + PageRepositoryInterface::class => PageRepository::class, + UserRepositoryInterface::class => UserRepository::class, + ]; + + /** + * Register any application services. + */ + public function register(): void + { + // + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + Str::macro('turkishSlug', function (string $text, string $separator = '-'): string { + $map = [ + 'ç' => 'c', + 'ğ' => 'g', + 'ı' => 'i', + 'ö' => 'o', + 'ş' => 's', + 'ü' => 'u', + 'Ç' => 'c', + 'Ğ' => 'g', + 'İ' => 'i', + 'Ö' => 'o', + 'Ş' => 's', + 'Ü' => 'u', + ]; + + $text = strtr($text, $map); + + return Str::slug($text, $separator); + }); + + RateLimiter::for('leads', function (Request $request) { + return Limit::perMinute(3)->by($request->ip()); + }); + + RateLimiter::for('comments', function (Request $request) { + return Limit::perMinute(3)->by($request->ip()); + }); + } +} diff --git a/app/Repositories/Contracts/AnnouncementRepositoryInterface.php b/app/Repositories/Contracts/AnnouncementRepositoryInterface.php new file mode 100644 index 0000000..0c98f93 --- /dev/null +++ b/app/Repositories/Contracts/AnnouncementRepositoryInterface.php @@ -0,0 +1,20 @@ + + */ +interface AnnouncementRepositoryInterface extends BaseRepositoryInterface +{ + public function findBySlug(string $slug): ?Announcement; + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; +} diff --git a/app/Repositories/Contracts/BaseRepositoryInterface.php b/app/Repositories/Contracts/BaseRepositoryInterface.php new file mode 100644 index 0000000..d303cae --- /dev/null +++ b/app/Repositories/Contracts/BaseRepositoryInterface.php @@ -0,0 +1,37 @@ + $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + /** + * @return T|null + */ + public function findById(int $id): ?Model; + + /** + * @param array $data + * @return T + */ + public function create(array $data): Model; + + /** + * @param array $data + * @return T + */ + public function update(Model $model, array $data): Model; + + public function delete(Model $model): bool; +} diff --git a/app/Repositories/Contracts/CategoryRepositoryInterface.php b/app/Repositories/Contracts/CategoryRepositoryInterface.php new file mode 100644 index 0000000..744aa70 --- /dev/null +++ b/app/Repositories/Contracts/CategoryRepositoryInterface.php @@ -0,0 +1,20 @@ + + */ +interface CategoryRepositoryInterface extends BaseRepositoryInterface +{ + public function findBySlug(string $slug): ?Category; + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; +} diff --git a/app/Repositories/Contracts/CommentRepositoryInterface.php b/app/Repositories/Contracts/CommentRepositoryInterface.php new file mode 100644 index 0000000..8e43ebc --- /dev/null +++ b/app/Repositories/Contracts/CommentRepositoryInterface.php @@ -0,0 +1,24 @@ + + */ +interface CommentRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + /** + * @return Collection + */ + public function getApprovedByCommentable(string $commentableType, int $commentableId): Collection; +} diff --git a/app/Repositories/Contracts/CourseRepositoryInterface.php b/app/Repositories/Contracts/CourseRepositoryInterface.php new file mode 100644 index 0000000..8026ec8 --- /dev/null +++ b/app/Repositories/Contracts/CourseRepositoryInterface.php @@ -0,0 +1,20 @@ + + */ +interface CourseRepositoryInterface extends BaseRepositoryInterface +{ + public function findBySlug(string $slug): ?Course; + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; +} diff --git a/app/Repositories/Contracts/FaqRepositoryInterface.php b/app/Repositories/Contracts/FaqRepositoryInterface.php new file mode 100644 index 0000000..9e8ff84 --- /dev/null +++ b/app/Repositories/Contracts/FaqRepositoryInterface.php @@ -0,0 +1,25 @@ + + */ +interface FaqRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + /** + * @return Collection + */ + public function getByCategory(FaqCategory $category): Collection; +} diff --git a/app/Repositories/Contracts/GuideCardRepositoryInterface.php b/app/Repositories/Contracts/GuideCardRepositoryInterface.php new file mode 100644 index 0000000..a085133 --- /dev/null +++ b/app/Repositories/Contracts/GuideCardRepositoryInterface.php @@ -0,0 +1,24 @@ + + */ +interface GuideCardRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + /** + * @return Collection + */ + public function active(): Collection; +} diff --git a/app/Repositories/Contracts/HeroSlideRepositoryInterface.php b/app/Repositories/Contracts/HeroSlideRepositoryInterface.php new file mode 100644 index 0000000..6112b08 --- /dev/null +++ b/app/Repositories/Contracts/HeroSlideRepositoryInterface.php @@ -0,0 +1,24 @@ + + */ +interface HeroSlideRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + /** + * @return Collection + */ + public function active(): Collection; +} diff --git a/app/Repositories/Contracts/LeadRepositoryInterface.php b/app/Repositories/Contracts/LeadRepositoryInterface.php new file mode 100644 index 0000000..e16f450 --- /dev/null +++ b/app/Repositories/Contracts/LeadRepositoryInterface.php @@ -0,0 +1,18 @@ + + */ +interface LeadRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; +} diff --git a/app/Repositories/Contracts/MenuRepositoryInterface.php b/app/Repositories/Contracts/MenuRepositoryInterface.php new file mode 100644 index 0000000..4507692 --- /dev/null +++ b/app/Repositories/Contracts/MenuRepositoryInterface.php @@ -0,0 +1,30 @@ + + */ +interface MenuRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + /** + * @return Collection + */ + public function getByLocation(MenuLocation $location): Collection; + + /** + * @param array $items + */ + public function reorder(array $items): void; +} diff --git a/app/Repositories/Contracts/PageRepositoryInterface.php b/app/Repositories/Contracts/PageRepositoryInterface.php new file mode 100644 index 0000000..1efb9ad --- /dev/null +++ b/app/Repositories/Contracts/PageRepositoryInterface.php @@ -0,0 +1,20 @@ + + */ +interface PageRepositoryInterface extends BaseRepositoryInterface +{ + public function findBySlug(string $slug): ?Page; + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; +} diff --git a/app/Repositories/Contracts/ScheduleRepositoryInterface.php b/app/Repositories/Contracts/ScheduleRepositoryInterface.php new file mode 100644 index 0000000..4d9563b --- /dev/null +++ b/app/Repositories/Contracts/ScheduleRepositoryInterface.php @@ -0,0 +1,24 @@ + + */ +interface ScheduleRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + /** + * @return Collection + */ + public function upcoming(int $limit = 20): Collection; +} diff --git a/app/Repositories/Contracts/SettingRepositoryInterface.php b/app/Repositories/Contracts/SettingRepositoryInterface.php new file mode 100644 index 0000000..cc8f2f3 --- /dev/null +++ b/app/Repositories/Contracts/SettingRepositoryInterface.php @@ -0,0 +1,51 @@ + + */ +interface SettingRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @return Collection + */ + public function all(): Collection; + + /** + * Get all public settings grouped by group name (nested key-value format). + * + * @return array> + */ + public function publicGrouped(): array; + + /** + * Get public settings for a single group (key-value format). + * + * @return array + */ + public function publicByGroup(SettingGroup $group): array; + + public function findByKey(string $key): ?Setting; + + /** + * @return Collection + */ + public function getByGroup(SettingGroup $group): Collection; + + /** + * Bulk update settings using dot notation keys (e.g. "general.site_name"). + * + * @param array $settings + */ + public function bulkUpdate(array $settings): void; + + /** + * Clear all setting caches. + */ + public function clearCache(): void; +} diff --git a/app/Repositories/Contracts/UserRepositoryInterface.php b/app/Repositories/Contracts/UserRepositoryInterface.php new file mode 100644 index 0000000..bb15983 --- /dev/null +++ b/app/Repositories/Contracts/UserRepositoryInterface.php @@ -0,0 +1,20 @@ + + */ +interface UserRepositoryInterface extends BaseRepositoryInterface +{ + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator; + + public function findByEmail(string $email): ?User; +} diff --git a/app/Repositories/Eloquent/AnnouncementRepository.php b/app/Repositories/Eloquent/AnnouncementRepository.php new file mode 100644 index 0000000..704a537 --- /dev/null +++ b/app/Repositories/Eloquent/AnnouncementRepository.php @@ -0,0 +1,60 @@ + + */ +class AnnouncementRepository extends BaseRepository implements AnnouncementRepositoryInterface +{ + public function __construct(Announcement $model) + { + parent::__construct($model); + } + + public function findBySlug(string $slug): ?Announcement + { + return $this->model->newQuery()->where('slug', $slug)->first(); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + if (! empty($filters['category'])) { + $query->where('category', $filters['category']); + } + + if (! empty($filters['featured'])) { + $query->where('is_featured', true); + } + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('title', 'like', '%'.$filters['search'].'%') + ->orWhere('excerpt', 'like', '%'.$filters['search'].'%'); + }); + } + + $sortField = $filters['sort'] ?? '-published_at'; + $direction = str_starts_with($sortField, '-') ? 'desc' : 'asc'; + $sortField = ltrim($sortField, '-'); + + $allowedSorts = ['title', 'published_at', 'created_at']; + if (in_array($sortField, $allowedSorts)) { + $query->orderBy($sortField, $direction); + } else { + $query->latest('published_at'); + } + + return $query->paginate($perPage); + } +} diff --git a/app/Repositories/Eloquent/BaseRepository.php b/app/Repositories/Eloquent/BaseRepository.php new file mode 100644 index 0000000..d608e78 --- /dev/null +++ b/app/Repositories/Eloquent/BaseRepository.php @@ -0,0 +1,62 @@ + + */ +abstract class BaseRepository implements BaseRepositoryInterface +{ + /** + * @param T $model + */ + public function __construct(protected Model $model) {} + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery()->latest()->paginate($perPage); + } + + /** + * @return T|null + */ + public function findById(int $id): ?Model + { + return $this->model->newQuery()->find($id); + } + + /** + * @param array $data + * @return T + */ + public function create(array $data): Model + { + return $this->model->newQuery()->create($data); + } + + /** + * @param array $data + * @return T + */ + public function update(Model $model, array $data): Model + { + $model->update($data); + + return $model->fresh(); + } + + public function delete(Model $model): bool + { + return $model->delete(); + } +} diff --git a/app/Repositories/Eloquent/CategoryRepository.php b/app/Repositories/Eloquent/CategoryRepository.php new file mode 100644 index 0000000..bb8059e --- /dev/null +++ b/app/Repositories/Eloquent/CategoryRepository.php @@ -0,0 +1,38 @@ + + */ +class CategoryRepository extends BaseRepository implements CategoryRepositoryInterface +{ + public function __construct(Category $model) + { + parent::__construct($model); + } + + public function findBySlug(string $slug): ?Category + { + return $this->model->newQuery()->with('menuCourses')->where('slug', $slug)->first(); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery()->with('menuCourses'); + + if (! empty($filters['search'])) { + $query->where('label', 'like', '%'.$filters['search'].'%'); + } + + return $query->latest()->paginate($perPage); + } +} diff --git a/app/Repositories/Eloquent/CommentRepository.php b/app/Repositories/Eloquent/CommentRepository.php new file mode 100644 index 0000000..485acd4 --- /dev/null +++ b/app/Repositories/Eloquent/CommentRepository.php @@ -0,0 +1,58 @@ + + */ +class CommentRepository extends BaseRepository implements CommentRepositoryInterface +{ + public function __construct(Comment $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + if (isset($filters['is_approved'])) { + $query->where('is_approved', filter_var($filters['is_approved'], FILTER_VALIDATE_BOOLEAN)); + } + + if (! empty($filters['commentable_type'])) { + $query->where('commentable_type', $filters['commentable_type']); + } + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('author_name', 'like', '%'.$filters['search'].'%') + ->orWhere('content', 'like', '%'.$filters['search'].'%'); + }); + } + + return $query->latest()->paginate($perPage); + } + + /** + * @return Collection + */ + public function getApprovedByCommentable(string $commentableType, int $commentableId): Collection + { + return $this->model->newQuery() + ->where('commentable_type', $commentableType) + ->where('commentable_id', $commentableId) + ->where('is_approved', true) + ->latest() + ->get(); + } +} diff --git a/app/Repositories/Eloquent/CourseRepository.php b/app/Repositories/Eloquent/CourseRepository.php new file mode 100644 index 0000000..48860d9 --- /dev/null +++ b/app/Repositories/Eloquent/CourseRepository.php @@ -0,0 +1,59 @@ + + */ +class CourseRepository extends BaseRepository implements CourseRepositoryInterface +{ + public function __construct(Course $model) + { + parent::__construct($model); + } + + public function findBySlug(string $slug): ?Course + { + return $this->model->newQuery() + ->with(['category', 'schedules', 'blocks']) + ->where('slug', $slug) + ->first(); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery()->with('category'); + + if (! empty($filters['category'])) { + $query->whereHas('category', fn ($q) => $q->where('slug', $filters['category'])); + } + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('title', 'like', '%'.$filters['search'].'%') + ->orWhere('desc', 'like', '%'.$filters['search'].'%'); + }); + } + + $sortField = $filters['sort'] ?? '-created_at'; + $direction = str_starts_with($sortField, '-') ? 'desc' : 'asc'; + $sortField = ltrim($sortField, '-'); + + $allowedSorts = ['title', 'created_at', 'students', 'rating']; + if (in_array($sortField, $allowedSorts)) { + $query->orderBy($sortField, $direction); + } else { + $query->latest(); + } + + return $query->paginate($perPage); + } +} diff --git a/app/Repositories/Eloquent/FaqRepository.php b/app/Repositories/Eloquent/FaqRepository.php new file mode 100644 index 0000000..6a3918d --- /dev/null +++ b/app/Repositories/Eloquent/FaqRepository.php @@ -0,0 +1,48 @@ + + */ +class FaqRepository extends BaseRepository implements FaqRepositoryInterface +{ + public function __construct(Faq $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + if (! empty($filters['category'])) { + $query->where('category', $filters['category']); + } + + return $query->orderBy('order_index')->paginate($perPage); + } + + /** + * @return Collection + */ + public function getByCategory(FaqCategory $category): Collection + { + return Cache::remember("faqs.{$category->value}", 3600, fn () => $this->model->newQuery() + ->where('category', $category) + ->where('is_active', true) + ->orderBy('order_index') + ->get()); + } +} diff --git a/app/Repositories/Eloquent/GuideCardRepository.php b/app/Repositories/Eloquent/GuideCardRepository.php new file mode 100644 index 0000000..e797736 --- /dev/null +++ b/app/Repositories/Eloquent/GuideCardRepository.php @@ -0,0 +1,42 @@ + + */ +class GuideCardRepository extends BaseRepository implements GuideCardRepositoryInterface +{ + public function __construct(GuideCard $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->orderBy('order_index') + ->paginate($perPage); + } + + /** + * @return Collection + */ + public function active(): Collection + { + return Cache::remember('guide_cards', 3600, fn () => $this->model->newQuery() + ->where('is_active', true) + ->orderBy('order_index') + ->get()); + } +} diff --git a/app/Repositories/Eloquent/HeroSlideRepository.php b/app/Repositories/Eloquent/HeroSlideRepository.php new file mode 100644 index 0000000..cce35a5 --- /dev/null +++ b/app/Repositories/Eloquent/HeroSlideRepository.php @@ -0,0 +1,42 @@ + + */ +class HeroSlideRepository extends BaseRepository implements HeroSlideRepositoryInterface +{ + public function __construct(HeroSlide $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + return $this->model->newQuery() + ->orderBy('order_index') + ->paginate($perPage); + } + + /** + * @return Collection + */ + public function active(): Collection + { + return Cache::remember('hero_slides', 3600, fn () => $this->model->newQuery() + ->where('is_active', true) + ->orderBy('order_index') + ->get()); + } +} diff --git a/app/Repositories/Eloquent/LeadRepository.php b/app/Repositories/Eloquent/LeadRepository.php new file mode 100644 index 0000000..ced7f27 --- /dev/null +++ b/app/Repositories/Eloquent/LeadRepository.php @@ -0,0 +1,48 @@ + + */ +class LeadRepository extends BaseRepository implements LeadRepositoryInterface +{ + public function __construct(Lead $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + if (! empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (! empty($filters['source'])) { + $query->where('source', $filters['source']); + } + + if (isset($filters['is_read'])) { + $query->where('is_read', filter_var($filters['is_read'], FILTER_VALIDATE_BOOLEAN)); + } + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('name', 'like', '%'.$filters['search'].'%') + ->orWhere('phone', 'like', '%'.$filters['search'].'%'); + }); + } + + return $query->latest()->paginate($perPage); + } +} diff --git a/app/Repositories/Eloquent/MenuRepository.php b/app/Repositories/Eloquent/MenuRepository.php new file mode 100644 index 0000000..5c1287a --- /dev/null +++ b/app/Repositories/Eloquent/MenuRepository.php @@ -0,0 +1,68 @@ + + */ +class MenuRepository extends BaseRepository implements MenuRepositoryInterface +{ + public function __construct(Menu $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + if (! empty($filters['location'])) { + $query->where('location', $filters['location']); + } + + return $query->whereNull('parent_id') + ->with('children') + ->orderBy('order_index') + ->paginate($perPage); + } + + /** + * @return Collection + */ + public function getByLocation(MenuLocation $location): Collection + { + return Cache::remember("menus.{$location->value}", 3600, fn () => $this->model->newQuery() + ->where('location', $location) + ->where('is_active', true) + ->whereNull('parent_id') + ->with(['children' => fn ($q) => $q->where('is_active', true)->orderBy('order_index')]) + ->orderBy('order_index') + ->get()); + } + + /** + * @param array $items + */ + public function reorder(array $items): void + { + foreach ($items as $item) { + $this->model->newQuery() + ->where('id', $item['id']) + ->update([ + 'order_index' => $item['order_index'], + 'parent_id' => $item['parent_id'] ?? null, + ]); + } + } +} diff --git a/app/Repositories/Eloquent/PageRepository.php b/app/Repositories/Eloquent/PageRepository.php new file mode 100644 index 0000000..780566d --- /dev/null +++ b/app/Repositories/Eloquent/PageRepository.php @@ -0,0 +1,41 @@ + + */ +class PageRepository extends BaseRepository implements PageRepositoryInterface +{ + public function __construct(Page $model) + { + parent::__construct($model); + } + + public function findBySlug(string $slug): ?Page + { + return $this->model->newQuery() + ->with('blocks') + ->where('slug', $slug) + ->first(); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery(); + + if (! empty($filters['search'])) { + $query->where('title', 'like', '%'.$filters['search'].'%'); + } + + return $query->latest()->paginate($perPage); + } +} diff --git a/app/Repositories/Eloquent/ScheduleRepository.php b/app/Repositories/Eloquent/ScheduleRepository.php new file mode 100644 index 0000000..19a099a --- /dev/null +++ b/app/Repositories/Eloquent/ScheduleRepository.php @@ -0,0 +1,47 @@ + + */ +class ScheduleRepository extends BaseRepository implements ScheduleRepositoryInterface +{ + public function __construct(CourseSchedule $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery()->with('course.category'); + + if (! empty($filters['course_id'])) { + $query->where('course_id', $filters['course_id']); + } + + return $query->orderBy('start_date')->paginate($perPage); + } + + /** + * @return Collection + */ + public function upcoming(int $limit = 20): Collection + { + return $this->model->newQuery() + ->with('course.category') + ->where('start_date', '>=', now()) + ->orderBy('start_date') + ->limit($limit) + ->get(); + } +} diff --git a/app/Repositories/Eloquent/SettingRepository.php b/app/Repositories/Eloquent/SettingRepository.php new file mode 100644 index 0000000..5eff5fd --- /dev/null +++ b/app/Repositories/Eloquent/SettingRepository.php @@ -0,0 +1,108 @@ + + */ +class SettingRepository extends BaseRepository implements SettingRepositoryInterface +{ + public function __construct(Setting $model) + { + parent::__construct($model); + } + + /** + * @return Collection + */ + public function all(): Collection + { + return $this->model->newQuery() + ->orderBy('group') + ->orderBy('order_index') + ->get(); + } + + /** + * @return array> + */ + public function publicGrouped(): array + { + return Cache::remember('site_settings_all', 3600, function () { + return $this->model->newQuery() + ->where('is_public', true) + ->orderBy('group') + ->orderBy('order_index') + ->get() + ->filter(fn (Setting $s) => ! $s->isSensitive()) + ->groupBy(fn (Setting $s) => $s->group->value) + ->map(fn ($group) => $group->pluck('value', 'key')->all()) + ->all(); + }); + } + + /** + * @return array + */ + public function publicByGroup(SettingGroup $group): array + { + return Cache::remember("site_settings_{$group->value}", 3600, function () use ($group) { + return $this->model->newQuery() + ->where('group', $group) + ->where('is_public', true) + ->orderBy('order_index') + ->get() + ->filter(fn (Setting $s) => ! $s->isSensitive()) + ->pluck('value', 'key') + ->all(); + }); + } + + public function findByKey(string $key): ?Setting + { + return $this->model->newQuery()->where('key', $key)->first(); + } + + /** + * @return Collection + */ + public function getByGroup(SettingGroup $group): Collection + { + return $this->model->newQuery() + ->where('group', $group) + ->orderBy('order_index') + ->get(); + } + + /** + * @param array $settings + */ + public function bulkUpdate(array $settings): void + { + foreach ($settings as $dotKey => $value) { + [$group, $key] = explode('.', $dotKey, 2); + + $this->model->newQuery() + ->where('group', $group) + ->where('key', $key) + ->update(['value' => $value]); + } + + $this->clearCache(); + } + + public function clearCache(): void + { + Cache::forget('site_settings_all'); + + foreach (SettingGroup::cases() as $group) { + Cache::forget("site_settings_{$group->value}"); + } + } +} diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php new file mode 100644 index 0000000..d15b98b --- /dev/null +++ b/app/Repositories/Eloquent/UserRepository.php @@ -0,0 +1,46 @@ + + */ +class UserRepository extends BaseRepository implements UserRepositoryInterface +{ + public function __construct(User $model) + { + parent::__construct($model); + } + + /** + * @param array $filters + * @return LengthAwarePaginator + */ + public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator + { + $query = $this->model->newQuery()->with('roles'); + + if (! empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + }); + } + + if (! empty($filters['role'])) { + $query->role($filters['role']); + } + + return $query->latest()->paginate($perPage); + } + + public function findByEmail(string $email): ?User + { + return $this->model->newQuery()->where('email', $email)->first(); + } +} diff --git a/artisan b/artisan new file mode 100755 index 0000000..c35e31d --- /dev/null +++ b/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/backend_ve_admin_dokuman.md b/backend_ve_admin_dokuman.md new file mode 100644 index 0000000..94efc21 --- /dev/null +++ b/backend_ve_admin_dokuman.md @@ -0,0 +1,802 @@ +# Boğaziçi Denizcilik - Backend (Laravel) & Admin Panel (Next.js) Gereksinim Dökümanı (v2) + +Bu döküman, mevcut Next.js (Frontend) projenizin dinamik veri altyapısını bir **Laravel API**'sine bağlamak ve bu verileri yönetmek için tamamen ayrı bir **Next.js Admin Panel** geliştirmek üzere hazırlanmıştır. + +*(v2 Güncellemesi: Gelişmiş Setting altyapısı, Pagination standartları, Modüler Page Builder API'leri, Soft Deletes, Caching, Audit Logs, Rate Limiting ve Çok Dilli (i18n) destek notları eklenmiştir.)* + +> **Çok Dilli (i18n) Altyapı Notu:** Şimdilik v1 aşamasında tek dil (TR) olarak çıkılacaktır. Ancak v2+ versiyonları için i18n (Çoklu dil) planlanıyorsa, veritabanı tablolarında JSON field veya harici translations tablosu yaklaşımı değerlendirilecektir. + +> **🤖 GitHub Copilot (VS Code) İçin Geliştirme Talimatı (Master Prompt):** +> *Laravel Herd ile kendi oluşturduğunuz projeyi VS Code'da açtıktan sonra, Copilot Chat'e (veya Cursor/Cline'a) şu mesajı yazmanız yeterlidir:* +> +> "Merhaba Copilot, bu projeyi **Laravel 12+** ile geliştireceğiz. Lütfen ana dizindeki **`backend_ve_admin_dokuman.md`** dosyasını dikkatlice baştan sona oku ve mimariyi kavra. +> Ardından şu işlemleri benim onayımı alarak **adım adım** kodlamaya başla: +> 1. Önce "1. Veritabanı Şeması" bölümündeki tüm tablolar için Laravel Migration, Model ve Factory dosyalarını oluştur. Önemli tablolarda SoftDeletes kullan. Bu adımı bitirince onayımı bekle. +> 2. Ardından "REST API" ve "Klasör Mimarisi" bölümüne geç. Kesinlikle spagetti controller yazma. Her modül için sırayla; `FormRequest`, `DTO`, `Action (Service)`, `Repository` ve en son `Controller` ile `api.php` rotalarını oluştur. Response standartlarına ve Caching kurallarına dikkat et. +> 3. Her modülü (Örn: Courses, Categories) tek tek kodla ve her modül bitiminde bana bilgi ver. Asla tüm projeyi tek seferde yazmaya çalışma." + +--- + +## 1. Veritabanı Şeması (Laravel Modelleri & Migrations) + +*(Güvenlik: Kaza eseri veri kayıplarını önlemek için yetki, kurs, kategori ve lead tablolarında `SoftDeletes` kullanılacaktır.)* + +### 1.0. `users` & `roles` (Yetkilendirme / Auth) +- Laravel'in standart `users` tablosu. Admin panele giriş yapacak personeli temsil eder. +- Sisteme **Spatie Permission** paketi kurularak Role ve Yetki altyapısı tanımlanacaktır (Örn: `super-admin`, `editor`). +- `SoftDeletes` aktif olmalıdır. + +### 1.1. `categories` (Eğitim Kategorileri) +- `id` (PK) +- `slug` (string, unique) - *Örn: guverte* +- `label` (string) - *Örn: Güverte Eğitimleri* +- `desc` (text, nullable) +- `image` (string, nullable) - *Sadece görsel path'i tutar* +- `meta_title` (string, nullable) - *SEO Title* +- `meta_description` (text, nullable) - *SEO Description* +- `created_at`, `updated_at`, `deleted_at` *(SoftDeletes)* + +### 1.2. `courses` (Eğitimler/Kurslar) +- `id` (PK) +- `category_id` (FK -> categories.id) +- `slug` (string, unique) +- `title` (string) +- `sub` (string, nullable) - *Örn: STCW II/1* +- `desc` (text) - *Kısa açıklama* +- `long_desc` (longText) - *Detaylı açıklama* +- `duration` (string) - *Örn: 5 Gün* +- `students` (integer, default: 0) - *Kayıtlı öğrenci sayısı* +- `rating` (decimal: 2,1, default: 5.0) +- `badge` (string, nullable) - *Örn: Simülatör* +- `image` (string, nullable) - *Sadece path tutar* +- `price` (string, nullable) +- `includes` (json, nullable) - *Fiyata dahil olanlar listesi. Örn: `["Eğitim materyali", "Sertifika ücreti", "İkram"]`* +- `requirements` (json, nullable) - *Katılım koşulları/gereksinimler listesi. Örn: `["18 yaşını doldurmuş olmak", "Lise mezunu olmak"]`* +- `meta_title` (string, nullable) - *SEO Title* +- `meta_description` (text, nullable) - *SEO Description* +- `scope` (json, nullable) - *Eğitim kapsamı konu başlıkları dizisi. Örn: `["Ana makine sistemleri", "Yağlama ve soğutma sistemleri", "Yakıt sistemleri"]`* +- `standard` (string, nullable) - *Uyum standardı. Örn: `"STCW / IMO Uyumlu"`* +- `language` (string, nullable, default: `"Türkçe"`) - *Eğitim dili* +- `location` (string, nullable) - *Kursun varsayılan lokasyonu. Örn: `"Kadıköy, İstanbul"`* +- `created_at`, `updated_at`, `deleted_at` *(SoftDeletes)* + +### 1.3. `course_schedules` (Takvim / Planlanan Eğitimler) +- `id` (PK) +- `course_id` (FK -> courses.id) +- `start_date` (date) - *Örn: 2026-02-24* +- `end_date` (date) - *Örn: 2026-02-28* +- `location` (string) - *Örn: Kadıköy veya Online* +- `quota` (integer) - *Toplam Kontenjan* +- `available_seats` (integer) - *Kalan Koltuk (Otomatik `quota - reserved_count` mantığıyla çalışmalı veya admin üzerinden manuel update edilmeli. Proje ilerleyişine göre netleşecek).* +- `is_urgent` (boolean, default: false) - *Sınırlı Kontenjan uyarısı* +- `created_at`, `updated_at` + +### 1.4. `announcements` (Duyurular & Haberler) +- `id` (PK) +- `slug` (string, unique) +- `title` (string) +- `category` (string, enum) - *Tutarsızlığı önlemek için Enum kullanılmalı: `announcement` | `news` | `event`* +- `excerpt` (text) +- `content` (longText) +- `image` (string, nullable) +- `is_featured` (boolean, default: false) +- `meta_title` (string, nullable) +- `meta_description` (text, nullable) +- `published_at` (timestamp) - *Frontend'deki day/month yerine gerçek tarih tutulmalı* +- `created_at`, `updated_at` + +### 1.5. `hero_slides` (Ana Sayfa Slider) +- `id` (PK) +- `label` (string) - *Örn: STCW Sertifikasyonu* +- `title` (string) - *Satır başı için \n destekli* +- `description` (text) +- `image` (string, nullable) +- `order` (integer, default: 0) +- `is_active` (boolean, default: true) +- `created_at`, `updated_at` + +### 1.6. `leads` (Ön Kayıtlar / Form Başvuruları) +- `id` (PK) +- `name` (string) +- `phone` (string) +- `target_course` (string, nullable) - *İlgilenilen eğitim* +- `education_level` (string, nullable) - *Öğrenim durumu* +- `subject` (string, nullable) - *İlgilenilen konu (Danışmanlık formu için Örn: stcw, kaptanlik, belge, diger)* +- `message` (text, nullable) - *Ziyaretçinin ek mesajı (Danışmanlık formu için)* +- `status` (string, default: 'new') - *`new`, `contacted`, `enrolled`, `rejected`* +- `notes` (text, nullable) - *Admin notları* +- `is_read` (boolean, default: false) - *Admin panelde okunmamışları vurgulamak için* +- `source` (string) - *`hero_form` | `contact_form` | `consultation_form` | `whatsapp_widget`* +- `utm` (json, nullable) - *`utm_source`, `utm_campaign`, vb.* +- `consent_kvkk` (boolean, default: false) +- `consent_text_version` (string, nullable) +- `created_at`, `updated_at`, `deleted_at` *(SoftDeletes)* + +### 1.7. `menus` (Dinamik Menü & Navigasyon Yönetimi) +Tüm site üst (Navbar) ve alt (Footer) bağlantılarının, Mega Menü içeriklerinin admin panelinden yönetilebilmesi için. +- `id` (PK) +- `location` (string) - *Örn: `header_main`, `footer_corporate`, `footer_education`, `footer_quicklinks`* +- `label` (string) - *Menüde Gözükecek Metin Örn: "Hakkımızda"* +- `url` (string) - *Örn: "/kurumsal/hakkimizda"* +- `type` (string, default: 'link') - *`link` (Normal Link) | `mega_menu_education` (Özel Hover Mega Menü) | `mega_menu_calendar` (Takvim Dropdown)* +- `parent_id` (integer, nullable) - *Alt menüler için (Self Referencing FK)* +- `order` (integer, default: 0) - *Sıralama* +- `is_active` (boolean, default: true) +- `created_at`, `updated_at` + +### 1.8. `comments` (Eğitim ve Blog Yorumları / Soru-Cevap) +*Kullanıcıların üye olmadan, sadece Ad, Soyad ve Telefon numarası girerek Eğitimlere veya Duyurulara/Bloglara yorum yapabileceği ve adminlerin cevaplayabileceği sistem.* +- `id` (PK) +- `commentable_id` (integer) - *Hangi eğitime/duyuruya yapıldı? (Polymorphic Relation)* +- `commentable_type` (string) - *`App\\Models\\Course` veya `App\\Models\\Announcement`* +- `name_surname` (string) - *Kullanıcının Adı Soyadı* +- `phone` (string) - *Kullanıcının Telefon Numarası (Soru sorana ulaşmak gerekirse)* +- `body` (text) - *Yorum / Soru metni* +- `admin_reply` (text, nullable) - *Admin'in verdiği cevap (Önyüzde Admin Yanıtı olarak gözükecek)* +- `is_approved` (boolean, default: false) - *Admin onaylamadan sitede gözükmemeli* +- `created_at`, `updated_at`, `deleted_at` *(SoftDeletes)* + +### 1.9. `faqs` (Sıkça Sorulan Sorular) +*Frontend'deki `/sss` sayfası ve anasayfadaki FAQ bileşeni için. Adminler soru-cevapları kategorili olarak yönetebilir.* +- `id` (PK) +- `category` (string) - *`egitimler` | `kayit` | `iletisim` (Tab bazlı filtreleme için)* +- `question` (text) - *Soru metni* +- `answer` (text) - *Cevap metni* +- `order` (integer, default: 0) - *Kategori içi sıralama* +- `is_active` (boolean, default: true) - *Pasif olanlar önyüzde gösterilmez* +- `created_at`, `updated_at` + +### 1.10. `guide_cards` (Eğitim Rehberi Kartları) +*Frontend'deki `/egitim-rehberi` sayfasındaki kariyer yönlendirme kartları için. Admin panelden yönetilebilir.* +- `id` (PK) +- `title` (string) - *Örn: "Yeni Başlayanlar", "Yat Kaptanları"* +- `description` (text) - *Kart içeriği açıklaması* +- `icon` (string) - *Lucide icon adı (Örn: `anchor`, `compass`, `shield`, `briefcase`)* +- `color` (string) - *Gradient renk sınıfı (Örn: `from-blue-500 to-blue-700`)* +- `link` (string) - *Yönlendirme URL'i (Örn: `/egitimler/stcw`)* +- `order` (integer, default: 0) - *Sıralama* +- `is_active` (boolean, default: true) +- `created_at`, `updated_at` + +### 1.11. `settings` (Sistem Ayarları) +*Admin panelinde otomatik tab/sekme yapısı kurmak ve public/private yalıtımı sağlamak için güçlendirilmiş yapı. Logo, sosyal medya linkleri gibi statik veriler buradan yönetilir.* +- `id` (PK) +- `group` (string) - *`general` | `contact` | `social` | `seo` | `forms` | `footer` | `guide`* +- `key` (string, unique) +- `value` (text/json, nullable) +- `type` (string) - *`text` | `textarea` | `image` | `boolean` | `json` | `richtext`* +- `is_public` (boolean, default: true) - *Public API'de görünsün mü? (Örn: `hero_form_email` gizli kalmalıdır).* +- `created_at`, `updated_at` + +**Önerilen Settings Key Listesi (Otomatik Seeder ile Girecekler):** +- **Genel:** `site_name`, `site_logo`, `footer_logo`, `favicon` +- **Anasayfa İçerikleri (Sabit Alanlar):** `home_about_title`, `home_about_text`, `home_about_image_1`, `home_about_image_2`, `home_stats_students`, `home_stats_instructors`, `home_stats_courses` (Anasayfadaki "Biz Kimiz", "Kurum Hakkında" veya istatistik alanlarındaki resim/yazıların yönetimi için). +- **İletişim:** `contact_email`, `contact_cc_emails`, `phone_1`, `phone_2`, `whatsapp_phone`, `address`, `google_maps_embed_url`, `working_hours` +- **Sosyal Medya (Dinamik Logolar / Linkler):** `instagram_url`, `facebook_url`, `tiktok_url`, `x_url`, `youtube_url`, `linkedin_url` (Bu alanlar boş bırakılırsa önyüzde iconlar gizlenmelidir). +- **SEO:** `default_meta_title`, `default_meta_description`, `canonical_base`, `og_default_image`, `robots_indexing_enabled` +- **Formlar / KVKK / Kampanya:** `lead_notify_email`, `kvkk_text`, `privacy_policy_url`, `newsletter_title`, `newsletter_desc` (E-bülten ve "Sana özel kampanyalar" kutusundaki metinler). +- **Eğitim Rehberi:** `guide_page_title`, `guide_page_subtitle`, `guide_cta_title`, `guide_cta_text`, `guide_cta_button_text`, `guide_cta_button_url` (Eğitim Rehberi sayfasındaki başlık, alt metin ve CTA alanlarının yönetimi). +- **Danışmanlık:** `consultation_page_title`, `consultation_page_subtitle`, `consultation_form_title` (Danışmanlık sayfasının başlık ve açıklama alanlarının yönetimi). + +--- + +## 2. Laravel REST API Uç Noktaları (Endpoints) & Standartları + +**Cevap (Response) Format Standardı:** +Frontend tarafının hızlı ilerlemesi için tüm başarılı API listeleme talepleri paginate ve metadata içermeli, Laravel Resource üzerinden standartlaştırılmalıdır. +- Liste endpoint'leri: `{ "data": [], "meta": { "total": 0 }, "links": { } }` +- Tekil endpoint'ler: `{ "data": { } }` + +**Parametrik Arama, Filtreleme, Sıralama:** +- `GET /api/v1/courses?category=guverte&search=stcw&sort=title&page=1` +- `GET /api/v1/announcements?category=news&featured=1&sort=-published_at&page=1` + +### Public API (Frontend İçin - `api/v1/...`) +*(Performans için aktif olan sık kullanılan Public GET endpoint'leri **Laravel Cache / Redis** arkasında çalıştırılmalıdır. Veri Admin Panel'den güncellenince Cache Event ile temizlenmelidir.)* + +- `GET /api/v1/settings` -> `is_public=true` olan ayarları döner. +- `GET /api/v1/hero-slides` -> Aktif slider içeriklerini sıralı getirir. +- `GET /api/v1/menus` -> Header ve Footer navigasyon ağacını hiyerarşik getirir. +- `GET /api/v1/categories` -> Kategorileri listeler. +- `GET /api/v1/courses` -> Eğitimleri listeler. +- `GET /api/v1/courses/{slug}` -> Tekil eğitim detayı. +- `GET /api/v1/schedules` -> Yaklaşan takvim eğitimlerini getirir. (Tarihe göre sıralı) +- `GET /api/v1/announcements` -> Duyuruları (featured olanlar dahil) getirir. +- `GET /api/v1/announcements/{slug}` -> Tekil duyuru detayı. +- `GET /api/v1/comments/{type}/{id}` -> Bir kursun veya blogun onaylanmış (`is_approved=true`) yorumlarını ve admin cevaplarını getirir. +- `POST /api/v1/comments` -> Ziyaretçiden yeni yorum/soru kaydı alır (Spam/Rate Limiting zorunlu). +- `GET /api/v1/faqs` -> Aktif SSS verilerini kategorili olarak getirir. `?category=egitimler` filtresi desteklenmeli. +- `GET /api/v1/guide-cards` -> Eğitim Rehberi kartlarını sıralı getirir. +- `GET /api/v1/pages/{slug}` -> Page builder bloklarını sırayla (`order_index`) getirir. *(Sadece `/page/` altındaki dinamik sayfalar için)* +- `POST /api/v1/leads` -> Ön kayıt formu alımı. `source` alanı ile form kaynağı (`hero_form`, `contact_form`, `consultation_form`) belirlenir. *(Spam koruması için IP bazlı Rate Limiting - Throttle uygulanmalıdır. Dakikada maks 2-3 gibi)* +- `GET /api/v1/sitemap-data` -> Frontend'in dinamik `sitemap.xml` oluşturması için tüm public slug'ları ve `updated_at` tarihlerini döner. *(Bkz: Bölüm 7.4)* + +### Protected API (Admin Panel İçin - `api/admin/...`) +Tüm yetkili işlemler burada yalıtılmıştır. +- `POST /api/admin/login` -> Bearer token döndürür (Sanctum/Passport). +- `GET /api/admin/me` -> Giriş yapan admin detaylarını + Spatie Permission rollerini/izinlerini döner. +- `POST /api/admin/uploads` -> Multipart/form-data kabul eder. Standart resim yükleme ucudur. DB'ye URL değil **path** kaydeder. Response: `{ "path": "uploads/...", "url": "https://.../uploads/..." }` +- Modüller için tüm standart `POST` (Oluşturma), `PUT/PATCH` (Güncelleme), `DELETE` (Silme) uçları. +- Menü Yönetimi: + - `CRUD /api/admin/menus` -> Menü ekleme ve ağaç dökümü + - `POST /api/admin/menus/reorder` -> Sürükle-Bırak sonrası hiyerarşiyi (parent_id ve order) toplu güncelleme +- SSS (FAQ) Yönetimi: + - `CRUD /api/admin/faqs` -> Soru ekleme, düzenleme, silme, listeleme +- Eğitim Rehberi Kartları: + - `CRUD /api/admin/guide-cards` -> Kart ekleme, düzenleme, silme, listeleme +- Page Builder Admin uçları: + - `CRUD /api/admin/pages` + - `CRUD /api/admin/pages/{id}/blocks` + - `POST /api/admin/pages/{id}/blocks/reorder` -> Sürükle bırak ile sıra (`order_index`) güncelleme. + +--- + +## 3. Laravel Katmanlı Mimari (Repository Pattern) Uygulama Standartları + +Backend kodlanırken kesinlikle **Spagetti Controller** (Controller içine direkt `Course::create()` yazmak) kullanılmayacaktır. Tüm iş mantığı ve veritabanı sorguları şu katmanlara ayrılmalıdır: + +**Önerilen Devasa (Enterprise) Klasör Mimarisi (`app/`):** +```text +app/ + ├── Http/ + │ ├── Controllers/ + │ │ └── Api/ + │ │ └── CourseController.php # HTTP İsteğini alır, DTO'yu oluşturup Action'a yollar. Geriye Response döner. + │ ├── Requests/ + │ │ └── StoreCourseRequest.php # Gelen verilerin validasyon zorunluluklarını (required, string) yapar. + │ └── Resources/ + │ └── CourseResource.php # {YENİ} Frontend'e gidecek JSON formatını filtreler ve standartlaştırır (Presenter). + ├── DTOs/ + │ └── CourseData.php # Validasyonu geçmiş temiz veriyi, tiplendirilmiş Nesneye (DTO) dönüştürür. + ├── Actions/ (veya Services/) + │ └── Course/ + │ └── CreateCourseAction.php # Tüm "İş Mantığı". İndirim hesapla, Event fırlat, Repository'e yolla. + ├── Repositories/ + │ ├── Contracts/ + │ │ └── CourseRepositoryInterface.php # Sınırlar ve Kurallar (Interface) + │ └── Eloquent/ + │ └── CourseRepository.php # Veritabanı (DB) sorguları sadece buradadır (Course::create()). + ├── Models/ + │ └── Course.php # Tablo yapısı, İlişkiler (Relations) + ├── Enums/ + │ └── CourseStatus.php # Güvenli Sabitler + ├── Events/ + │ └── CourseCreatedEvent.php # Asenkron Olaylar (Decoupling için) -> Cache temizleme vb. burada ateşlenir. + ├── Listeners/ + │ └── NotifySubscribersListener.php # Olayı dinleyen arka plan işlemleri + ├── Policies/ + │ └── CoursePolicy.php # Yetki ve Rol kontrolleri (Örn: Yetkisiz kişi kurs silemez) + ├── Exceptions/ + │ └── CourseFullException.php # Domain'e özel hata sınıfları (400) +``` + +**Kusursuz (Ultimate) Veri Akış Yönü:** +`Route` ➔ `Middleware (Auth)` ➔ `Policy (Yetki)` ➔ `FormRequest (Validasyon)` ➔ `Controller` ➔ `DTO` ➔ `Action` ➔ `Repository (Interface)` ➔ `Model` ➔ `DB` +*(İşlem Başarılıysa)* ➔ `Action => Event Fırlatır / Activity Log Atılır` +*(Controller Çıkışı)* ➔ `API Resource (JSON Formatlama)` ➔ `Next.js FrontEnd` + +**Audit Log / Kullanıcı İşlem Kayıtları Notu:** +Sisteme yetkili girişler yapılacağı için (Super Admin, Editor vs) veri güncellemelerini, silmeleri (SoftDelete edilen datalar) izlemek amacıyla projeye `spatie/laravel-activitylog` paketi kurulması, oluşan Transaction'ların tutulması önerilir. + +--- + +## 4. Next.js Admin Panel Mimarisi Önerisi + +Admin Paneli tamamen ayrı bir Next.js projesi olarak (Örn: `admin.bogazicidenizcilik.com` veya `localhost:3001` portunda) kurulmalıdır. + +### **Mimari Prensip (Katmanlı Mimari - Layered Architecture):** +Kodların tek bir klasöre yığılmasını önlemek için Frontend kodları işlevselliğe göre ayrılmalıdır. Örnek `src/` mimarisi: +- `src/app/`: Sadece sayfalar (Routing) ve sayfa Layout'ları. +- `src/components/`: Ortak kullanılan UI elementleri (Button, Input, Table, **Ortak ImageUpload Bileşeni**). +- `src/features/`: Modül bazlı klasörleme (Örn: `features/courses/components`, `features/courses/api`, `features/courses/types`). +- `src/lib/`: Axios instance, yardımcı fonksiyonlar. + +### Önerilen Klasör Dizilimi (`src/app/` Route Alanı): + +```text +src/app/ + ├── (auth)/ + │ └── login/page.tsx # Giriş ekranı (Kullanıcı Adı, Şifre -> Laravel Sanctum Token) + ├── (dashboard)/ + │ ├── layout.tsx # Sol Sidebar ve Üst Header kapsayıcısı (Role-based menu gösterimi) + │ ├── page.tsx # Dashboard Özeti (İstatistik kartları: Toplam Öğrenci, Yeni Formlar) +... +``` + +### Tüm Admin Modülleri ve Ekran Detayları + +#### 4.1. Kategoriler (`/categories`) +- **`page.tsx`:** Kategori Tablosu (Resim, Kategori Adı, İşlemler (Düzenle/Sil)). +- **`[id]/page.tsx` (Ekleme / Düzenleme Formu):** + - `label` *(Text Input)* - Kategori Adı + - `slug` *(Text Input)* - (Girilen isimden otomatik slug yaratılmalı) + - `desc` *(Textarea)* - Kısa Tanıtım + - `image` *(Upload Component)* - `POST /api/admin/uploads` tetikleyecek tek kullanımlık standart bileşen + - **SEO Alanı:** `meta_title`, `meta_description` *(Text Input)* + +#### 4.2. Eğitimler (`/courses`) +- **`page.tsx`:** Eğitimler Tablosu (Kategorisine göre filtrelenebilir, Öğrenci Sayısı vs.). +- **`[id]/page.tsx` (Ekleme / Düzenleme Formu):** + - **Genel Bilgiler:** + - `title`, `sub`, `category_id`, `price`, `duration`, `badge` + - **İçerik Editörü:** + - `desc` *(Textarea)* + - `longDesc` *(Rich Text Editor - CKEditor/Quill/Notion-stile Editor)* + - `image` *(Upload Component)* + - **Dinamik Diziler (Field Arrays / Repeaters):** + - `includes` *(Dynamic Input Array)* - "Fiyata Dahil Olanlar" (+) + - `requirements` *(Dynamic Input Array)* - "Kayıt Şartları" (+) + - **SEO Alanı:** `meta_title`, `meta_description` *(Text Input)* + +#### 4.3. Eğitim Takvimi / Planlama (`/schedules`) +- **`page.tsx`:** Takvim Tablosu (Kurs Adı, Başlangıç, Bitiş, Kontenjan). +- **`[id]/page.tsx` (Ekleme / Düzenleme Formu):** + - `course_id`, `start_date`, `end_date`, `location`, `quota`, `available_seats` + - `is_urgent` *(Switch / Checkbox)* - "Son 3 Gün" uyarı ateşleyicisi + +#### 4.4. Duyurular ve Haberler (`/announcements`) +- **`page.tsx`:** Olayların Tablosu. +- **`[id]/page.tsx` (Ekleme / Düzenleme Formu):** + - `title`, `category` (Seçim ile), `excerpt`, `content` (RTE), `image` (Upload), `meta_title`, `meta_description` + - `published_at` *(Date/Time Picker)* - İleri tarihli yayınlanma opsiyonu + - `is_featured` *(Switch)* - "Ana Sayfada Manşet Yap" onay kutusu + +#### 4.5. Ön Kayıt Başvuruları (CRM) (`/leads`) +- **`page.tsx`:** Form başvuruları listesi. **`is_read = false` olan kayıtlar kalın (bold) veya fosforlu arka planla ayırt edici gösterilmelidir.** +- **`[id]/page.tsx` (Görüntüleme Müşteri Detayı):** + - Gelen veriler: *Adı, Telefon, Durumu, İlgilendiği Eğitim, Kaynak (Source), UTMEketleri, KVKK Onay Vrs.* (Salt Okunur - Read/Only). + - Yalnızca Tıklandığında backend'e PATCH isteği atılıp `is_read = true` yapılır. + - Admin işlemleri: + - `status` *(Select)* - (Yeni, Arandı, Kayıt Oldu, İptal) + - `notes` *(Textarea)* - Admin/Müşteri Temsilcisi iç notları. Form POST ile güncellenir. + +#### 4.6. Menü ve Navigasyon Yönetimi (`/menus`) +- **`page.tsx`:** Menü lokasyonlarına (`header_main`, `footer_corporate`, `footer_education`, vb.) göre sekmeli (tab) bir liste. Her lokasyon için alt alta ağaç (tree) görünümlü kayıtlar. Sürükle-bırak (Drag & Drop) ile `POST /api/admin/menus/reorder` tetiklenerek sıralama ve parent/child ilişkisi hızlıca kurulabilmeli. +- **`[id]/page.tsx` (Ekleme / Düzenleme):** + - `location` *(Select)* - Header Ana Menü mü Yoksa Footer mı? + - `label` *(Text Input)* - "Eğitimler" + - `url` *(Text Input)* - "/egitimler" + - `type` *(Select)* - Normal Link mi (`link`), Eğitim Mega Menüsü mü (`mega_menu_education`), Takvim mi (`mega_menu_calendar`) + - `parent_id` *(Select)* - Bir ana menünün altına eklenecekse o ana menü seçilir. + - `is_active` *(Switch)* + +#### 4.7. Yorum / Soru-Cevap Yönetimi (`/comments`) +- **`page.tsx`:** Kullanıcılardan (üye olmadan) Eğitim ve Bloglara gelen tüm soruların/yorumların düştüğü ekran. `is_approved = false` olanlar (yeni gelenler) belirgin şekilde listelenir. +- **`[id]/page.tsx` (Yorum Onay ve Yanıtlama):** + - Gelen veriler: *Adı Soyadı, Telefon Numarası, Yorum Yapılan İçerik (Kurs Adı vs), Yorum Metni* (Read-Only). + - `admin_reply` *(Textarea)* - Yorum/soruyu cevaplamak için adminin dolduracağı alan. + - `is_approved` *(Switch)* - Yorumun sitede herkes tarafından gözüküp gözükmeyeceği. + +#### 4.8. SSS / Sıkça Sorulan Sorular Yönetimi (`/faqs`) +- **`page.tsx`:** FAQ Tablosu (Kategori, Soru, Aktif/Pasif durumu). Kategoriye göre filtrelenebilir. +- **`[id]/page.tsx` (Ekleme / Düzenleme Formu):** + - `category` *(Select)* - `egitimler` | `kayit` | `iletisim` + - `question` *(Textarea)* - Soru metni + - `answer` *(Textarea / Rich Text)* - Cevap metni + - `order` *(Number Input)* - Kategori içi sıralama + - `is_active` *(Switch)* - Aktif/Pasif + +#### 4.9. Eğitim Rehberi Kartları Yönetimi (`/guide-cards`) +- **`page.tsx`:** Rehber kartlarının listesi (Başlık, Icon, Link, Sıra). +- **`[id]/page.tsx` (Ekleme / Düzenleme Formu):** + - `title` *(Text Input)* - Kart başlığı (Örn: "Yeni Başlayanlar") + - `description` *(Textarea)* - Açıklama metni + - `icon` *(Select)* - Lucide icon seçimi (`anchor`, `compass`, `shield`, `briefcase` vb.) + - `color` *(Text Input / Color Picker)* - Gradient renk sınıfı + - `link` *(Text Input)* - Yönlendirme URL'i + - `order` *(Number Input)* - Sıralama + - `is_active` *(Switch)* + +#### 4.10. Sitenin Genel Ayarları (`/settings`) +Bu tek bir sayfada çalışır: +- Sayfa ilk açıldığında `GET /api/admin/settings` ile tüm satırları çeker. +- `group` verisine göre yatay ya da dikey Tabs (Sekmeler) oluşturur (Genel, Anasayfa, İletişim, Sosyal Medya, SEO, Formlar, Eğitim Rehberi, Danışmanlık). +- **Anasayfa Sekmesi:** Anasayfadaki "Neden Biz", "Biz Kimiz", info/görsel alanlarındaki başlıklar (`home_about_title`), metinler (`home_about_text`) ve resimlerin (`home_about_image_1`) yönetimi. +- **Sosyal Medya Sekmesi:** Tüm hesapların (Instagram, Facebook, Tiktok, LinkedIn vb.) linkleri buraya girilir. Frontend, boş olmayan değerleri render eder. +- **Eğitim Rehberi Sekmesi:** Eğitim rehberi sayfasının başlık, alt metin ve CTA (Call to Action) alanlarının yönetimi. +- **Danışmanlık Sekmesi:** Danışmanlık sayfasının başlık ve açıklama metinleri. +- `type` verisine göre ekranda Text Input, Textarea veya Toggle Switch render eder. Kaydederken array olarak tek bir PATCH isteğinde sunucuya yollar. + +--- + +## 5. Mevcut Frontend Projesi (Boğaziçi) Entegrasyon Adımları + +Backend ve Admin Panel hazır olduğunda, mevcut statik yapınızda şu işlemler gerçekleştirilecektir: + +1. **Çevresel Değişkenler:** `.env.local` içine `NEXT_PUBLIC_API_URL=http://api.bogazici.test/api/v1` eklenmeli. Yüklenen resimleri parse edebilmek için `NEXT_PUBLIC_ASSET_URL=http://api.bogazici.test` konulmalı. +2. **Data Fetching:** + - Statik `src/data/*.ts` dosyaları silinmeli. + - Tüm sayfalarda `fetch` kullanılarak (veya Axios) ISR yapısıyla veriler çekilmeli. + - *Örnek:* `const res = await fetch(\`\${process.env.NEXT_PUBLIC_API_URL}/courses\`, { next: { revalidate: 3600 } });` +3. **Form Gönderimi:** Hero'daki form submit edildiğinde veriler `POST /api/v1/leads` ucuna JSON olarak yollanmalı. + +--- + +## 6. Dinamik Sayfalar İçin Headless Page Builder (`/page/` Rotası) + +> **Kapsam Notu:** Page Builder sistemi **yalnızca** `/page/` rotası altında oluşturulan dinamik sayfalar için kullanılacaktır. Kurumsal sayfalar (`/kurumsal/hakkimizda`, `/kurumsal/vizyon-misyon`, vb.) ve `/danismanlik`, `/egitim-rehberi`, `/sss` gibi özel tasarımlı sayfalar frontend'de statik bileşenler olarak kalacak ve verilerini kendi API endpoint'lerinden veya `settings` tablosundan çekeceklerdir. + +Admin panelden yeni landing page'ler, kampanya sayfaları veya özel içerik sayfaları oluşturmak için **Modüler Blok Sistemi** kullanılacaktır. + +### Mevcut Frontend (Boğaziçi Denizcilik) Tarafında Okunması + +**Örnek `src/app/page/[slug]/page.tsx` Kullanımı:** +```tsx +export default async function DynamicPage({ params }: { params: { slug: string } }) { + // 1. Laravel'den sayfanın aktif bloklarını çek + const page = await fetch(`.../api/v1/pages/${params.slug}`).then(r => r.json()); + + return ( +
+ {/* 2. Gelen blokları (Lego parçalarını) sırası ve tipine göre render et */} + {page.blocks.map((block) => { + switch (block.type) { + case 'hero': + return ; + + case 'stats_grid': + return ; + + case 'text_image': + return ; + + default: + return null; + } + })} +
+ ); +} +``` + +> **Özet Tavsiye:** Geliştirme süresi uzasa da, bir kere kurulduğunda kodlamaya bir daha dokunmadan şirketin sayısız ve benzersiz tasarımda yeni sayfalar veya landing page'ler üretmesini sağlar. Endüstri standardı bir çözümdür. + +--- + +## 7. SEO & Yapısal Veri (Structured Data) Standartları + +Sitenin arama motorlarında üst sıralarda yer alabilmesi için backend API'nin aşağıdaki SEO gereksinimlerini eksiksiz karşılaması gerekmektedir. + +### 7.1. Meta Alanları (Her Modülde Zorunlu) + +Aşağıdaki tablolarda `meta_title` ve `meta_description` alanları mevcuttur ve **zorunlu olarak** doldurulmalıdır: +- `categories` — ✅ `meta_title`, `meta_description` +- `courses` — ✅ `meta_title`, `meta_description` +- `announcements` — ✅ `meta_title`, `meta_description` + +> **Kural:** Admin panelde bu alanlar boş bırakıldığında, `title` veya `excerpt` alanından otomatik üretilmelidir (Frontend veya Backend tarafında fallback mantığı ile). + +### 7.2. OpenGraph Görsel Desteği + +Sosyal medyada paylaşıldığında güzel bir önizleme kartı oluşturabilmek için: +- `courses.image` ve `announcements.image` alanları aynı zamanda **OG Image** olarak kullanılacaktır. +- Frontend'de `generateMetadata` fonksiyonunda bu görseller `openGraph.images` altına eklenmelidir. +- Görseller **minimum 1200x630px** boyutunda olmalıdır. Admin panelde resim yüklerken bu uyarı gösterilmelidir. + +### 7.3. JSON-LD Yapısal Veri Şemaları (Schema.org) + +Google Rich Snippets, Knowledge Panel ve zengin arama sonuçları için aşağıdaki JSON-LD blokları frontend'de render edilmelidir: + +#### Ana Sayfa (`/`) +```json +{ + "@context": "https://schema.org", + "@type": "EducationalOrganization", + "name": "Boğaziçi Denizcilik", + "url": "https://www.bogazicidenizcilik.com.tr", + "logo": "...", + "description": "...", + "address": { "@type": "PostalAddress", ... }, + "contactPoint": { "@type": "ContactPoint", ... }, + "sameAs": ["instagram", "facebook", "youtube", ...] +} +``` +*(Mevcut frontend'de zaten var ✅ — Veriler settings API'den çekilmeli)* + +#### Eğitim Detay Sayfası (`/egitimler/{kategori}/{slug}`) +```json +{ + "@context": "https://schema.org", + "@type": "Course", + "name": "STCW Temel Güvenlik (BST)", + "description": "...", + "provider": { + "@type": "Organization", + "name": "Boğaziçi Denizcilik", + "sameAs": "https://www.bogazicidenizcilik.com.tr" + }, + "hasCourseInstance": { + "@type": "CourseInstance", + "courseMode": "onsite", + "duration": "P5D", + "location": { "@type": "Place", "name": "Kadıköy, İstanbul" } + } +} +``` +> **Backend Notu:** API'den `GET /api/v1/courses/{slug}` response'unda `json_ld` alanı opsiyonel olarak döndürülebilir veya frontend tarafında oluşturulabilir. + +#### Duyuru Detay Sayfası (`/duyurular/{slug}`) +```json +{ + "@context": "https://schema.org", + "@type": "NewsArticle", + "headline": "...", + "datePublished": "2026-02-15", + "author": { "@type": "Organization", "name": "Boğaziçi Denizcilik" }, + "image": "...", + "publisher": { "@type": "Organization", "name": "Boğaziçi Denizcilik", "logo": "..." } +} +``` + +#### SSS Sayfası (`/sss`) — **FAQPage Schema (Çok Önemli)** +```json +{ + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": [ + { + "@type": "Question", + "name": "Eğitimleriniz hangi kurumlar tarafından onaylıdır?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Tüm eğitimlerimiz Ulaştırma ve Altyapı Bakanlığı onaylıdır..." + } + } + ] +} +``` +> ⚠️ **Google FAQPage Rich Result**: SSS sayfasında bu şema kullanılırsa arama sonuçlarında soru-cevaplar **doğrudan** gösterilir. Bu `GET /api/v1/faqs` endpoint'inden dönen verilerle frontend'de dinamik olarak oluşturulmalıdır. + +#### Tüm Sayfalarda — **BreadcrumbList Schema** +```json +{ + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": [ + { "@type": "ListItem", "position": 1, "name": "Anasayfa", "item": "https://..." }, + { "@type": "ListItem", "position": 2, "name": "Eğitimler", "item": "https://.../egitimler" }, + { "@type": "ListItem", "position": 3, "name": "STCW Temel Güvenlik" } + ] +} +``` + +### 7.4. Dinamik Sitemap (API Destekli) + +Mevcut `sitemap.ts` statik datayla çalışmaktadır. Backend hazır olduğunda: +- `GET /api/v1/sitemap-data` → Tüm public slug'ları, `updated_at` tarihleriyle döndüren özel endpoint. Frontend `sitemap.ts` bunu kullanarak dinamik sitemap oluşturacak. +- Sitemap `lastModified` alanları gerçek `updated_at` tarihlerinden beslenmelidir (şu an tüm sayfalarda `new Date()` kullanılıyor). +- Yeni kurs/duyuru/sayfa eklendiğinde sitemap otomatik olarak güncellenecektir. + +**Önerilen endpoint response yapısı:** +```json +{ + "courses": [{ "slug": "stcw-temel-guvenlik", "kategori": "stcw", "updated_at": "2026-02-15" }], + "announcements": [{ "slug": "kayitlar-basladi", "updated_at": "2026-02-10" }], + "pages": [{ "slug": "kampanya-bahar-2026", "updated_at": "2026-02-01" }] +} +``` + +### 7.5. Canonical URL'ler + +- Tüm sayfalarda `` bulunmalıdır. Mevcut frontend'de `alternates.canonical` zaten desteklenmekte. ✅ +- Backend API'den gelen slug'lar ile frontend URL yapısı **birebir eşleşmelidir**. +- `courses` slug'ları: `/egitimler/{kategoriSlug}/{courseSlug}` +- `announcements` slug'ları: `/duyurular/{slug}` + +### 7.6. Hızlı SEO Kontrol Listesi (Checklist) + +| Öğe | Durum | Açıklama | +|---|---|---| +| `` tag — Her sayfada benzersiz | ✅ | `generateMetadata` ile dinamik | +| `<meta description>` — Her sayfada benzersiz | ✅ | DB'den `meta_description` alanı | +| `<link rel="canonical">` | ✅ | `alternates.canonical` ile | +| OpenGraph tags (og:title, og:description, og:image) | ✅ | Dinamik sayfalarda var | +| Twitter Card tags | ✅ | Layout'ta global tanımlı | +| `robots.txt` | ✅ | `/api/` bloklanmış | +| XML Sitemap | ✅ | Dinamik hale getirilecek (7.4) | +| JSON-LD EducationalOrganization | ✅ | Ana sayfada mevcut | +| JSON-LD Course | ⚠️ | Detay sayfasına eklenmeli | +| JSON-LD NewsArticle | ⚠️ | Duyuru detay sayfasına eklenmeli | +| JSON-LD FAQPage | ❌ | SSS sayfasına eklenmeli | +| JSON-LD BreadcrumbList | ❌ | Tüm sayfalara eklenmeli | +| `<html lang="tr">` | ✅ | Layout'ta mevcut | +| Semantic HTML (h1, h2, article, nav) | ✅ | Tüm sayfalarda uygulanmış | +| Image alt attributes | ✅ | Görsellerde title kullanılıyor | +| Lazy loading images | ✅ | İlk 6 eager, geri kalan lazy | +| `generateStaticParams` (SSG) | ✅ | Tüm dinamik rotalarda var | +| ISR / Revalidate | ⚠️ | API entegrasyonunda `revalidate` eklenecek | + +> **Öncelik Sırası:** FAQPage schema → BreadcrumbList schema → Course JSON-LD → NewsArticle JSON-LD → Dinamik Sitemap + +--- + +## 8. Backend Uygulama Durumu (Implementation Status) + +> *Bu bölüm, yukarıdaki gereksinimlere göre Laravel backend'de yapılan tüm implementasyonları belgelemektedir.* + +### 8.1. Veritabanı & Model Katmanı + +| Bileşen | Sayı | Durum | Detay | +|---------|------|-------|-------| +| Migration | 22 | ✅ | users, cache, jobs, permission_tables, activity_log (×3), personal_access_tokens, soft_deletes_users, categories, courses, course_schedules, announcements, hero_slides, leads, menus, comments, faqs, guide_cards, settings, pages, page_blocks | +| Model | 14 | ✅ | User, Category, Course, CourseSchedule, Announcement, HeroSlide, Lead, Menu, Comment, Faq, GuideCard, Setting, Page, PageBlock | +| Factory | 13 | ✅ | Tüm modeller için (User dahil) | +| Enum | 8 | ✅ | AnnouncementCategory, FaqCategory, LeadSource, LeadStatus, MenuLocation, MenuType, SettingGroup, SettingType | +| Seeder | 5 | ✅ | DatabaseSeeder, RolePermissionSeeder, AdminUserSeeder, SettingSeeder, RoleSeeder (artık kullanılmıyor) | + +### 8.2. Katmanlı Mimari (Repository Pattern) Bileşenleri + +Dökümanın 3. bölümünde belirtilen mimari tam olarak uygulanmıştır: + +``` +Route ➔ Middleware (Auth) ➔ Policy (Yetki) ➔ FormRequest (Validasyon) ➔ Controller ➔ DTO ➔ Action ➔ Repository (Interface) ➔ Model ➔ DB +``` + +| Katman | Sayı | Kapsam | +|--------|------|--------| +| Repository Interface | 13 | BaseRepositoryInterface + 12 modül | +| Repository Eloquent | 13 | BaseRepository + 12 modül | +| DTO (Data Transfer Object) | 11 | Tüm CRUD modülleri | +| Action (Service) | 34 | Create/Update/Delete × 11 modül + UpdateSettingsAction | +| FormRequest | 25 | Store/Update × 11 modül + LoginRequest + ReorderMenuRequest + UpdateSettingsRequest | +| API Resource | 13 | Tüm modüller + PageBlockResource | +| Controller | 28 | 13 Public (V1) + 15 Admin | +| Policy | 11 | Category, Course, CourseSchedule, Announcement, HeroSlide, Lead, Menu, Comment, Faq, GuideCard, Page | + +### 8.3. Yetkilendirme & Permission Sistemi (Spatie Permission v7) + +#### Roller +| Rol | Açıklama | +|-----|----------| +| `super-admin` | Tüm 48 permission'a sahip | +| `editor` | Silme (`delete-*`) hariç 36 permission'a sahip | + +#### Permission Yapısı (12 Modül × 4 Aksiyon = 48 Permission) + +| Modül | `view-*` | `create-*` | `update-*` | `delete-*` | +|-------|----------|------------|------------|------------| +| category | ✅ | ✅ | ✅ | ✅ | +| course | ✅ | ✅ | ✅ | ✅ | +| schedule | ✅ | ✅ | ✅ | ✅ | +| announcement | ✅ | ✅ | ✅ | ✅ | +| hero-slide | ✅ | ✅ | ✅ | ✅ | +| lead | ✅ | ✅ | ✅ | ✅ | +| menu | ✅ | ✅ | ✅ | ✅ | +| comment | ✅ | ✅ | ✅ | ✅ | +| faq | ✅ | ✅ | ✅ | ✅ | +| guide-card | ✅ | ✅ | ✅ | ✅ | +| setting | ✅ | ✅ | ✅ | ✅ | +| page | ✅ | ✅ | ✅ | ✅ | + +> **Not:** `editor` rolü `delete-*` permission'larından hariç tutulmuştur. Tüm policy dosyaları `hasPermissionTo()` ile çalışmaktadır (rol kontrolü değil, permission kontrolü). + +### 8.4. API Rotaları + +Toplam **97 rota** kayıtlıdır. + +#### Public API — `GET /api/v1/...` + +| Endpoint | Controller | Açıklama | +|----------|-----------|----------| +| `GET /api/v1/categories` | CategoryController@index | Kategorileri listeler | +| `GET /api/v1/categories/{slug}` | CategoryController@show | Tekil kategori | +| `GET /api/v1/courses` | CourseController@index | Eğitimleri listeler | +| `GET /api/v1/courses/{slug}` | CourseController@show | Tekil eğitim detayı | +| `GET /api/v1/schedules` | ScheduleController@index | Takvim eğitimleri | +| `GET /api/v1/schedules/upcoming` | ScheduleController@upcoming | Yaklaşan eğitimler | +| `GET /api/v1/schedules/{id}` | ScheduleController@show | Tekil takvim kaydı | +| `GET /api/v1/announcements` | AnnouncementController@index | Duyurular | +| `GET /api/v1/announcements/{slug}` | AnnouncementController@show | Tekil duyuru | +| `GET /api/v1/hero-slides` | HeroSlideController@index | Aktif slider | +| `POST /api/v1/leads` | LeadController@store | Ön kayıt formu (Rate Limit: 3/dk) | +| `GET /api/v1/comments/{type}/{id}` | CommentController@index | Onaylı yorumlar | +| `POST /api/v1/comments` | CommentController@store | Yeni yorum (Rate Limit: 3/dk) | +| `GET /api/v1/menus/{location}` | MenuController@index | Menü ağacı | +| `GET /api/v1/faqs` | FaqController@index | SSS listesi | +| `GET /api/v1/faqs/{category}` | FaqController@index | Kategoriye göre SSS | +| `GET /api/v1/guide-cards` | GuideCardController@index | Rehber kartları | +| `GET /api/v1/settings` | SettingController@index | Public ayarlar (`is_public=true`) | +| `GET /api/v1/pages/{slug}` | PageController@show | Dinamik sayfa (Page Builder) | +| `GET /api/v1/sitemap-data` | SitemapController@index | Sitemap verileri | + +#### Admin API — `POST|GET|PUT|DELETE /api/admin/...` (Sanctum Auth) + +| Endpoint | Controller | Açıklama | +|----------|-----------|----------| +| `POST /api/admin/login` | AuthController@login | Giriş → Bearer Token | +| `GET /api/admin/me` | AuthController@me | Giriş yapan admin bilgisi | +| `POST /api/admin/logout` | AuthController@logout | Çıkış | +| `POST /api/admin/uploads` | UploadController@store | Dosya yükleme | +| `CRUD /api/admin/categories` | AdminCategoryController | apiResource | +| `CRUD /api/admin/courses` | AdminCourseController | apiResource | +| `CRUD /api/admin/schedules` | AdminScheduleController | apiResource | +| `CRUD /api/admin/announcements` | AdminAnnouncementController | apiResource | +| `CRUD /api/admin/hero-slides` | AdminHeroSlideController | apiResource | +| `CRUD /api/admin/leads` | AdminLeadController | apiResource (store hariç) | +| `POST /api/admin/menus/reorder` | AdminMenuController@reorder | Menü sıralaması | +| `CRUD /api/admin/menus` | AdminMenuController | apiResource | +| `CRUD /api/admin/comments` | AdminCommentController | apiResource (store hariç) | +| `CRUD /api/admin/faqs` | AdminFaqController | apiResource | +| `CRUD /api/admin/guide-cards` | AdminGuideCardController | apiResource | +| `GET /api/admin/settings` | AdminSettingController@index | Tüm ayarlar | +| `GET /api/admin/settings/group/{group}` | AdminSettingController@group | Grup bazlı | +| `PUT /api/admin/settings` | AdminSettingController@update | Toplu güncelleme | +| `CRUD /api/admin/pages` | AdminPageController | apiResource | +| `POST /api/admin/pages/{page}/blocks/reorder` | AdminBlockController@reorder | Blok sıralaması | +| `CRUD /api/admin/pages/{page}/blocks` | AdminBlockController | Nested apiResource | + +### 8.5. Cache & Event Sistemi + +| Bileşen | Dosya | Açıklama | +|---------|-------|----------| +| Event | `app/Events/ModelChanged.php` | Tüm CRUD Action'larda dispatch edilir | +| Listener | `app/Listeners/ClearModelCache.php` | ModelChanged event'ini dinler, ilgili cache key'lerini temizler | + +**Cache Uygulanan Repository'ler:** + +| Repository | Cache Key | TTL | Açıklama | +|-----------|-----------|-----|----------| +| SettingRepository | `settings:public` | 1 saat | Public ayarlar | +| HeroSlideRepository | `hero_slides:active` | 1 saat | Aktif slider | +| GuideCardRepository | `guide_cards:active` | 1 saat | Aktif rehber kartları | +| MenuRepository | `menus:{location}` | 1 saat | Lokasyona göre menü | +| FaqRepository | `faqs:{category}` | 1 saat | Kategoriye göre SSS | + +> Admin panelden veri güncellendiğinde `ModelChanged` event'i fırlatılır ve `ClearModelCache` listener ilgili cache key'lerini otomatik olarak temizler. + +### 8.6. Güvenlik & Middleware + +| Özellik | Durum | Detay | +|---------|-------|-------| +| Sanctum Token Auth | ✅ | Admin API koruması (`auth:sanctum`) | +| Rate Limiting | ✅ | `leads`: 3/dk, `comments`: 3/dk (IP bazlı) | +| Global API Throttle | ✅ | 60 istek/dk (`bootstrap/app.php`) | +| Stateful API | ✅ | SPA oturumu için | +| CORS | ✅ | `max_age: 86400` | +| 404 Handler | ✅ | API isteklerinde JSON hata dönüşü | +| SoftDeletes | ✅ | users, categories, courses, leads, comments | +| Spatie Activity Log | ✅ | `spatie/laravel-activitylog` kurulu | + +### 8.7. Swagger / OpenAPI Dökümantasyonu + +- **Paket:** `darkaonline/l5-swagger v10.1.0` +- **Erişim:** `GET /api/documentation` +- **Ana Annotation Dosyası:** `app/Http/Controllers/SwaggerAnnotations.php` +- **Toplam Path:** 45+ endpoint, 77+ operasyon +- Tüm controller'larda `@OA\Get`, `@OA\Post`, `@OA\Put`, `@OA\Delete` annotation'ları mevcuttur. + +### 8.8. Teknik Altyapı + +| Paket | Versiyon | Kullanım | +|-------|----------|----------| +| PHP | 8.4 | Runtime | +| Laravel Framework | v12 | Ana framework | +| Laravel Sanctum | v4 | Token auth | +| Spatie Permission | v7.2 | RBAC (48 permission, 2 rol) | +| Spatie Activity Log | v4.12 | Audit log | +| darkaonline/l5-swagger | v10.1 | API dökümantasyonu | +| Pest | v4 | Test framework | +| Laravel Pint | v1 | Kod formatlama | +| Laravel Herd | - | Yerel sunucu | + +### 8.9. Dosya Özeti + +| Kategori | Sayı | +|----------|------| +| Migration | 22 | +| Model | 14 | +| Enum | 8 | +| Controller | 28 (+2 base/swagger) | +| FormRequest | 25 | +| API Resource | 13 | +| Repository Interface | 13 | +| Repository Eloquent | 13 | +| Action | 34 | +| DTO | 11 | +| Policy | 11 | +| Event | 1 | +| Listener | 1 | +| Seeder | 5 | +| Factory | 13 | +| **Toplam** | **~214 dosya** | + diff --git a/boost.json b/boost.json new file mode 100644 index 0000000..509bcb1 --- /dev/null +++ b/boost.json @@ -0,0 +1,19 @@ +{ + "agents": [ + "junie", + "claude_code", + "codex", + "cursor", + "gemini", + "copilot", + "opencode" + ], + "guidelines": true, + "herd_mcp": true, + "mcp": true, + "nightwatch_mcp": false, + "sail": false, + "skills": [ + "pest-testing" + ] +} diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..767b791 --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,30 @@ +<?php + +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Configuration\Exceptions; +use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Http\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +return Application::configure(basePath: dirname(__DIR__)) + ->withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + apiPrefix: 'api', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->statefulApi(); + + $middleware->throttleApi('60,1'); + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->render(function (NotFoundHttpException $e, Request $request) { + if ($request->is('api/*')) { + return response()->json([ + 'message' => 'Kayıt bulunamadı.', + ], 404); + } + }); + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..fc94ae6 --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,7 @@ +<?php + +use App\Providers\AppServiceProvider; + +return [ + AppServiceProvider::class, +]; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c09bf51 --- /dev/null +++ b/composer.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://getcomposer.org/schema.json", + "name": "laravel/laravel", + "type": "project", + "description": "The skeleton application for the Laravel framework.", + "keywords": [ + "laravel", + "framework" + ], + "license": "MIT", + "require": { + "php": "^8.2", + "darkaonline/l5-swagger": "^10.0", + "laravel/framework": "^12.0", + "laravel/sanctum": "^4.0", + "laravel/tinker": "^2.10.1", + "spatie/laravel-activitylog": "^4.12", + "spatie/laravel-permission": "^7.2" + }, + "require-dev": { + "fakerphp/faker": "^1.23", + "laravel/boost": "^2.0", + "laravel/pail": "^1.2.2", + "laravel/pint": "^1.24", + "laravel/sail": "^1.41", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^8.6", + "pestphp/pest": "^4.4", + "pestphp/pest-plugin-laravel": "^4.1" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "setup": [ + "composer install", + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"", + "@php artisan key:generate", + "@php artisan migrate --force", + "npm install", + "npm run build" + ], + "dev": [ + "Composer\\Config::disableProcessTimeout", + "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" + ], + "test": [ + "@php artisan config:clear --ansi", + "@php artisan test" + ], + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force", + "@php artisan boost:update --ansi" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi", + "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", + "@php artisan migrate --graceful --ansi" + ], + "pre-package-uninstall": [ + "Illuminate\\Foundation\\ComposerScripts::prePackageUninstall" + ] + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..d4b3967 --- /dev/null +++ b/composer.lock @@ -0,0 +1,10241 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "39e5c9c4ab303c58ebfe6b5dffe10666", + "packages": [ + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "darkaonline/l5-swagger", + "version": "10.1.0", + "source": { + "type": "git", + "url": "https://github.com/DarkaOnLine/L5-Swagger.git", + "reference": "62582008f851bdcda40d27898a1ec609da9e509c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DarkaOnLine/L5-Swagger/zipball/62582008f851bdcda40d27898a1ec609da9e509c", + "reference": "62582008f851bdcda40d27898a1ec609da9e509c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^12.1 || ^11.44", + "php": "^8.2", + "swagger-api/swagger-ui": ">=5.18.3", + "symfony/yaml": "^5.0 || ^6.0 || ^7.0", + "zircote/swagger-php": "^6.0" + }, + "require-dev": { + "mockery/mockery": "1.*", + "orchestra/testbench": "^10.0 || ^9.0 || ^8.0 || 7.* || ^6.15 || 5.*", + "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "L5Swagger": "L5Swagger\\L5SwaggerFacade" + }, + "providers": [ + "L5Swagger\\L5SwaggerServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "L5Swagger\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Darius Matulionis", + "email": "darius@matulionis.lt" + } + ], + "description": "OpenApi or Swagger integration to Laravel", + "keywords": [ + "api", + "documentation", + "laravel", + "openapi", + "specification", + "swagger", + "ui" + ], + "support": { + "issues": "https://github.com/DarkaOnLine/L5-Swagger/issues", + "source": "https://github.com/DarkaOnLine/L5-Swagger/tree/10.1.0" + }, + "funding": [ + { + "url": "https://github.com/DarkaOnLine", + "type": "github" + } + ], + "time": "2026-01-16T06:31:36+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.54.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/325497463e7599cd14224c422c6e5dd2fe832868", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-03-10T20:25:56+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.14", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.14" + }, + "time": "2026-03-01T09:02:38+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-02-07T17:19:31+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-20T19:59:49+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "84b1ca48347efdbe775426f108622a42735a6579" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", + "reference": "84b1ca48347efdbe775426f108622a42735a6579", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-05T21:37:03+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.32.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" + }, + "time": "2026-02-25T17:01:41+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-03-11T17:23:39+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.3" + }, + "time": "2026-02-13T03:05:33+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.21", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.21" + }, + "time": "2026-03-06T21:21:28+00:00" + }, + { + "name": "radebatz/type-info-extras", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DerManoMann/type-info-extras.git", + "reference": "95a524a74a61648b44e355cb33d38db4b17ef5ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DerManoMann/type-info-extras/zipball/95a524a74a61648b44e355cb33d38db4b17ef5ce", + "reference": "95a524a74a61648b44e355cb33d38db4b17ef5ce", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "phpstan/phpdoc-parser": "^2.0", + "symfony/type-info": "^7.3.8 || ^7.4.1 || ^8.0 || ^8.1-@dev" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.70", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Radebatz\\TypeInfoExtras\\": "src" + }, + "exclude-from-classmap": [ + "/tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Martin Rademacher", + "email": "mano@radebatz.org" + } + ], + "description": "Extras for symfony/type-info", + "homepage": "http://radebatz.net/mano/", + "keywords": [ + "component", + "symfony", + "type-info", + "types" + ], + "support": { + "issues": "https://github.com/DerManoMann/type-info-extras/issues", + "source": "https://github.com/DerManoMann/type-info-extras/tree/1.0.7" + }, + "time": "2026-03-06T22:40:29+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "spatie/laravel-activitylog", + "version": "4.12.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "bf66b5bbe9a946e977e876420d16b30b9aff1b2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/bf66b5bbe9a946e977e876420d16b30b9aff1b2d", + "reference": "bf66b5bbe9a946e977e876420d16b30b9aff1b2d", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.6 || ^10.0 || ^11.0", + "pestphp/pest": "^1.20 || ^2.0 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev.gummibeer@gmail.com", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "A very simple activity logger to monitor the users of your website or application", + "homepage": "https://github.com/spatie/activitylog", + "keywords": [ + "activity", + "laravel", + "log", + "spatie", + "user" + ], + "support": { + "issues": "https://github.com/spatie/laravel-activitylog/issues", + "source": "https://github.com/spatie/laravel-activitylog/tree/4.12.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-22T08:37:18+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.93.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-21T12:49:54+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "7.2.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "062b0cd8e3a1753fa7a53e468b918710004aa06b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/062b0cd8e3a1753fa7a53e468b918710004aa06b", + "reference": "062b0cd8e3a1753fa7a53e468b918710004aa06b", + "shasum": "" + }, + "require": { + "illuminate/auth": "^12.0|^13.0", + "illuminate/container": "^12.0|^13.0", + "illuminate/contracts": "^12.0|^13.0", + "illuminate/database": "^12.0|^13.0", + "php": "^8.4", + "spatie/laravel-package-tools": "^1.0" + }, + "require-dev": { + "larastan/larastan": "^3.9", + "laravel/passport": "^13.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^10.0|^11.0", + "pestphp/pest": "^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.1", + "phpstan/phpstan": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "7.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 12 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/7.2.3" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-23T20:30:07+00:00" + }, + { + "name": "swagger-api/swagger-ui", + "version": "v5.32.0", + "source": { + "type": "git", + "url": "https://github.com/swagger-api/swagger-ui.git", + "reference": "1b33a5976d8f5e02d3bc40de1deae3a4a3642e36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/1b33a5976d8f5e02d3bc40de1deae3a4a3642e36", + "reference": "1b33a5976d8f5e02d3bc40de1deae3a4a3642e36", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Anna Bodnia", + "email": "anna.bodnia@gmail.com" + }, + { + "name": "Buu Nguyen", + "email": "buunguyen@gmail.com" + }, + { + "name": "Josh Ponelat", + "email": "jponelat@gmail.com" + }, + { + "name": "Kyle Shockey", + "email": "kyleshockey1@gmail.com" + }, + { + "name": "Robert Barnwell", + "email": "robert@robertismy.name" + }, + { + "name": "Sahar Jafari", + "email": "shr.jafari@gmail.com" + } + ], + "description": " Swagger UI is a collection of HTML, Javascript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.", + "homepage": "http://swagger.io", + "keywords": [ + "api", + "documentation", + "openapi", + "specification", + "swagger", + "ui" + ], + "support": { + "issues": "https://github.com/swagger-api/swagger-ui/issues", + "source": "https://github.com/swagger-api/swagger-ui/tree/v5.32.0" + }, + "time": "2026-02-27T11:52:15+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:46:48+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T14:06:20+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "2a178bf80f05dbbe469a337730eba79d61315262" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262", + "reference": "2a178bf80f05dbbe469a337730eba79d61315262", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T13:07:04+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-20T16:42:42+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:55+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:40:50+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T13:15:18+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T16:33:18+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:50:00+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-05T15:24:09+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:07:59+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:50:00+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T10:14:57+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T13:07:04+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/type-info", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/3c7de103dd6cb68be24e155838a64ef4a70ae195", + "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-04T13:55:34+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-03T23:30:35+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-15T10:53:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T09:33:46+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "6.0.6", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "9447c1f45b5ae93185caea9a0c8e298399188a25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/9447c1f45b5ae93185caea9a0c8e298399188a25", + "reference": "9447c1f45b5ae93185caea9a0c8e298399188a25", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/php-parser": "^4.19 || ^5.0", + "php": ">=8.2", + "phpstan/phpdoc-parser": "^2.0", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "radebatz/type-info-extras": "^1.0.2", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "conflict": { + "symfony/process": ">=6, <6.4.14" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^2.0", + "friendsofphp/php-cs-fixer": "^3.62.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5", + "rector/rector": "^2.3.1" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "https://bfanger.nl" + }, + { + "name": "Martin Rademacher", + "email": "mano@radebatz.net", + "homepage": "https://radebatz.net" + } + ], + "description": "Generate interactive documentation for your RESTful API using PHP attributes (preferred) or PHPDoc annotations", + "homepage": "https://github.com/zircote/swagger-php", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "support": { + "issues": "https://github.com/zircote/swagger-php/issues", + "source": "https://github.com/zircote/swagger-php/tree/6.0.6" + }, + "funding": [ + { + "url": "https://github.com/zircote", + "type": "github" + } + ], + "time": "2026-02-28T06:42:58+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.19.0", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.9 || ^13", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/process": "^7.4.5 || ^8.0.5" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.38", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.12", + "phpstan/phpstan-strict-rules": "^2.0.8", + "symfony/filesystem": "^7.4.0 || ^8.0.1" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2026-02-06T10:53:26+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "laravel/boost", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/ba0a9e6497398b6ce8243f5517b67d6761509150", + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.5.1|^0.6.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^0.5.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.27.0", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2026-03-12T09:06:47+00:00" + }, + { + "name": "laravel/mcp", + "version": "v0.6.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/f696e44735b95ff275392eab8ce5a3b4b42a2223", + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.2.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2026-03-10T20:00:23+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-03-12T15:51:39+00:00" + }, + { + "name": "laravel/roster", + "version": "v0.5.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2026-03-05T07:58:43+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.53.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-02-06T12:16:02+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.2", + "laravel/framework": "^11.48.0 || ^12.52.0", + "laravel/pint": "^1.27.1", + "orchestra/testbench-core": "^9.12.0 || ^10.9.0", + "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-02-17T17:33:08+00:00" + }, + { + "name": "pestphp/pest", + "version": "v4.4.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", + "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.19.0", + "nunomaduro/collision": "^8.9.1", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.12", + "symfony/process": "^7.4.5|^8.0.5" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.12", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.0", + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.21" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.4.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-03-10T21:09:12+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" + }, + "require-dev": { + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.5" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-20T13:10:51+00:00" + }, + { + "name": "pestphp/pest-plugin-laravel", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-laravel.git", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", + "shasum": "" + }, + "require": { + "laravel/framework": "^11.45.2|^12.52.0|^13.0", + "pestphp/pest": "^4.4.1", + "php": "^8.3.0" + }, + "require-dev": { + "laravel/dusk": "^8.3.6", + "orchestra/testbench": "^9.13.0|^10.9.0|^11.0", + "pestphp/pest-dev-tools": "^4.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Laravel\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Pest\\Laravel\\PestServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Laravel Plugin", + "keywords": [ + "framework", + "laravel", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-02-21T00:29:45+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.2" + }, + "time": "2026-03-01T18:43:49+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-02-06T06:01:44+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/418e06b3b46b0d54bad749ff4907fc7dfb530199", + "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.12" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-16T08:34:36+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:28:48+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-03-15T07:05:40+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.7", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" + }, + "time": "2026-02-17T17:25:14+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.1.6" + }, + "time": "2026-02-27T10:28:38+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..7321131 --- /dev/null +++ b/config/app.php @@ -0,0 +1,128 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Application Name + |-------------------------------------------------------------------------- + | + | This value is the name of your application, which will be used when the + | framework needs to place the application's name in a notification or + | other UI elements where an application name needs to be displayed. + | + */ + + 'name' => env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'), + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/config/auth.php @@ -0,0 +1,117 @@ +<?php + +use App\Models\User; + +return [ + + /* + |-------------------------------------------------------------------------- + | Authentication Defaults + |-------------------------------------------------------------------------- + | + | This option defines the default authentication "guard" and password + | reset "broker" for your application. You may change these values + | as required, but they're a perfect start for most applications. + | + */ + + 'defaults' => [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/config/cache.php @@ -0,0 +1,117 @@ +<?php + +use Illuminate\Support\Str; + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Cache Store + |-------------------------------------------------------------------------- + | + | This option controls the default cache store that will be used by the + | framework. This connection is utilized if another isn't explicitly + | specified when running a cache operation inside the application. + | + */ + + 'default' => env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..0e5ddd3 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,36 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Cross-Origin Resource Sharing (CORS) Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your settings for cross-origin resource sharing + | or "CORS". This determines what cross-origin operations may execute + | in web browsers. You are free to adjust these settings as needed. + | + */ + + 'paths' => ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => [ + env('FRONTEND_URL', 'http://localhost:3000'), + env('ADMIN_URL', 'http://localhost:3001'), + 'http://localhost:5173', + ], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 86400, + + 'supports_credentials' => true, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..64709ce --- /dev/null +++ b/config/database.php @@ -0,0 +1,184 @@ +<?php + +use Illuminate\Support\Str; +use Pdo\Mysql; + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Database Connection Name + |-------------------------------------------------------------------------- + | + | Here you may specify which of the database connections below you wish + | to use as your default connection for database operations. This is + | the connection which will be utilized unless another connection + | is explicitly specified when you execute a query / statement. + | + */ + + 'default' => env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,80 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Filesystem Disk + |-------------------------------------------------------------------------- + | + | Here you may specify the default filesystem disk that should be used + | by the framework. The "local" disk, as well as a variety of cloud + | based disks are available to your application for file storage. + | + */ + + 'default' => env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/l5-swagger.php b/config/l5-swagger.php new file mode 100644 index 0000000..d2f8689 --- /dev/null +++ b/config/l5-swagger.php @@ -0,0 +1,305 @@ +<?php + +use L5Swagger\Generator; +use OpenApi\scan; + +return [ + 'default' => 'default', + 'documentations' => [ + 'default' => [ + 'api' => [ + 'title' => 'Boğaziçi Denizcilik API', + ], + + 'routes' => [ + /* + * Route for accessing api documentation interface + */ + 'api' => 'api/documentation', + ], + 'paths' => [ + /* + * Edit to include full URL in ui for assets + */ + 'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true), + + /* + * Edit to set path where swagger ui assets should be stored + */ + 'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'), + + /* + * File name of the generated json documentation file + */ + 'docs_json' => 'api-docs.json', + + /* + * File name of the generated YAML documentation file + */ + 'docs_yaml' => 'api-docs.yaml', + + /* + * Set this to `json` or `yaml` to determine which documentation file to use in UI + */ + 'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'), + + /* + * Absolute paths to directory containing the swagger annotations are stored. + */ + 'annotations' => [ + base_path('app'), + ], + ], + ], + ], + 'defaults' => [ + 'routes' => [ + /* + * Route for accessing parsed swagger annotations. + */ + 'docs' => 'docs', + + /* + * Route for Oauth2 authentication callback. + */ + 'oauth2_callback' => 'api/oauth2-callback', + + /* + * Middleware allows to prevent unexpected access to API documentation + */ + 'middleware' => [ + 'api' => [], + 'asset' => [], + 'docs' => [], + 'oauth2_callback' => [], + ], + + /* + * Route Group options + */ + 'group_options' => [], + ], + + 'paths' => [ + /* + * Absolute path to location where parsed annotations will be stored + */ + 'docs' => storage_path('api-docs'), + + /* + * Absolute path to directory where to export views + */ + 'views' => base_path('resources/views/vendor/l5-swagger'), + + /* + * Edit to set the api's base path + */ + 'base' => env('L5_SWAGGER_BASE_PATH', null), + + /* + * Absolute path to directories that should be excluded from scanning + * @deprecated Please use `scanOptions.exclude` + * `scanOptions.exclude` overwrites this + */ + 'excludes' => [], + ], + + 'scanOptions' => [ + /** + * Configuration for default processors. Allows to pass processors configuration to swagger-php. + * + * @link https://zircote.github.io/swagger-php/reference/processors.html + */ + 'default_processors_configuration' => [ + /** Example */ + /** + * 'operationId.hash' => true, + * 'pathFilter' => [ + * 'tags' => [ + * '/pets/', + * '/store/', + * ], + * ],. + */ + ], + + /** + * analyser: defaults to \OpenApi\StaticAnalyser . + * + * @see scan + */ + 'analyser' => null, + + /** + * analysis: defaults to a new \OpenApi\Analysis . + * + * @see scan + */ + 'analysis' => null, + + /** + * Custom query path processors classes. + * + * @link https://github.com/zircote/swagger-php/tree/master/Examples/processors/schema-query-parameter + * @see scan + */ + 'processors' => [ + // new \App\SwaggerProcessors\SchemaQueryParameter(), + ], + + /** + * pattern: string $pattern File pattern(s) to scan (default: *.php) . + * + * @see scan + */ + 'pattern' => null, + + /* + * Absolute path to directories that should be excluded from scanning + * @note This option overwrites `paths.excludes` + * @see \OpenApi\scan + */ + 'exclude' => [], + + /* + * Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0. + * By default the spec will be in version 3.0.0 + */ + 'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', Generator::OPEN_API_DEFAULT_SPEC_VERSION), + ], + + /* + * API security definitions. Will be generated into documentation file. + */ + 'securityDefinitions' => [ + 'securitySchemes' => [ + /* + * Examples of Security schemes + */ + /* + 'api_key_security_example' => [ // Unique name of security + 'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'A short description for security scheme', + 'name' => 'api_key', // The name of the header or query parameter to be used. + 'in' => 'header', // The location of the API key. Valid values are "query" or "header". + ], + 'oauth2_security_example' => [ // Unique name of security + 'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'A short description for oauth2 security scheme.', + 'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode". + 'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode) + //'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode) + 'scopes' => [ + 'read:projects' => 'read your projects', + 'write:projects' => 'modify projects in your account', + ] + ], + */ + + 'sanctum' => [ + 'type' => 'apiKey', + 'description' => 'Enter token in format (Bearer <token>)', + 'name' => 'Authorization', + 'in' => 'header', + ], + ], + 'security' => [ + /* + * Examples of Securities + */ + [ + /* + 'oauth2_security_example' => [ + 'read', + 'write' + ], + + 'passport' => [] + */ + ], + ], + ], + + /* + * Set this to `true` in development mode so that docs would be regenerated on each request + * Set this to `false` to disable swagger generation on production + */ + 'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', true), + + /* + * Set this to `true` to generate a copy of documentation in yaml format + */ + 'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false), + + /* + * Edit to trust the proxy's ip address - needed for AWS Load Balancer + * string[] + */ + 'proxy' => false, + + /* + * Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle. + * See more at: https://github.com/swagger-api/swagger-ui#configs-plugin + */ + 'additional_config_url' => null, + + /* + * Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically), + * 'method' (sort by HTTP method). + * Default is the order returned by the server unchanged. + */ + 'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null), + + /* + * Pass the validatorUrl parameter to SwaggerUi init on the JS side. + * A null value here disables validation. + */ + 'validator_url' => null, + + /* + * Swagger UI configuration parameters + */ + 'ui' => [ + 'display' => [ + 'dark_mode' => env('L5_SWAGGER_UI_DARK_MODE', false), + /* + * Controls the default expansion setting for the operations and tags. It can be : + * 'list' (expands only the tags), + * 'full' (expands the tags and operations), + * 'none' (expands nothing). + */ + 'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'), + + /** + * If set, enables filtering. The top bar will show an edit box that + * you can use to filter the tagged operations that are shown. Can be + * Boolean to enable or disable, or a string, in which case filtering + * will be enabled using that string as the filter expression. Filtering + * is case-sensitive matching the filter expression anywhere inside + * the tag. + */ + 'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false + ], + + 'authorization' => [ + /* + * If set to true, it persists authorization data, and it would not be lost on browser close/refresh + */ + 'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false), + + 'oauth2' => [ + /* + * If set to true, adds PKCE to AuthorizationCodeGrant flow + */ + 'use_pkce_with_authorization_code_grant' => false, + ], + ], + ], + /* + * Constants which can be used in annotations + */ + 'constants' => [ + 'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://bogazici-api.test'), + ], + ], +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..9e998a4 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ +<?php + +use Monolog\Handler\NullHandler; +use Monolog\Handler\StreamHandler; +use Monolog\Handler\SyslogUdpHandler; +use Monolog\Processor\PsrLogMessageProcessor; + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Log Channel + |-------------------------------------------------------------------------- + | + | This option defines the default log channel that is utilized to write + | messages to your logs. The value provided here should match one of + | the channels present in the list of "channels" configured below. + | + */ + + 'default' => env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..522b284 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,118 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Mailer + |-------------------------------------------------------------------------- + | + | This option controls the default mailer that is used to send all email + | messages unless another mailer is explicitly specified when sending + | the message. All additional mailers can be configured within the + | "mailers" array. Examples of each type of mailer are provided. + | + */ + + 'default' => env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..392636d --- /dev/null +++ b/config/permission.php @@ -0,0 +1,206 @@ +<?php + +use Spatie\Permission\DefaultTeamResolver; +use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; + +return [ + + 'models' => [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttachedEvent + * \Spatie\Permission\Events\RoleDetachedEvent + * \Spatie\Permission\Events\PermissionAttachedEvent + * \Spatie\Permission\Events\PermissionDetachedEvent + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/config/queue.php @@ -0,0 +1,129 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Queue Connection Name + |-------------------------------------------------------------------------- + | + | Laravel's queue supports a variety of backends via a single, unified + | API, giving you convenient access to each backend using identical + | syntax for each. The default queue connection is defined below. + | + */ + + 'default' => env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..cde73cf --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,87 @@ +<?php + +use Illuminate\Cookie\Middleware\EncryptCookies; +use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken; +use Laravel\Sanctum\Http\Middleware\AuthenticateSession; +use Laravel\Sanctum\Sanctum; + +return [ + + /* + |-------------------------------------------------------------------------- + | Stateful Domains + |-------------------------------------------------------------------------- + | + | Requests from the following domains / hosts will receive stateful API + | authentication cookies. Typically, these should include your local + | and production domains which access your API via a frontend SPA. + | + */ + + 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => AuthenticateSession::class, + 'encrypt_cookies' => EncryptCookies::class, + 'validate_csrf_token' => ValidateCsrfToken::class, + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Third Party Services + |-------------------------------------------------------------------------- + | + | This file is for storing the credentials for third party services such + | as Mailgun, Postmark, AWS and more. This file provides the de facto + | location for this type of information, allowing packages to have + | a conventional file to locate the various service credentials. + | + */ + + 'postmark' => [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..5b541b7 --- /dev/null +++ b/config/session.php @@ -0,0 +1,217 @@ +<?php + +use Illuminate\Support\Str; + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Session Driver + |-------------------------------------------------------------------------- + | + | This option determines the default session driver that is utilized for + | incoming requests. Laravel supports a variety of storage options to + | persist session data. Database storage is a great default choice. + | + | Supported: "file", "cookie", "database", "memcached", + | "redis", "dynamodb", "array" + | + */ + + 'driver' => env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/AnnouncementFactory.php b/database/factories/AnnouncementFactory.php new file mode 100644 index 0000000..7fa8b4e --- /dev/null +++ b/database/factories/AnnouncementFactory.php @@ -0,0 +1,42 @@ +<?php + +namespace Database\Factories; + +use App\Enums\AnnouncementCategory; +use App\Models\Announcement; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; + +/** + * @extends Factory<Announcement> + */ +class AnnouncementFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + $title = fake()->unique()->sentence(4); + + return [ + 'slug' => Str::slug($title), + 'title' => $title, + 'category' => fake()->randomElement(AnnouncementCategory::cases()), + 'excerpt' => fake()->paragraph(), + 'content' => fake()->paragraphs(5, true), + 'image' => null, + 'is_featured' => fake()->boolean(20), + 'meta_title' => fake()->sentence(4), + 'meta_description' => fake()->sentence(10), + 'published_at' => fake()->dateTimeBetween('-6 months', 'now'), + ]; + } + + public function featured(): static + { + return $this->state(fn (array $attributes) => [ + 'is_featured' => true, + ]); + } +} diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php new file mode 100644 index 0000000..88ed72c --- /dev/null +++ b/database/factories/CategoryFactory.php @@ -0,0 +1,30 @@ +<?php + +namespace Database\Factories; + +use App\Models\Category; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; + +/** + * @extends Factory<Category> + */ +class CategoryFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + $label = fake()->unique()->words(2, true); + + return [ + 'slug' => Str::slug($label), + 'label' => ucfirst($label), + 'desc' => fake()->paragraph(), + 'image' => null, + 'meta_title' => fake()->sentence(4), + 'meta_description' => fake()->sentence(10), + ]; + } +} diff --git a/database/factories/CommentFactory.php b/database/factories/CommentFactory.php new file mode 100644 index 0000000..1a053fd --- /dev/null +++ b/database/factories/CommentFactory.php @@ -0,0 +1,44 @@ +<?php + +namespace Database\Factories; + +use App\Models\Comment; +use App\Models\Course; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<Comment> + */ +class CommentFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'commentable_id' => Course::factory(), + 'commentable_type' => Course::class, + 'name_surname' => fake()->name(), + 'phone' => fake()->phoneNumber(), + 'body' => fake()->paragraph(), + 'admin_reply' => null, + 'is_approved' => false, + ]; + } + + public function approved(): static + { + return $this->state(fn (array $attributes) => [ + 'is_approved' => true, + ]); + } + + public function withReply(): static + { + return $this->state(fn (array $attributes) => [ + 'admin_reply' => fake()->paragraph(), + 'is_approved' => true, + ]); + } +} diff --git a/database/factories/CourseBlockFactory.php b/database/factories/CourseBlockFactory.php new file mode 100644 index 0000000..a6bfa77 --- /dev/null +++ b/database/factories/CourseBlockFactory.php @@ -0,0 +1,27 @@ +<?php + +namespace Database\Factories; + +use App\Models\Course; +use App\Models\CourseBlock; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<CourseBlock> + */ +class CourseBlockFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'course_id' => Course::factory(), + 'type' => fake()->randomElement(['hero', 'text', 'cards', 'stats_grid', 'cta', 'faq', 'gallery']), + 'content' => ['title' => fake()->sentence()], + 'order_index' => fake()->numberBetween(0, 10), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/CourseFactory.php b/database/factories/CourseFactory.php new file mode 100644 index 0000000..76254be --- /dev/null +++ b/database/factories/CourseFactory.php @@ -0,0 +1,45 @@ +<?php + +namespace Database\Factories; + +use App\Models\Category; +use App\Models\Course; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; + +/** + * @extends Factory<Course> + */ +class CourseFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + $title = fake()->unique()->words(3, true); + + return [ + 'category_id' => Category::factory(), + 'slug' => Str::slug($title), + 'title' => ucfirst($title), + 'sub' => fake()->optional()->word(), + 'desc' => fake()->paragraph(), + 'long_desc' => fake()->paragraphs(3, true), + 'duration' => fake()->randomElement(['3 Gün', '5 Gün', '10 Gün', '2 Hafta']), + 'students' => fake()->numberBetween(0, 500), + 'rating' => fake()->randomFloat(1, 3.0, 5.0), + 'badge' => fake()->optional()->randomElement(['Simülatör', 'Online', 'Yüz Yüze']), + 'image' => null, + 'price' => fake()->optional()->randomElement(['2.500 TL', '5.000 TL', '7.500 TL', '10.000 TL']), + 'includes' => fake()->words(4), + 'requirements' => fake()->words(3), + 'meta_title' => fake()->sentence(4), + 'meta_description' => fake()->sentence(10), + 'scope' => fake()->words(5), + 'standard' => fake()->optional()->randomElement(['STCW Uyumlu', 'IMO Uyumlu', 'STCW / IMO Uyumlu']), + 'language' => 'Türkçe', + 'location' => fake()->optional()->randomElement(['Kadıköy, İstanbul', 'Tuzla, İstanbul', 'Beşiktaş, İstanbul']), + ]; + } +} diff --git a/database/factories/CourseScheduleFactory.php b/database/factories/CourseScheduleFactory.php new file mode 100644 index 0000000..f1d015e --- /dev/null +++ b/database/factories/CourseScheduleFactory.php @@ -0,0 +1,33 @@ +<?php + +namespace Database\Factories; + +use App\Models\Course; +use App\Models\CourseSchedule; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<CourseSchedule> + */ +class CourseScheduleFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + $startDate = fake()->dateTimeBetween('+1 week', '+3 months'); + $endDate = (clone $startDate)->modify('+'.fake()->numberBetween(3, 14).' days'); + $quota = fake()->numberBetween(10, 30); + + return [ + 'course_id' => Course::factory(), + 'start_date' => $startDate, + 'end_date' => $endDate, + 'location' => fake()->randomElement(['Kadıköy', 'Tuzla', 'Online']), + 'quota' => $quota, + 'available_seats' => fake()->numberBetween(0, $quota), + 'is_urgent' => fake()->boolean(20), + ]; + } +} diff --git a/database/factories/FaqFactory.php b/database/factories/FaqFactory.php new file mode 100644 index 0000000..94472a4 --- /dev/null +++ b/database/factories/FaqFactory.php @@ -0,0 +1,34 @@ +<?php + +namespace Database\Factories; + +use App\Enums\FaqCategory; +use App\Models\Faq; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<Faq> + */ +class FaqFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'category' => fake()->randomElement(FaqCategory::cases()), + 'question' => fake()->sentence().'?', + 'answer' => fake()->paragraph(), + 'order_index' => fake()->numberBetween(0, 10), + 'is_active' => true, + ]; + } + + public function inactive(): static + { + return $this->state(fn (array $attributes) => [ + 'is_active' => false, + ]); + } +} diff --git a/database/factories/GuideCardFactory.php b/database/factories/GuideCardFactory.php new file mode 100644 index 0000000..0cb76fd --- /dev/null +++ b/database/factories/GuideCardFactory.php @@ -0,0 +1,28 @@ +<?php + +namespace Database\Factories; + +use App\Models\GuideCard; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<GuideCard> + */ +class GuideCardFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'title' => fake()->words(2, true), + 'description' => fake()->paragraph(), + 'icon' => fake()->randomElement(['anchor', 'compass', 'shield', 'briefcase', 'ship', 'navigation']), + 'color' => fake()->randomElement(['from-blue-500 to-blue-700', 'from-emerald-500 to-emerald-700', 'from-orange-500 to-orange-700', 'from-rose-500 to-rose-700']), + 'link' => '/'.fake()->slug(2), + 'order' => fake()->numberBetween(0, 10), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/HeroSlideFactory.php b/database/factories/HeroSlideFactory.php new file mode 100644 index 0000000..46a1faa --- /dev/null +++ b/database/factories/HeroSlideFactory.php @@ -0,0 +1,34 @@ +<?php + +namespace Database\Factories; + +use App\Models\HeroSlide; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<HeroSlide> + */ +class HeroSlideFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'label' => fake()->words(3, true), + 'title' => fake()->sentence(5), + 'description' => fake()->paragraph(), + 'image' => null, + 'order_index' => fake()->numberBetween(0, 10), + 'is_active' => true, + ]; + } + + public function inactive(): static + { + return $this->state(fn (array $attributes) => [ + 'is_active' => false, + ]); + } +} diff --git a/database/factories/LeadFactory.php b/database/factories/LeadFactory.php new file mode 100644 index 0000000..5bcff57 --- /dev/null +++ b/database/factories/LeadFactory.php @@ -0,0 +1,44 @@ +<?php + +namespace Database\Factories; + +use App\Enums\LeadSource; +use App\Enums\LeadStatus; +use App\Models\Lead; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<Lead> + */ +class LeadFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'phone' => fake()->phoneNumber(), + 'target_course' => fake()->optional()->words(2, true), + 'education_level' => fake()->optional()->randomElement(['Lise', 'Üniversite', 'Lisans', 'Yüksek Lisans']), + 'subject' => fake()->optional()->randomElement(['stcw', 'kaptanlik', 'belge', 'diger']), + 'message' => fake()->optional()->paragraph(), + 'status' => fake()->randomElement(LeadStatus::cases()), + 'notes' => null, + 'is_read' => fake()->boolean(30), + 'source' => fake()->randomElement(LeadSource::cases()), + 'utm' => null, + 'consent_kvkk' => true, + 'consent_text_version' => 'v1.0', + ]; + } + + public function unread(): static + { + return $this->state(fn (array $attributes) => [ + 'is_read' => false, + 'status' => LeadStatus::New, + ]); + } +} diff --git a/database/factories/MenuFactory.php b/database/factories/MenuFactory.php new file mode 100644 index 0000000..65e153b --- /dev/null +++ b/database/factories/MenuFactory.php @@ -0,0 +1,30 @@ +<?php + +namespace Database\Factories; + +use App\Enums\MenuLocation; +use App\Enums\MenuType; +use App\Models\Menu; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<Menu> + */ +class MenuFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'location' => fake()->randomElement(MenuLocation::cases()), + 'label' => fake()->words(2, true), + 'url' => '/'.fake()->slug(2), + 'type' => MenuType::Link, + 'parent_id' => null, + 'order' => fake()->numberBetween(0, 10), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/PageBlockFactory.php b/database/factories/PageBlockFactory.php new file mode 100644 index 0000000..728125f --- /dev/null +++ b/database/factories/PageBlockFactory.php @@ -0,0 +1,30 @@ +<?php + +namespace Database\Factories; + +use App\Models\Page; +use App\Models\PageBlock; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<PageBlock> + */ +class PageBlockFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'page_id' => Page::factory(), + 'type' => fake()->randomElement(['hero', 'text_image', 'stats_grid', 'cta', 'gallery']), + 'content' => [ + 'title' => fake()->sentence(3), + 'text' => fake()->paragraph(), + ], + 'order_index' => fake()->numberBetween(0, 10), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/PageFactory.php b/database/factories/PageFactory.php new file mode 100644 index 0000000..1182bc4 --- /dev/null +++ b/database/factories/PageFactory.php @@ -0,0 +1,29 @@ +<?php + +namespace Database\Factories; + +use App\Models\Page; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; + +/** + * @extends Factory<Page> + */ +class PageFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + $title = fake()->unique()->words(3, true); + + return [ + 'slug' => Str::slug($title), + 'title' => ucfirst($title), + 'meta_title' => fake()->sentence(4), + 'meta_description' => fake()->sentence(10), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/StoryFactory.php b/database/factories/StoryFactory.php new file mode 100644 index 0000000..01e4e47 --- /dev/null +++ b/database/factories/StoryFactory.php @@ -0,0 +1,29 @@ +<?php + +namespace Database\Factories; + +use App\Models\Story; +use Illuminate\Database\Eloquent\Factories\Factory; + +/** + * @extends Factory<Story> + */ +class StoryFactory extends Factory +{ + /** + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'title' => fake()->sentence(4), + 'badge' => fake()->randomElement(['Tanıtım', 'Yeni Dönem', null]), + 'content' => fake()->paragraphs(2, true), + 'image' => null, + 'cta_text' => fake()->randomElement(['Detaylı Bilgi', 'Hakkımızda', null]), + 'cta_url' => fake()->randomElement(['/kurumsal/hakkimizda', '/egitimler', null]), + 'order_index' => fake()->numberBetween(0, 10), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..c4ceb07 --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,45 @@ +<?php + +namespace Database\Factories; + +use App\Models\User; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; + +/** + * @extends Factory<User> + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array<string, mixed> + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('users', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..ed758bd --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('cache', function (Blueprint $table) { + $table->string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('jobs', function (Blueprint $table) { + $table->id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/2026_03_02_081826_create_permission_tables.php b/database/migrations/2026_03_02_081826_create_permission_tables.php new file mode 100644 index 0000000..8986275 --- /dev/null +++ b/database/migrations/2026_03_02_081826_create_permission_tables.php @@ -0,0 +1,137 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + $teams = config('permission.teams'); + $tableNames = config('permission.table_names'); + $columnNames = config('permission.column_names'); + $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id'; + $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id'; + + throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'); + throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'); + + /** + * See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered. + */ + Schema::create($tableNames['permissions'], static function (Blueprint $table) { + $table->id(); // permission id + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + /** + * See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered. + */ + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + $table->id(); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->cascadeOnDelete(); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->cascadeOnDelete(); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + + Schema::dropIfExists($tableNames['role_has_permissions']); + Schema::dropIfExists($tableNames['model_has_roles']); + Schema::dropIfExists($tableNames['model_has_permissions']); + Schema::dropIfExists($tableNames['roles']); + Schema::dropIfExists($tableNames['permissions']); + } +}; diff --git a/database/migrations/2026_03_02_081827_create_activity_log_table.php b/database/migrations/2026_03_02_081827_create_activity_log_table.php new file mode 100644 index 0000000..b788f65 --- /dev/null +++ b/database/migrations/2026_03_02_081827_create_activity_log_table.php @@ -0,0 +1,27 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +class CreateActivityLogTable extends Migration +{ + public function up() + { + Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->nullableMorphs('subject', 'subject'); + $table->nullableMorphs('causer', 'causer'); + $table->json('properties')->nullable(); + $table->timestamps(); + $table->index('log_name'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); + } +} diff --git a/database/migrations/2026_03_02_081828_add_event_column_to_activity_log_table.php b/database/migrations/2026_03_02_081828_add_event_column_to_activity_log_table.php new file mode 100644 index 0000000..78d9a0e --- /dev/null +++ b/database/migrations/2026_03_02_081828_add_event_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +class AddEventColumnToActivityLogTable extends Migration +{ + public function up() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->string('event')->nullable()->after('subject_type'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('event'); + }); + } +} diff --git a/database/migrations/2026_03_02_081829_add_batch_uuid_column_to_activity_log_table.php b/database/migrations/2026_03_02_081829_add_batch_uuid_column_to_activity_log_table.php new file mode 100644 index 0000000..320ef5c --- /dev/null +++ b/database/migrations/2026_03_02_081829_add_batch_uuid_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +class AddBatchUuidColumnToActivityLogTable extends Migration +{ + public function up() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->uuid('batch_uuid')->nullable()->after('properties'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('batch_uuid'); + }); + } +} diff --git a/database/migrations/2026_03_02_081832_create_personal_access_tokens_table.php b/database/migrations/2026_03_02_081832_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2026_03_02_081832_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('personal_access_tokens', function (Blueprint $table) { + $table->id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2026_03_02_090001_add_soft_deletes_to_users_table.php b/database/migrations/2026_03_02_090001_add_soft_deletes_to_users_table.php new file mode 100644 index 0000000..a7bda58 --- /dev/null +++ b/database/migrations/2026_03_02_090001_add_soft_deletes_to_users_table.php @@ -0,0 +1,28 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('users', function (Blueprint $table) { + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2026_03_02_090002_create_categories_table.php b/database/migrations/2026_03_02_090002_create_categories_table.php new file mode 100644 index 0000000..f91cc35 --- /dev/null +++ b/database/migrations/2026_03_02_090002_create_categories_table.php @@ -0,0 +1,34 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('categories', function (Blueprint $table) { + $table->id(); + $table->string('slug')->unique(); + $table->string('label'); + $table->text('desc')->nullable(); + $table->string('image')->nullable(); + $table->string('meta_title')->nullable(); + $table->text('meta_description')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2026_03_02_090003_create_courses_table.php b/database/migrations/2026_03_02_090003_create_courses_table.php new file mode 100644 index 0000000..b5f8edb --- /dev/null +++ b/database/migrations/2026_03_02_090003_create_courses_table.php @@ -0,0 +1,44 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('courses', function (Blueprint $table) { + $table->id(); + $table->foreignId('category_id')->constrained()->cascadeOnDelete(); + $table->string('slug')->unique(); + $table->string('title'); + $table->string('sub')->nullable(); + $table->text('desc'); + $table->longText('long_desc'); + $table->string('duration'); + $table->unsignedInteger('students')->default(0); + $table->decimal('rating', 2, 1)->default(5.0); + $table->string('badge')->nullable(); + $table->string('image')->nullable(); + $table->string('price')->nullable(); + $table->json('includes')->nullable(); + $table->json('requirements')->nullable(); + $table->string('meta_title')->nullable(); + $table->text('meta_description')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('courses'); + } +}; diff --git a/database/migrations/2026_03_02_090004_create_course_schedules_table.php b/database/migrations/2026_03_02_090004_create_course_schedules_table.php new file mode 100644 index 0000000..a5f1121 --- /dev/null +++ b/database/migrations/2026_03_02_090004_create_course_schedules_table.php @@ -0,0 +1,34 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('course_schedules', function (Blueprint $table) { + $table->id(); + $table->foreignId('course_id')->constrained()->cascadeOnDelete(); + $table->date('start_date'); + $table->date('end_date'); + $table->string('location'); + $table->unsignedInteger('quota'); + $table->unsignedInteger('available_seats'); + $table->boolean('is_urgent')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('course_schedules'); + } +}; diff --git a/database/migrations/2026_03_02_090005_create_announcements_table.php b/database/migrations/2026_03_02_090005_create_announcements_table.php new file mode 100644 index 0000000..8adaef7 --- /dev/null +++ b/database/migrations/2026_03_02_090005_create_announcements_table.php @@ -0,0 +1,37 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('announcements', function (Blueprint $table) { + $table->id(); + $table->string('slug')->unique(); + $table->string('title'); + $table->string('category'); + $table->text('excerpt'); + $table->longText('content'); + $table->string('image')->nullable(); + $table->boolean('is_featured')->default(false); + $table->string('meta_title')->nullable(); + $table->text('meta_description')->nullable(); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('announcements'); + } +}; diff --git a/database/migrations/2026_03_02_090006_create_hero_slides_table.php b/database/migrations/2026_03_02_090006_create_hero_slides_table.php new file mode 100644 index 0000000..2a48e20 --- /dev/null +++ b/database/migrations/2026_03_02_090006_create_hero_slides_table.php @@ -0,0 +1,33 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('hero_slides', function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->string('title'); + $table->text('description'); + $table->string('image')->nullable(); + $table->unsignedInteger('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('hero_slides'); + } +}; diff --git a/database/migrations/2026_03_02_090007_create_leads_table.php b/database/migrations/2026_03_02_090007_create_leads_table.php new file mode 100644 index 0000000..c59cfb3 --- /dev/null +++ b/database/migrations/2026_03_02_090007_create_leads_table.php @@ -0,0 +1,41 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('leads', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('phone'); + $table->string('target_course')->nullable(); + $table->string('education_level')->nullable(); + $table->string('subject')->nullable(); + $table->text('message')->nullable(); + $table->string('status')->default('new'); + $table->text('notes')->nullable(); + $table->boolean('is_read')->default(false); + $table->string('source'); + $table->json('utm')->nullable(); + $table->boolean('consent_kvkk')->default(false); + $table->string('consent_text_version')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('leads'); + } +}; diff --git a/database/migrations/2026_03_02_090008_create_menus_table.php b/database/migrations/2026_03_02_090008_create_menus_table.php new file mode 100644 index 0000000..f32d024 --- /dev/null +++ b/database/migrations/2026_03_02_090008_create_menus_table.php @@ -0,0 +1,36 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('menus', function (Blueprint $table) { + $table->id(); + $table->string('location'); + $table->string('label'); + $table->string('url'); + $table->string('type')->default('link'); + $table->unsignedBigInteger('parent_id')->nullable(); + $table->unsignedInteger('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->foreign('parent_id')->references('id')->on('menus')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('menus'); + } +}; diff --git a/database/migrations/2026_03_02_090009_create_comments_table.php b/database/migrations/2026_03_02_090009_create_comments_table.php new file mode 100644 index 0000000..9b1adab --- /dev/null +++ b/database/migrations/2026_03_02_090009_create_comments_table.php @@ -0,0 +1,34 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('comments', function (Blueprint $table) { + $table->id(); + $table->morphs('commentable'); + $table->string('name_surname'); + $table->string('phone'); + $table->text('body'); + $table->text('admin_reply')->nullable(); + $table->boolean('is_approved')->default(false); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('comments'); + } +}; diff --git a/database/migrations/2026_03_02_090010_create_faqs_table.php b/database/migrations/2026_03_02_090010_create_faqs_table.php new file mode 100644 index 0000000..bcc192a --- /dev/null +++ b/database/migrations/2026_03_02_090010_create_faqs_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('faqs', function (Blueprint $table) { + $table->id(); + $table->string('category'); + $table->text('question'); + $table->text('answer'); + $table->unsignedInteger('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('faqs'); + } +}; diff --git a/database/migrations/2026_03_02_090011_create_guide_cards_table.php b/database/migrations/2026_03_02_090011_create_guide_cards_table.php new file mode 100644 index 0000000..b022f85 --- /dev/null +++ b/database/migrations/2026_03_02_090011_create_guide_cards_table.php @@ -0,0 +1,34 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('guide_cards', function (Blueprint $table) { + $table->id(); + $table->string('title'); + $table->text('description'); + $table->string('icon'); + $table->string('color'); + $table->string('link'); + $table->unsignedInteger('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('guide_cards'); + } +}; diff --git a/database/migrations/2026_03_02_090012_create_settings_table.php b/database/migrations/2026_03_02_090012_create_settings_table.php new file mode 100644 index 0000000..849058d --- /dev/null +++ b/database/migrations/2026_03_02_090012_create_settings_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('settings', function (Blueprint $table) { + $table->id(); + $table->string('group'); + $table->string('key')->unique(); + $table->text('value')->nullable(); + $table->string('type')->default('text'); + $table->boolean('is_public')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('settings'); + } +}; diff --git a/database/migrations/2026_03_02_090013_create_pages_table.php b/database/migrations/2026_03_02_090013_create_pages_table.php new file mode 100644 index 0000000..d679ff5 --- /dev/null +++ b/database/migrations/2026_03_02_090013_create_pages_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('pages', function (Blueprint $table) { + $table->id(); + $table->string('slug')->unique(); + $table->string('title'); + $table->string('meta_title')->nullable(); + $table->text('meta_description')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pages'); + } +}; diff --git a/database/migrations/2026_03_02_090014_create_page_blocks_table.php b/database/migrations/2026_03_02_090014_create_page_blocks_table.php new file mode 100644 index 0000000..c5e3889 --- /dev/null +++ b/database/migrations/2026_03_02_090014_create_page_blocks_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('page_blocks', function (Blueprint $table) { + $table->id(); + $table->foreignId('page_id')->constrained()->cascadeOnDelete(); + $table->string('type'); + $table->json('content')->nullable(); + $table->unsignedInteger('order_index')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('page_blocks'); + } +}; diff --git a/database/migrations/2026_03_10_070801_add_scope_standard_language_location_to_courses_table.php b/database/migrations/2026_03_10_070801_add_scope_standard_language_location_to_courses_table.php new file mode 100644 index 0000000..4830300 --- /dev/null +++ b/database/migrations/2026_03_10_070801_add_scope_standard_language_location_to_courses_table.php @@ -0,0 +1,31 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('courses', function (Blueprint $table) { + $table->json('scope')->nullable()->after('meta_description'); + $table->string('standard')->nullable()->after('scope'); + $table->string('language')->nullable()->default('Türkçe')->after('standard'); + $table->string('location')->nullable()->after('language'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('courses', function (Blueprint $table) { + $table->dropColumn(['scope', 'standard', 'language', 'location']); + }); + } +}; diff --git a/database/migrations/2026_03_15_105759_add_menu_order_to_courses_table.php b/database/migrations/2026_03_15_105759_add_menu_order_to_courses_table.php new file mode 100644 index 0000000..f87a519 --- /dev/null +++ b/database/migrations/2026_03_15_105759_add_menu_order_to_courses_table.php @@ -0,0 +1,27 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('courses', function (Blueprint $table) { + // null = mega menu'de görünmez + // 1, 2, 3 = kategori kartında gösterilecek kurs sırası + $table->unsignedTinyInteger('menu_order')->nullable()->after('badge'); + }); + } + + public function down(): void + { + Schema::table('courses', function (Blueprint $table) { + $table->dropColumn('menu_order'); + }); + } +}; diff --git a/database/migrations/2026_03_17_175316_add_label_and_order_index_to_settings_table.php b/database/migrations/2026_03_17_175316_add_label_and_order_index_to_settings_table.php new file mode 100644 index 0000000..ee75f9d --- /dev/null +++ b/database/migrations/2026_03_17_175316_add_label_and_order_index_to_settings_table.php @@ -0,0 +1,35 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('settings', function (Blueprint $table) { + $table->string('label', 150)->nullable()->after('type'); + $table->integer('order_index')->default(0)->after('label'); + + // Replace unique key constraint to (group, key) combo + $table->dropUnique(['key']); + $table->unique(['group', 'key']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('settings', function (Blueprint $table) { + $table->dropUnique(['group', 'key']); + $table->unique('key'); + $table->dropColumn(['label', 'order_index']); + }); + } +}; diff --git a/database/migrations/2026_03_18_094702_create_course_blocks_table.php b/database/migrations/2026_03_18_094702_create_course_blocks_table.php new file mode 100644 index 0000000..388b33f --- /dev/null +++ b/database/migrations/2026_03_18_094702_create_course_blocks_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('course_blocks', function (Blueprint $table) { + $table->id(); + $table->foreignId('course_id')->constrained()->cascadeOnDelete(); + $table->string('type', 50); + $table->json('content')->nullable(); + $table->unsignedInteger('order_index')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('course_blocks'); + } +}; diff --git a/database/migrations/2026_03_24_070634_add_email_and_marketing_consent_to_leads_table.php b/database/migrations/2026_03_24_070634_add_email_and_marketing_consent_to_leads_table.php new file mode 100644 index 0000000..8e9e302 --- /dev/null +++ b/database/migrations/2026_03_24_070634_add_email_and_marketing_consent_to_leads_table.php @@ -0,0 +1,29 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('leads', function (Blueprint $table) { + $table->string('email')->nullable()->after('phone'); + $table->boolean('marketing_consent')->default(false)->after('consent_kvkk'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('leads', function (Blueprint $table) { + $table->dropColumn(['email', 'marketing_consent']); + }); + } +}; diff --git a/database/migrations/2026_03_24_095911_rename_order_to_order_index_on_faqs_and_hero_slides.php b/database/migrations/2026_03_24_095911_rename_order_to_order_index_on_faqs_and_hero_slides.php new file mode 100644 index 0000000..39159f8 --- /dev/null +++ b/database/migrations/2026_03_24_095911_rename_order_to_order_index_on_faqs_and_hero_slides.php @@ -0,0 +1,36 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('faqs', function (Blueprint $table) { + $table->renameColumn('order', 'order_index'); + }); + + Schema::table('hero_slides', function (Blueprint $table) { + $table->renameColumn('order', 'order_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('faqs', function (Blueprint $table) { + $table->renameColumn('order_index', 'order'); + }); + + Schema::table('hero_slides', function (Blueprint $table) { + $table->renameColumn('order_index', 'order'); + }); + } +}; diff --git a/database/migrations/2026_03_24_110525_create_stories_table.php b/database/migrations/2026_03_24_110525_create_stories_table.php new file mode 100644 index 0000000..416a9fe --- /dev/null +++ b/database/migrations/2026_03_24_110525_create_stories_table.php @@ -0,0 +1,35 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('stories', function (Blueprint $table) { + $table->id(); + $table->string('title'); + $table->string('badge')->nullable(); + $table->text('content'); + $table->string('image')->nullable(); + $table->string('cta_text')->nullable(); + $table->string('cta_url')->nullable(); + $table->unsignedSmallInteger('order_index')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stories'); + } +}; diff --git a/database/migrations/2026_03_25_180508_add_extended_fields_to_course_schedules_table.php b/database/migrations/2026_03_25_180508_add_extended_fields_to_course_schedules_table.php new file mode 100644 index 0000000..9c6424b --- /dev/null +++ b/database/migrations/2026_03_25_180508_add_extended_fields_to_course_schedules_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('course_schedules', function (Blueprint $table) { + $table->string('instructor')->nullable()->after('location'); + $table->unsignedInteger('enrolled_count')->default(0)->after('available_seats'); + $table->decimal('price_override', 10, 2)->nullable()->after('enrolled_count'); + $table->string('status')->default('planned')->after('price_override'); + $table->text('notes')->nullable()->after('is_urgent'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('course_schedules', function (Blueprint $table) { + $table->dropColumn(['instructor', 'enrolled_count', 'price_override', 'status', 'notes']); + }); + } +}; diff --git a/database/migrations/2026_03_25_193512_add_media_type_fields_to_hero_slides_table.php b/database/migrations/2026_03_25_193512_add_media_type_fields_to_hero_slides_table.php new file mode 100644 index 0000000..69e56c9 --- /dev/null +++ b/database/migrations/2026_03_25_193512_add_media_type_fields_to_hero_slides_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('hero_slides', function (Blueprint $table) { + $table->string('media_type')->default('image')->after('description'); + $table->string('video_url')->nullable()->after('image'); + $table->string('mobile_image')->nullable()->after('video_url'); + $table->string('button_text')->nullable()->after('mobile_image'); + $table->string('button_url')->nullable()->after('button_text'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('hero_slides', function (Blueprint $table) { + $table->dropColumn(['media_type', 'video_url', 'mobile_image', 'button_text', 'button_url']); + }); + } +}; diff --git a/database/migrations/2026_03_26_063014_add_mobile_video_url_to_hero_slides_table.php b/database/migrations/2026_03_26_063014_add_mobile_video_url_to_hero_slides_table.php new file mode 100644 index 0000000..5cf849f --- /dev/null +++ b/database/migrations/2026_03_26_063014_add_mobile_video_url_to_hero_slides_table.php @@ -0,0 +1,28 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('hero_slides', function (Blueprint $table) { + $table->string('mobile_video_url')->nullable()->after('video_url'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('hero_slides', function (Blueprint $table) { + $table->dropColumn('mobile_video_url'); + }); + } +}; diff --git a/database/seeders/AdminUserSeeder.php b/database/seeders/AdminUserSeeder.php new file mode 100644 index 0000000..6bfc33d --- /dev/null +++ b/database/seeders/AdminUserSeeder.php @@ -0,0 +1,27 @@ +<?php + +namespace Database\Seeders; + +use App\Models\User; +use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; + +class AdminUserSeeder extends Seeder +{ + /** + * Run the database seeds. + */ + public function run(): void + { + $admin = User::query()->firstOrCreate( + ['email' => 'admin@bogazicidenizcilik.com.tr'], + [ + 'name' => 'Super Admin', + 'password' => Hash::make('password'), + 'email_verified_at' => now(), + ] + ); + + $admin->assignRole('super-admin'); + } +} diff --git a/database/seeders/AnnouncementSeeder.php b/database/seeders/AnnouncementSeeder.php new file mode 100644 index 0000000..bf37346 --- /dev/null +++ b/database/seeders/AnnouncementSeeder.php @@ -0,0 +1,94 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; + +class AnnouncementSeeder extends Seeder +{ + public function run(): void + { + DB::table('announcements')->truncate(); + + $announcements = [ + [ + 'slug' => '2026-bahar-donemi-kayitlari-basladi', + 'title' => '2026 Bahar Dönemi Kayıtları Başladı', + 'category' => 'announcement', + 'excerpt' => 'Bahar dönemi eğitim programlarımıza kayıtlar başlamıştır. Kontenjanlarımız sınırlı olduğundan erken kayıt yaptırmanızı tavsiye ederiz.', + 'content' => "Bahar dönemi eğitim programlarımıza kayıtlar başlamıştır. Yeni dönemde hem yüz yüze hem de online eğitim seçeneklerimizle denizcilik kariyerinize hız kesmeden devam edebilir, eksik belgelerinizi tamamlayabilirsiniz.\n\n- Tüm eğitim başlıklarımızda %10 erken kayıt avantajı sunulmaktadır.\n- Kesin kayıt esnasında istenilen evrak listesini detay inceleyiniz.\n- Kontenjanlar dolmadan yerinizi ayırtmayı unutmayın.", + 'image' => null, + 'is_featured' => true, + 'meta_title' => '2026 Bahar Dönemi Kayıtları Başladı | Boğaziçi Denizcilik', + 'meta_description' => 'Bahar dönemi eğitim programlarımıza kayıtlar başlamıştır. %10 erken kayıt avantajı için hemen başvurun.', + 'published_at' => '2026-02-15 09:00:00', + ], + [ + 'slug' => 'imo-standartlari-guncellemesi', + 'title' => 'IMO Standartları Güncellemesi', + 'category' => 'news', + 'excerpt' => 'Uluslararası Denizcilik Örgütü\'nün (IMO) 2026 standart güncellemeleri eğitim programlarımıza yansıtıldı.', + 'content' => "Dünya çapında uygulanan denizcilik standartları, IMO (Uluslararası Denizcilik Örgütü) tarafından periyodik olarak güncellenmektedir. 2026 yılı için duyurulan yeni yönetmeliklere uygun olacak şekilde müfredatlarımız Ulaştırma ve Altyapı Bakanlığı onayıyla revize edilmiştir.\n\nYeni standartlarla birlikte güvenlik ve çevre alanındaki konularda eğitime ayrılan süre artırılmış olup, simülatör kullanımında gerçekçilik senaryoları çeşitlendirilmiştir.", + 'image' => null, + 'is_featured' => false, + 'meta_title' => 'IMO 2026 Standartları Güncellemesi | Boğaziçi Denizcilik', + 'meta_description' => 'IMO 2026 standart güncellemeleri eğitim programlarımıza yansıtıldı. Güncel müfredat hakkında bilgi alın.', + 'published_at' => '2026-02-12 09:00:00', + ], + [ + 'slug' => 'denizcilik-kariyer-fuari-2026', + 'title' => 'Denizcilik Kariyer Fuarı 2026', + 'category' => 'event', + 'excerpt' => 'İstanbul\'da düzenlenecek Kariyer Fuarı\'nda 40\'tan fazla firma stant açacak. Öğrencilerimize ücretsiz giriş.', + 'content' => "Kariyer yolculuğunuzda önemli bir kilometre taşı olan Denizcilik Kariyer Fuarı, bu yıl da sektörün öncü firmalarını öğrencilerimiz ve mezunlarımızla buluşturuyor.\n\nEtkinlik kapsamında uzman konuşmacılarla düzenlenecek panellere katılabilir, mülakat simülasyonlarıyla kendinizi sınayabilirsiniz. Boğaziçi Denizcilik kursiyerlerine özel ücretsiz giriş imkanından faydalanmak için öğrenci işleri birimimizle iletişime geçiniz.", + 'image' => null, + 'is_featured' => false, + 'meta_title' => 'Denizcilik Kariyer Fuarı 2026 | Boğaziçi Denizcilik', + 'meta_description' => 'Denizcilik Kariyer Fuarı 2026\'da 40\'tan fazla firma ile buluşun. Öğrencilerimize ücretsiz giriş.', + 'published_at' => '2026-02-05 09:00:00', + ], + [ + 'slug' => 'mezunlarimiz-uluslararasi-yarismada-birinci', + 'title' => 'Mezunlarımız Uluslararası Yarışmada 1. Oldu', + 'category' => 'news', + 'excerpt' => 'Öğrencilerimiz Avrupa Denizcilik Simülasyon Yarışması\'nda birinci olarak Türkiye\'yi temsil etti.', + 'content' => "Kurumumuz eğitimlerini başarıyla tamamlayan 3 mezunumuzdan oluşan Türkiye ekibi, Hollanda'da düzenlenen Avrupa Denizcilik Simülasyon Yarışması'nda (E-Sea Challenge 2026) kriz yönetimi ve yanaşma-kalkma operasyonlarındaki takım çalışmalarıyla 1. oldu.\n\nGeleceğin denizcilerini yetiştirmekten gurur duyuyor, başarılarının devamını diliyoruz.", + 'image' => null, + 'is_featured' => false, + 'meta_title' => 'Mezunlarımız Uluslararası Yarışmada 1. Oldu | Boğaziçi Denizcilik', + 'meta_description' => 'Boğaziçi Denizcilik mezunları Avrupa Denizcilik Simülasyon Yarışması\'nda Türkiye\'yi birinci sıraya taşıdı.', + 'published_at' => '2026-01-28 09:00:00', + ], + [ + 'slug' => 'yeni-simulasyon-sistemimiz-hizmete-girdi', + 'title' => 'Yeni Simülasyon Sistemimiz Hizmete Girdi', + 'category' => 'news', + 'excerpt' => 'En son teknoloji deniz simülatörlerimiz ile gerçek deniz koşullarında eğitim imkânı sunuyoruz.', + 'content' => 'Eğitim kalitemizi her geçen gün arttırmak amacıyla köprüüstü vizyon simülatör sistemimizi yeniledik. 4K panoramik görüntü sağlayan yeni cihazlarımız ile 30 farklı limana ve 20 farklı gemi modeline erişimimiz bulunmaktadır. Öğrencilerimiz, acil durum senaryolarını sıfır risk ile deneyimleme şansı bulmaktadır.', + 'image' => null, + 'is_featured' => false, + 'meta_title' => 'Yeni Simülasyon Sistemimiz Hizmete Girdi | Boğaziçi Denizcilik', + 'meta_description' => '4K panoramik köprüüstü simülatörümüz ile gerçek deniz koşullarında eğitim alın. 30 liman, 20 gemi modeli.', + 'published_at' => '2026-01-20 09:00:00', + ], + [ + 'slug' => 'online-egitim-platformumuz-yenilendi', + 'title' => 'Online Eğitim Platformumuz Yenilendi', + 'category' => 'announcement', + 'excerpt' => 'Kullanıcı deneyimini iyileştiren yeni online eğitim platformumuz aktif olmuştur.', + 'content' => 'Özellikle yenileme eğitimlerinde sıkça kullandığımız online uzaktan eğitim platformumuzu güncelledik. İnteraktif eğitim materyalleri, hızlı canlı yayın sistemi ve mobil uyumlu arayüzü ile dilediğiniz yerden eğitimlerinize daha rahat bir şekilde katılabileceksiniz. Kursiyerlerimiz şifrelerini güncelleyerek sisteme giriş yapabilir.', + 'image' => null, + 'is_featured' => false, + 'meta_title' => 'Online Eğitim Platformumuz Yenilendi | Boğaziçi Denizcilik', + 'meta_description' => 'Yenilenen online eğitim platformumuz ile uzaktan eğitimlerinize mobil ve masaüstünden kolayca katılın.', + 'published_at' => '2026-01-10 09:00:00', + ], + ]; + + DB::table('announcements')->insert(array_map(fn ($a) => array_merge($a, [ + 'created_at' => now(), + 'updated_at' => now(), + ]), $announcements)); + } +} diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php new file mode 100644 index 0000000..89f3200 --- /dev/null +++ b/database/seeders/CategorySeeder.php @@ -0,0 +1,73 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; + +class CategorySeeder extends Seeder +{ + public function run(): void + { + $categories = [ + [ + 'slug' => 'guverte', + 'label' => 'Güverte Eğitimleri', + 'desc' => 'Seyir, köprüüstü operasyonu, ARPA/ECDIS simülatör eğitimleri', + 'image' => null, + 'meta_title' => 'Güverte Eğitimleri | Boğaziçi Denizcilik', + 'meta_description' => 'Seyir, köprüüstü operasyonu, ARPA/ECDIS simülatör eğitimleri. Boğaziçi Denizcilik Eğitim Kurumu.', + ], + [ + 'slug' => 'stcw', + 'label' => 'STCW Eğitimleri', + 'desc' => 'Zorunlu emniyet, yangın söndürme ve ilkyardım sertifikaları', + 'image' => null, + 'meta_title' => 'STCW Eğitimleri | Boğaziçi Denizcilik', + 'meta_description' => 'Zorunlu emniyet, yangın söndürme ve ilkyardım STCW sertifika programları. IMO onaylı eğitimler.', + ], + [ + 'slug' => 'makine', + 'label' => 'Makine Eğitimleri', + 'desc' => 'Dizel motor, elektrik sistemleri ve makine dairesi operasyonu', + 'image' => null, + 'meta_title' => 'Makine Eğitimleri | Boğaziçi Denizcilik', + 'meta_description' => 'Dizel motor, elektrik sistemleri ve makine dairesi operasyonu eğitimleri. STCW uyumlu programlar.', + ], + [ + 'slug' => 'yat-kaptanligi', + 'label' => 'Yat Kaptanlığı', + 'desc' => 'Tüm tonaj basamaklarında kaptan yeterlik', + 'image' => null, + 'meta_title' => 'Yat Kaptanlığı Eğitimleri | Boğaziçi Denizcilik', + 'meta_description' => '25 GT, 149 GT, 499 GT ve 3000 GT yat kaptanlığı yeterlik programları. Türkiye\'nin lider denizcilik okulu.', + ], + [ + 'slug' => 'yenileme', + 'label' => 'Yenileme Eğitimleri', + 'desc' => 'Süresi dolan sertifikalar için kısa güncelleme programları', + 'image' => null, + 'meta_title' => 'Yenileme Eğitimleri | Boğaziçi Denizcilik', + 'meta_description' => 'Süresi dolan denizcilik sertifikalarınız için kısa süreli güncelleme ve yenileme eğitimleri.', + ], + [ + 'slug' => 'guvenlik', + 'label' => 'Güvenlik (ISPS)', + 'desc' => 'Gemi ve liman güvenlik görevi sertifikaları', + 'image' => null, + 'meta_title' => 'Güvenlik (ISPS) Eğitimleri | Boğaziçi Denizcilik', + 'meta_description' => 'ISPS Kod kapsamında gemi ve liman güvenlik görevi sertifikaları. Uluslararası geçerliliğe sahip programlar.', + ], + ]; + + foreach ($categories as $category) { + DB::table('categories')->updateOrInsert( + ['slug' => $category['slug']], + array_merge($category, [ + 'created_at' => now(), + 'updated_at' => now(), + ]) + ); + } + } +} diff --git a/database/seeders/CorporatePagesSeeder.php b/database/seeders/CorporatePagesSeeder.php new file mode 100644 index 0000000..c15d64e --- /dev/null +++ b/database/seeders/CorporatePagesSeeder.php @@ -0,0 +1,250 @@ +<?php + +namespace Database\Seeders; + +use App\Models\Page; +use App\Models\PageBlock; +use Illuminate\Database\Seeder; + +class CorporatePagesSeeder extends Seeder +{ + /** + * Seed corporate pages: Kalite Politikası, Hakkımızda, Vizyon ve Misyon. + */ + public function run(): void + { + $this->seedKalitePolitikasi(); + $this->seedHakkimizda(); + $this->seedVizyonMisyon(); + } + + private function seedKalitePolitikasi(): void + { + $page = Page::firstOrCreate( + ['slug' => 'kalite-politikasi'], + [ + 'title' => 'Kalite Politikamız', + 'meta_title' => 'Kalite Politikamız | Boğaziçi Denizcilik – ISO 9001:2015 Sertifikalı Eğitim', + 'meta_description' => 'Boğaziçi Denizcilik Eğitim Kurumu kalite politikası. ISO 9001:2015 sertifikalı, STCW yetkili, MET akredite denizcilik eğitim kurumu.', + 'is_active' => true, + ], + ); + + $blocks = [ + [ + 'order_index' => 0, + 'type' => 'hero', + 'content' => [ + 'breadcrumb' => 'Kurumsal|/kurumsal / Kalite Politikamız', + 'title' => "Kalite Bir Hedef Değil,\nBir Standart", + 'highlight' => 'Bir Standart', + 'description' => 'ISO 9001:2015 sertifikalı Kalite Yönetim Sistemimiz ile her süreçte mükemmelliği güvence altına alıyoruz.', + ], + ], + [ + 'order_index' => 1, + 'type' => 'cards', + 'content' => [ + 'label' => 'AKREDİTASYONLAR', + 'title' => 'Sertifika & Belgelerimiz', + 'card_1_title' => 'ISO 9001:2015', + 'card_1_desc' => 'Kalite Yönetim Sistemi', + 'card_1_text' => "Türk Loydu · 2019'den beri", + 'card_1_icon' => 'award', + 'card_2_title' => 'STCW Yetki Belgesi', + 'card_2_desc' => 'Denizci Eğitim Kurumu Akreditasyonu', + 'card_2_text' => "UAB Denizcilik Genel Müdürlüğü · 2020'den beri", + 'card_2_icon' => 'shield', + 'card_3_title' => 'MET Akreditasyonu', + 'card_3_desc' => 'Uluslararası Denizcilik Eğitimi', + 'card_3_text' => "IMO Denizcilik Eğitim Standartları · 2021'den beri", + 'card_3_icon' => 'globe', + ], + ], + [ + 'order_index' => 2, + 'type' => 'text', + 'content' => [ + '_width' => 'half', + 'label' => 'KALİTE İLKELERİ', + 'title' => 'Taahhütlerimiz', + 'body' => '<ul><li>UAB ve IMO standartlarına tam uyumluluk</li><li>STCW Sözleşmesi müfredat gerekliliklerinin eksiksiz karşılanması</li><li>Eğitim kadrosunun denizcilik sektöründe aktif tecrübeye sahip olması</li><li>Simülatör ve laboratuvar ekipmanlarının güncel tutulması</li><li>Kursiyer memnuniyet oranının %95 üzerinde sürdürülmesi</li><li>Mezun istihdam takibi ve sektörel geri bildirim analizi</li><li>İç ve dış denetim bulgularına göre sürekli iyileştirme</li><li>Eğitim materyallerinin yıllık güncellenmesi</li></ul>', + ], + ], + [ + 'order_index' => 3, + 'type' => 'stats_grid', + 'content' => [ + '_width' => 'half', + 'label' => 'KALİTE DÖNGÜSÜ', + 'title' => 'PDCA Yaklaşımı', + 'stat_1_value' => '01', + 'stat_1_label' => 'Planlama — Yıllık kalite hedefleri ve iyileştirme planı oluşturulur.', + 'stat_2_value' => '02', + 'stat_2_label' => 'Uygulama — Planlar eğitim programlarına ve süreçlere entegre edilir.', + 'stat_3_value' => '03', + 'stat_3_label' => 'İzleme — Performans göstergeleri ve öğrenci memnuniyeti ölçülür.', + 'stat_4_value' => '04', + 'stat_4_label' => 'İyileştirme — Bulgular analiz edilerek sisteme geri besleme yapılır.', + 'note' => 'Bu döngü her yıl sistematik olarak tekrar edilir.', + ], + ], + ]; + + $this->syncBlocks($page, $blocks); + } + + private function seedHakkimizda(): void + { + $page = Page::firstOrCreate( + ['slug' => 'hakkimizda'], + [ + 'title' => 'Hakkımızda', + 'meta_title' => 'Hakkımızda | Boğaziçi Denizcilik – 25 Yıllık Deneyim', + 'meta_description' => "1998'den bu yana 15.000+ denizci yetiştiren Boğaziçi Denizcilik Eğitim Kurumu hakkında bilgi alın.", + 'is_active' => true, + ], + ); + + $blocks = [ + [ + 'order_index' => 0, + 'type' => 'hero', + 'content' => [ + 'breadcrumb' => 'Kurumsal|/kurumsal / Hakkımızda', + 'title' => "Türkiye'nin Köklü\nDenizcilik Okulu", + 'highlight' => 'Denizcilik Okulu', + 'description' => "1998'den bu yana 15.000'den fazla denizciyi uluslararası geçerli sertifikalarla donatıyoruz.", + ], + ], + [ + 'order_index' => 1, + 'type' => 'stats_grid', + 'content' => [ + 'style' => 'dark', + 'stat_1_value' => '1998', + 'stat_1_label' => 'Kuruluş Yılı', + 'stat_2_value' => '25+', + 'stat_2_label' => 'Yıllık Tecrübe', + 'stat_3_value' => '15.000+', + 'stat_3_label' => 'Mezun Sayısı', + 'stat_4_value' => '%98', + 'stat_4_label' => 'Başarı Oranı', + ], + ], + [ + 'order_index' => 2, + 'type' => 'text_image', + 'content' => [ + 'label' => 'HİKAYEMİZ', + 'title' => '25 Yılı Aşkın Deneyim ve Güven', + 'body' => "<p>Boğaziçi Denizcilik Eğitim Kurumu, 1998 yılında Kaptan Ahmet Yıldız tarafından İstanbul Kadıköy'de kurulmuştur. Ulaştırma ve Altyapı Bakanlığı (UAB) tarafından yetkilendirilmiş kurumumuz, STCW Sözleşmesi'nin gerektirdiği tüm zorunlu eğitimleri vermektedir.</p><p>Kuruluşumuzdan bu yana 15.000'i aşkın denizciyi sektöre kazandırdık. Mezunlarımız; Türk Deniz Kuvvetleri, MSC, CMA CGM, Maersk ve Türkiye'nin önde gelen denizcilik şirketlerinde aktif olarak görev yapmaktadır.</p><p>Eğitim merkezimiz; tam donanımlı köprüüstü simülatörü, ARPA/Radar eğitim istasyonları, ECDIS terminalleri, GMDSS telsiz laboratuvarı ve yangın tatbikat alanıyla donatılmıştır.</p>", + 'image' => 'h3.jpg', + 'image_alt' => 'Boğaziçi Denizcilik Eğitim Kurumu', + 'image_position' => 'right', + ], + ], + ]; + + $this->syncBlocks($page, $blocks); + } + + private function seedVizyonMisyon(): void + { + $page = Page::firstOrCreate( + ['slug' => 'vizyon-misyon'], + [ + 'title' => 'Vizyon ve Misyon', + 'meta_title' => 'Vizyon ve Misyon | Boğaziçi Denizcilik', + 'meta_description' => "Boğaziçi Denizcilik Eğitim Kurumu'nun vizyonu, misyonu ve temel değerleri.", + 'is_active' => true, + ], + ); + + $blocks = [ + [ + 'order_index' => 0, + 'type' => 'hero', + 'content' => [ + 'breadcrumb' => 'Kurumsal|/kurumsal / Vizyon ve Misyon', + 'title' => "Nereye Gidiyoruz,\nNeden Buradayız?", + 'highlight' => 'Neden Buradayız?', + 'description' => 'Denizcilik eğitiminde mükemmelliği bir hedef değil, bir standart olarak benimsiyoruz.', + ], + ], + [ + 'order_index' => 1, + 'type' => 'text', + 'content' => [ + '_width' => 'half', + 'label' => 'VİZYONUMUZ', + 'title' => 'Vizyonumuz', + 'body' => "<p>Türkiye'nin denizcilik eğitiminde referans noktası olarak, uluslararası standartları belirleyen, teknoloji odaklı ve sürekli gelişen bir eğitim kurumu olmak. Mezunlarımızın dünya denizlerinde tercih edilen profesyoneller olmasını sağlamak.</p>", + ], + ], + [ + 'order_index' => 2, + 'type' => 'text', + 'content' => [ + '_width' => 'half', + 'label' => 'MİSYONUMUZ', + 'title' => 'Misyonumuz', + 'body' => '<p>Ulaştırma ve Altyapı Bakanlığı ile IMO standartlarına uygun, STCW Sözleşmesi gerekliliklerini karşılayan eğitim programları sunmak. Denizcilik sektörünün ihtiyaç duyduğu nitelikli, güvenlik bilincine sahip personel yetiştirmek.</p>', + ], + ], + [ + 'order_index' => 3, + 'type' => 'cards', + 'content' => [ + 'label' => 'TEMEL DEĞERLERİMİZ', + 'title' => 'Bizi Biz Yapan İlkeler', + 'card_1_title' => 'Denizde Güvenlik Önceliğimiz', + 'card_1_text' => 'Tüm eğitim programlarımızın temelinde denizde can ve mal güvenliği ilkesi yer alır.', + 'card_1_icon' => 'shield', + 'card_2_title' => 'Uluslararası Standartlar', + 'card_2_text' => 'IMO ve STCW standartlarına tam uyum, UAB denetim ve onayıyla kalitemizi garanti altına alırız.', + 'card_2_icon' => 'globe', + 'card_3_title' => 'Uygulamalı Eğitim', + 'card_3_text' => 'Simülatör, laboratuvar ve tatbikat alanlarında gerçek senaryolarla öğrenme deneyimi sunarız.', + 'card_3_icon' => 'heart', + 'card_4_title' => 'Sürekli Gelişim', + 'card_4_text' => 'Müfredatımızı sektörel gelişmeler doğrultusunda sürekli günceller, eğitim kadromuzun gelişimini destekleriz.', + 'card_4_icon' => 'zap', + 'card_5_title' => 'Kursiyer Odaklılık', + 'card_5_text' => 'Her kursiyerin bireysel ihtiyaçlarına yönelik danışmanlık ve destek hizmeti sunuruz.', + 'card_5_icon' => 'target', + 'card_6_title' => 'Şeffaflık', + 'card_6_text' => 'Eğitim süreçlerimizi, başarı oranlarımızı ve mezun istatistiklerimizi kamuoyuyla paylaşırız.', + 'card_6_icon' => 'eye', + ], + ], + [ + 'order_index' => 4, + 'type' => 'cta', + 'content' => [ + 'title' => 'Bu Değerleri Birlikte Yaşayalım', + 'description' => 'Kalite Politikamız ve akreditasyonlarımız hakkında bilgi alın.', + 'button_text' => 'Kalite Politikamız', + 'button_url' => '/kurumsal/kalite-politikasi', + 'button_2_text' => 'Eğitimleri İncele', + 'button_2_url' => '/egitimler', + ], + ], + ]; + + $this->syncBlocks($page, $blocks); + } + + /** + * @param array<int, array{order_index: int, type: string, content: array<string, mixed>}> $blocks + */ + private function syncBlocks(Page $page, array $blocks): void + { + foreach ($blocks as $block) { + PageBlock::updateOrCreate( + ['page_id' => $page->id, 'order_index' => $block['order_index']], + ['type' => $block['type'], 'content' => $block['content']], + ); + } + } +} diff --git a/database/seeders/CourseBlockExampleSeeder.php b/database/seeders/CourseBlockExampleSeeder.php new file mode 100644 index 0000000..a7c3967 --- /dev/null +++ b/database/seeders/CourseBlockExampleSeeder.php @@ -0,0 +1,143 @@ +<?php + +namespace Database\Seeders; + +use App\Models\Course; +use Illuminate\Database\Seeder; + +class CourseBlockExampleSeeder extends Seeder +{ + /** + * Seed example blocks for "Gemici (Birleşik) Eğitimi" course. + * Existing CourseSeeder remains untouched. + */ + public function run(): void + { + $course = Course::where('slug', 'gemici-birlesik-egitimi')->first(); + + if (! $course) { + $this->command->warn('Gemici (Birleşik) Eğitimi bulunamadı, atlanıyor.'); + + return; + } + + // Mevcut blokları temizle + $course->blocks()->delete(); + + $blocks = [ + // Blok 0 — Eğitim Hakkında (detaylı açıklama) + [ + 'order_index' => 0, + 'type' => 'text', + 'content' => [ + 'label' => 'EĞİTİM HAKKINDA', + 'title' => 'Neden Bu Eğitim?', + 'body' => '<p>Gemici (Birleşik) Eğitimi, güverte bölümünde kariyer hedefleyen adayları <strong>tek bir program</strong> altında denizci olmaya hazırlar. Hem zorunlu güvenlik eğitimlerini hem de mesleki güverte operasyonlarını kapsayan bu birleşik format sayesinde iki ayrı eğitime katılma zorunluluğu ortadan kalkar.</p><p>32 günlük yoğun program boyunca kursiyerler; <strong>gerçek simülatör ortamlarında</strong> seyir gözetleme yapar, yangın tatbikat alanında bizzat müdahale eder ve can kurtarma botlarıyla açık deniz tatbikatı gerçekleştirir.</p><p>Eğitimi başarıyla tamamlayan adaylar <strong>UAB onaylı Gemici Yeterlik Belgesi</strong> almaya hak kazanır ve ticari gemilerde güverte tayfası olarak uluslararası sularda görev yapabilir.</p>', + ], + ], + + // Blok 1 — Eğitim Kapsamı (sol yarım) + [ + 'order_index' => 1, + 'type' => 'text', + 'content' => [ + '_width' => 'half', + 'label' => 'EĞİTİM KAPSAMI', + 'title' => 'Ne Öğreneceksiniz?', + 'body' => '<ul><li><strong>Denizde Kişisel Güvenlik</strong> — Can kurtarma salı, şişme yelek, immersion suit kullanımı ve denizde hayatta kalma teknikleri</li><li><strong>Yangınla Mücadele</strong> — A, B, C sınıfı yangın söndürme, SCBA tüplü ortamda müdahale tatbikatları</li><li><strong>Gemi Manevrası</strong> — Yanaşma/kalkma operasyonları, halat atma, bağlama donanımı ve vinç kullanımı</li><li><strong>Seyir Gözetleme</strong> — Köprüüstü vardiya prosedürleri, dürbün ile gözetleme, COLREG kuralları</li><li><strong>Güverte Bakımı</strong> — Paslama, boyama, bakım-onarım prosedürleri ve iş güvenliği</li><li><strong>Çevre Koruma</strong> — MARPOL Sözleşmesi, deniz kirliliği önleme ve atık yönetimi</li></ul>', + ], + ], + + // Blok 2 — Katılım Koşulları (sağ yarım) + [ + 'order_index' => 2, + 'type' => 'text', + 'content' => [ + '_width' => 'half', + 'label' => 'BAŞVURU ŞARTLARI', + 'title' => 'Kimler Katılabilir?', + 'body' => '<ul><li><strong>Yaş:</strong> 16 yaşını doldurmuş olmak</li><li><strong>Eğitim:</strong> En az ortaöğretim (lise) mezunu olmak</li><li><strong>Sağlık:</strong> Denizci sağlık raporu almaya engel durumu bulunmamak</li><li><strong>Adli Sicil:</strong> Denizcilik mesleğine engel sabıka kaydı bulunmamak</li></ul><p><strong>Önemli:</strong> Sağlık raporu eğitim başlangıcından önce temin edilmelidir. Kurumumuz, anlaşmalı sağlık kuruluşlarına yönlendirme yapmaktadır.</p>', + ], + ], + + // Blok 3 — Eğitim Süreci (adım kartları) + [ + 'order_index' => 3, + 'type' => 'stats_grid', + 'content' => [ + 'label' => 'EĞİTİM SÜRECİ', + 'title' => 'Başvurudan Belgeye 4 Adım', + 'stat_1_value' => '01', + 'stat_1_label' => 'Ön Kayıt — Online formu doldurun, danışmanlarımız sizi bilgilendirsin.', + 'stat_2_value' => '02', + 'stat_2_label' => 'Evrak & Sağlık — Gerekli belgeleri ve denizci sağlık raporunu hazırlayın.', + 'stat_3_value' => '03', + 'stat_3_label' => 'Eğitim — 32 gün boyunca teorik dersler ve uygulamalı tatbikatlar.', + 'stat_4_value' => '04', + 'stat_4_label' => 'Belgelendirme — Sınavı geçin, UAB onaylı Gemici Yeterlik Belgenizi alın.', + ], + ], + + // Blok 4 — Kazanımlar (kartlar) + [ + 'order_index' => 4, + 'type' => 'cards', + 'content' => [ + 'label' => 'KAZANIMLAR', + 'title' => 'Eğitim Sonunda Ne Kazanırsınız?', + 'card_1_title' => 'Gemici Yeterlik Belgesi', + 'card_1_text' => 'UAB tarafından düzenlenen, uluslararası geçerli yeterlik belgesi.', + 'card_1_icon' => 'award', + 'card_2_title' => 'Denizde Can Kurtarma Sertifikası', + 'card_2_text' => 'STCW A-VI/1 kapsamında kişisel güvenlik ve can kurtarma belgesi.', + 'card_2_icon' => 'life-buoy', + 'card_3_title' => 'Yangınla Mücadele Sertifikası', + 'card_3_text' => 'Temel yangın önleme ve söndürme yetkinlik belgesi.', + 'card_3_icon' => 'flame', + 'card_4_title' => 'Kariyer İmkanı', + 'card_4_text' => 'Ticari gemilerde güverte tayfası olarak uluslararası sularda çalışma hakkı.', + 'card_4_icon' => 'anchor', + ], + ], + + // Blok 5 — SSS + [ + 'order_index' => 5, + 'type' => 'faq', + 'content' => [ + 'title' => 'Sıkça Sorulan Sorular', + 'faq_1_question' => 'Bu eğitim ile Gemici (Temel) arasındaki fark nedir?', + 'faq_1_answer' => 'Birleşik eğitim, güvenlik eğitimleri (can kurtarma, yangın, kişisel emniyet) ile mesleki gemici eğitimini tek programda birleştirir. Temel eğitim ise sadece mesleki kısmı kapsar — güvenlik eğitimlerinizi daha önce almış olmanız gerekir.', + 'faq_2_question' => 'Devamsızlık sınırı nedir?', + 'faq_2_answer' => 'STCW düzenlemeleri gereği eğitim süresinin en az %90\'ına katılım zorunludur. Mazeretsiz devamsızlıkta eğitim tekrarı gerekebilir.', + 'faq_3_question' => 'Eğitim dili nedir?', + 'faq_3_answer' => 'Eğitim Türkçe olarak verilmektedir. Uluslararası denizcilik terminolojisi İngilizce olarak da öğretilmektedir.', + 'faq_4_question' => 'Sınav nasıl yapılıyor?', + 'faq_4_answer' => 'Eğitim sonunda UAB gözetiminde yazılı ve uygulamalı sınav yapılmaktadır. Başarılı olan adaylara yeterlik belgesi düzenlenir.', + 'faq_5_question' => 'Eğitim sonrası iş garantisi var mı?', + 'faq_5_answer' => 'İş garantisi verilmemekle birlikte, kurumumuz sektördeki denizcilik şirketleriyle iş birliği içindedir ve mezunlarımıza kariyer danışmanlığı sunmaktadır.', + ], + ], + + // Blok 6 — CTA + [ + 'order_index' => 6, + 'type' => 'cta', + 'content' => [ + 'title' => 'Denizcilik Kariyerinize İlk Adımı Atın', + 'description' => 'Ön kayıt formunu doldurun, eğitim danışmanlarımız sizinle en kısa sürede iletişime geçsin.', + 'button_text' => 'Ön Kayıt Yap', + 'button_url' => '/kayit?course=gemici-birlesik-egitimi', + 'button_2_text' => 'WhatsApp ile Bilgi Al', + 'button_2_url' => '/danismanlik', + ], + ], + ]; + + foreach ($blocks as $block) { + $course->blocks()->create($block); + } + + $this->command->info("Gemici (Birleşik) Eğitimi: {$course->blocks()->count()} blok oluşturuldu."); + } +} diff --git a/database/seeders/CourseContentSeeder.php b/database/seeders/CourseContentSeeder.php new file mode 100644 index 0000000..47bb23f --- /dev/null +++ b/database/seeders/CourseContentSeeder.php @@ -0,0 +1,169 @@ +<?php + +namespace Database\Seeders; + +use App\Models\Category; +use App\Models\Course; +use App\Models\CourseBlock; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Seeder; +use Illuminate\Support\Str; + +class CourseContentSeeder extends Seeder +{ + /** + * Seed course blocks (rich content) from parsed docx articles. + * Updates long_desc/requirements/scope on existing courses if empty. + * Creates new courses from articles that don't match existing ones. + * + * Source: storage/app/course_blocks_data.json + * Run AFTER CourseSeeder. Existing CourseSeeder is untouched. + */ + public function run(): void + { + $jsonPath = storage_path('app/course_blocks_data.json'); + + if (! file_exists($jsonPath)) { + $this->command->error('course_blocks_data.json bulunamadı. Önce Python parse script çalıştırın.'); + + return; + } + + /** @var array<int, array<string, mixed>> $data */ + $data = json_decode(file_get_contents($jsonPath), true); + $cats = Category::pluck('id', 'slug'); + $allCourses = Course::all(); + $matched = 0; + $created = 0; + $newCourses = 0; + $blockCount = 0; + + foreach ($data as $item) { + $course = $this->findCourse($allCourses, $item); + + if (! $course) { + // Create new course from article data + $categoryId = $cats[$item['category_slug']] ?? null; + if (! $categoryId) { + $this->command->warn("Kategori bulunamadı: {$item['category_slug']} -> {$item['title_short']}"); + + continue; + } + + $course = Course::create([ + 'category_id' => $categoryId, + 'slug' => Str::slug($item['title_short']), + 'title' => $item['title_short'], + 'desc' => Str::limit($item['long_desc'], 200), + 'long_desc' => $item['long_desc'], + 'duration' => '', + 'requirements' => $item['requirements'], + 'scope' => $item['scope'], + 'standard' => 'STCW / IMO Uyumlu', + 'language' => 'Türkçe', + 'location' => 'Kadıköy, İstanbul', + 'meta_title' => $item['title_short'].' | Boğaziçi Denizcilik', + 'meta_description' => Str::limit($item['long_desc'], 155), + ]); + $newCourses++; + $allCourses->push($course); + } else { + $matched++; + $this->updateCourseFields($course, $item); + } + + // Sync blocks + $count = $this->syncBlocks($course, $item['blocks']); + $blockCount += $count; + } + + $total = count($data); + $this->command->info("Toplam: {$total} makale, {$matched} eşleşme, {$newCourses} yeni kurs, {$blockCount} blok."); + } + + /** + * @param Collection<int, Course> $allCourses + * @param array<string, mixed> $item + */ + private function findCourse($allCourses, array $item): ?Course + { + $articleSlug = Str::slug($item['title_short']); + $titleNorm = $this->normalize($item['title_short']); + + // 1. Exact slug match + $found = $allCourses->first(fn (Course $c) => $c->slug === $articleSlug); + if ($found) { + return $found; + } + + // 2. Normalized title contains match + $found = $allCourses->first(fn (Course $c) => $this->normalize($c->title) === $titleNorm); + if ($found) { + return $found; + } + + // 3. Fuzzy — first 25 chars of normalized title + $prefix = mb_substr($titleNorm, 0, 25); + + return $allCourses->first(fn (Course $c) => str_starts_with($this->normalize($c->title), $prefix)); + } + + /** + * Normalize text: lowercase, strip accents, collapse whitespace. + */ + private function normalize(string $text): string + { + $text = mb_strtolower($text); + // Turkish specific replacements + $text = str_replace( + ['ç', 'ğ', 'ı', 'ö', 'ş', 'ü', 'â', 'î', 'û'], + ['c', 'g', 'i', 'o', 's', 'u', 'a', 'i', 'u'], + $text, + ); + $text = preg_replace('/[^a-z0-9\s]/', '', $text); + + return preg_replace('/\s+/', ' ', trim($text)); + } + + /** + * @param array<string, mixed> $item + */ + private function updateCourseFields(Course $course, array $item): void + { + $updates = []; + + if (empty($course->long_desc) && ! empty($item['long_desc'])) { + $updates['long_desc'] = $item['long_desc']; + } + + if ((empty($course->requirements) || $course->requirements === []) && ! empty($item['requirements'])) { + $updates['requirements'] = $item['requirements']; + } + + if ((empty($course->scope) || $course->scope === []) && ! empty($item['scope'])) { + $updates['scope'] = $item['scope']; + } + + if (! empty($updates)) { + $course->update($updates); + } + } + + /** + * @param array<int, array<string, mixed>> $blocks + */ + private function syncBlocks(Course $course, array $blocks): int + { + $count = 0; + + foreach ($blocks as $block) { + CourseBlock::updateOrCreate( + ['course_id' => $course->id, 'order_index' => $block['order_index']], + ['type' => $block['type'], 'content' => $block['content']], + ); + $count++; + } + + return $count; + } +} diff --git a/database/seeders/CourseScheduleSeeder.php b/database/seeders/CourseScheduleSeeder.php new file mode 100644 index 0000000..ef39b5c --- /dev/null +++ b/database/seeders/CourseScheduleSeeder.php @@ -0,0 +1,154 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; + +class CourseScheduleSeeder extends Seeder +{ + public function run(): void + { + DB::table('course_schedules')->truncate(); + + // Kurs slug → id eşlemesi + $courseIds = DB::table('courses')->pluck('id', 'slug'); + + $schedules = [ + // ── ŞUBAT 2026 ───────────────────────────────────────────────── + [ + 'course_id' => $courseIds['stcw-temel-guvenlik'] ?? null, + 'start_date' => '2026-02-24', + 'end_date' => '2026-02-28', + 'location' => 'Kadıköy', + 'quota' => 20, + 'available_seats' => 8, + 'is_urgent' => false, + ], + [ + 'course_id' => $courseIds['yat-kaptani-25-gt'] ?? null, + 'start_date' => '2026-02-26', + 'end_date' => '2026-02-28', + 'location' => 'Kadıköy', + 'quota' => 12, + 'available_seats' => 4, + 'is_urgent' => true, + ], + + // ── MART 2026 ─────────────────────────────────────────────────── + [ + 'course_id' => $courseIds['arpa-radar-simulator'] ?? null, + 'start_date' => '2026-03-03', + 'end_date' => '2026-03-05', + 'location' => 'Online', + 'quota' => 20, + 'available_seats' => 12, + 'is_urgent' => false, + ], + [ + 'course_id' => $courseIds['gmdss-genel-telsiz-operatoru-goc'] ?? null, + 'start_date' => '2026-03-10', + 'end_date' => '2026-03-17', + 'location' => 'Kadıköy', + 'quota' => 15, + 'available_seats' => 6, + 'is_urgent' => true, + ], + [ + 'course_id' => $courseIds['aff-yangin-sondurme'] ?? null, + 'start_date' => '2026-03-17', + 'end_date' => '2026-03-19', + 'location' => 'Kadıköy', + 'quota' => 20, + 'available_seats' => 10, + 'is_urgent' => false, + ], + [ + 'course_id' => $courseIds['dizel-motor-teknigi'] ?? null, + 'start_date' => '2026-03-24', + 'end_date' => '2026-03-28', + 'location' => 'Kadıköy', + 'quota' => 15, + 'available_seats' => 9, + 'is_urgent' => false, + ], + + // ── NİSAN 2026 ────────────────────────────────────────────────── + [ + 'course_id' => $courseIds['stcw-temel-guvenlik'] ?? null, + 'start_date' => '2026-04-07', + 'end_date' => '2026-04-11', + 'location' => 'Kadıköy', + 'quota' => 20, + 'available_seats' => 14, + 'is_urgent' => false, + ], + [ + 'course_id' => $courseIds['yat-kaptani-149-gt-egitimi-temel'] ?? null, + 'start_date' => '2026-04-14', + 'end_date' => '2026-04-19', + 'location' => 'Kadıköy', + 'quota' => 12, + 'available_seats' => 3, + 'is_urgent' => true, + ], + [ + 'course_id' => $courseIds['bst-yenileme'] ?? null, + 'start_date' => '2026-04-21', + 'end_date' => '2026-04-22', + 'location' => 'Online', + 'quota' => 30, + 'available_seats' => 20, + 'is_urgent' => false, + ], + [ + 'course_id' => $courseIds['ecdis-tip-bazli-egitim'] ?? null, + 'start_date' => '2026-04-28', + 'end_date' => '2026-04-30', + 'location' => 'Kadıköy', + 'quota' => 15, + 'available_seats' => 7, + 'is_urgent' => false, + ], + + // ── MAYIS 2026 ────────────────────────────────────────────────── + [ + 'course_id' => $courseIds['stcw-temel-guvenlik'] ?? null, + 'start_date' => '2026-05-05', + 'end_date' => '2026-05-09', + 'location' => 'Kadıköy', + 'quota' => 20, + 'available_seats' => 15, + 'is_urgent' => false, + ], + [ + 'course_id' => $courseIds['yat-kaptani-3000-gt'] ?? null, + 'start_date' => '2026-05-12', + 'end_date' => '2026-05-23', + 'location' => 'Kadıköy', + 'quota' => 10, + 'available_seats' => 5, + 'is_urgent' => false, + ], + [ + 'course_id' => $courseIds['makine-dairesi-operasyonu'] ?? null, + 'start_date' => '2026-05-19', + 'end_date' => '2026-05-21', + 'location' => 'Kadıköy', + 'quota' => 15, + 'available_seats' => 8, + 'is_urgent' => false, + ], + ]; + + // Null olan course_id'leri filtrele (kurs bulunamadıysa atla) + $validSchedules = array_filter($schedules, fn ($s) => $s['course_id'] !== null); + + foreach ($validSchedules as $schedule) { + DB::table('course_schedules')->insert(array_merge($schedule, [ + 'created_at' => now(), + 'updated_at' => now(), + ])); + } + } +} diff --git a/database/seeders/CourseSeeder.php b/database/seeders/CourseSeeder.php new file mode 100644 index 0000000..46aa8be --- /dev/null +++ b/database/seeders/CourseSeeder.php @@ -0,0 +1,1248 @@ +<?php + +namespace Database\Seeders; + +use App\Models\Category; +use App\Models\Course; +use Illuminate\Database\Seeder; + +class CourseSeeder extends Seeder +{ + public function run(): void + { + $cats = Category::pluck('id', 'slug'); + + $courses = [ + // ── GÜVERTE EĞİTİMLERİ ──────────────────────────────────────── + [ + 'slug' => 'gemici-birlesik-egitimi', + 'category_id' => $cats['guverte'], + 'title' => 'Gemici (Birleşik) Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemici (Birleşik) Eğitimi, denizcilik sektöründe güverte bölümünde kariyer hedefleyen adaylar için tasarlanmış kapsamlı bir yetiştirme programıdır.', + 'long_desc' => 'Gemici (Birleşik) Eğitimi, denizcilik sektöründe güverte bölümünde kariyer hedefleyen adaylar için tasarlanmış kapsamlı bir yetiştirme programıdır. Boğaziçi Denizcilik Eğitim Kurumu\'nun uzman eğitmen kadrosuyla sunulan bu eğitim, STCW Sözleşmesi gerekliliklerine uygun olarak teorik ve pratik bilgileri bir arada sunmaktadır. Eğitimi başarıyla tamamlayan adaylar, gemici yeterlik belgesi almaya hak kazanarak ticari gemilerde güverte tayfası olarak görev yapabilirler.', + 'duration' => '32 Gün', + 'students' => 772, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺5.200', + 'includes' => [ + 'Denizde kişisel güvenlik ve can kurtarma teknikleri', + 'Yangın önleme ve yangınla mücadele uygulamaları', + 'Gemi manevrası ve iskele operasyonları', + 'Halat, tel ve bağlama donanımı kullanımı', + 'Seyir gözetleme ve vardiya tutma esasları', + 'Güverte bakım ve boyama işlemleri', + 'Kişisel emniyet ve sosyal sorumluluk', + 'Deniz çevre bilinci ve kirlilik önleme', + ], + 'requirements' => [ + '16 yaşını bitirmiş olmak', + 'En az ortaöğretim mezunu olmak', + 'Sağlık raporu almaya engel durumu bulunmamak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Gemici (Birleşik) Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'Gemici (Birleşik) Eğitimi için ön kayıt formu. STCW uyumlu, 32 günlük kapsamlı güverte tayfası yetiştirme programı.', + ], + [ + 'slug' => 'gemici-temel-egitimi', + 'category_id' => $cats['guverte'], + 'title' => 'Gemici (Temel) Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemici (Temel) Eğitimi, denizde güvenlik eğitimlerini daha önce tamamlamış adayların gemici yeterlik belgesi alabilmesi için gereken mesleki bilgi ve becerileri kazandıran bir eğitim programıdır.', + 'long_desc' => 'Gemici (Temel) Eğitimi, denizde güvenlik eğitimlerini daha önce tamamlamış adayların gemici yeterlik belgesi alabilmesi için gereken mesleki bilgi ve becerileri kazandıran bir eğitim programıdır. Boğaziçi Denizcilik Eğitim Kurumu bünyesinde STCW standartlarına uygun şekilde yürütülen bu eğitim, güverte operasyonlarında görev alacak personelin yetiştirilmesini hedeflemektedir.', + 'duration' => '19 Gün', + 'students' => 314, + 'rating' => 4.6, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'Güverte operasyonları ve gemi manevraları', + 'Halat, tel ve bağlama donanımı kullanımı', + 'Seyir gözetleme ve vardiya tutma esasları', + 'Güverte bakım ve boyama işlemleri', + 'Gemi yapısı ve donanımı temel bilgisi', + 'İskele ve yanaşma operasyonları', + ], + 'requirements' => [ + '16 yaşını bitirmiş olmak', + 'En az ortaöğretim mezunu olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Gemici (Temel) Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'Gemici (Temel) Eğitimi başvuru formu. 19 günlük STCW uyumlu güverte tayfası programı.', + ], + [ + 'slug' => 'usta-gemici-yetistirme-egitimi-a-ii-5', + 'category_id' => $cats['guverte'], + 'title' => 'Usta Gemici Yetiştirme Eğitimi (A-II/5)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Usta Gemici Yetiştirme Eğitimi, STCW Kod A-II/5 gerekliliklerine uygun olarak hazırlanmış ileri düzey bir güverte tayfası eğitimidir.', + 'long_desc' => 'Usta Gemici Yetiştirme Eğitimi, STCW Kod A-II/5 gerekliliklerine uygun olarak hazırlanmış ileri düzey bir güverte tayfası eğitimidir. Köprüüstü ekipmanlarının kullanımından ileri seyir tekniklerine, liderlik becerilerinden güverte operasyonlarının yönetimine kadar kapsamlı bir içerik sunan bu eğitim, güverte bölümünde uzmanlaşmak isteyen denizcilere yeni kariyer fırsatları açmaktadır.', + 'duration' => '10 Gün', + 'students' => 485, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'İleri güverte operasyonları ve yönetimi', + 'Köprüüstü seyir cihazları kullanımı', + 'Yük elleçleme ve istifleme prosedürleri', + 'Gemi bakım ve onarım planlaması', + 'Liderlik ve ekip yönetimi becerileri', + 'COLREG ve seyir kuralları', + ], + 'requirements' => [ + '18 yaşını bitirmiş olmak', + 'Gemici yeterlik belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Usta Gemici Yetiştirme Eğitimi (A-II/5) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW A-II/5 uyumlu Usta Gemici Yetiştirme Eğitimi. Güverte kariyerinizde bir üst basamağa çıkın.', + ], + [ + 'slug' => 'sinirli-vardiya-zabiti-egitimi-a-ii-3-temel', + 'category_id' => $cats['guverte'], + 'title' => 'Sınırlı Vardiya Zabiti Eğitimi (A-II/3) (Temel)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Sınırlı Vardiya Zabiti Eğitimi, STCW Kod A-II/3 gerekliliklerini karşılayan kıyıya yakın sefer bölgelerine yönelik zabit yetiştirme programıdır.', + 'long_desc' => 'Sınırlı Vardiya Zabiti Eğitimi (A-II/3) Temel programı, daha önce belirli modülleri tamamlamış adayların kalan eğitimlerini alarak sınırlı vardiya zabiti yeterliliği kazanmasını sağlayan bir programdır. Kıyıya yakın sefer bölgelerinde görev yapacak zabitler için seyir, gemi yönetimi ve güvenlik konularında derinlemesine bilgi sunar.', + 'duration' => '31 Gün', + 'students' => 320, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Kıyı seyri ve seyir planlaması', + 'Radar ve ARPA kullanımı', + 'Gemi manevra kabiliyeti', + 'Meteoroloji ve deniz durumu değerlendirme', + 'COLREG ve seyir kuralları uygulamaları', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'En az ortaöğretim mezunu olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Sınırlı Vardiya Zabiti Eğitimi (A-II/3) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW A-II/3 Sınırlı Vardiya Zabiti Eğitimi başvurusu. 31 günlük kapsamlı güverte zabit programı.', + ], + [ + 'slug' => 'gmdss-genel-telsiz-operatoru-goc', + 'category_id' => $cats['guverte'], + 'title' => 'GMDSS Genel Telsiz Operatörü (GOC)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'GMDSS GOC Eğitimi, tüm deniz alanlarında (A1-A4) sefer yapan gemilerde telsiz operatörü olarak görev yapmak isteyen denizcilere yönelik uluslararası geçerliliğe sahip bir yeterlilik programıdır.', + 'long_desc' => 'GMDSS Genel Telsiz Operatörü (GOC) Eğitimi, tüm deniz alanlarında (A1-A4) sefer yapan gemilerde telsiz operatörü olarak görev yapmak isteyen denizcilere yönelik uluslararası geçerliliğe sahip bir yeterlilik programıdır. GOC belgesi ile tüm okyanus seferlerinde görev yapabilir, denizde tehlike ve güvenlik haberleşmesini etkin biçimde yönetebilirsiniz.', + 'duration' => '13 Gün', + 'students' => 876, + 'rating' => 4.7, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'GMDSS sistemi ve bileşenleri', + 'DSC (Sayısal Seçmeli Çağrı) kullanımı', + 'INMARSAT uydu haberleşme sistemleri', + 'NAVTEX ve güvenlik bilgi hizmetleri', + 'Tehlike, aciliyet ve güvenlik haberleşmesi', + 'EPIRB, SART ve VHF el telsizi kullanımı', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'En az ilköğretim mezunu olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'GMDSS Genel Telsiz Operatörü (GOC) Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'GMDSS GOC belgesi için eğitim başvurusu. Tüm deniz alanlarında geçerli telsiz operatörlüğü sertifikası.', + ], + [ + 'slug' => 'gmdss-tahditli-telsiz-operatoru-roc', + 'category_id' => $cats['guverte'], + 'title' => 'GMDSS Tahditli Telsiz Operatörü (ROC)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'GMDSS ROC Eğitimi, A1 ve A2 deniz alanlarında sefer yapan gemilerde telsiz haberleşme görevlerini yürütecek personel için tasarlanmış bir yeterlilik programıdır.', + 'long_desc' => 'GMDSS Tahditli Telsiz Operatörü (ROC) Eğitimi, A1 ve A2 deniz alanlarında sefer yapan gemilerde telsiz haberleşme görevlerini yürütecek personel için tasarlanmış bir yeterlilik programıdır. ROC belgesi, özellikle kabotaj hattında ve kıyı seferlerinde çalışan gemilerde zorunlu bir yeterliliktir.', + 'duration' => '7 Gün', + 'students' => 817, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'VHF ve MF haberleşme sistemleri', + 'DSC (Sayısal Seçmeli Çağrı) operasyonları', + 'NAVTEX kullanımı ve mesaj analizi', + 'Tehlike ve güvenlik haberleşme prosedürleri', + 'EPIRB ve SART cihazlarının kullanımı', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'En az ilköğretim mezunu olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'GMDSS Tahditli Telsiz Operatörü (ROC) | Boğaziçi Denizcilik', + 'meta_description' => 'GMDSS ROC belgesi için 7 günlük eğitim. A1-A2 deniz alanlarında geçerli telsiz yeterliği.', + ], + [ + 'slug' => 'arpa-radar-simulator', + 'category_id' => $cats['guverte'], + 'title' => 'ARPA / Radar Simülatör Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Kongsberg simülatörleri üzerinde ARPA ve Radar sistemlerinin kullanımını kapsayan uygulamalı köprüüstü eğitimi.', + 'long_desc' => 'Kongsberg simülatörleri üzerinde ARPA ve Radar sistemlerinin kullanımını kapsayan uygulamalı köprüüstü eğitimi. Çatışma önleme, seyir planlaması ve acil durum senaryolarını simüle ortamda deneyimleyerek yetkinlik kazanın.', + 'duration' => '2 Gün', + 'students' => 540, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'Radar teori ve pratik kullanımı', + 'ARPA ile çatışma önleme uygulamaları', + 'Simülatör üzerinde senaryo çalışmaları', + 'Seyir planlaması ve rota optimizasyonu', + ], + 'requirements' => [ + 'Güverte zabitliği veya kaptan belgesi sahibi olmak', + 'STCW BST sertifikası', + 'Seyir çizelgesi okuma bilgisi', + ], + 'meta_title' => 'ARPA / Radar Simülatör Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'Kongsberg simülatörleri ile ARPA/Radar eğitimi. Köprüüstü operasyon yetkinliğinizi geliştirin.', + ], + [ + 'slug' => 'ecdis-tip-bazli-egitim', + 'category_id' => $cats['guverte'], + 'title' => 'ECDIS Tip Bazlı Eğitim', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Elektronik Harita Görüntüleme ve Bilgi Sistemleri (ECDIS) cihazlarının tip bazlı kullanımını kapsayan uygulamalı eğitim programı.', + 'long_desc' => 'Elektronik Harita Görüntüleme ve Bilgi Sistemleri (ECDIS) cihazlarının tip bazlı kullanımını kapsayan uygulamalı eğitim programı. SOLAS gerekliliklerini karşılamak için zorunlu olan tip bazlı ECDIS eğitimi ile cihaz operasyonunda uzmanlaşın.', + 'duration' => '2 Gün', + 'students' => 430, + 'rating' => 4.7, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'ECDIS cihaz menüsü ve arayüzü', + 'Rota planlama ve izleme', + 'Alarm yönetimi', + 'Harita güncellemeleri ve seyir güvenliği', + ], + 'requirements' => [ + 'OOW veya üzeri denizcilik belgesi', + 'Temel ECDIS sertifikası (jenerik)', + 'STCW BST sertifikası', + ], + 'meta_title' => 'ECDIS Tip Bazlı Eğitim | Boğaziçi Denizcilik', + 'meta_description' => 'SOLAS uyumlu ECDIS tip bazlı eğitim. Elektronik seyir sistemlerinde yetkinlik kazanın.', + ], + [ + 'slug' => 'gmdss-genel-telsiz-operator-goc-yeterligi-tazeleme-egitimi', + 'category_id' => $cats['guverte'], + 'title' => 'GMDSS Genel Telsiz Operatör (GOC) Yeterliği Tazeleme Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'GOC belgesinin geçerlilik süresini uzatmak isteyen denizcilerin bilgi ve becerilerini güncellemesini sağlayan yenileme programı.', + 'long_desc' => 'GMDSS GOC Yeterliği Tazeleme Eğitimi, GOC belgesinin geçerlilik süresini uzatmak isteyen denizcilerin bilgi ve becerilerini güncellemesini sağlayan bir yenileme programıdır. GMDSS sistemlerindeki güncel gelişmeleri ve teknolojik yenilikleri kapsamaktadır.', + 'duration' => '2 Gün', + 'students' => 593, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'GMDSS sistemlerindeki güncel gelişmeler', + 'DSC ve uydu haberleşme güncellemeleri', + 'e-Navigation ve modern haberleşme teknolojileri', + 'Mevzuat güncellemeleri', + ], + 'requirements' => [ + 'Geçerli veya süresi dolmuş GOC belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'GMDSS GOC Tazeleme Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'GOC belgenizi yenileyin. 2 günlük GMDSS tazeleme programı ile belgenizin geçerliliğini uzatın.', + ], + [ + 'slug' => 'gmdss-tahditli-telsiz-operator-roc-yeterligi-tazeleme-egitimi', + 'category_id' => $cats['guverte'], + 'title' => 'GMDSS Tahditli Telsiz Operatör (ROC) Yeterliği Tazeleme Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'ROC belgesinin geçerlilik süresini uzatmak isteyen denizcilere yönelik kısa süreli yenileme programı.', + 'long_desc' => 'GMDSS ROC Yeterliği Tazeleme Eğitimi, ROC belgesinin geçerlilik süresini uzatmak isteyen denizcilere yönelik bir yenileme programıdır. VHF ve MF haberleşme sistemlerindeki güncellemeleri ve yeni uygulamaları kapsamaktadır.', + 'duration' => '1 Gün', + 'students' => 373, + 'rating' => 4.7, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'VHF ve MF haberleşme güncellemeleri', + 'DSC operasyon pratiği', + 'Tehlike haberleşme prosedürleri tazelemesi', + 'Mevzuat değişiklikleri', + ], + 'requirements' => [ + 'Geçerli veya süresi dolmuş ROC belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'GMDSS ROC Tazeleme Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'ROC belgenizi yenileyin. 1 günlük tazeleme programı ile belgenizin geçerliliğini uzatın.', + ], + + // ── STCW EĞİTİMLERİ ─────────────────────────────────────────── + [ + 'slug' => 'stcw-temel-guvenlik', + 'category_id' => $cats['stcw'], + 'title' => 'STCW Temel Güvenlik Eğitimi (BST)', + 'sub' => 'STCW VI/1 – IMO Uyumlu', + 'desc' => 'STCW Temel Güvenlik Eğitimi (BST), gemide görev yapacak tüm personel için zorunlu olan temel can kurtarma, yangın, ilk yardım ve kişisel güvenlik sertifika programıdır.', + 'long_desc' => 'STCW Temel Güvenlik Eğitimi (BST), STCW Sözleşmesi Kural VI/1 kapsamında gemide görev yapacak tüm personelin alması zorunlu olan temel güvenlik eğitimidir. Denizcilik kariyerinin ilk ve zorunlu adımı olan BST belgesi; can kurtarma, yangınla mücadele, ilk yardım ve kişisel emniyet konularını kapsamaktadır.', + 'duration' => '5 Gün', + 'students' => 1240, + 'rating' => 4.9, + 'badge' => 'most_preferred', + 'price' => '₺2.500', + 'includes' => [ + 'Denizde kişisel can kurtarma teknikleri', + 'Yangın önleme ve yangınla mücadele', + 'Temel ilk yardım uygulamaları', + 'Kişisel emniyet ve sosyal sorumluluk', + 'Can yelekleri ve can salı kullanımı', + ], + 'requirements' => [ + 'En az 16 yaşında olmak', + 'Lise veya dengi okul mezunu olmak', + 'Sağlık raporu (denize elverişli)', + 'Nüfus cüzdanı veya pasaport fotokopisi', + ], + 'meta_title' => 'STCW Temel Güvenlik (BST) Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'Denizcilik kariyerinin ilk adımı BST belgesi için eğitim başvurusu. IMO onaylı, 5 günlük temel güvenlik programı.', + ], + [ + 'slug' => 'aff-yangin-sondurme', + 'category_id' => $cats['stcw'], + 'title' => 'Gelişmiş Yangın Söndürme (AFF)', + 'sub' => 'STCW VI/3 – IMO Uyumlu', + 'desc' => 'Gemide yangın söndürme ekibinde liderlik rolü üstlenecek personel için tasarlanmış ileri düzey yangın söndürme eğitimi.', + 'long_desc' => 'Gelişmiş Yangın Söndürme (AFF) Eğitimi, STCW Sözleşmesi VI/3 kapsamında yangın söndürme ekiplerinde liderlik rolü üstlenecek personelin yetiştirilmesine yönelik ileri düzey bir eğitimdir. Büyük çaplı yangın senaryolarında doğru karar verme ve ekip koordinasyonu konularında yetkinlik kazandırır.', + 'duration' => '2 Gün', + 'students' => 720, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'İleri yangın söndürme teknikleri ve taktikleri', + 'Sabit yangın söndürme sistemlerinin yönetimi', + 'Yangın söndürme ekibi liderliği', + 'Yangın tatbikatı planlama ve değerlendirme', + ], + 'requirements' => [ + 'STCW BST sertifikası', + 'Fiziksel sağlık durumu uygun olmak', + 'En az 18 yaşında olmak', + ], + 'meta_title' => 'Gelişmiş Yangın Söndürme (AFF) Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'STCW VI/3 Gelişmiş Yangın Söndürme (AFF) belgesi için eğitim başvurusu.', + ], + [ + 'slug' => 'yolcu-gemisinde-calisma-egitimi', + 'category_id' => $cats['stcw'], + 'title' => 'Yolcu Gemisinde Çalışma Eğitimi', + 'sub' => 'STCW A-V/2 – IMO Uyumlu', + 'desc' => 'Yolcu gemilerinde görev yapacak tüm personelin yolcu güvenliği ve hizmet kalitesi konusunda yetkinlik kazanmasını sağlayan zorunlu bir programdır.', + 'long_desc' => 'Yolcu Gemisinde Çalışma Eğitimi, yolcu gemilerinde görev yapacak tüm personelin yolcu güvenliği ve hizmet kalitesi konusunda yetkinlik kazanmasını sağlayan zorunlu bir programdır. Kruvaziyer ve feribot sektöründe kariyer hedefleyen profesyoneller için kalabalık yönetiminden kriz iletişimine kadar kritik becerileri kazandırmaktadır.', + 'duration' => '3 Gün', + 'students' => 165, + 'rating' => 4.5, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Yolcu güvenliği ve acil durum yönetimi', + 'Kalabalık yönetimi teknikleri', + 'Kriz iletişimi ve yolcu bilgilendirme', + 'Engelli ve özel ihtiyaçlı yolculara yardım', + ], + 'requirements' => [ + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Yolcu Gemisinde Çalışma Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'STCW A-V/2 Yolcu Gemisi Çalışma sertifikası. Kruvaziyer ve feribot sektörü için zorunlu program.', + ], + [ + 'slug' => 'denizde-guvenlik-egitimi-teorik', + 'category_id' => $cats['stcw'], + 'title' => 'Denizde Güvenlik Eğitimi (Teorik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Tüm gemi personelinin alması zorunlu olan temel denizde güvenlik bilincini kazandıran teorik eğitim programı.', + 'long_desc' => 'Denizde Güvenlik Eğitimi, STCW Sözleşmesi gereği tüm gemi personelinin alması zorunlu olan temel güvenlik eğitimlerinden biridir. Denizde karşılaşılabilecek tehlike durumlarına karşı bilinç ve hazırlık düzeyini artırmayı hedeflemektedir.', + 'duration' => '12 Gün', + 'students' => 162, + 'rating' => 4.5, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Denizde güvenlik bilinci ve genel prensipler', + 'Acil durum prosedürleri ve toplanma', + 'Gemi yapısı ve bölümleri tanıtımı', + 'Deniz çevresi koruma bilinci', + ], + 'requirements' => [ + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Denizde Güvenlik Eğitimi (Teorik) | Boğaziçi Denizcilik', + 'meta_description' => 'Teorik denizde güvenlik eğitimi başvurusu. STCW uyumlu zorunlu güvenlik sertifikası.', + ], + [ + 'slug' => 'denizde-kisisel-can-kurtarma-teknikleri-egitimi-teorik', + 'category_id' => $cats['stcw'], + 'title' => 'Denizde Kişisel Can Kurtarma Teknikleri Eğitimi (Teorik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Denize düşme veya gemiyi terk etme gibi acil durumlarda hayatta kalma becerilerini kazandıran STCW zorunlu teorik eğitim.', + 'long_desc' => 'Denizde Kişisel Can Kurtarma Teknikleri Eğitimi, gemi personelinin denize düşme veya gemiyi terk etme gibi acil durumlarda hayatta kalma becerilerini kazandıran temel STCW eğitimlerinden biridir.', + 'duration' => '2 Gün', + 'students' => 568, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'Can yelekleri ve kişisel yüzdürme araçları', + 'Can salları ve kullanım prosedürleri', + 'Hipotermiden korunma teknikleri', + 'Terk-i gemi prosedürleri', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Denizde Can Kurtarma Teknikleri (Teorik) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW kişisel can kurtarma teknikleri teorik eğitimi. Hayatta kalma becerilerinizi geliştirin.', + ], + [ + 'slug' => 'yangin-onleme-ve-yanginla-mucadele-egitimi-teorik', + 'category_id' => $cats['stcw'], + 'title' => 'Yangın Önleme ve Yangınla Mücadele Eğitimi (Teorik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemide yangın risklerinin anlaşılması ve yangın durumunda etkin müdahale için gerekli bilgileri kazandıran teorik STCW eğitimi.', + 'long_desc' => 'Yangın Önleme ve Yangınla Mücadele Eğitimi, gemide yangın risklerinin anlaşılması ve yangın durumunda etkin müdahale edilmesi için gerekli bilgileri kazandıran temel STCW eğitimlerinden biridir.', + 'duration' => '2 Gün', + 'students' => 824, + 'rating' => 4.6, + 'badge' => null, + 'price' => '₺4.500', + 'includes' => [ + 'Yangın üçgeni ve yangın sınıfları', + 'Yangın söndürme araç ve gereçleri', + 'Sabit yangın söndürme sistemleri', + 'Yangında tahliye prosedürleri', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Yangın Önleme ve Mücadele (Teorik) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW teorik yangın eğitimi başvurusu. Gemide yangın güvenliği bilinci kazanın.', + ], + [ + 'slug' => 'kisisel-emniyet-ve-sosyal-sorumluluk', + 'category_id' => $cats['stcw'], + 'title' => 'Kişisel Emniyet ve Sosyal Sorumluluk', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemide güvenli çalışma uygulamalarını, çevre bilincini ve kişilerarası iletişim becerilerini kapsayan zorunlu STCW eğitimi.', + 'long_desc' => 'Kişisel Emniyet ve Sosyal Sorumluluk eğitimi, STCW Sözleşmesi kapsamında tüm denizcilerin alması zorunlu olan temel eğitimlerden biridir. Gemide güvenli çalışma uygulamalarını, çevre bilincini ve kişilerarası iletişim becerilerini kapsamaktadır.', + 'duration' => '3 Gün', + 'students' => 573, + 'rating' => 4.6, + 'badge' => null, + 'price' => '₺5.200', + 'includes' => [ + 'Gemide iş güvenliği ve sağlık kuralları', + 'Deniz çevresi koruma ve kirlilik önleme', + 'Ekip çalışması ve iletişim becerileri', + 'Yorgunluk yönetimi ve vardiya düzeni', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + ], + 'meta_title' => 'Kişisel Emniyet ve Sosyal Sorumluluk | Boğaziçi Denizcilik', + 'meta_description' => 'STCW kişisel emniyet ve sosyal sorumluluk eğitimi başvurusu. Zorunlu denizcilik sertifikası.', + ], + [ + 'slug' => 'can-kurtarma-araclarini-kullanma-egitimi-teorik', + 'category_id' => $cats['stcw'], + 'title' => 'Can Kurtarma Araçlarını Kullanma Eğitimi (Teorik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Can kurtarma botları, can salları ve diğer kurtarma araçlarının doğru ve etkin kullanımını kapsayan STCW zorunlu teorik eğitim.', + 'long_desc' => 'Can Kurtarma Araçlarını Kullanma Eğitimi, gemideki tüm can kurtarma araç ve gereçlerinin doğru ve etkin kullanılmasını sağlayan STCW zorunlu eğitimlerinden biridir.', + 'duration' => '2 Gün', + 'students' => 673, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Can kurtarma botlarının çeşitleri ve özellikleri', + 'Can salı kullanım ve aktivasyon prosedürleri', + 'SOLAS gereklilikleri ve düzenlemeler', + 'Terk-i gemi tatbikat prosedürleri', + ], + 'requirements' => [ + '18 yaşını bitirmiş olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Can Kurtarma Araçları (Teorik) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW can kurtarma araçları teorik eğitimi. Acil durum yetkinliğinizi geliştirin.', + ], + [ + 'slug' => 'hizli-cankurtarma-botu-kullanma-yeterligi-egitimi-teorik', + 'category_id' => $cats['stcw'], + 'title' => 'Hızlı Cankurtarma Botu Kullanma Yeterliği Eğitimi (Teorik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Hızlı cankurtarma botlarının operasyonunu öğreten ve kurtarma operasyonlarında görev alacak personelin yetiştirilmesini amaçlayan teorik eğitim.', + 'long_desc' => 'Hızlı Cankurtarma Botu Kullanma Yeterliği Eğitimi, denize düşen kişilerin kurtarılmasında kullanılan hızlı kurtarma botlarının operasyonunu kapsamlı şekilde öğreten bir yeterlilik programıdır.', + 'duration' => '3 Gün', + 'students' => 257, + 'rating' => 4.5, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'Hızlı cankurtarma botu özellikleri ve donanımı', + 'Bot indirme, kaldırma ve manevra teknikleri', + 'Denize düşen kişi kurtarma yöntemleri', + 'Bot bakım ve kontrol prosedürleri', + ], + 'requirements' => [ + 'Geçerli sağlık raporuna sahip olmak', + '18 yaş ve üzeri olmak', + ], + 'meta_title' => 'Hızlı Cankurtarma Botu (Teorik) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW hızlı cankurtarma botu yeterliği teorik eğitimi.', + ], + [ + 'slug' => 'i-leri-yanginla-mucadele-egitimi-teorik', + 'category_id' => $cats['stcw'], + 'title' => 'İleri Yangınla Mücadele Eğitimi (Teorik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Yangın söndürme ekiplerinde liderlik rolü üstlenecek personelin yetiştirilmesine yönelik ileri düzey teorik STCW yangın eğitimi.', + 'long_desc' => 'İleri Yangınla Mücadele Eğitimi, yangın söndürme ekiplerinde ve yangın müdahale operasyonlarında liderlik rolü üstlenecek personelin yetiştirilmesine yönelik ileri düzey bir STCW eğitimidir.', + 'duration' => '3 Gün', + 'students' => 128, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'Yangın müdahale stratejileri ve taktikleri', + 'Yangın söndürme ekibi liderliği', + 'Sabit yangın söndürme sistemlerinin yönetimi', + 'Yangın tatbikatı planlama ve değerlendirme', + ], + 'requirements' => [ + 'Geçerli sağlık raporuna sahip olmak', + '18 yaş ve üzeri olmak', + ], + 'meta_title' => 'İleri Yangınla Mücadele (Teorik) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW ileri yangınla mücadele teorik eğitimi. Yangın liderliği yetkinliği kazanın.', + ], + [ + 'slug' => 'petrol-ve-kimyasal-tankerlerinde-yuk-i-slemleri-temel-egitimi', + 'category_id' => $cats['stcw'], + 'title' => 'Petrol ve Kimyasal Tankerlerinde Yük İşlemleri Temel Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Tanker gemilerinde görev yapacak personelin tehlikeli yüklerle güvenli çalışma konusunda temel bilgi kazanmasını sağlayan STCW zorunlu eğitim.', + 'long_desc' => 'Petrol ve Kimyasal Tankerlerinde Yük İşlemleri Temel Eğitimi, tanker gemilerinde görev yapacak personelin tehlikeli yüklerle güvenli çalışma konusunda temel bilgi ve becerilerini kazandıran STCW zorunlu eğitimlerinden biridir.', + 'duration' => '2 Gün', + 'students' => 282, + 'rating' => 4.5, + 'badge' => null, + 'price' => '₺5.200', + 'includes' => [ + 'Petrol ve kimyasal maddelerin özellikleri ve tehlikeleri', + 'Yük elleçleme temel prosedürleri', + 'Statik elektrik ve inert gaz sistemleri', + 'MARPOL ve çevre koruma gereklilikleri', + ], + 'requirements' => [ + 'Geçerli sağlık raporuna sahip olmak', + '18 yaş ve üzeri olmak', + ], + 'meta_title' => 'Petrol ve Kimyasal Tanker Yük İşlemleri | Boğaziçi Denizcilik', + 'meta_description' => 'STCW petrol ve kimyasal tanker temel eğitimi. Tanker sektörüne ilk adımı atın.', + ], + [ + 'slug' => 'sivilastirilmis-gaz-tankerlerinde-yuk-i-slemleri-temel-egitimi', + 'category_id' => $cats['stcw'], + 'title' => 'Sıvılaştırılmış Gaz Tankerlerinde Yük İşlemleri Temel Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'LPG ve LNG tankerlerinde görev yapacak personelin sıvılaştırılmış gaz yükleriyle güvenli çalışma yetkinliği kazanmasını sağlayan STCW eğitimi.', + 'long_desc' => 'Sıvılaştırılmış Gaz Tankerlerinde Yük İşlemleri Temel Eğitimi, LPG ve LNG tankerlerinde görev yapacak personelin sıvılaştırılmış gaz yükleriyle güvenli çalışma konusunda yetkinlik kazanmasını sağlayan bir STCW eğitimidir.', + 'duration' => '2 Gün', + 'students' => 840, + 'rating' => 4.5, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'Sıvılaştırılmış gazların fiziksel ve kimyasal özellikleri', + 'Yük elleçleme ve transfer prosedürleri', + 'Gaz algılama ve kişisel güvenlik', + 'IGC Kod gereklilikleri', + ], + 'requirements' => [ + 'Geçerli sağlık raporuna sahip olmak', + '18 yaş ve üzeri olmak', + ], + 'meta_title' => 'Sıvılaştırılmış Gaz Tanker Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'LPG/LNG tankerlerinde çalışmak için STCW temel eğitim başvurusu.', + ], + [ + 'slug' => 'temel-i-lk-yardim', + 'category_id' => $cats['stcw'], + 'title' => 'Temel İlk Yardım', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemide meydana gelebilecek kaza ve acil sağlık durumlarında ilk müdahaleyi yapabilecek personelin yetiştirilmesine yönelik STCW zorunlu eğitim.', + 'long_desc' => 'Temel İlk Yardım eğitimi, gemide meydana gelebilecek kaza ve acil sağlık durumlarında ilk müdahaleyi yapabilecek personelin yetiştirilmesine yönelik STCW zorunlu eğitimlerinden biridir.', + 'duration' => '3 Gün', + 'students' => 299, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Temel yaşam desteği (CPR) uygulamaları', + 'Kanama kontrolü ve yara bakımı', + 'Yanık ve hipotermi tedavisi', + 'Hasta ve yaralı taşıma teknikleri', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'Denizcilik sektöründe çalışıyor veya çalışacak olmak', + ], + 'meta_title' => 'Temel İlk Yardım Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'STCW temel ilk yardım eğitimi başvurusu. Gemide acil sağlık durumlarına hazır olun.', + ], + [ + 'slug' => 'tibbi-bakim-egitimi', + 'category_id' => $cats['stcw'], + 'title' => 'Tıbbi Bakım Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemide tıbbi sorumlu olarak görev yapacak zabitlerin ileri düzey tıbbi bakım ve müdahale yetkinliği kazanmasını sağlayan STCW eğitimi.', + 'long_desc' => 'Tıbbi Bakım Eğitimi, gemide tıbbi sorumlu olarak görev yapacak zabitlerin ileri düzey tıbbi bakım ve müdahale yetkinliği kazanmasını sağlayan bir STCW eğitimidir. Uzak deniz seferlerinde tıbbi desteğin sınırlı olduğu durumlarda etkin müdahale becerisi kazandırır.', + 'duration' => '3 Gün', + 'students' => 369, + 'rating' => 4.6, + 'badge' => null, + 'price' => '₺3.000', + 'includes' => [ + 'İleri yaşam desteği uygulamaları', + 'İlaç yönetimi ve gemi eczanesi', + 'TMAS ile iletişim ve teletıp', + 'Psikolojik ilk yardım ve kriz müdahalesi', + ], + 'requirements' => [ + 'En az zabit düzeyinde yeterliğe sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Tıbbi Bakım Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'STCW tıbbi bakım eğitimi. Gemide sağlık sorumluluğu üstlenmek için gerekli yeterlilik.', + ], + + // ── MAKİNE EĞİTİMLERİ ───────────────────────────────────────── + [ + 'slug' => 'yagci-birlesik', + 'category_id' => $cats['makine'], + 'title' => 'Yağcı (Birleşik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemilerin makine dairesinde yağcı olarak görev yapacak adayların yetiştirilmesine yönelik kapsamlı eğitim programı.', + 'long_desc' => 'Yağcı (Birleşik) Eğitimi, gemilerin makine dairesinde yağcı olarak görev yapacak adayların yetiştirilmesine yönelik kapsamlı bir eğitim programıdır. Denizde güvenlik modülleri ile mesleki eğitimi tek bir programda birleştirmektedir.', + 'duration' => '32 Gün', + 'students' => 404, + 'rating' => 4.5, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'Denizde güvenlik ve can kurtarma teknikleri', + 'Ana makine ve yardımcı makine tanıtımı', + 'Makine dairesi vardiya prosedürleri', + 'Yağlama sistemleri ve bakımı', + 'Pompa ve boru sistemleri', + ], + 'requirements' => [ + '16 yaşını bitirmiş olmak', + 'En az ortaöğretim mezunu olmak', + 'Sağlık raporu almaya engel durumu bulunmamak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Yağcı (Birleşik) Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => '32 günlük Yağcı (Birleşik) eğitimi başvurusu. STCW uyumlu makine dairesi personel programı.', + ], + [ + 'slug' => 'yagci-temel', + 'category_id' => $cats['makine'], + 'title' => 'Yağcı (Temel)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Denizde güvenlik eğitimlerini tamamlamış adayların yağcı yeterlik belgesi alabilmesi için gereken mesleki bilgi ve becerileri kazandıran program.', + 'long_desc' => 'Yağcı (Temel) Eğitimi, denizde güvenlik eğitimlerini daha önce tamamlamış adayların yağcı yeterlik belgesi alabilmesi için gereken mesleki bilgi ve becerileri kazandıran bir programdır.', + 'duration' => '19 Gün', + 'students' => 519, + 'rating' => 4.7, + 'badge' => null, + 'price' => '₺4.500', + 'includes' => [ + 'Ana makine ve yardımcı makine sistemleri', + 'Makine dairesi vardiya prosedürleri', + 'Yağlama ve soğutma sistemleri', + 'Temel elektrik bilgisi', + ], + 'requirements' => [ + '16 yaş ve üzeri olmak', + 'En az orta öğretim mezunu olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Yağcı (Temel) Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'Yağcı (Temel) eğitimi başvurusu. 19 günlük STCW uyumlu makine dairesi programı.', + ], + [ + 'slug' => 'usta-makine-tayfasi-yetistirme-egitimi-a-iii-5', + 'category_id' => $cats['makine'], + 'title' => 'Usta Makine Tayfası Yetiştirme Eğitimi (A-III/5)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'STCW Kod A-III/5 gerekliliklerine uygun, deneyimli makine tayfasının ileri düzey yetkinlik kazanmasını sağlayan eğitim.', + 'long_desc' => 'Usta Makine Tayfası Yetiştirme Eğitimi, STCW Kod A-III/5 gerekliliklerine uygun olarak deneyimli makine tayfasının ileri düzey yetkinlik kazanmasını sağlayan bir programdır. Makine bölümünde kariyer basamaklarını tırmanan profesyoneller için tasarlanmıştır.', + 'duration' => '10 Gün', + 'students' => 807, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'İleri makine bakım ve onarım teknikleri', + 'Otomasyon ve kontrol sistemleri', + 'Enerji verimliliği ve yakıt yönetimi', + 'Arıza teşhis ve giderme yöntemleri', + ], + 'requirements' => [ + '18 yaşını bitirmiş olmak', + 'Yağcı yeterlik belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Usta Makine Tayfası (A-III/5) | Boğaziçi Denizcilik', + 'meta_description' => 'STCW A-III/5 Usta Makine Tayfası eğitimi. Makine bölümünde kariyer basamağınızı yükseltin.', + ], + [ + 'slug' => 'dizel-motor-teknigi', + 'category_id' => $cats['makine'], + 'title' => 'Dizel Motor Tekniği', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemi dizel motorlarının yapısını, çalışma prensibini ve bakım prosedürlerini kapsayan uygulamalı makine dairesi eğitimi.', + 'long_desc' => 'Gemi dizel motorlarının yapısını, çalışma prensibini ve bakım prosedürlerini kapsayan uygulamalı makine dairesi eğitimi. Deneyimli makine mühendisleri eşliğinde motor bakımı ve arıza giderme becerilerinizi geliştirin.', + 'duration' => '4 Gün', + 'students' => 380, + 'rating' => 4.7, + 'badge' => null, + 'price' => '₺4.000', + 'includes' => [ + 'Dizel motor çalışma prensibi ve bileşenleri', + 'Motor bakım planlaması ve prosedürleri', + 'Arıza teşhis ve giderme', + 'Yakıt sistemleri ve yönetimi', + ], + 'requirements' => [ + 'Makine bölümünde çalışma deneyimi', + 'Teknik lise mezunu (tercih)', + 'İş güvenliği sertifikası', + ], + 'meta_title' => 'Dizel Motor Tekniği Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'Gemi dizel motor tekniği eğitimi başvurusu. Makine dairesinde uzmanlık kazanın.', + ], + [ + 'slug' => 'makine-dairesi-operasyonu', + 'category_id' => $cats['makine'], + 'title' => 'Makine Dairesi Operasyonu', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemi makine dairesinin günlük operasyonu, sistemlerin yönetimi ve güvenli çalışma prosedürlerini kapsayan kapsamlı makine eğitimi.', + 'long_desc' => 'Gemi makine dairesinin günlük operasyonu, sistemlerin yönetimi ve güvenli çalışma prosedürlerini kapsayan kapsamlı makine eğitimi. Makine dairesindeki tüm sistemler ve ekipmanların tanıtımını içeren uygulamalı bir programdır.', + 'duration' => '3 Gün', + 'students' => 310, + 'rating' => 4.6, + 'badge' => null, + 'price' => '₺3.500', + 'includes' => [ + 'Makine dairesi sistemleri tanıtımı', + 'Günlük operasyon prosedürleri', + 'Güvenli çalışma uygulamaları', + 'Acil durum prosedürleri', + ], + 'requirements' => [ + 'Makine bölümünde çalışıyor olmak veya olmak istemek', + 'Temel elektrik ve mekanik bilgisi', + ], + 'meta_title' => 'Makine Dairesi Operasyonu Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'Gemi makine dairesi operasyonu eğitimi. Sistematik ve güvenli makine yönetimi öğrenin.', + ], + + // ── YAT KAPTANLIĞI ───────────────────────────────────────────── + [ + 'slug' => 'yat-kaptani-25-gt', + 'category_id' => $cats['yat-kaptanligi'], + 'title' => 'Yat Kaptanı (25 GT)', + 'sub' => 'Kaptan Yeterlik Belgesi', + 'desc' => '25 GT\'ye kadar yat ve teknelerde kaptan olarak görev yapabilmek için gereken yeterlik eğitimi.', + 'long_desc' => '25 GT\'ye kadar yat ve teknelerde kaptan olarak görev yapabilmek için gereken yeterlik eğitimi. Kıyı seyri, temel seyir bilgisi ve deniz güvenliği konularını kapsayan bu program, yat kaptanlığı kariyerinin ilk adımıdır.', + 'duration' => '3 Gün', + 'students' => 880, + 'rating' => 4.8, + 'badge' => 'popular', + 'price' => '₺3.500', + 'includes' => [ + 'Kıyı seyri temel bilgileri', + 'Yat manevrası ve demirleme', + 'Deniz hukuku ve mevzuat', + 'VHF telsiz kullanımı', + 'Temel meteoroloji bilgisi', + ], + 'requirements' => [ + 'En az 18 yaşında olmak', + 'STCW Temel Güvenlik (BST) sertifikası', + 'En az 200 saat deniz seyir tecrübesi', + 'Doktor raporu (denizcilik sağlık belgesi)', + ], + 'meta_title' => 'Yat Kaptanı 25 GT Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => '25 GT yat kaptanlığı eğitimi ve sertifika programı. Denizcilik kariyerinize yön verin.', + ], + [ + 'slug' => 'yat-kaptani-149-gt-egitimi-birlesik', + 'category_id' => $cats['yat-kaptanligi'], + 'title' => 'Yat Kaptanı (149 GT) Eğitimi (Birleşik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => '149 GT\'ye kadar yatlarda kaptan olarak görev yapacak adayları yetiştiren kapsamlı eğitim. Denizde güvenlik modülleri ile yat kaptanlığını birleştirir.', + 'long_desc' => 'Yat Kaptanı (149 GT) Eğitimi Birleşik programı, 149 Gross Ton\'a kadar olan ticari ve özel yatlarda kaptan olarak görev yapacak adayları yetiştiren kapsamlı bir eğitimdir. Türkiye\'nin zengin kıyı şeridi ve büyüyen yat turizmi sektörü, nitelikli yat kaptanlarına olan talebi artırmaktadır.', + 'duration' => '25 Gün', + 'students' => 463, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺5.200', + 'includes' => [ + 'Denizde güvenlik ve can kurtarma', + 'Kıyı seyri ve seyir planlaması', + 'Deniz haritaları ve seyir aletleri kullanımı', + 'Deniz hukuku ve liman mevzuatı', + 'Yat manevraları ve demirleme', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'En az lise mezunu olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Yat Kaptanı (149 GT) Birleşik Eğitim | Boğaziçi Denizcilik', + 'meta_description' => '149 GT yat kaptanlığı birleşik eğitimi. Güvenlik modülleri dahil kapsamlı program.', + ], + [ + 'slug' => 'yat-kaptani-149-gt-egitimi-temel', + 'category_id' => $cats['yat-kaptanligi'], + 'title' => 'Yat Kaptanı (149 GT) Eğitimi (Temel)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Güvenlik eğitimlerini tamamlamış adayların 149 GT yat kaptanlığı yeterliliği kazanması için tasarlanmış temel mesleki program.', + 'long_desc' => 'Yat Kaptanı (149 GT) Eğitimi Temel programı, denizde güvenlik eğitimlerini tamamlamış adayların yat kaptanlığı yeterliliği kazanması için tasarlanmış bir eğitimdir. 149 GT\'ye kadar yatlarda kaptan olarak görev yapacak adayların mesleki bilgi ve becerilerini geliştirmeye odaklanmaktadır.', + 'duration' => '10 Gün', + 'students' => 830, + 'rating' => 5.0, + 'badge' => null, + 'price' => '₺4.500', + 'includes' => [ + 'Kıyı seyri ve harita çalışması', + 'Seyir aletleri ve GPS/plotter kullanımı', + 'Deniz hukuku ve mevzuat', + 'VHF haberleşme prosedürleri', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'En az lise mezunu olmak', + 'Denizde güvenlik eğitimlerini tamamlamış olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Yat Kaptanı 149 GT Temel Eğitim | Boğaziçi Denizcilik', + 'meta_description' => '149 GT yat kaptanlığı temel eğitimi. BST belgesi olanlar için 10 günlük yoğun program.', + ], + [ + 'slug' => 'yat-kaptani-499-gt-temel', + 'category_id' => $cats['yat-kaptanligi'], + 'title' => 'Yat Kaptanı (499 GT) (Temel)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => '149 GT yat kaptanı belgesine sahip adayların 499 GT yat kaptanlığına yükselmesini sağlayan ileri düzey eğitim.', + 'long_desc' => 'Yat Kaptanı (499 GT) Temel programı, güvenlik eğitimlerini tamamlamış ve 149 GT yat kaptanı belgesine sahip adayların 499 GT yat kaptanlığına yükselmesini sağlayan ileri düzey bir eğitimdir.', + 'duration' => '35 Gün', + 'students' => 410, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'İleri seyir ve rota planlama', + 'ECDIS ve elektronik harita sistemleri', + 'Radar/ARPA çatışma önleme', + 'Gemi stabilitesi ve yük yönetimi', + ], + 'requirements' => [ + 'Yat Kaptanı (149 GT) belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Yat Kaptanı 499 GT Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => '499 GT yat kaptanlığı eğitimi. 149 GT belge sahiplerine özel ileri yeterlik programı.', + ], + [ + 'slug' => 'yat-kaptani-3000-gt', + 'category_id' => $cats['yat-kaptanligi'], + 'title' => 'Yat Kaptanı (3000 GT)', + 'sub' => 'Kaptan Yeterlik Belgesi', + 'desc' => '3000 GT\'ye kadar büyük yatlarda kaptan olarak görev yapabilmek için gereken en üst düzey yat kaptanlığı yeterlik programı.', + 'long_desc' => '3000 GT\'ye kadar büyük yatlarda kaptan olarak görev yapabilmek için gereken en üst düzey yat kaptanlığı yeterlik programı. Süperyat sektöründe kariyer hedefleyen deneyimli kaptanlar için kapsamlı bir ileri eğitimdir.', + 'duration' => '10 Gün', + 'students' => 195, + 'rating' => 4.9, + 'badge' => null, + 'price' => '₺9.500', + 'includes' => [ + 'İleri uluslararası seyir teknikleri', + 'Büyük yat operasyonları ve yönetimi', + 'Mürettebat yönetimi ve liderlik', + 'Uluslararası deniz hukuku', + ], + 'requirements' => [ + 'Yat Kaptanı 499 GT belgesi', + 'En az 2000 saat deniz seyir tecrübesi', + 'GMDSS GOC sertifikası', + ], + 'meta_title' => 'Yat Kaptanı 3000 GT Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => '3000 GT yat kaptanlığı eğitimi. Süperyat sektöründe kariyer yapmak için en üst yeterlik.', + ], + [ + 'slug' => 'yat-kaptani-149-gt-tazeleme-egitimi', + 'category_id' => $cats['yat-kaptanligi'], + 'title' => 'Yat Kaptanı (149 GT) Tazeleme Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => '149 GT yat kaptanlığı belgesinin geçerlilik süresini uzatmak için seyir teknolojileri ve güncel mevzuatı kapsayan tazeleme programı.', + 'long_desc' => 'Yat Kaptanı (149 GT) Tazeleme Eğitimi, 149 GT yat kaptanlığı belgesinin geçerlilik süresini uzatmak isteyen kaptanların mesleki yetkinliklerini güncellemesini sağlayan bir yenileme programıdır.', + 'duration' => '5 Gün', + 'students' => 679, + 'rating' => 4.7, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Güncel seyir teknolojileri ve uygulamaları', + 'Güvenlik standartları güncellemeleri', + 'Deniz mevzuatı değişiklikleri', + 'Acil durum prosedürleri tazelemesi', + ], + 'requirements' => [ + 'Geçerli veya süresi dolmuş Yat Kaptanı (149 GT) belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Yat Kaptanı 149 GT Tazeleme | Boğaziçi Denizcilik', + 'meta_description' => '149 GT yat kaptanlığı belge tazeleme eğitimi. Belgenizin geçerliliğini uzatın.', + ], + [ + 'slug' => 'yat-kaptani-499-gt-tazeleme-egitimi', + 'category_id' => $cats['yat-kaptanligi'], + 'title' => 'Yat Kaptanı (499 GT) Tazeleme Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => '499 GT yat kaptanlığı belgesinin geçerlilik süresini uzatmak isteyen kaptanlar için ileri düzey mesleki bilgi güncelleme programı.', + 'long_desc' => 'Yat Kaptanı (499 GT) Tazeleme Eğitimi, 499 GT yat kaptanlığı belgesinin geçerlilik süresini uzatmak isteyen kaptanların ileri düzey mesleki bilgilerini güncellemesini sağlayan bir yenileme programıdır.', + 'duration' => '5 Gün', + 'students' => 564, + 'rating' => 4.6, + 'badge' => null, + 'price' => '₺5.200', + 'includes' => [ + 'İleri seyir teknolojileri ve ECDIS güncellemeleri', + 'Uluslararası deniz hukuku güncellemeleri', + 'Büyük yat güvenlik prosedürleri', + 'Acil durum yönetimi ve tatbikat değerlendirme', + ], + 'requirements' => [ + 'Geçerli veya süresi dolmuş Yat Kaptanı (499 GT) belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Yat Kaptanı 499 GT Tazeleme | Boğaziçi Denizcilik', + 'meta_description' => '499 GT yat kaptanlığı belge tazeleme eğitimi. Mesleki bilgilerinizi güncelleyin.', + ], + + // ── YENİLEME EĞİTİMLERİ ─────────────────────────────────────── + [ + 'slug' => 'denizde-guvenlik-egitimleri-yenileme-teorik', + 'category_id' => $cats['yenileme'], + 'title' => 'Denizde Güvenlik Eğitimleri Yenileme (Teorik)', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Süresi dolan denizde güvenlik belgelerini yenilemek isteyen denizciler için kısa süreli teorik güncelleme programı.', + 'long_desc' => 'Denizde Güvenlik Eğitimleri Yenileme programı, daha önce alınan denizde güvenlik eğitimlerinin süresi dolan denizcilerin bilgi ve becerilerini güncellemesini sağlayan zorunlu bir tazeleme eğitimidir.', + 'duration' => '2 Gün', + 'students' => 561, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'Güncellenmiş deniz güvenliği prosedürleri', + 'Can kurtarma teknikleri tazelemesi', + 'Mevzuat değişiklikleri ve yeni düzenlemeler', + 'Vaka çalışmaları ve senaryo analizleri', + ], + 'requirements' => [ + 'Daha önce denizde güvenlik eğitimlerini tamamlamış olmak', + 'Belge yenileme süresinde olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Denizde Güvenlik Yenileme (Teorik) | Boğaziçi Denizcilik', + 'meta_description' => 'BST/STCW belge yenileme eğitimi. 2 günlük teorik güncelleme programı.', + ], + [ + 'slug' => 'bst-yenileme', + 'category_id' => $cats['yenileme'], + 'title' => 'BST Yenileme', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Süresi dolmak üzere olan veya süresi dolmuş BST belgesini yenilemek isteyen denizciler için kısa güncelleme eğitimi.', + 'long_desc' => 'BST Yenileme Eğitimi, Basic Safety Training belgesinin yenilenmesi için gerekli kısa süreli güncelleme programıdır. Güncel güvenlik prosedürleri ve mevzuat değişiklikleri konusunda bilgi tazeleyerek belgenizin geçerliliğini uzatın.', + 'duration' => '1 Gün', + 'students' => 420, + 'rating' => 4.7, + 'badge' => null, + 'price' => '₺1.800', + 'includes' => [ + 'BST kapsamı güvenlik bilgisi tazelemesi', + 'Can kurtarma araçları güncelleme', + 'Yangın güvenliği güncel uygulamalar', + 'Mevzuat ve standart değişiklikleri', + ], + 'requirements' => [ + 'Geçerli/Süresi dolmuş BST sertifikası', + 'Denizcilik sağlık belgesi', + ], + 'meta_title' => 'BST Yenileme Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'BST belge yenileme eğitimi. 1 günlük kısa güncelleme ile sertifikanızı yenileyin.', + ], + [ + 'slug' => 'guverte-sinirli-i-sletim-duzeyi-tazeleme-egitimi', + 'category_id' => $cats['yenileme'], + 'title' => 'Güverte Sınırlı İşletim Düzeyi Tazeleme Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Sınırlı vardiya zabiti belgesinin geçerlilik süresini uzatmak isteyen güverte zabitlerinin mesleki bilgilerini güncelleyen tazeleme programı.', + 'long_desc' => 'Güverte Sınırlı İşletim Düzeyi Tazeleme Eğitimi, sınırlı vardiya zabiti belgesinin geçerlilik süresini uzatmak isteyen güverte zabitlerinin mesleki bilgilerini güncellemesini sağlayan bir yenileme programıdır.', + 'duration' => '5 Gün', + 'students' => 506, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Güncel seyir teknolojileri ve ECDIS', + 'COLREG ve mevzuat güncellemeleri', + 'Radar/ARPA kullanım pratiği', + 'Deniz kazaları vaka çalışmaları', + ], + 'requirements' => [ + 'Geçerli veya süresi dolmuş sınırlı vardiya zabiti belgesine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Güverte Sınırlı İşletim Tazeleme | Boğaziçi Denizcilik', + 'meta_description' => 'Güverte sınırlı vardiya zabiti belge tazeleme eğitimi. 5 günlük güncelleme programı.', + ], + + // ── GÜVENLİK (ISPS) ─────────────────────────────────────────── + [ + 'slug' => 'gemi-guvenlik-zabiti', + 'category_id' => $cats['guvenlik'], + 'title' => 'Gemi Güvenlik Zabiti', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'ISPS Kod gereklilikleri doğrultusunda gemide güvenlik sorumluluğunu üstlenecek zabitler için kapsamlı güvenlik yeterlilik programı.', + 'long_desc' => 'Gemi Güvenlik Zabiti Eğitimi, ISPS Kod gereklilikleri doğrultusunda gemide güvenlik sorumluluğunu üstlenecek zabitlerin yetiştirilmesine yönelik önemli bir programdır. Gemi güvenlik planının uygulanması ve güvenlik tehditlerinin yönetilmesi konularında kapsamlı bilgi sunar.', + 'duration' => '2 Gün', + 'students' => 465, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'ISPS Kod gereklilikleri ve uygulamaları', + 'Gemi güvenlik planı hazırlama ve yönetimi', + 'Güvenlik değerlendirmesi ve denetim', + 'Güvenlik ekipmanları ve sistemleri', + ], + 'requirements' => [ + 'En az 12 ay deniz hizmetine sahip olmak', + 'En az zabit düzeyinde yeterliğe sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + ], + 'meta_title' => 'Gemi Güvenlik Zabiti Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'ISPS Kod Gemi Güvenlik Zabiti eğitimi. Gemide güvenlik sorumluluğu için zorunlu yeterlik.', + ], + [ + 'slug' => 'sirket-guvenlik-sorumlusu', + 'category_id' => $cats['guvenlik'], + 'title' => 'Şirket Güvenlik Sorumlusu', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'ISPS Kod kapsamında denizcilik şirketlerinin kara ofislerinde güvenlik yönetim sorumluluğunu üstlenecek personelin yetiştirildiği program.', + 'long_desc' => 'Şirket Güvenlik Sorumlusu Eğitimi, ISPS Kod kapsamında denizcilik şirketlerinin kara ofislerinde güvenlik yönetim sorumluluğunu üstlenecek personelin yetiştirilmesini hedefleyen bir programdır. Filo güvenlik yönetimi konusunda derinlemesine bilgi ve beceri kazandırır.', + 'duration' => '3 Gün', + 'students' => 435, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺4.500', + 'includes' => [ + 'ISPS Kod ve uluslararası güvenlik mevzuatı', + 'Şirket güvenlik politikası oluşturma', + 'Güvenlik değerlendirmesi ve risk analizi', + 'İç ve dış denetim süreçleri', + ], + 'requirements' => [ + 'Zabit yeterliğine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Şirket Güvenlik Sorumlusu Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'ISPS Şirket Güvenlik Sorumlusu sertifika programı. Denizcilik şirketleri için zorunlu yeterlik.', + ], + [ + 'slug' => 'birlestirilmis-gemi-guvenlik-zabiti-ve-sirket-guvenlik-sorumlusu-egitimi', + 'category_id' => $cats['guvenlik'], + 'title' => 'Birleştirilmiş Gemi Güvenlik Zabiti ve Şirket Güvenlik Sorumlusu Eğitimi', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'Gemi Güvenlik Zabiti ve Şirket Güvenlik Sorumlusu yeterliklerini tek bir programda birleştiren entegre eğitim.', + 'long_desc' => 'Birleştirilmiş Gemi Güvenlik Zabiti ve Şirket Güvenlik Sorumlusu Eğitimi, ISPS Kod gerekliliklerini hem gemi hem de şirket tarafında karşılayacak personelin yetiştirilmesi için tasarlanmış entegre bir programdır. İki yeterliliği tek bir programda birleştirerek zaman ve maliyet avantajı sağlar.', + 'duration' => '4 Gün', + 'students' => 118, + 'rating' => 4.8, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'ISPS Kod kapsamlı uygulama ve gereklilikler', + 'Gemi güvenlik planı hazırlama ve yönetimi', + 'Şirket güvenlik politikası ve prosedürleri', + 'Gemi-kıyı güvenlik koordinasyonu', + ], + 'requirements' => [ + 'Zabit yeterliğine sahip olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Birleştirilmiş Gemi + Şirket Güvenlik Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'ISPS kapsamlı iki yeterliği tek programda alın. Gemi ve Şirket Güvenlik Sorumlusu birleşik eğitim.', + ], + [ + 'slug' => 'balikci-gemisi-tayfasi-deniz-guvenlik-egitimleri', + 'category_id' => $cats['guvenlik'], + 'title' => 'Balıkçı Gemisi Tayfası Deniz Güvenlik Eğitimleri', + 'sub' => 'STCW-F / IMO Uyumlu', + 'desc' => 'Balıkçı gemilerinde görev yapacak tayfaların denizde karşılaşabilecekleri tehlikelere karşı hazırlanmasını sağlayan zorunlu eğitim.', + 'long_desc' => 'Balıkçı Gemisi Tayfası Deniz Güvenlik Eğitimleri, balıkçı gemilerinde görev yapacak tayfaların denizde karşılaşabilecekleri tehlikelere karşı hazırlanmasını sağlayan zorunlu bir eğitim programıdır. STCW-F Sözleşmesi gerekliliklerine uygun olarak verilmektedir.', + 'duration' => '4 Gün', + 'students' => 206, + 'rating' => 4.6, + 'badge' => null, + 'price' => '₺2.500', + 'includes' => [ + 'Denizde kişisel can kurtarma teknikleri', + 'Yangın önleme ve söndürme yöntemleri', + 'Denizde hayatta kalma teknikleri', + 'Temel ilk yardım bilgisi', + ], + 'requirements' => [ + '18 yaş ve üzeri olmak', + 'En az ilköğretim mezunu olmak', + 'Geçerli sağlık raporuna sahip olmak', + 'Adli sicil kaydında engel bulunmamak', + ], + 'meta_title' => 'Balıkçı Gemisi Tayfası Güvenlik Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'STCW-F balıkçı gemisi tayfası deniz güvenlik eğitimi. Balıkçılık sektöründe güvenli çalışın.', + ], + [ + 'slug' => 'gemi-guvenlik-egitimleri', + 'category_id' => $cats['guvenlik'], + 'title' => 'Gemi Güvenlik Eğitimleri', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'ISPS Kod kapsamında tüm gemi personelinin güvenlik bilincini artırmaya yönelik zorunlu eğitim programı.', + 'long_desc' => 'Gemi Güvenlik Eğitimleri, ISPS Kod kapsamında tüm gemi personelinin güvenlik bilincini artırmaya yönelik zorunlu bir eğitim programıdır. Gemide güvenlik kültürünün oluşturulması ve sürdürülmesi için temel bilgileri kapsamaktadır.', + 'duration' => '2 Gün', + 'students' => 416, + 'rating' => 5.0, + 'badge' => null, + 'price' => '₺7.500', + 'includes' => [ + 'ISPS Kod genel bilgi ve gereklilikler', + 'Güvenlik tehditleri ve risk değerlendirmesi', + 'Güvenlik seviyeleri ve prosedürler', + 'Güvenlik tatbikatları ve uygulamaları', + ], + 'requirements' => [ + 'Gemide görev yapıyor veya yapacak olmak', + '18 yaş ve üzeri olmak', + ], + 'meta_title' => 'Gemi Güvenlik Eğitimi | Boğaziçi Denizcilik', + 'meta_description' => 'ISPS Kod gemi güvenlik eğitimi. Tüm gemi personeli için zorunlu güvenlik bilinci programı.', + ], + [ + 'slug' => 'liman-tesisi-guvenlik-sorumlusu', + 'category_id' => $cats['guvenlik'], + 'title' => 'Liman Tesisi Güvenlik Sorumlusu', + 'sub' => 'STCW / IMO Uyumlu', + 'desc' => 'ISPS Kod kapsamında liman tesislerinin güvenlik yönetiminden sorumlu olacak personelin yetiştirilmesine yönelik program.', + 'long_desc' => 'Liman Tesisi Güvenlik Sorumlusu Eğitimi, ISPS Kod kapsamında liman tesislerinin güvenlik yönetiminden sorumlu olacak personelin yetiştirilmesine yönelik bir programdır. Liman tesisi güvenlik planının hazırlanması, uygulanması ve denetlenmesi konularında kapsamlı bilgi kazandırmaktadır.', + 'duration' => '3 Gün', + 'students' => 769, + 'rating' => 5.0, + 'badge' => null, + 'price' => '₺5.200', + 'includes' => [ + 'ISPS Kod ve liman tesisi güvenliği', + 'Liman tesisi güvenlik değerlendirmesi', + 'Erişim kontrolü ve gözetleme sistemleri', + 'Güvenlik denetimi ve iç tetkik', + ], + 'requirements' => [ + 'Liman tesisinde görev yapıyor veya yapacak olmak', + 'En az ortaöğretim mezunu olmak', + ], + 'meta_title' => 'Liman Tesisi Güvenlik Sorumlusu | Boğaziçi Denizcilik', + 'meta_description' => 'ISPS Liman Tesisi Güvenlik Sorumlusu sertifika programı. Liman güvenlik yönetimi için zorunlu yeterlik.', + ], + ]; + + foreach ($courses as $data) { + Course::updateOrCreate( + ['slug' => $data['slug']], + $data, + ); + } + + // ── Mega menu sırası (her kategoriden 3 kurs) ───────────────────── + // Admin panelinden kurs düzenleyerek menu_order = 1, 2, 3 verilir. + // null olan kurslar mega menu'de görünmez. + $menuOrder = [ + // Güverte + 'arpa-radar-simulator' => 1, + 'ecdis-tip-bazli-egitim' => 2, + 'gmdss-genel-telsiz-operatoru-goc' => 3, + // STCW + 'stcw-temel-guvenlik' => 1, + 'aff-yangin-sondurme' => 2, + 'denizde-kisisel-can-kurtarma-teknikleri-egitimi-teorik' => 3, + // Makine + 'dizel-motor-teknigi' => 1, + 'usta-makine-tayfasi-yetistirme-egitimi-a-iii-5' => 2, + 'makine-dairesi-operasyonu' => 3, + // Yat Kaptanlığı + 'yat-kaptani-25-gt' => 1, + 'yat-kaptani-149-gt-egitimi-temel' => 2, + 'yat-kaptani-3000-gt' => 3, + // Yenileme + 'bst-yenileme' => 1, + 'guverte-sinirli-i-sletim-duzeyi-tazeleme-egitimi' => 2, + 'denizde-guvenlik-egitimleri-yenileme-teorik' => 3, + // Güvenlik (ISPS) + 'gemi-guvenlik-zabiti' => 1, + 'liman-tesisi-guvenlik-sorumlusu' => 2, + 'gemi-guvenlik-egitimleri' => 3, + ]; + + foreach ($menuOrder as $slug => $order) { + Course::where('slug', $slug)->update(['menu_order' => $order]); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..bec1c45 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,31 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Console\Seeds\WithoutModelEvents; +use Illuminate\Database\Seeder; + +class DatabaseSeeder extends Seeder +{ + use WithoutModelEvents; + + /** + * Seed the application's database. + */ + public function run(): void + { + $this->call([ + RolePermissionSeeder::class, + AdminUserSeeder::class, + SettingSeeder::class, + CategorySeeder::class, + CourseSeeder::class, + CourseScheduleSeeder::class, + AnnouncementSeeder::class, + HeroSlideSeeder::class, + FaqSeeder::class, + // MenuSeeder kaldırıldı - navbar categories + courses tablosundan dinamik olarak besleniyor + CorporatePagesSeeder::class, + ]); + } +} diff --git a/database/seeders/FaqContentSeeder.php b/database/seeders/FaqContentSeeder.php new file mode 100644 index 0000000..91658d9 --- /dev/null +++ b/database/seeders/FaqContentSeeder.php @@ -0,0 +1,68 @@ +<?php + +namespace Database\Seeders; + +use App\Models\Faq; +use Illuminate\Database\Seeder; + +class FaqContentSeeder extends Seeder +{ + /** + * Seed FAQs from parsed docx articles. + * Source: storage/app/parsed_faqs.json + * Uses firstOrCreate (question-based) — safe to re-run. + */ + public function run(): void + { + $jsonPath = storage_path('app/parsed_faqs.json'); + + if (! file_exists($jsonPath)) { + $this->command->error('parsed_faqs.json bulunamadı. Önce Python parse script çalıştırın.'); + + return; + } + + /** @var array<int, array{question: string, answer: string, source: string}> $faqs */ + $faqs = json_decode(file_get_contents($jsonPath), true); + + $categoryMap = [ + 'Guverte_Egitimleri' => 'egitimler', + 'STCW_Egitimleri' => 'stcw', + 'Makine_Egitimleri' => 'makine', + 'Yat_Kaptanligi_Egitimleri' => 'yat-kaptanligi', + 'Yenileme_Egitimleri' => 'yenileme', + 'Seminer_Sertifikalari' => 'guvenlik', + ]; + + $created = 0; + $skipped = 0; + $orderCounters = []; + + foreach ($faqs as $faq) { + // Determine category from source folder + $sourceFolder = explode('/', $faq['source'])[0] ?? ''; + $category = $categoryMap[$sourceFolder] ?? 'egitimler'; + + // Track order per category + $orderCounters[$category] = ($orderCounters[$category] ?? -1) + 1; + + $result = Faq::firstOrCreate( + ['question' => $faq['question']], + [ + 'category' => $category, + 'answer' => $faq['answer'], + 'order_index' => $orderCounters[$category], + 'is_active' => true, + ], + ); + + if ($result->wasRecentlyCreated) { + $created++; + } else { + $skipped++; + } + } + + $this->command->info("FAQ: {$created} oluşturuldu, {$skipped} zaten mevcuttu."); + } +} diff --git a/database/seeders/FaqSeeder.php b/database/seeders/FaqSeeder.php new file mode 100644 index 0000000..18f1fcc --- /dev/null +++ b/database/seeders/FaqSeeder.php @@ -0,0 +1,83 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; + +class FaqSeeder extends Seeder +{ + public function run(): void + { + DB::table('faqs')->truncate(); + + $faqs = [ + // EĞİTİMLER + [ + 'category' => 'egitimler', + 'question' => 'Eğitimleriniz hangi kurumlar tarafından onaylıdır?', + 'answer' => 'Tüm eğitimlerimiz Ulaştırma ve Altyapı Bakanlığı Denizcilik Genel Müdürlüğü onaylı olmakla birlikte, uluslararası IMO ve STCW sözleşmelerine %100 uyumludur.', + 'order_index' => 1, + 'is_active' => true, + ], + [ + 'category' => 'egitimler', + 'question' => 'Simülatör eğitimleriniz nasıl gerçekleştiriliyor?', + 'answer' => 'Köprüüstü ARPA/Radar ve ECDIS eğitimlerimiz ile Makine Dairesi operasyonlarımız son teknoloji Kongsberg simülatör sistemleri ile uygulamalı olarak gerçekleştirilmektedir.', + 'order_index' => 2, + 'is_active' => true, + ], + [ + 'category' => 'egitimler', + 'question' => 'Eğitim süreleri ne kadar devam ediyor?', + 'answer' => 'Temel sertifika programları 2-5 gün, yeterlik hazırlık kursları (Yat Kaptanlığı vb.) ise 10-15 gün arasında değişmektedir.', + 'order_index' => 3, + 'is_active' => true, + ], + + // KAYIT SÜREÇLERİ + [ + 'category' => 'kayit', + 'question' => 'Kayıt olmak için hangi belgelere ihtiyacım var?', + 'answer' => 'Kayıt olmak istediğiniz eğitim kategorisine göre değişiklik göstermekle beraber genel olarak; Gemiadamı Cüzdanı fotokopisi, Nüfus Cüzdanı, Fotoğraf ve Sağlık Raporu istenmektedir.', + 'order_index' => 4, + 'is_active' => true, + ], + [ + 'category' => 'kayit', + 'question' => 'Ön kayıt sonrası ödeme süreci nasıl ilerliyor?', + 'answer' => 'Web sitemiz üzerinden ön kayıt oluşturduğunuzda eğitim danışmanlarımız sizi arayarak belgeleriniz ve kontenjan durumu hakkında bilgi verir. Kesin kayıt sırasında havale, EFT veya kredi kartı ile ödeme yapabilirsiniz.', + 'order_index' => 5, + 'is_active' => true, + ], + [ + 'category' => 'kayit', + 'question' => 'Eğitimlere uzaktan (online) katılmak mümkün mü?', + 'answer' => 'Ulaştırma Bakanlığı mevzuatı gereği STCW kapsamındaki güvenlik eğitimleri ve harita uygulamaları örgün (yüz yüze) olmak zorundadır. Ancak bazı seminer ve yenileme teorik dersleri uzaktan eğitim modeliyle sunulabilmektedir.', + 'order_index' => 6, + 'is_active' => true, + ], + + // İLETİŞİM & ULAŞIM + [ + 'category' => 'iletisim', + 'question' => 'Eğitim merkezinize nasıl ulaşabilirim?', + 'answer' => 'Eğitim merkezimiz Kadıköy Rıhtım\'a 5 dakika yürüme mesafesinde, merkezi bir konumdadır. Şehir içinden metro, vapur veya otobüs ile kolayca ulaşım sağlayabilirsiniz.', + 'order_index' => 7, + 'is_active' => true, + ], + [ + 'category' => 'iletisim', + 'question' => 'Şehir dışından gelenler için konaklama desteği var mı?', + 'answer' => 'Kurumumuza ait bir misafirhane bulunmamakla birlikte, anlaşmalı olduğumuz yakın otel ve yurtlarda öğrencilerimize özel indirimli fiyat avantajlarından yararlanmanızı sağlıyoruz.', + 'order_index' => 8, + 'is_active' => true, + ], + ]; + + DB::table('faqs')->insert(array_map(fn ($f) => array_merge($f, [ + 'created_at' => now(), + 'updated_at' => now(), + ]), $faqs)); + } +} diff --git a/database/seeders/HeroSlideSeeder.php b/database/seeders/HeroSlideSeeder.php new file mode 100644 index 0000000..5ed6f96 --- /dev/null +++ b/database/seeders/HeroSlideSeeder.php @@ -0,0 +1,46 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; + +class HeroSlideSeeder extends Seeder +{ + public function run(): void + { + DB::table('hero_slides')->truncate(); + + $slides = [ + [ + 'label' => 'Güvenlik Eğitimleri', + 'title' => 'Profesyonel Eğitimle Güvenli Seferler İçin', + 'description' => 'IMO standartlarında yangın, cankurtarma ve tehlikeli madde eğitimleriyle kariyerinizi güvene alın.', + 'image' => null, + 'order_index' => 1, + 'is_active' => true, + ], + [ + 'label' => 'Yat Kaptanlığı', + 'title' => 'Denizde Kendi Rotanızı Çizin', + 'description' => '149 GT ve 499 GT yat kaptanlığı eğitimleriyle Türkiye\'nin en gözde kıyılarında profesyonel kaptan olarak görev yapın.', + 'image' => null, + 'order_index' => 2, + 'is_active' => true, + ], + [ + 'label' => 'Belge Yenileme', + 'title' => 'Belgelerinizi Zamanında Yenileyin', + 'description' => 'STCW yenileme, GOC/ROC tazeleme ve yat kaptanlığı tazeleme eğitimleriyle yeterliliklerinizi güncel tutun.', + 'image' => null, + 'order_index' => 3, + 'is_active' => true, + ], + ]; + + DB::table('hero_slides')->insert(array_map(fn ($s) => array_merge($s, [ + 'created_at' => now(), + 'updated_at' => now(), + ]), $slides)); + } +} diff --git a/database/seeders/MenuSeeder.php b/database/seeders/MenuSeeder.php new file mode 100644 index 0000000..ff603d8 --- /dev/null +++ b/database/seeders/MenuSeeder.php @@ -0,0 +1,100 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; + +class MenuSeeder extends Seeder +{ + public function run(): void + { + DB::table('menus')->truncate(); + + $now = now(); + + // ── HEADER menüsü ───────────────────────────────────────────────── + $headerItems = [ + ['location' => 'header', 'label' => 'Anasayfa', 'url' => '/', 'type' => 'link', 'parent_id' => null, 'order' => 1, 'is_active' => true], + ['location' => 'header', 'label' => 'Kurumsal', 'url' => '/kurumsal', 'type' => 'link', 'parent_id' => null, 'order' => 2, 'is_active' => true], + ['location' => 'header', 'label' => 'Eğitimler', 'url' => '/egitimler', 'type' => 'link', 'parent_id' => null, 'order' => 3, 'is_active' => true], + ['location' => 'header', 'label' => 'Eğitim Takvimi', 'url' => '/egitim-takvimi', 'type' => 'link', 'parent_id' => null, 'order' => 4, 'is_active' => true], + ['location' => 'header', 'label' => 'Duyurular', 'url' => '/duyurular', 'type' => 'link', 'parent_id' => null, 'order' => 5, 'is_active' => true], + ['location' => 'header', 'label' => 'S.S.S.', 'url' => '/sss', 'type' => 'link', 'parent_id' => null, 'order' => 6, 'is_active' => true], + ['location' => 'header', 'label' => 'İletişim', 'url' => '/iletisim', 'type' => 'link', 'parent_id' => null, 'order' => 7, 'is_active' => true], + ]; + + foreach ($headerItems as $item) { + DB::table('menus')->insert(array_merge($item, ['created_at' => $now, 'updated_at' => $now])); + } + + // Kurumsal dropdown parent id + $kurumsalId = DB::table('menus')->where('label', 'Kurumsal')->where('location', 'header')->value('id'); + + $kurumsalChildren = [ + ['location' => 'header', 'label' => 'Hakkımızda', 'url' => '/kurumsal/hakkimizda', 'type' => 'link', 'parent_id' => $kurumsalId, 'order' => 1, 'is_active' => true], + ['location' => 'header', 'label' => 'Vizyon ve Misyon', 'url' => '/kurumsal/vizyon-ve-misyon', 'type' => 'link', 'parent_id' => $kurumsalId, 'order' => 2, 'is_active' => true], + ['location' => 'header', 'label' => 'Kalite Politikamız', 'url' => '/kurumsal/kalite-politikamiz', 'type' => 'link', 'parent_id' => $kurumsalId, 'order' => 3, 'is_active' => true], + ]; + + foreach ($kurumsalChildren as $item) { + DB::table('menus')->insert(array_merge($item, ['created_at' => $now, 'updated_at' => $now])); + } + + // Eğitimler dropdown parent id + $egitimlerParentId = DB::table('menus')->where('label', 'Eğitimler')->where('location', 'header')->value('id'); + + $egitimlerChildren = [ + ['location' => 'header', 'label' => 'Güverte Eğitimleri', 'url' => '/egitimler/guverte', 'type' => 'category', 'parent_id' => $egitimlerParentId, 'order' => 1, 'is_active' => true], + ['location' => 'header', 'label' => 'STCW Eğitimleri', 'url' => '/egitimler/stcw', 'type' => 'category', 'parent_id' => $egitimlerParentId, 'order' => 2, 'is_active' => true], + ['location' => 'header', 'label' => 'Makine Eğitimleri', 'url' => '/egitimler/makine', 'type' => 'category', 'parent_id' => $egitimlerParentId, 'order' => 3, 'is_active' => true], + ['location' => 'header', 'label' => 'Yat Kaptanlığı', 'url' => '/egitimler/yat-kaptanligi', 'type' => 'category', 'parent_id' => $egitimlerParentId, 'order' => 4, 'is_active' => true], + ['location' => 'header', 'label' => 'Yenileme Eğitimleri', 'url' => '/egitimler/yenileme', 'type' => 'category', 'parent_id' => $egitimlerParentId, 'order' => 5, 'is_active' => true], + ['location' => 'header', 'label' => 'Güvenlik (ISPS)', 'url' => '/egitimler/guvenlik', 'type' => 'category', 'parent_id' => $egitimlerParentId, 'order' => 6, 'is_active' => true], + ]; + + foreach ($egitimlerChildren as $item) { + DB::table('menus')->insert(array_merge($item, ['created_at' => $now, 'updated_at' => $now])); + } + + // ── FOOTER menüsü ───────────────────────────────────────────────── + $footerItems = [ + // Kurumsal grubu + ['location' => 'footer', 'label' => 'Kurumsal', 'url' => '/kurumsal', 'type' => 'link', 'parent_id' => null, 'order' => 1, 'is_active' => true], + ['location' => 'footer', 'label' => 'Eğitimler', 'url' => '/egitimler', 'type' => 'link', 'parent_id' => null, 'order' => 2, 'is_active' => true], + ['location' => 'footer', 'label' => 'Hızlı Linkler', 'url' => '#', 'type' => 'link', 'parent_id' => null, 'order' => 3, 'is_active' => true], + ]; + + foreach ($footerItems as $item) { + DB::table('menus')->insert(array_merge($item, ['created_at' => $now, 'updated_at' => $now])); + } + + $footerKurumsalId = DB::table('menus')->where('label', 'Kurumsal')->where('location', 'footer')->value('id'); + $footerEgitimId = DB::table('menus')->where('label', 'Eğitimler')->where('location', 'footer')->value('id'); + $footerHizliId = DB::table('menus')->where('label', 'Hızlı Linkler')->where('location', 'footer')->value('id'); + + $footerChildren = [ + // Kurumsal alt menü + ['location' => 'footer', 'label' => 'Hakkımızda', 'url' => '/kurumsal/hakkimizda', 'type' => 'link', 'parent_id' => $footerKurumsalId, 'order' => 1, 'is_active' => true], + ['location' => 'footer', 'label' => 'Misyon & Vizyon', 'url' => '/kurumsal/vizyon-ve-misyon', 'type' => 'link', 'parent_id' => $footerKurumsalId, 'order' => 2, 'is_active' => true], + ['location' => 'footer', 'label' => 'Kalite Politikamız', 'url' => '/kurumsal/kalite-politikamiz', 'type' => 'link', 'parent_id' => $footerKurumsalId, 'order' => 3, 'is_active' => true], + ['location' => 'footer', 'label' => 'İletişim', 'url' => '/iletisim', 'type' => 'link', 'parent_id' => $footerKurumsalId, 'order' => 4, 'is_active' => true], + + // Eğitimler alt menü + ['location' => 'footer', 'label' => 'Tüm Eğitimler', 'url' => '/egitimler', 'type' => 'link', 'parent_id' => $footerEgitimId, 'order' => 1, 'is_active' => true], + ['location' => 'footer', 'label' => 'Güverte Eğitimleri', 'url' => '/egitimler/guverte', 'type' => 'link', 'parent_id' => $footerEgitimId, 'order' => 2, 'is_active' => true], + ['location' => 'footer', 'label' => 'STCW Eğitimleri', 'url' => '/egitimler/stcw', 'type' => 'link', 'parent_id' => $footerEgitimId, 'order' => 3, 'is_active' => true], + ['location' => 'footer', 'label' => 'Yat Kaptanlığı', 'url' => '/egitimler/yat-kaptanligi', 'type' => 'link', 'parent_id' => $footerEgitimId, 'order' => 4, 'is_active' => true], + + // Hızlı Linkler alt menü + ['location' => 'footer', 'label' => 'Eğitim Takvimi', 'url' => '/egitim-takvimi', 'type' => 'link', 'parent_id' => $footerHizliId, 'order' => 1, 'is_active' => true], + ['location' => 'footer', 'label' => 'Duyurular & Haberler', 'url' => '/duyurular', 'type' => 'link', 'parent_id' => $footerHizliId, 'order' => 2, 'is_active' => true], + ['location' => 'footer', 'label' => 'Ön Kayıt Ekranı', 'url' => '/kayit', 'type' => 'link', 'parent_id' => $footerHizliId, 'order' => 3, 'is_active' => true], + ['location' => 'footer', 'label' => 'S.S.S.', 'url' => '/sss', 'type' => 'link', 'parent_id' => $footerHizliId, 'order' => 4, 'is_active' => true], + ]; + + foreach ($footerChildren as $item) { + DB::table('menus')->insert(array_merge($item, ['created_at' => $now, 'updated_at' => $now])); + } + } +} diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php new file mode 100644 index 0000000..2f73a1c --- /dev/null +++ b/database/seeders/RolePermissionSeeder.php @@ -0,0 +1,78 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; +use Spatie\Permission\PermissionRegistrar; + +class RolePermissionSeeder extends Seeder +{ + /** + * Modellere göre tanımlanacak permission grupları. + * + * @var list<string> + */ + private const MODULES = [ + 'category', + 'course', + 'schedule', + 'announcement', + 'hero-slide', + 'lead', + 'menu', + 'comment', + 'faq', + 'guide-card', + 'setting', + 'page', + 'user', + 'role', + ]; + + /** + * Her modül için oluşturulacak CRUD aksiyonlar. + * + * @var list<string> + */ + private const ACTIONS = ['view', 'create', 'update', 'delete']; + + /** + * Editor rolüne verilmeyecek aksiyonlar. + * + * @var list<string> + */ + private const EDITOR_EXCLUDED_ACTIONS = ['delete']; + + /** + * Run the database seeds. + */ + public function run(): void + { + app()[PermissionRegistrar::class]->forgetCachedPermissions(); + + $allPermissions = []; + $editorPermissions = []; + + foreach (self::MODULES as $module) { + foreach (self::ACTIONS as $action) { + $permissionName = "{$action}-{$module}"; + Permission::firstOrCreate(['name' => $permissionName, 'guard_name' => 'web']); + $allPermissions[] = $permissionName; + + if (! in_array($action, self::EDITOR_EXCLUDED_ACTIONS)) { + $editorPermissions[] = $permissionName; + } + } + } + + // super-admin: tüm yetkiler + $superAdmin = Role::firstOrCreate(['name' => 'super-admin', 'guard_name' => 'web']); + $superAdmin->syncPermissions($allPermissions); + + // editor: silme hariç tüm yetkiler + $editor = Role::firstOrCreate(['name' => 'editor', 'guard_name' => 'web']); + $editor->syncPermissions($editorPermissions); + } +} diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php new file mode 100644 index 0000000..cc25aca --- /dev/null +++ b/database/seeders/RoleSeeder.php @@ -0,0 +1,18 @@ +<?php + +namespace Database\Seeders; + +use Illuminate\Database\Seeder; +use Spatie\Permission\Models\Role; + +class RoleSeeder extends Seeder +{ + /** + * Run the database seeds. + */ + public function run(): void + { + Role::findOrCreate('super-admin', 'web'); + Role::findOrCreate('editor', 'web'); + } +} diff --git a/database/seeders/SettingSeeder.php b/database/seeders/SettingSeeder.php new file mode 100644 index 0000000..d2218e6 --- /dev/null +++ b/database/seeders/SettingSeeder.php @@ -0,0 +1,274 @@ +<?php + +namespace Database\Seeders; + +use App\Models\Setting; +use Illuminate\Database\Seeder; + +class SettingSeeder extends Seeder +{ + /** + * Seed site settings across all groups. + */ + public function run(): void + { + $settings = [ + ...$this->general(), + ...$this->contact(), + ...$this->maps(), + ...$this->social(), + ...$this->seo(), + ...$this->analytics(), + ...$this->header(), + ...$this->footer(), + ...$this->integrations(), + ...$this->infoSections(), + ]; + + foreach ($settings as $setting) { + $value = $setting['value']; + unset($setting['value']); + + Setting::query()->updateOrCreate( + ['group' => $setting['group'], 'key' => $setting['key']], + array_merge($setting, [ + 'value' => Setting::query() + ->where('group', $setting['group']) + ->where('key', $setting['key']) + ->value('value') ?? $value, + ]), + ); + } + } + + /** + * @return list<array<string, mixed>> + */ + private function general(): array + { + $g = 'general'; + + return [ + ['group' => $g, 'key' => 'site_name', 'value' => 'Boğaziçi Denizcilik Eğitim Kurumu', 'type' => 'text', 'label' => 'Site Adı', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'site_tagline', 'value' => null, 'type' => 'text', 'label' => 'Slogan', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'site_description', 'value' => null, 'type' => 'textarea', 'label' => 'Kısa Site Açıklaması', 'order_index' => 2, 'is_public' => true], + ['group' => $g, 'key' => 'logo_light', 'value' => null, 'type' => 'image', 'label' => 'Logo — Açık Tema (beyaz navbar)', 'order_index' => 3, 'is_public' => true], + ['group' => $g, 'key' => 'logo_dark', 'value' => null, 'type' => 'image', 'label' => 'Logo — Koyu Tema (dark bg)', 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'favicon', 'value' => null, 'type' => 'image', 'label' => 'Favicon (32x32 PNG)', 'order_index' => 5, 'is_public' => true], + ['group' => $g, 'key' => 'apple_touch_icon', 'value' => null, 'type' => 'image', 'label' => 'Apple Touch Icon (180x180)', 'order_index' => 6, 'is_public' => true], + ['group' => $g, 'key' => 'announcement_bar_active', 'value' => 'false', 'type' => 'boolean', 'label' => 'Üst Bar Aktif mi', 'order_index' => 7, 'is_public' => true], + ['group' => $g, 'key' => 'announcement_bar_text', 'value' => null, 'type' => 'text', 'label' => 'Üst Bar Metni', 'order_index' => 8, 'is_public' => true], + ['group' => $g, 'key' => 'announcement_bar_url', 'value' => null, 'type' => 'url', 'label' => 'Üst Bar Linki', 'order_index' => 9, 'is_public' => true], + ['group' => $g, 'key' => 'announcement_bar_bg_color', 'value' => '#1a3e74', 'type' => 'color', 'label' => 'Üst Bar Arka Plan Rengi', 'order_index' => 10, 'is_public' => true], + ['group' => $g, 'key' => 'maintenance_mode', 'value' => 'false', 'type' => 'boolean', 'label' => 'Bakım Modu', 'order_index' => 11, 'is_public' => true], + ['group' => $g, 'key' => 'maintenance_message', 'value' => null, 'type' => 'textarea', 'label' => 'Bakım Modu Mesajı', 'order_index' => 12, 'is_public' => true], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function contact(): array + { + $g = 'contact'; + + return [ + ['group' => $g, 'key' => 'phone_primary', 'value' => null, 'type' => 'text', 'label' => 'Ana Telefon', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'phone_secondary', 'value' => null, 'type' => 'text', 'label' => 'İkinci Telefon', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'email_info', 'value' => null, 'type' => 'text', 'label' => 'Bilgi E-postası', 'order_index' => 2, 'is_public' => true], + ['group' => $g, 'key' => 'email_support', 'value' => null, 'type' => 'text', 'label' => 'Destek E-postası', 'order_index' => 3, 'is_public' => true], + ['group' => $g, 'key' => 'email_kayit', 'value' => null, 'type' => 'text', 'label' => 'Kayıt E-postası', 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'address_full', 'value' => null, 'type' => 'textarea', 'label' => 'Tam Adres', 'order_index' => 5, 'is_public' => true], + ['group' => $g, 'key' => 'address_short', 'value' => null, 'type' => 'text', 'label' => 'Kısa Adres (navbar için)', 'order_index' => 6, 'is_public' => true], + ['group' => $g, 'key' => 'district', 'value' => null, 'type' => 'text', 'label' => 'İlçe', 'order_index' => 7, 'is_public' => true], + ['group' => $g, 'key' => 'city', 'value' => null, 'type' => 'text', 'label' => 'Şehir', 'order_index' => 8, 'is_public' => true], + ['group' => $g, 'key' => 'postal_code', 'value' => null, 'type' => 'text', 'label' => 'Posta Kodu', 'order_index' => 9, 'is_public' => true], + ['group' => $g, 'key' => 'working_hours_weekday', 'value' => null, 'type' => 'text', 'label' => 'Hafta İçi Saatleri', 'order_index' => 10, 'is_public' => true], + ['group' => $g, 'key' => 'working_hours_saturday', 'value' => null, 'type' => 'text', 'label' => 'Cumartesi Saatleri', 'order_index' => 11, 'is_public' => true], + ['group' => $g, 'key' => 'working_hours_sunday', 'value' => null, 'type' => 'text', 'label' => 'Pazar Saatleri', 'order_index' => 12, 'is_public' => true], + ['group' => $g, 'key' => 'whatsapp_number', 'value' => null, 'type' => 'text', 'label' => 'WhatsApp (+90 ile başlayan)', 'order_index' => 13, 'is_public' => true], + ['group' => $g, 'key' => 'whatsapp_message', 'value' => null, 'type' => 'text', 'label' => 'WhatsApp Varsayılan Mesaj', 'order_index' => 14, 'is_public' => true], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function maps(): array + { + $g = 'maps'; + + return [ + ['group' => $g, 'key' => 'google_maps_embed_url', 'value' => null, 'type' => 'textarea', 'label' => 'Google Maps Embed URL (iframe src)', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'google_maps_place_url', 'value' => null, 'type' => 'url', 'label' => 'Google Maps Profil Linki', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'google_maps_api_key', 'value' => null, 'type' => 'text', 'label' => 'Google Maps API Key', 'order_index' => 2, 'is_public' => false], + ['group' => $g, 'key' => 'latitude', 'value' => null, 'type' => 'text', 'label' => 'Enlem', 'order_index' => 3, 'is_public' => true], + ['group' => $g, 'key' => 'longitude', 'value' => null, 'type' => 'text', 'label' => 'Boylam', 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'map_zoom_level', 'value' => '15', 'type' => 'text', 'label' => 'Harita Zoom (1-20)', 'order_index' => 5, 'is_public' => true], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function social(): array + { + $g = 'social'; + + return [ + ['group' => $g, 'key' => 'instagram_url', 'value' => null, 'type' => 'url', 'label' => 'Instagram Profil URL', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'instagram_handle', 'value' => null, 'type' => 'text', 'label' => 'Instagram Kullanıcı Adı (@siz)', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'facebook_url', 'value' => null, 'type' => 'url', 'label' => 'Facebook Sayfası URL', 'order_index' => 2, 'is_public' => true], + ['group' => $g, 'key' => 'facebook_page_id', 'value' => null, 'type' => 'text', 'label' => 'Facebook Page ID', 'order_index' => 3, 'is_public' => false], + ['group' => $g, 'key' => 'twitter_url', 'value' => null, 'type' => 'url', 'label' => 'X (Twitter) Profil URL', 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'twitter_handle', 'value' => null, 'type' => 'text', 'label' => 'X Kullanıcı Adı (@siz)', 'order_index' => 5, 'is_public' => true], + ['group' => $g, 'key' => 'youtube_url', 'value' => null, 'type' => 'url', 'label' => 'YouTube Kanal URL', 'order_index' => 6, 'is_public' => true], + ['group' => $g, 'key' => 'youtube_channel_id', 'value' => null, 'type' => 'text', 'label' => 'YouTube Channel ID', 'order_index' => 7, 'is_public' => false], + ['group' => $g, 'key' => 'linkedin_url', 'value' => null, 'type' => 'url', 'label' => 'LinkedIn Sayfa URL', 'order_index' => 8, 'is_public' => true], + ['group' => $g, 'key' => 'tiktok_url', 'value' => null, 'type' => 'url', 'label' => 'TikTok Profil URL', 'order_index' => 9, 'is_public' => true], + ['group' => $g, 'key' => 'pinterest_url', 'value' => null, 'type' => 'url', 'label' => 'Pinterest URL', 'order_index' => 10, 'is_public' => true], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function seo(): array + { + $g = 'seo'; + + return [ + // Temel SEO + ['group' => $g, 'key' => 'meta_title_suffix', 'value' => 'Boğaziçi Denizcilik', 'type' => 'text', 'label' => 'Title Eki', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'meta_title_separator', 'value' => '|', 'type' => 'text', 'label' => 'Ayraç Karakteri', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'default_meta_description', 'value' => null, 'type' => 'textarea', 'label' => 'Varsayılan Meta Açıklama', 'order_index' => 2, 'is_public' => true], + ['group' => $g, 'key' => 'default_meta_keywords', 'value' => null, 'type' => 'textarea', 'label' => 'Varsayılan Keywords (virgülle)', 'order_index' => 3, 'is_public' => true], + ['group' => $g, 'key' => 'robots', 'value' => 'index, follow', 'type' => 'text', 'label' => 'Robots', 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'canonical_domain', 'value' => null, 'type' => 'url', 'label' => 'Canonical Domain', 'order_index' => 5, 'is_public' => true], + // Open Graph + ['group' => $g, 'key' => 'og_title', 'value' => null, 'type' => 'text', 'label' => 'OG Default Title', 'order_index' => 6, 'is_public' => true], + ['group' => $g, 'key' => 'og_description', 'value' => null, 'type' => 'textarea', 'label' => 'OG Default Description', 'order_index' => 7, 'is_public' => true], + ['group' => $g, 'key' => 'og_image', 'value' => null, 'type' => 'image', 'label' => 'OG Default Görsel (1200x630 px)', 'order_index' => 8, 'is_public' => true], + ['group' => $g, 'key' => 'og_type', 'value' => 'website', 'type' => 'text', 'label' => 'OG Type', 'order_index' => 9, 'is_public' => true], + ['group' => $g, 'key' => 'og_locale', 'value' => 'tr_TR', 'type' => 'text', 'label' => 'OG Locale', 'order_index' => 10, 'is_public' => true], + ['group' => $g, 'key' => 'og_site_name', 'value' => null, 'type' => 'text', 'label' => 'OG Site Name', 'order_index' => 11, 'is_public' => true], + ['group' => $g, 'key' => 'facebook_app_id', 'value' => null, 'type' => 'text', 'label' => 'Facebook App ID', 'order_index' => 12, 'is_public' => false], + // Twitter / X Card + ['group' => $g, 'key' => 'twitter_card_type', 'value' => 'summary_large_image', 'type' => 'text', 'label' => 'Card Tipi', 'order_index' => 13, 'is_public' => true], + ['group' => $g, 'key' => 'twitter_site', 'value' => null, 'type' => 'text', 'label' => 'Site @handle', 'order_index' => 14, 'is_public' => true], + ['group' => $g, 'key' => 'twitter_creator', 'value' => null, 'type' => 'text', 'label' => 'İçerik Sahibi @handle', 'order_index' => 15, 'is_public' => true], + ['group' => $g, 'key' => 'twitter_title', 'value' => null, 'type' => 'text', 'label' => 'Twitter Default Title', 'order_index' => 16, 'is_public' => true], + ['group' => $g, 'key' => 'twitter_description', 'value' => null, 'type' => 'textarea', 'label' => 'Twitter Default Description', 'order_index' => 17, 'is_public' => true], + ['group' => $g, 'key' => 'twitter_image', 'value' => null, 'type' => 'image', 'label' => 'Twitter Card Görseli (1200x600 px)', 'order_index' => 18, 'is_public' => true], + // Doğrulama Kodları + ['group' => $g, 'key' => 'google_site_verification', 'value' => null, 'type' => 'text', 'label' => 'Google Search Console Kodu', 'order_index' => 19, 'is_public' => true], + ['group' => $g, 'key' => 'bing_site_verification', 'value' => null, 'type' => 'text', 'label' => 'Bing Webmaster Kodu', 'order_index' => 20, 'is_public' => true], + ['group' => $g, 'key' => 'yandex_verification', 'value' => null, 'type' => 'text', 'label' => 'Yandex Webmaster Kodu', 'order_index' => 21, 'is_public' => true], + ['group' => $g, 'key' => 'pinterest_verification', 'value' => null, 'type' => 'text', 'label' => 'Pinterest Doğrulama Kodu', 'order_index' => 22, 'is_public' => true], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function analytics(): array + { + $g = 'analytics'; + + return [ + ['group' => $g, 'key' => 'google_analytics_id', 'value' => null, 'type' => 'text', 'label' => 'Google Analytics 4 ID (G-XXXXXXXX)', 'order_index' => 0, 'is_public' => false], + ['group' => $g, 'key' => 'google_tag_manager_id', 'value' => null, 'type' => 'text', 'label' => 'Google Tag Manager ID (GTM-XXXXXXX)', 'order_index' => 1, 'is_public' => false], + ['group' => $g, 'key' => 'google_ads_id', 'value' => null, 'type' => 'text', 'label' => 'Google Ads Conversion ID', 'order_index' => 2, 'is_public' => false], + ['group' => $g, 'key' => 'facebook_pixel_id', 'value' => null, 'type' => 'text', 'label' => 'Meta (Facebook) Pixel ID', 'order_index' => 3, 'is_public' => false], + ['group' => $g, 'key' => 'hotjar_id', 'value' => null, 'type' => 'text', 'label' => 'Hotjar Site ID', 'order_index' => 4, 'is_public' => false], + ['group' => $g, 'key' => 'clarity_id', 'value' => null, 'type' => 'text', 'label' => 'Microsoft Clarity ID', 'order_index' => 5, 'is_public' => false], + ['group' => $g, 'key' => 'tiktok_pixel_id', 'value' => null, 'type' => 'text', 'label' => 'TikTok Pixel ID', 'order_index' => 6, 'is_public' => false], + ['group' => $g, 'key' => 'crisp_website_id', 'value' => null, 'type' => 'text', 'label' => 'Crisp Chat Website ID', 'order_index' => 7, 'is_public' => false], + ['group' => $g, 'key' => 'custom_head_scripts', 'value' => null, 'type' => 'textarea', 'label' => '<head> içine özel script', 'order_index' => 8, 'is_public' => false], + ['group' => $g, 'key' => 'custom_body_scripts', 'value' => null, 'type' => 'textarea', 'label' => '<body> sonuna özel script', 'order_index' => 9, 'is_public' => false], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function header(): array + { + $g = 'header'; + + return [ + ['group' => $g, 'key' => 'navbar_style_default', 'value' => 'transparent', 'type' => 'text', 'label' => 'Varsayılan Navbar Stili (transparent/white)', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'cta_button_text', 'value' => 'Başvuru Yap', 'type' => 'text', 'label' => 'Sağ Üst Buton Metni', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'cta_button_url', 'value' => '/kayit', 'type' => 'url', 'label' => 'Sağ Üst Buton Linki', 'order_index' => 2, 'is_public' => true], + ['group' => $g, 'key' => 'cta_button_color', 'value' => '#1a3e74', 'type' => 'color', 'label' => 'Sağ Üst Buton Rengi', 'order_index' => 3, 'is_public' => true], + ['group' => $g, 'key' => 'show_phone_topbar', 'value' => 'true', 'type' => 'boolean', 'label' => "Üst Bar'da Telefon Göster", 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'show_email_topbar', 'value' => 'true', 'type' => 'boolean', 'label' => "Üst Bar'da E-posta Göster", 'order_index' => 5, 'is_public' => true], + ['group' => $g, 'key' => 'show_address_topbar', 'value' => 'true', 'type' => 'boolean', 'label' => "Üst Bar'da Adres Göster", 'order_index' => 6, 'is_public' => true], + ['group' => $g, 'key' => 'show_hours_topbar', 'value' => 'true', 'type' => 'boolean', 'label' => "Üst Bar'da Saat Göster", 'order_index' => 7, 'is_public' => true], + ['group' => $g, 'key' => 'show_social_navbar', 'value' => 'true', 'type' => 'boolean', 'label' => "Navbar'da Sosyal Medya İkonları Göster", 'order_index' => 8, 'is_public' => true], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function footer(): array + { + $g = 'footer'; + + return [ + ['group' => $g, 'key' => 'footer_description', 'value' => null, 'type' => 'textarea', 'label' => 'Footer Açıklaması', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'footer_logo', 'value' => null, 'type' => 'image', 'label' => 'Footer Logo (varsa ayrı)', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'copyright_text', 'value' => '© 2026 Boğaziçi Denizcilik', 'type' => 'text', 'label' => 'Copyright Metni', 'order_index' => 2, 'is_public' => true], + ['group' => $g, 'key' => 'footer_address', 'value' => null, 'type' => 'textarea', 'label' => 'Footer Adres', 'order_index' => 3, 'is_public' => true], + ['group' => $g, 'key' => 'footer_phone', 'value' => null, 'type' => 'text', 'label' => 'Footer Telefon', 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'footer_email', 'value' => null, 'type' => 'text', 'label' => 'Footer E-posta', 'order_index' => 5, 'is_public' => true], + ['group' => $g, 'key' => 'footer_bg_color', 'value' => '#0f2847', 'type' => 'color', 'label' => 'Footer Arka Plan Rengi', 'order_index' => 6, 'is_public' => true], + ['group' => $g, 'key' => 'show_social_footer', 'value' => 'true', 'type' => 'boolean', 'label' => "Footer'da Sosyal Medya Göster", 'order_index' => 7, 'is_public' => true], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function integrations(): array + { + $g = 'integrations'; + + return [ + ['group' => $g, 'key' => 'recaptcha_site_key', 'value' => null, 'type' => 'text', 'label' => 'reCAPTCHA v3 Site Key', 'order_index' => 0, 'is_public' => false], + ['group' => $g, 'key' => 'recaptcha_secret_key', 'value' => null, 'type' => 'text', 'label' => 'reCAPTCHA v3 Secret Key', 'order_index' => 1, 'is_public' => false], + ['group' => $g, 'key' => 'smtp_host', 'value' => null, 'type' => 'text', 'label' => 'SMTP Host', 'order_index' => 2, 'is_public' => false], + ['group' => $g, 'key' => 'smtp_port', 'value' => '587', 'type' => 'text', 'label' => 'SMTP Port', 'order_index' => 3, 'is_public' => false], + ['group' => $g, 'key' => 'smtp_username', 'value' => null, 'type' => 'text', 'label' => 'SMTP Kullanıcı Adı', 'order_index' => 4, 'is_public' => false], + ['group' => $g, 'key' => 'smtp_password', 'value' => null, 'type' => 'text', 'label' => 'SMTP Şifre', 'order_index' => 5, 'is_public' => false], + ['group' => $g, 'key' => 'smtp_encryption', 'value' => 'tls', 'type' => 'text', 'label' => 'SMTP Şifreleme (tls/ssl)', 'order_index' => 6, 'is_public' => false], + ['group' => $g, 'key' => 'smtp_from_name', 'value' => 'Boğaziçi Denizcilik', 'type' => 'text', 'label' => 'Mail Gönderen Adı', 'order_index' => 7, 'is_public' => false], + ['group' => $g, 'key' => 'smtp_from_email', 'value' => null, 'type' => 'text', 'label' => 'Mail Gönderen Adresi', 'order_index' => 8, 'is_public' => false], + ['group' => $g, 'key' => 'notification_emails', 'value' => null, 'type' => 'textarea', 'label' => 'Bildirim E-postaları (virgülle)', 'order_index' => 9, 'is_public' => false], + ]; + } + + /** + * @return list<array<string, mixed>> + */ + private function infoSections(): array + { + $g = 'info_sections'; + + return [ + // Info Section 1 + ['group' => $g, 'key' => 'info_section_1_badge', 'value' => 'Neden Boğaziçi Denizcilik?', 'type' => 'text', 'label' => 'Bölüm 1 — Etiket', 'order_index' => 0, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_1_title', 'value' => 'Uluslararası Standartlarda Denizcilik Eğitimi', 'type' => 'text', 'label' => 'Bölüm 1 — Başlık', 'order_index' => 1, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_1_body', 'value' => 'Boğaziçi Denizcilik Eğitim Kurumu, Ulaştırma ve Altyapı Bakanlığı onaylı eğitim programlarıyla denizcilik sektörüne nitelikli personel yetiştirmektedir. STCW Sözleşmesi gerekliliklerini karşılayan müfredatımız; köprüüstü simülatörleri, GMDSS telsiz laboratuvarı ve yangın tatbikat alanında uygulamalı olarak verilmektedir.', 'type' => 'textarea', 'label' => 'Bölüm 1 — İçerik', 'order_index' => 2, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_1_quote', 'value' => "Boğaziçi Denizcilik'ten aldığım STCW eğitimleri ve simülatör deneyimi sayesinde uluslararası bir denizcilik şirketinde güverte zabiti olarak göreve başladım.", 'type' => 'text', 'label' => 'Bölüm 1 — Alıntı', 'order_index' => 3, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_1_quote_author', 'value' => 'Kpt. Murat Aydın — Vardiya Zabiti, MSC Denizcilik', 'type' => 'text', 'label' => 'Bölüm 1 — Alıntı Yazarı', 'order_index' => 4, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_1_image', 'value' => null, 'type' => 'image', 'label' => 'Bölüm 1 — Görsel', 'order_index' => 5, 'is_public' => true], + // Info Section 2 + ['group' => $g, 'key' => 'info_section_2_badge', 'value' => 'Simülatör Destekli Eğitim', 'type' => 'text', 'label' => 'Bölüm 2 — Etiket', 'order_index' => 6, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_2_title', 'value' => 'Teoriden Pratiğe, Sınıftan Köprüüstüne', 'type' => 'text', 'label' => 'Bölüm 2 — Başlık', 'order_index' => 7, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_2_body', 'value' => 'Eğitim merkezimizde bulunan tam donanımlı köprüüstü simülatörü, ARPA/radar eğitim istasyonları ve ECDIS terminalleri ile kursiyerlerimiz gerçek seyir senaryolarında deneyim kazanmaktadır. GMDSS laboratuvarımızda DSC, NAVTEX, Inmarsat-C ve VHF/MF/HF cihazları üzerinde birebir uygulama yapılmaktadır.', 'type' => 'textarea', 'label' => 'Bölüm 2 — İçerik', 'order_index' => 8, 'is_public' => true], + ['group' => $g, 'key' => 'info_section_2_image', 'value' => null, 'type' => 'image', 'label' => 'Bölüm 2 — Görsel', 'order_index' => 9, 'is_public' => true], + ]; + } +} diff --git a/database/seeders/StorySeeder.php b/database/seeders/StorySeeder.php new file mode 100644 index 0000000..64c838b --- /dev/null +++ b/database/seeders/StorySeeder.php @@ -0,0 +1,79 @@ +<?php + +namespace Database\Seeders; + +use App\Models\Story; +use Illuminate\Database\Seeder; + +class StorySeeder extends Seeder +{ + /** + * Seed stories for homepage. + */ + public function run(): void + { + $stories = [ + [ + 'title' => 'Boğaziçi Denizcilik', + 'badge' => 'Tanıtım', + 'content' => "1998'den bu yana Türk denizciliğine nitelikli personel yetiştiriyoruz. IMO standartlarında, STCW uyumlu eğitimlerimizle 15.000+ mezun.", + 'image' => null, + 'cta_text' => 'Hakkımızda', + 'cta_url' => '/kurumsal/hakkimizda', + 'order_index' => 0, + ], + [ + 'title' => 'Eğitimler Başlıyor', + 'badge' => 'Yeni Dönem', + 'content' => 'Güverte, makine ve STCW eğitim gruplarımız açılıyor. Teori ve simülatör destekli uygulamalı müfredat.', + 'image' => null, + 'cta_text' => 'Eğitim Takvimi', + 'cta_url' => '/egitimler/takvim', + 'order_index' => 1, + ], + [ + 'title' => 'Eğitim Kategorileri', + 'badge' => 'Katalog', + 'content' => 'Güverte, makine, STCW güvenlik, yat kaptanlığı, telsiz ve belge yenileme — 6 ana kategoride 45+ eğitim programı.', + 'image' => null, + 'cta_text' => 'Tüm Eğitimler', + 'cta_url' => '/egitimler', + 'order_index' => 2, + ], + [ + 'title' => 'Belge Yenileme', + 'badge' => 'Yenileme', + 'content' => 'BST, AFF, ilk yardım ve güverte/makine yeterlik belgelerinizin süresi mi doluyor? Hafta sonu tazeleme programlarımız açık.', + 'image' => null, + 'cta_text' => 'Yenileme Eğitimleri', + 'cta_url' => '/egitimler/yenileme', + 'order_index' => 3, + ], + [ + 'title' => 'Yat Kaptanlığı', + 'badge' => 'Yat', + 'content' => '25 GT, 149 GT ve 3000 GT tonaj sınıflarında yat kaptanlığı eğitimleri. Kıyı seyri, manevra, deniz hukuku ve güvenlik.', + 'image' => null, + 'cta_text' => 'Detaylı Bilgi', + 'cta_url' => '/egitimler/yat-kaptanligi', + 'order_index' => 4, + ], + [ + 'title' => 'STCW Güvenlik', + 'badge' => 'STCW', + 'content' => 'Temel Güvenlik (BST), ileri yangın söndürme (AFF), denizde kişisel can kurtarma ve ilk yardım — zorunlu STCW sertifikaları.', + 'image' => null, + 'cta_text' => 'STCW Eğitimleri', + 'cta_url' => '/egitimler/stcw', + 'order_index' => 5, + ], + ]; + + foreach ($stories as $story) { + Story::firstOrCreate( + ['title' => $story['title']], + $story, + ); + } + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..3a15cb5 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,15 @@ +services: + bdc-api-prod: + build: + context: . + dockerfile: Dockerfile + container_name: bdc-api-prod + restart: unless-stopped + volumes: + - ./:/var/www/html + - ./docker/apache/000-default.conf:/etc/apache2/sites-available/000-default.conf + - /opt/projects/bogazici/corporate-api/prod/uploads:/var/www/html/public/uploads + ports: + - "127.0.0.1:9201:80" + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..aadb2d2 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,15 @@ +services: + bdc-api-test: + build: + context: . + dockerfile: Dockerfile + container_name: bdc-api-test + restart: unless-stopped + volumes: + - ./:/var/www/html + - ./docker/apache/000-default.conf:/etc/apache2/sites-available/000-default.conf + - /opt/projects/bogazici/corporate-api/test/uploads:/var/www/html/public/uploads + ports: + - "127.0.0.1:9101:80" + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/docker/apache/000-default.conf b/docker/apache/000-default.conf new file mode 100644 index 0000000..67ccfd2 --- /dev/null +++ b/docker/apache/000-default.conf @@ -0,0 +1,13 @@ +<VirtualHost *:80> + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html/public + + <Directory /var/www/html/public> + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + </Directory> + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined +</VirtualHost> diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..5febd47 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +cd /var/www/html + +if [ ! -d "vendor" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader +fi + +chown -R www-data:www-data storage bootstrap/cache +chmod -R 775 storage bootstrap/cache + +exec apache2-foreground diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..d9843b0 --- /dev/null +++ b/opencode.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "laravel-boost": { + "type": "local", + "enabled": true, + "command": [ + "php", + "artisan", + "boost:mcp" + ] + }, + "herd": { + "type": "local", + "enabled": true, + "command": [ + "php", + "/Applications/Herd.app/Contents/Resources/herd-mcp.phar" + ], + "environment": { + "SITE_PATH": "/Users/bulutkuru/Herd/bogazici-api" + } + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..7686b29 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.7" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d703241 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" + bootstrap="vendor/autoload.php" + colors="true" +> + <testsuites> + <testsuite name="Unit"> + <directory>tests/Unit</directory> + </testsuite> + <testsuite name="Feature"> + <directory>tests/Feature</directory> + </testsuite> + </testsuites> + <source> + <include> + <directory>app</directory> + </include> + </source> + <php> + <env name="APP_ENV" value="testing"/> + <env name="APP_MAINTENANCE_DRIVER" value="file"/> + <env name="BCRYPT_ROUNDS" value="4"/> + <env name="BROADCAST_CONNECTION" value="null"/> + <env name="CACHE_STORE" value="array"/> + <env name="DB_CONNECTION" value="sqlite"/> + <env name="DB_DATABASE" value=":memory:"/> + <env name="MAIL_MAILER" value="array"/> + <env name="QUEUE_CONNECTION" value="sync"/> + <env name="SESSION_DRIVER" value="array"/> + <env name="PULSE_ENABLED" value="false"/> + <env name="TELESCOPE_ENABLED" value="false"/> + <env name="NIGHTWATCH_ENABLED" value="false"/> + </php> +</phpunit> diff --git a/prompts/admin-course-blocks.md b/prompts/admin-course-blocks.md new file mode 100644 index 0000000..36e6afb --- /dev/null +++ b/prompts/admin-course-blocks.md @@ -0,0 +1,209 @@ +# Admin Panel — Eğitim Blokları (Course Blocks) + +## Genel Bakış + +Eğitim detay sayfalarında artık **Page Builder** mantığı var. Her eğitimin altına sıralı bloklar eklenebilir. Yapı, Page Blocks ile birebir aynı — aynı blok tipleri, aynı `_width` desteği, aynı content JSON formatı. + +--- + +## API Endpoints + +Tüm endpoint'ler `auth:sanctum` ile korunuyor. Base URL: `{API_URL}/api/admin` + +| Method | Endpoint | Açıklama | +|--------|----------|----------| +| `GET` | `/admin/courses/{course}/blocks` | Eğitimin bloklarını listele | +| `POST` | `/admin/courses/{course}/blocks` | Yeni blok oluştur | +| `GET` | `/admin/courses/{course}/blocks/{block}` | Blok detayı | +| `PUT` | `/admin/courses/{course}/blocks/{block}` | Blok güncelle | +| `DELETE` | `/admin/courses/{course}/blocks/{block}` | Blok sil | +| `POST` | `/admin/courses/{course}/blocks/reorder` | Sıralama güncelle | + +--- + +## GET /admin/courses/{course}/blocks + +```json +{ + "data": [ + { + "id": 1, + "type": "hero", + "content": { + "title": "Köprüüstü Kaynak Yönetimi", + "subtitle": "BRM", + "description": "STCW uyumlu ileri düzey eğitim..." + }, + "order_index": 0 + }, + { + "id": 2, + "type": "text", + "content": { + "_width": "half", + "title": "Eğitim Kapsamı", + "body": "<ul><li>Liderlik</li><li>İletişim</li></ul>" + }, + "order_index": 1 + } + ] +} +``` + +--- + +## POST /admin/courses/{course}/blocks — Yeni Blok + +### Request: +```json +{ + "type": "hero", + "content": { + "title": "Blok başlığı", + "description": "Açıklama..." + }, + "order_index": 0, + "is_active": true +} +``` + +### Response (201): +```json +{ + "id": 5, + "type": "hero", + "content": { "title": "Blok başlığı", "description": "Açıklama..." }, + "order_index": 0 +} +``` + +### Validation Kuralları: +| Alan | Kural | +|------|-------| +| `type` | required, string, max:50 | +| `content` | present, array (boş obje `{}` gönderilebilir) | +| `order_index` | optional, integer, min:0 (gönderilmezse otomatik son sıraya eklenir) | +| `is_active` | optional, boolean | + +--- + +## PUT /admin/courses/{course}/blocks/{block} — Güncelle + +Sadece değişen alanları gönder: + +```json +{ + "content": { + "_width": "half", + "title": "Güncel Başlık", + "body": "<p>Yeni içerik</p>" + } +} +``` + +--- + +## DELETE /admin/courses/{course}/blocks/{block} + +```json +{ "message": "Blok silindi." } +``` + +--- + +## POST /admin/courses/{course}/blocks/reorder — Sıralama + +```json +{ + "items": [ + { "id": 3, "order_index": 0 }, + { "id": 1, "order_index": 1 }, + { "id": 2, "order_index": 2 } + ] +} +``` + +```json +{ "message": "Blok sıralaması güncellendi." } +``` + +--- + +## Blok Tipleri + +Page Blocks ile aynı blok tipleri kullanılır: + +| type | Açıklama | Örnek Kullanım | +|------|----------|----------------| +| `hero` | Üst banner/başlık alanı | Eğitim hero bölümü | +| `text` | Zengin metin bloğu | Eğitim kapsamı, açıklama | +| `text_image` | Metin + görsel yan yana | Eğitim tanıtımı | +| `cards` | Kart grid | Özellikler, sertifikalar | +| `stats_grid` | İstatistik/adım kartları | Süre, katılımcı, başarı oranı | +| `cta` | Call-to-action | Kayıt ol butonu | +| `faq` | Sıkça sorulan sorular | Eğitimle ilgili SSS | +| `gallery` | Görsel galeri | Eğitim ortamı fotoğrafları | +| `video` | Video embed | Tanıtım videosu | +| `testimonials` | Yorumlar/referanslar | Mezun görüşleri | +| `html` | Serbest HTML | Özel içerik | + +--- + +## `_width` Desteği + +`content` JSON içinde `_width` key'i blok genişliğini belirler: + +| Değer | Açıklama | +|-------|----------| +| `"full"` | Tam genişlik (varsayılan — key gönderilmezse otomatik full) | +| `"half"` | Yarım genişlik — ardışık iki half blok yan yana render edilir | + +```json +// Yan yana iki blok +{ "type": "text", "content": { "_width": "half", "title": "Sol", "body": "..." }, "order_index": 0 } +{ "type": "stats_grid", "content": { "_width": "half", "title": "Sağ", ... }, "order_index": 1 } +``` + +--- + +## Public API — Frontend + +`GET /api/v1/courses/{slug}` artık `blocks` array'ini de döner: + +```json +{ + "data": { + "id": 1, + "slug": "kopruustu-kaynak-yonetimi", + "title": "Köprüüstü Kaynak Yönetimi (BRM)", + "category": { ... }, + "blocks": [ + { "id": 1, "type": "hero", "content": { ... }, "order_index": 0 }, + { "id": 2, "type": "text", "content": { "_width": "half", ... }, "order_index": 1 }, + { "id": 3, "type": "stats_grid", "content": { "_width": "half", ... }, "order_index": 2 } + ], + "schedules": [ ... ], + ... + } +} +``` + +Frontend, sayfa blokları ile aynı `BlockRenderer` bileşenini kullanabilir. + +--- + +## Admin Panel Entegrasyonu + +### Önerilen UI: +Eğitim düzenleme sayfasında (`/admin/courses/{id}/edit`) mevcut form alanlarının altına bir **"Bloklar"** sekmesi/bölümü ekle. Bu bölüm Page Builder ile aynı mantıkta çalışır: + +1. Blok listesi `order_index` sıralı gösterilir +2. Sürükle-bırak ile sıralama → `POST .../reorder` +3. "Blok Ekle" butonu → tip seçimi → `POST .../blocks` +4. Blok düzenleme → inline edit veya modal → `PUT .../blocks/{id}` +5. Blok silme → onay dialog → `DELETE .../blocks/{id}` + +### Aynı bileşenleri paylaşabilirsin: +Page Blocks için yazdığın `BlockEditor`, `BlockTypeSelector`, `ContentEditor` bileşenlerini **doğrudan** course blocks için de kullan. Sadece API endpoint prefix'i değişir: +- Page: `/admin/pages/{id}/blocks` +- Course: `/admin/courses/{id}/blocks` diff --git a/prompts/admin-leads.md b/prompts/admin-leads.md new file mode 100644 index 0000000..ac4243d --- /dev/null +++ b/prompts/admin-leads.md @@ -0,0 +1,246 @@ +# Admin Panel — Leads (Başvuru Yönetimi) Modülü + +## Genel Bakış + +Web sitesindeki formlardan gelen tüm başvurular `leads` tablosuna yazılır. Admin panelden başvurular listelenir, detay görüntülenir, durum güncellenir ve not eklenir. **Lead'ler sadece API'den oluşur — admin panelde "Yeni Oluştur" butonu olmayacak.** + +--- + +## API Endpoints (Admin — auth:sanctum) + +| Method | Endpoint | Açıklama | +|--------|----------|----------| +| `GET` | `/admin/leads` | Başvuruları listele (paginated, filtrelenebilir) | +| `GET` | `/admin/leads/{id}` | Başvuru detayı (otomatik okundu işaretler) | +| `PUT` | `/admin/leads/{id}` | Durum/not güncelle | +| `DELETE` | `/admin/leads/{id}` | Başvuru sil (soft delete) | + +--- + +## GET /admin/leads — Liste + +### Query Parametreleri: +| Parametre | Tip | Açıklama | +|-----------|-----|----------| +| `status` | string | Durum filtresi: `new`, `contacted`, `enrolled`, `cancelled` | +| `source` | string | Kaynak filtresi: `kurs_kayit`, `danismanlik`, `duyuru`, `iletisim` | +| `is_read` | boolean | Okundu/okunmadı filtresi | +| `search` | string | İsim/telefon arama | +| `per_page` | integer | Sayfa başına (varsayılan: 15) | + +### Response: +```json +{ + "data": [ + { + "id": 1, + "name": "Ahmet Yılmaz", + "phone": "+90 532 724 15 32", + "email": "ahmet@email.com", + "source": "kurs_kayit", + "status": "new", + "target_course": "gemici-birlesik-egitimi", + "education_level": "lise", + "subject": null, + "message": "Eğitim hakkında bilgi almak istiyorum", + "is_read": false, + "kvkk_consent": true, + "marketing_consent": false, + "utm_source": "google", + "utm_medium": "cpc", + "utm_campaign": "denizcilik-2026", + "admin_note": null, + "created_at": "2026-03-24T10:30:00.000000Z", + "updated_at": "2026-03-24T10:30:00.000000Z" + } + ], + "meta": { "current_page": 1, "last_page": 5, "per_page": 15, "total": 72 } +} +``` + +--- + +## GET /admin/leads/{id} — Detay + +Detay açıldığında backend otomatik olarak `is_read: true` yapar. + +--- + +## PUT /admin/leads/{id} — Güncelle + +Admin sadece şu alanları güncelleyebilir: + +```json +{ + "status": "contacted", + "is_read": true, + "admin_note": "Arandı, bilgi verildi. Nisan dönemine kayıt olacak." +} +``` + +--- + +## Veri Modeli + +### Lead Alanları + +| Alan | Tip | Açıklama | +|------|-----|----------| +| `id` | integer | — | +| `name` | string | Ad Soyad | +| `phone` | string | Telefon | +| `email` | string (nullable) | E-posta | +| `source` | enum | Başvuru kaynağı (form tipi) | +| `status` | enum | İşlem durumu | +| `target_course` | string (nullable) | Hedef eğitim slug'ı | +| `education_level` | string (nullable) | Eğitim seviyesi | +| `subject` | string (nullable) | Konu (iletişim formunda) | +| `message` | text (nullable) | Mesaj | +| `is_read` | boolean | Admin tarafından okundu mu | +| `kvkk_consent` | boolean | KVKK onayı | +| `marketing_consent` | boolean | Pazarlama onayı | +| `utm_source` | string (nullable) | UTM Source | +| `utm_medium` | string (nullable) | UTM Medium | +| `utm_campaign` | string (nullable) | UTM Campaign | +| `admin_note` | text (nullable) | Admin notu | +| `created_at` | datetime | Başvuru tarihi | + +### Source (Kaynak) Değerleri + +| Değer | Açıklama | Hangi Form? | +|-------|----------|-------------| +| `kurs_kayit` | Kurs ön kayıt | Eğitim detay sayfası kayıt formu | +| `danismanlik` | Danışmanlık | Danışmanlık sayfası formu | +| `duyuru` | Duyuru | Duyuru sidebar'daki mini form | +| `iletisim` | İletişim | İletişim sayfası formu | +| `hero_form` | Hero form | Anasayfa hero bölümü formu | +| `whatsapp_widget` | WhatsApp | WhatsApp widget üzerinden gelen | + +### Status (Durum) Değerleri + +| Değer | Label | Badge Rengi | Açıklama | +|-------|-------|-------------|----------| +| `new` | Yeni | `warning` (sarı) | Henüz işlenmemiş | +| `contacted` | İletişim Kuruldu | `info` (mavi) | Aranmış/e-posta gönderilmiş | +| `enrolled` | Kayıt Oldu | `success` (yeşil) | Eğitime kaydolmuş | +| `cancelled` | İptal | `danger` (kırmızı) | Vazgeçmiş/iptal | + +--- + +## Önerilen Admin Panel Sayfası + +### Liste Sayfası (`/admin/leads`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Başvurular [Filtreler ▼] │ +├─────────────────────────────────────────────────────────────┤ +│ Filtreler: [Kaynak ▼] [Durum ▼] [Okundu ▼] [Ara...] │ +├──────┬──────────────┬──────────────┬────────────┬───────────┤ +│ ● │ Ahmet Yılmaz │ +90 532 ... │ kurs_kayit │ 🟡 Yeni │ +│ ○ │ Mehmet Kaya │ +90 555 ... │ iletisim │ 🔵 İlet. │ +│ ○ │ Ayşe Demir │ +90 212 ... │ danismanlik│ 🟢 Kayıt │ +└──────┴──────────────┴──────────────┴────────────┴───────────┘ + ● = okunmadı (bold göster) ○ = okundu +``` + +#### Tablo Sütunları: +| Sütun | Açıklama | +|-------|----------| +| Okundu göstergesi | `is_read` false ise bold/dot göster | +| Ad Soyad | `name` | +| Telefon | `phone` | +| E-posta | `email` (varsa) | +| Kaynak | `source` — badge ile göster | +| Hedef Eğitim | `target_course` (varsa) | +| Durum | `status` — renkli badge | +| Tarih | `created_at` — relative (2 saat önce) | + +#### Kaynak Badge Renkleri: +| Source | Renk | +|--------|------| +| `kurs_kayit` | `primary` (mavi) | +| `danismanlik` | `purple` | +| `duyuru` | `orange` | +| `iletisim` | `teal` | +| `hero_form` | `indigo` | +| `whatsapp_widget` | `green` | + +### Detay/Düzenleme Sayfası (`/admin/leads/{id}`) + +``` +┌─────────────────────────────────────────────────┐ +│ Başvuru #42 — Ahmet Yılmaz │ +├─────────────────────────────────────────────────┤ +│ Bilgiler │ +│ ┌─────────────────┬───────────────────────────┐ │ +│ │ Ad Soyad │ Ahmet Yılmaz │ │ +│ │ Telefon │ +90 532 724 15 32 │ │ +│ │ E-posta │ ahmet@email.com │ │ +│ │ Kaynak │ 🔵 kurs_kayit │ │ +│ │ Hedef Eğitim │ Gemici (Birleşik) Eğitimi │ │ +│ │ Eğitim Seviyesi │ Lise │ │ +│ │ Mesaj │ Bilgi almak istiyorum │ │ +│ │ KVKK Onay │ ✅ Evet │ │ +│ │ Pazarlama Onay │ ❌ Hayır │ │ +│ │ Tarih │ 24 Mar 2026, 10:30 │ │ +│ └─────────────────┴───────────────────────────┘ │ +│ │ +│ UTM Bilgileri │ +│ ┌─────────────────┬───────────────────────────┐ │ +│ │ utm_source │ google │ │ +│ │ utm_medium │ cpc │ │ +│ │ utm_campaign │ denizcilik-2026 │ │ +│ └─────────────────┴───────────────────────────┘ │ +│ │ +│ İşlem │ +│ ┌───────────────────────────────────────────────┐│ +│ │ Durum: [Yeni ▼] → contacted / enrolled /.. ││ +│ │ ││ +│ │ Admin Notu: ││ +│ │ ┌─────────────────────────────────────────┐ ││ +│ │ │ Arandı, bilgi verildi. Nisan dönemine │ ││ +│ │ │ kayıt olacak. │ ││ +│ │ └─────────────────────────────────────────┘ ││ +│ │ ││ +│ │ [Kaydet] ││ +│ └───────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────┘ +``` + +#### Düzenleme Kuralları: +- Başvuru bilgileri (name, phone, message vb.) **readonly** — sadece görüntüleme +- Sadece `status`, `is_read` ve `admin_note` düzenlenebilir +- `target_course` slug ise, linke çevir: `/admin/courses/{slug}/edit` +- "Yeni Oluştur" butonu **yok** — lead'ler sadece frontend formlarından gelir + +--- + +## İstatistik Kartları (Dashboard için, opsiyonel) + +Liste sayfasının üstüne istatistik kartları eklenebilir: + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ 🟡 Yeni │ │ 🔵 İletişim │ │ 🟢 Kayıt │ │ 📊 Toplam │ +│ 12 │ │ 28 │ │ 45 │ │ 93 │ +│ Bu hafta: 5 │ │ Bu hafta: 8 │ │ Bu ay: 15 │ │ Bu ay: 32 │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ +``` + +API'den alınabilir: +``` +GET /admin/leads?status=new → meta.total +GET /admin/leads?status=contacted → meta.total +GET /admin/leads?status=enrolled → meta.total +GET /admin/leads → meta.total +``` + +--- + +## Notlar + +- `is_read` detay sayfası açıldığında otomatik `true` olur (backend tarafından) +- Soft delete kullanılıyor — silinen lead'ler geri alınabilir +- UTM verileri backend'de tek JSON sütununda (`utm`) saklanır, API response'da ayrı ayrı döner +- KVKK ve pazarlama onayları readonly gösterilmeli — admin tarafından değiştirilemez diff --git a/prompts/admin-settings.md b/prompts/admin-settings.md new file mode 100644 index 0000000..d744283 --- /dev/null +++ b/prompts/admin-settings.md @@ -0,0 +1,375 @@ +# Admin Panel — Site Ayarları Modülü + +## Genel Bakış + +Backend'de `settings` tablosu güncellendi. Artık her ayarın **label** (Türkçe etiket) ve **order_index** (sıralama) alanları var. Ayarlar **9 gruba** ayrılmış durumda. Admin panelde her grup kendi sekmesi/sayfası olacak. + +--- + +## API Endpoints + +Tüm admin endpoint'leri `auth:sanctum` ile korunuyor. Base URL: `{API_URL}/api/admin` + +| Method | Endpoint | Açıklama | +|--------|----------|----------| +| `GET` | `/admin/settings` | Tüm ayarları flat liste olarak getir | +| `GET` | `/admin/settings/group/{group}` | Tek grup ayarlarını getir | +| `PUT` | `/admin/settings` | Toplu güncelleme (dot notation) | +| `POST` | `/admin/settings/clear-cache` | Ayar cache'ini temizle | + +--- + +## GET /admin/settings — Tüm Ayarlar + +Her ayar şu formatta döner: + +```json +{ + "data": [ + { + "id": 1, + "key": "site_name", + "value": "Boğaziçi Denizcilik Eğitim Kurumu", + "group": "general", + "type": "text", + "label": "Site Adı", + "order_index": 0 + }, + { + "id": 8, + "key": "announcement_bar_active", + "value": "true", + "group": "general", + "type": "boolean", + "label": "Üst Bar Aktif mi", + "order_index": 7 + } + ] +} +``` + +**Frontend'de `group` bazında filtrele** → Her sekme/tab kendi grubunu gösterir. + +--- + +## GET /admin/settings/group/{group} — Tek Grup + +Sadece o grubun ayarlarını döner. `order_index` sıralı gelir. + +Geçerli group değerleri: +- `general` — Genel site ayarları +- `contact` — İletişim bilgileri +- `maps` — Harita ayarları +- `social` — Sosyal medya linkleri +- `seo` — SEO, Open Graph, Twitter Card, doğrulama kodları +- `analytics` — Google Analytics, Tag Manager, Pixel ID'leri +- `header` — Navbar ve üst bar ayarları +- `footer` — Footer içerik ve stili +- `integrations` — SMTP, reCAPTCHA, bildirim ayarları + +--- + +## PUT /admin/settings — Toplu Güncelleme + +**Dot notation** formatında gönder: `{group}.{key}: value` + +### Request Body: + +```json +{ + "settings": { + "general.site_name": "Yeni Site Adı", + "general.announcement_bar_active": "true", + "contact.phone_primary": "+90 555 123 45 67", + "social.instagram_url": "https://instagram.com/bogazicidenizcilik", + "header.cta_button_text": "Kayıt Ol", + "footer.copyright_text": "© 2026 Boğaziçi Denizcilik" + } +} +``` + +### Response: +```json +{ "message": "Ayarlar güncellendi." } +``` + +### Önemli: +- Sadece değişen ayarları gönderin, hepsini göndermenize gerek yok +- PUT sonrası backend otomatik olarak tüm cache'leri temizler +- Validation: `settings` required array, her value nullable string + +--- + +## POST /admin/settings/clear-cache + +Body gerekmez. Manuel cache temizleme butonu için. + +```json +{ "message": "Ayar cache temizlendi." } +``` + +--- + +## Type'lara Göre Form Bileşenleri + +Her ayarın `type` alanı, admin panelde hangi input bileşeninin kullanılacağını belirler: + +| type | Bileşen | Açıklama | +|------|---------|----------| +| `text` | `<input type="text">` | Tek satır metin | +| `textarea` | `<textarea>` | Çok satır metin | +| `image` | Dosya yükleme + önizleme | Mevcut upload endpoint'i kullanılır (`POST /admin/uploads`) | +| `boolean` | Toggle/Switch | value `"true"` veya `"false"` string olarak saklanır | +| `url` | `<input type="url">` | URL formatı | +| `color` | Color picker | Hex renk kodu (#RRGGBB) | +| `richtext` | TipTap / zengin metin editör | HTML içerik | +| `json` | JSON editör | Yapısal veri | + +### Boolean Değerler +Backend'de `value` her zaman **string** olarak saklanır. Boolean'lar `"true"` / `"false"` string: +```js +// Gönderirken +{ "settings": { "general.maintenance_mode": "false" } } + +// Okurken +setting.value === "true" // → toggle ON +setting.value === "false" // → toggle OFF +``` + +### Image Alanları +1. Dosyayı `POST /admin/uploads` ile yükle → response'dan URL al +2. URL'yi settings update'e gönder: +```json +{ "settings": { "general.logo_light": "/storage/uploads/logo-white.png" } } +``` + +--- + +## Grup Detayları ve Alanlar + +### general (13 alan) +| key | type | label | +|-----|------|-------| +| site_name | text | Site Adı | +| site_tagline | text | Slogan | +| site_description | textarea | Kısa Site Açıklaması | +| logo_light | image | Logo — Açık Tema (beyaz navbar) | +| logo_dark | image | Logo — Koyu Tema (dark bg) | +| favicon | image | Favicon (32x32 PNG) | +| apple_touch_icon | image | Apple Touch Icon (180x180) | +| announcement_bar_active | boolean | Üst Bar Aktif mi | +| announcement_bar_text | text | Üst Bar Metni | +| announcement_bar_url | url | Üst Bar Linki | +| announcement_bar_bg_color | color | Üst Bar Arka Plan Rengi | +| maintenance_mode | boolean | Bakım Modu | +| maintenance_message | textarea | Bakım Modu Mesajı | + +### contact (15 alan) +| key | type | label | +|-----|------|-------| +| phone_primary | text | Ana Telefon | +| phone_secondary | text | İkinci Telefon | +| email_info | text | Bilgi E-postası | +| email_support | text | Destek E-postası | +| email_kayit | text | Kayıt E-postası | +| address_full | textarea | Tam Adres | +| address_short | text | Kısa Adres (navbar için) | +| district | text | İlçe | +| city | text | Şehir | +| postal_code | text | Posta Kodu | +| working_hours_weekday | text | Hafta İçi Saatleri | +| working_hours_saturday | text | Cumartesi Saatleri | +| working_hours_sunday | text | Pazar Saatleri | +| whatsapp_number | text | WhatsApp (+90 ile başlayan) | +| whatsapp_message | text | WhatsApp Varsayılan Mesaj | + +### maps (6 alan) +| key | type | label | +|-----|------|-------| +| google_maps_embed_url | textarea | Google Maps Embed URL (iframe src) | +| google_maps_place_url | url | Google Maps Profil Linki | +| google_maps_api_key | text | Google Maps API Key | +| latitude | text | Enlem | +| longitude | text | Boylam | +| map_zoom_level | text | Harita Zoom (1-20) | + +> **Not:** `google_maps_api_key` admin endpoint'ten görünür ama public API'den filtrelenir. + +### social (11 alan) +| key | type | label | +|-----|------|-------| +| instagram_url | url | Instagram Profil URL | +| instagram_handle | text | Instagram Kullanıcı Adı (@siz) | +| facebook_url | url | Facebook Sayfası URL | +| facebook_page_id | text | Facebook Page ID | +| twitter_url | url | X (Twitter) Profil URL | +| twitter_handle | text | X Kullanıcı Adı (@siz) | +| youtube_url | url | YouTube Kanal URL | +| youtube_channel_id | text | YouTube Channel ID | +| linkedin_url | url | LinkedIn Sayfa URL | +| tiktok_url | url | TikTok Profil URL | +| pinterest_url | url | Pinterest URL | + +### seo (23 alan) + +**Temel SEO:** +| key | type | label | +|-----|------|-------| +| meta_title_suffix | text | Title Eki | +| meta_title_separator | text | Ayraç Karakteri | +| default_meta_description | textarea | Varsayılan Meta Açıklama | +| default_meta_keywords | textarea | Varsayılan Keywords (virgülle) | +| robots | text | Robots | +| canonical_domain | url | Canonical Domain | + +**Open Graph:** +| key | type | label | +|-----|------|-------| +| og_title | text | OG Default Title | +| og_description | textarea | OG Default Description | +| og_image | image | OG Default Görsel (1200x630 px) | +| og_type | text | OG Type | +| og_locale | text | OG Locale | +| og_site_name | text | OG Site Name | +| facebook_app_id | text | Facebook App ID | + +**Twitter / X Card:** +| key | type | label | +|-----|------|-------| +| twitter_card_type | text | Card Tipi | +| twitter_site | text | Site @handle | +| twitter_creator | text | İçerik Sahibi @handle | +| twitter_title | text | Twitter Default Title | +| twitter_description | textarea | Twitter Default Description | +| twitter_image | image | Twitter Card Görseli (1200x600 px) | + +**Doğrulama Kodları:** +| key | type | label | +|-----|------|-------| +| google_site_verification | text | Google Search Console Kodu | +| bing_site_verification | text | Bing Webmaster Kodu | +| yandex_verification | text | Yandex Webmaster Kodu | +| pinterest_verification | text | Pinterest Doğrulama Kodu | + +> **UI Önerisi:** SEO grubunu 4 alt sekmeye bölebilirsin: Temel SEO, Open Graph, Twitter Card, Doğrulama Kodları. + +### analytics (10 alan) +| key | type | label | +|-----|------|-------| +| google_analytics_id | text | Google Analytics 4 ID (G-XXXXXXXX) | +| google_tag_manager_id | text | Google Tag Manager ID (GTM-XXXXXXX) | +| google_ads_id | text | Google Ads Conversion ID | +| facebook_pixel_id | text | Meta (Facebook) Pixel ID | +| hotjar_id | text | Hotjar Site ID | +| clarity_id | text | Microsoft Clarity ID | +| tiktok_pixel_id | text | TikTok Pixel ID | +| crisp_website_id | text | Crisp Chat Website ID | +| custom_head_scripts | textarea | `<head>` içine özel script | +| custom_body_scripts | textarea | `<body>` sonuna özel script | + +### header (9 alan) +| key | type | label | +|-----|------|-------| +| navbar_style_default | text | Varsayılan Navbar Stili (transparent/white) | +| cta_button_text | text | Sağ Üst Buton Metni | +| cta_button_url | url | Sağ Üst Buton Linki | +| cta_button_color | color | Sağ Üst Buton Rengi | +| show_phone_topbar | boolean | Üst Bar'da Telefon Göster | +| show_email_topbar | boolean | Üst Bar'da E-posta Göster | +| show_address_topbar | boolean | Üst Bar'da Adres Göster | +| show_hours_topbar | boolean | Üst Bar'da Saat Göster | +| show_social_navbar | boolean | Navbar'da Sosyal Medya İkonları Göster | + +### footer (8 alan) +| key | type | label | +|-----|------|-------| +| footer_description | textarea | Footer Açıklaması | +| footer_logo | image | Footer Logo (varsa ayrı) | +| copyright_text | text | Copyright Metni | +| footer_address | textarea | Footer Adres | +| footer_phone | text | Footer Telefon | +| footer_email | text | Footer E-posta | +| footer_bg_color | color | Footer Arka Plan Rengi | +| show_social_footer | boolean | Footer'da Sosyal Medya Göster | + +### integrations (10 alan) +| key | type | label | +|-----|------|-------| +| recaptcha_site_key | text | reCAPTCHA v3 Site Key | +| recaptcha_secret_key | text | reCAPTCHA v3 Secret Key | +| smtp_host | text | SMTP Host | +| smtp_port | text | SMTP Port | +| smtp_username | text | SMTP Kullanıcı Adı | +| smtp_password | text | SMTP Şifre | +| smtp_encryption | text | SMTP Şifreleme (tls/ssl) | +| smtp_from_name | text | Mail Gönderen Adı | +| smtp_from_email | text | Mail Gönderen Adresi | +| notification_emails | textarea | Bildirim E-postaları (virgülle) | + +> **Not:** `integrations` grubundaki tüm ayarlar `is_public: false` — public API'den hiçbiri görünmez. `smtp_password` ve `*_secret_key` alanları password input olarak gösterilmeli. + +--- + +## Önerilen Admin Panel Sayfa Yapısı + +``` +/admin/settings +├── Sidebar veya Tab Navigation +│ ├── Genel → general +│ ├── İletişim → contact +│ ├── Harita → maps +│ ├── Sosyal Medya → social +│ ├── SEO → seo (alt sekmeler: Temel, OG, Twitter, Doğrulama) +│ ├── Analitik → analytics +│ ├── Header → header +│ ├── Footer → footer +│ └── Entegrasyonlar → integrations +``` + +### Kaydet Akışı: +1. Kullanıcı bir gruptaki alanları düzenler +2. "Kaydet" butonuna basar +3. Sadece **değişen** alanlar `PUT /admin/settings` ile gönderilir +4. Başarılı response sonrası toast mesaj göster +5. Gerekirse `POST /admin/settings/clear-cache` butonu ekle (teknik kullanıcılar için) + +### Dinamik Form Render: +`label` alanını form label olarak, `type` alanını input bileşeni seçmek için kullan. Backend'den gelen sıralama (`order_index`) form alanlarının sırası olarak kullanılabilir. + +--- + +## Preview Sistemi (Page Builder) + +Admin panelden sayfa blokları düzenlenirken önizleme özelliği: + +| Method | Endpoint | Açıklama | +|--------|----------|----------| +| `POST` | `/admin/preview` | Önizleme token'ı oluştur | +| `DELETE` | `/admin/preview/{token}` | Önizlemeyi sil | +| `GET` | `/v1/preview/{token}` | Public — frontend fetch (auth yok) | + +### POST /admin/preview + +```json +// Request +{ + "page_id": 1, + "blocks": [ + { "type": "hero", "content": { "title": "...", "description": "..." }, "order_index": 0 }, + { "type": "text", "content": { "_width": "half", "title": "...", "body": "..." }, "order_index": 1 } + ] +} + +// Response (201) +{ + "token": "550e8400-e29b-41d4-a716-446655440000", + "preview_url": "http://localhost:3000/api/preview?token=550e8400...&slug=kalite-politikasi", + "expires_in": 600 +} +``` + +### Kullanım: +1. Admin panelde "Önizle" butonuna bas → `POST /admin/preview` +2. Response'daki `preview_url`'yi yeni sekmede aç +3. Önizleme 10 dakika geçerli, sonra otomatik silinir +4. İsteğe bağlı: modal kapatılınca `DELETE /admin/preview/{token}` ile temizle diff --git a/prompts/admin-stories-info-sections.md b/prompts/admin-stories-info-sections.md new file mode 100644 index 0000000..fd78465 --- /dev/null +++ b/prompts/admin-stories-info-sections.md @@ -0,0 +1,220 @@ +# Admin Panel — Stories + Info Sections + +## 1. Stories (Hikayeler) + +Anasayfadaki kayan hikaye/tanıtım kartları. Instagram stories benzeri yapı. + +### API Endpoints + +| Method | Endpoint | Auth | Açıklama | +|--------|----------|------|----------| +| `GET` | `/v1/stories` | public | Aktif hikayeler (order_index sıralı) | +| `GET` | `/admin/stories` | sanctum | Tüm hikayeler | +| `POST` | `/admin/stories` | sanctum | Yeni hikaye oluştur | +| `GET` | `/admin/stories/{id}` | sanctum | Hikaye detayı | +| `PUT` | `/admin/stories/{id}` | sanctum | Hikaye güncelle | +| `DELETE` | `/admin/stories/{id}` | sanctum | Hikaye sil | + +### Response Formatı + +```json +{ + "data": [ + { + "id": 1, + "title": "25 Yılı Aşkın Deneyim", + "badge": "Tanıtım", + "content": "1998'den bu yana 15.000+ denizci yetiştirdik...", + "image": "/storage/stories/hakkimizda.jpg", + "cta_text": "Hakkımızda", + "cta_url": "/kurumsal/hakkimizda", + "order_index": 0, + "is_active": true, + "created_at": "...", + "updated_at": "..." + } + ] +} +``` + +### Veri Modeli + +| Alan | Tip | Açıklama | +|------|-----|----------| +| `title` | string (zorunlu) | Hikaye başlığı | +| `badge` | string (nullable) | Üst etiket: "Tanıtım", "Yeni Dönem", vb. | +| `content` | text (zorunlu) | Hikaye içeriği | +| `image` | string (nullable) | Görsel path (storage/stories/) | +| `cta_text` | string (nullable) | Buton metni: "Hakkımızda", "Detaylı Bilgi" | +| `cta_url` | string (nullable) | Buton linki: "/kurumsal/hakkimizda" | +| `order_index` | integer | Sıralama (0'dan başlar) | +| `is_active` | boolean | Aktif/pasif | + +### Admin Panel Form + +``` +┌─────────────────────────────────────────────────┐ +│ Hikaye Düzenle │ +├─────────────────────────────────────────────────┤ +│ Başlık: [25 Yılı Aşkın Deneyim ] │ +│ Etiket: [Tanıtım ] │ +│ İçerik: ┌─────────────────────────────────┐ │ +│ │ 1998'den bu yana 15.000+... │ │ +│ └─────────────────────────────────┘ │ +│ Görsel: [📷 Dosya Seç] hakkimizda.jpg │ +│ Buton: [Hakkımızda ] [/kurumsal/hak..] │ +│ Sıralama: [0] │ +│ Aktif: [✅ Toggle] │ +│ [Kaydet] │ +└─────────────────────────────────────────────────┘ +``` + +#### Form Alanları: +| Alan | Bileşen | Açıklama | +|------|---------|----------| +| title | TextInput | Zorunlu, max 255 | +| badge | TextInput | İsteğe bağlı, max 100 | +| content | Textarea | Zorunlu | +| image | FileUpload | storage/public/stories, preview göster | +| cta_text | TextInput | İsteğe bağlı, max 100 | +| cta_url | TextInput (url) | İsteğe bağlı, max 255 | +| order_index | NumberInput | Varsayılan 0 | +| is_active | Toggle | Varsayılan true | + +#### Tablo Sütunları: +| Sütun | Açıklama | +|-------|----------| +| Görsel | Küçük thumbnail | +| Başlık | title | +| Etiket | badge (badge bileşeni) | +| Buton | cta_text (varsa göster) | +| Sıralama | order_index | +| Aktif | is_active (toggle/icon) | + +--- + +## 2. Info Sections (Tanıtım Bölümleri) + +Anasayfadaki 2 adet metin+görsel tanıtım bölümü. Settings tablosunda `info_sections` grubu altında saklanır. + +### API Erişimi + +Mevcut settings endpoint'inden gelir: + +``` +GET /api/v1/settings +GET /api/v1/settings/info_sections +``` + +### Response (settings/info_sections): + +```json +{ + "info_section_1_badge": "Neden Boğaziçi Denizcilik?", + "info_section_1_title": "Uluslararası Standartlarda Eğitim", + "info_section_1_body": "Uzun açıklama metni...", + "info_section_1_quote": "Denizcilik eğitiminde kalite...", + "info_section_1_quote_author": "Kpt. Murat Aydın, Kurucu", + "info_section_1_image": "/storage/uploads/info-1.jpg", + "info_section_2_badge": "Simülatör Destekli Eğitim", + "info_section_2_title": "Teoriden Pratiğe", + "info_section_2_body": "Uzun açıklama metni...", + "info_section_2_image": "/storage/uploads/info-2.jpg" +} +``` + +### Alanlar + +**Bölüm 1 (6 alan):** +| Key | Type | Label | +|-----|------|-------| +| `info_section_1_badge` | text | Bölüm 1 — Etiket | +| `info_section_1_title` | text | Bölüm 1 — Başlık | +| `info_section_1_body` | textarea | Bölüm 1 — İçerik | +| `info_section_1_quote` | text | Bölüm 1 — Alıntı | +| `info_section_1_quote_author` | text | Bölüm 1 — Alıntı Yazarı | +| `info_section_1_image` | image | Bölüm 1 — Görsel | + +**Bölüm 2 (4 alan):** +| Key | Type | Label | +|-----|------|-------| +| `info_section_2_badge` | text | Bölüm 2 — Etiket | +| `info_section_2_title` | text | Bölüm 2 — Başlık | +| `info_section_2_body` | textarea | Bölüm 2 — İçerik | +| `info_section_2_image` | image | Bölüm 2 — Görsel | + +### Admin Panel — Ayarlar Sayfası + +Mevcut Settings sayfasına yeni bir **"Tanıtım Bölümleri"** sekmesi ekle: + +``` +/admin/settings +├── ... (mevcut sekmeler) +└── Tanıtım Bölümleri → info_sections + ├── Bölüm 1 + │ ├── Etiket: [Neden Boğaziçi Denizcilik?] + │ ├── Başlık: [Uluslararası Standartlarda...] + │ ├── İçerik: [textarea — uzun metin] + │ ├── Alıntı: [Denizcilik eğitiminde kalite...] + │ ├── Alıntı Yazarı: [Kpt. Murat Aydın, Kurucu] + │ └── Görsel: [📷 FileUpload] + └── Bölüm 2 + ├── Etiket: [Simülatör Destekli Eğitim] + ├── Başlık: [Teoriden Pratiğe] + ├── İçerik: [textarea — uzun metin] + └── Görsel: [📷 FileUpload] +``` + +Kaydet: `PUT /admin/settings` ile dot notation: +```json +{ + "settings": { + "info_sections.info_section_1_badge": "Neden Boğaziçi Denizcilik?", + "info_sections.info_section_1_title": "Uluslararası Standartlarda Eğitim", + "info_sections.info_section_1_body": "...", + "info_sections.info_section_1_image": "/storage/uploads/info-1.jpg" + } +} +``` + +--- + +## Frontend Kullanım Özeti + +### Stories +```tsx +// Anasayfa +const { data: stories } = await fetch('/api/v1/stories'); + +{stories.map(story => ( + <StoryCard + key={story.id} + title={story.title} + badge={story.badge} + content={story.content} + image={getImageUrl(story.image)} + ctaText={story.cta_text} + ctaUrl={story.cta_url} + /> +))} +``` + +### Info Sections +```tsx +// Settings'ten al +const settings = await fetch('/api/v1/settings'); +const info1 = { + badge: settings.info_sections?.info_section_1_badge, + title: settings.info_sections?.info_section_1_title, + body: settings.info_sections?.info_section_1_body, + quote: settings.info_sections?.info_section_1_quote, + quoteAuthor: settings.info_sections?.info_section_1_quote_author, + image: settings.info_sections?.info_section_1_image, +}; + +// Bölüm 1 — görsel solda, metin sağda +<InfoSection {...info1} imagePosition="left" /> + +// Bölüm 2 — görsel sağda, metin solda (reverse) +<InfoSection {...info2} imagePosition="right" /> +``` diff --git a/prompts/frontend-api-integration.md b/prompts/frontend-api-integration.md new file mode 100644 index 0000000..072df3d --- /dev/null +++ b/prompts/frontend-api-integration.md @@ -0,0 +1,629 @@ +# Frontend (Next.js) — API Entegrasyon Rehberi + +## Genel Bilgi + +Backend: Laravel API, Base URL: `{API_URL}/api/v1` +Auth gerektirmeyen tüm public endpoint'ler burada. Frontend SSR/ISR ile fetch edebilir. + +--- + +## Tüm Public Endpoint'ler + +| Method | Endpoint | Açıklama | +|--------|----------|----------| +| `GET` | `/v1/settings` | Site ayarları (nested group format) | +| `GET` | `/v1/settings/{group}` | Tek grup ayarları | +| `GET` | `/v1/categories` | Eğitim kategorileri | +| `GET` | `/v1/categories/{slug}` | Kategori detayı | +| `GET` | `/v1/courses` | Eğitim listesi (paginated) | +| `GET` | `/v1/courses/{slug}` | Eğitim detayı (blocks + schedules dahil) | +| `GET` | `/v1/schedules` | Eğitim takvimi | +| `GET` | `/v1/schedules/upcoming` | Yaklaşan eğitimler | +| `GET` | `/v1/schedules/{id}` | Takvim detayı | +| `GET` | `/v1/announcements` | Duyurular | +| `GET` | `/v1/announcements/{slug}` | Duyuru detayı | +| `GET` | `/v1/hero-slides` | Hero slider (aktif, sıralı) | +| `GET` | `/v1/faqs` | SSS listesi | +| `GET` | `/v1/faqs/{category}` | Kategoriye göre SSS | +| `GET` | `/v1/guide-cards` | Eğitim rehberi kartları | +| `GET` | `/v1/menus/{location}` | Menü (header, footer, vb.) | +| `GET` | `/v1/pages/{slug}` | Sayfa detayı (blocks dahil) | +| `GET` | `/v1/preview/{token}` | Sayfa önizleme (cache bazlı) | +| `GET` | `/v1/comments/{type}/{id}` | Yorumlar | +| `POST` | `/v1/leads` | Lead/başvuru formu | +| `POST` | `/v1/comments` | Yorum gönder | +| `GET` | `/v1/sitemap-data` | Sitemap verisi | + +--- + +## Eğitimler (Courses) + +### GET /v1/courses — Liste + +Query parametreleri: +- `category` — Kategori slug filtresi (ör: `guverte`, `stcw`, `makine`) +- `search` — Başlık/açıklama arama +- `sort` — Sıralama: `title`, `-created_at` (varsayılan), `students`, `rating` (- prefix = desc) +- `per_page` — Sayfa başına (varsayılan: 15) + +``` +GET /v1/courses?category=guverte&sort=-rating&per_page=12 +``` + +Response (paginated): +```json +{ + "data": [ + { + "id": 1, + "category_id": 1, + "category": { "id": 1, "slug": "guverte", "label": "Güverte Eğitimleri" }, + "slug": "gemici-birlesik-egitimi", + "title": "Gemici (Birleşik) Eğitimi", + "sub": "STCW / IMO Uyumlu", + "desc": "Güverte bölümünde gemici olarak görev yapmak isteyen...", + "long_desc": "Detaylı açıklama...", + "duration": "32 Gün", + "students": 772, + "rating": 4.9, + "badge": "most_preferred", + "badge_label": "En Çok Tercih Edilen", + "image": null, + "price": "₺14.500", + "includes": ["Basılı eğitim materyalleri", "Uygulamalı güverte tatbikatları", "..."], + "requirements": ["En az 16 yaşında olmak", "..."], + "scope": ["Denizde kişisel güvenlik", "Yangınla mücadele", "..."], + "standard": "STCW / IMO Uyumlu", + "language": "Türkçe", + "location": "Kadıköy, İstanbul", + "meta_title": "Gemici (Birleşik) Eğitimi | Boğaziçi Denizcilik", + "meta_description": "STCW A-II/4 uyumlu...", + "created_at": "2026-03-02T...", + "updated_at": "2026-03-23T..." + } + ], + "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, + "meta": { "current_page": 1, "last_page": 3, "per_page": 12, "total": 32 } +} +``` + +### GET /v1/courses/{slug} — Detay + +Detayda ek olarak `blocks` ve `schedules` array'leri gelir: + +```json +{ + "data": { + "id": 1, + "slug": "gemici-birlesik-egitimi", + "title": "Gemici (Birleşik) Eğitimi", + "sub": "STCW / IMO Uyumlu", + "desc": "...", + "long_desc": "...", + "duration": "32 Gün", + "students": 772, + "rating": 4.9, + "badge": "most_preferred", + "badge_label": "En Çok Tercih Edilen", + "image": null, + "price": "₺14.500", + "includes": ["Basılı eğitim materyalleri", "..."], + "requirements": ["En az 16 yaşında olmak", "..."], + "scope": ["Denizde kişisel güvenlik", "..."], + "standard": "STCW / IMO Uyumlu", + "language": "Türkçe", + "location": "Kadıköy, İstanbul", + "category": { "id": 1, "slug": "guverte", "label": "Güverte Eğitimleri" }, + "blocks": [ + { "id": 1, "type": "text", "content": { "label": "EĞİTİM HAKKINDA", "title": "Neden Bu Eğitim?", "body": "<p>...</p>" }, "order_index": 0 }, + { "id": 2, "type": "text", "content": { "_width": "half", "label": "EĞİTİM KAPSAMI", "title": "Ne Öğreneceksiniz?", "body": "<ul>...</ul>" }, "order_index": 1 }, + { "id": 3, "type": "text", "content": { "_width": "half", "label": "BAŞVURU ŞARTLARI", "title": "Kimler Katılabilir?", "body": "<ul>...</ul>" }, "order_index": 2 }, + { "id": 4, "type": "stats_grid", "content": { "label": "EĞİTİM SÜRECİ", "title": "Başvurudan Belgeye 4 Adım", "stat_1_value": "01", "stat_1_label": "..." }, "order_index": 3 }, + { "id": 5, "type": "cards", "content": { "label": "KAZANIMLAR", "title": "...", "card_1_title": "...", "card_1_icon": "award" }, "order_index": 4 }, + { "id": 6, "type": "faq", "content": { "title": "...", "faq_1_question": "...", "faq_1_answer": "..." }, "order_index": 5 }, + { "id": 7, "type": "cta", "content": { "title": "...", "button_text": "Ön Kayıt Yap", "button_url": "/kayit?course=..." }, "order_index": 6 } + ], + "schedules": [ + { "id": 1, "start_date": "2026-04-07", "end_date": "2026-05-08", "location": "Kadıköy", "quota": 20, "available_seats": 8, "is_urgent": false } + ], + "meta_title": "...", + "meta_description": "..." + } +} +``` + +### Badge Değerleri +| badge | badge_label | Kullanım | +|-------|-------------|----------| +| `most_preferred` | En Çok Tercih Edilen | Kart üstü etiket | +| `popular` | Popüler | Kart üstü etiket | +| `null` | — | Etiket yok | + +--- + +## Eğitim Blokları (Course Blocks) + +Bloklar `order_index` sıralı gelir. Her bloğun `type` ve `content` JSON'ı var. **Aynı renderer Page Blocks ile paylaşılabilir.** + +### Blok Tipleri + +| type | Açıklama | content key'leri | +|------|----------|------------------| +| `text` | Zengin metin | `label`, `title`, `body` (HTML), `_width` | +| `cards` | Kart grid | `label`, `title`, `card_N_title`, `card_N_text`, `card_N_icon` | +| `stats_grid` | İstatistik/adım | `label`, `title`, `stat_N_value`, `stat_N_label`, `style` | +| `cta` | Call-to-action | `title`, `description`, `button_text`, `button_url`, `button_2_text`, `button_2_url` | +| `faq` | SSS | `title`, `faq_N_question`, `faq_N_answer` | +| `hero` | Hero banner | `breadcrumb`, `title`, `highlight`, `subtitle`, `description` | +| `text_image` | Metin + görsel | `label`, `title`, `body`, `image`, `image_alt`, `image_position` | +| `gallery` | Görsel galeri | Görsel listesi | +| `video` | Video embed | Video URL | +| `testimonials` | Referanslar | Yorum kartları | +| `html` | Serbest HTML | Ham HTML | + +### `_width` Sistemi +`content._width` blok genişliğini belirler: +- `"full"` (varsayılan, key yoksa otomatik) — tam genişlik +- `"half"` — yarım genişlik, ardışık iki half blok yan yana render edilir + +```tsx +// Blok renderer örneği +function BlockRenderer({ blocks }) { + const grouped = groupConsecutiveHalfBlocks(blocks); + + return grouped.map((item) => { + if (item.type === 'row') { + return ( + <div className="grid grid-cols-2 gap-8"> + {item.blocks.map(block => <Block key={block.id} {...block} />)} + </div> + ); + } + return <Block key={item.id} {...item} />; + }); +} +``` + +--- + +## Kategoriler + +### GET /v1/categories + +```json +{ + "data": [ + { + "id": 1, + "slug": "guverte", + "label": "Güverte Eğitimleri", + "desc": "...", + "image": null, + "meta_title": "...", + "meta_description": "...", + "courses_count": 10, + "menu_courses": [ + { "title": "ARPA / Radar Simülatör Eğitimi", "slug": "arpa-radar-simulator" }, + { "title": "ECDIS Tip Bazlı Eğitim", "slug": "ecdis-tip-bazli-egitim" }, + { "title": "GMDSS Genel Telsiz Operatörü (GOC)", "slug": "gmdss-genel-telsiz-operatoru-goc" } + ] + } + ] +} +``` + +**`menu_courses`**: Her kategoriden `menu_order` 1-3 olan kurslar. Mega menu dropdown'unda kullanılır. + +Kategori slug'ları: `guverte`, `stcw`, `makine`, `yat-kaptanligi`, `yenileme`, `guvenlik` + +--- + +## Sayfalar (Pages + Blocks) + +### GET /v1/pages/{slug} + +Kurumsal sayfalar: `kalite-politikasi`, `hakkimizda`, `vizyon-misyon` + +```json +{ + "data": { + "id": 1, + "slug": "kalite-politikasi", + "title": "Kalite Politikamız", + "meta_title": "...", + "meta_description": "...", + "is_active": true, + "blocks": [ + { "id": 1, "type": "hero", "content": { "breadcrumb": "...", "title": "...", "highlight": "..." }, "order_index": 0 }, + { "id": 2, "type": "cards", "content": { "label": "AKREDİTASYONLAR", "..." }, "order_index": 1 }, + { "id": 3, "type": "text", "content": { "_width": "half", "..." }, "order_index": 2 }, + { "id": 4, "type": "stats_grid", "content": { "_width": "half", "..." }, "order_index": 3 }, + { "id": 5, "type": "cta", "content": { "..." }, "order_index": 4 } + ] + } +} +``` + +Page blocks ve course blocks **aynı type/content yapısını** kullanır. Tek bir `BlockRenderer` bileşeni her ikisi için de çalışır. + +--- + +## Hero Slides + +### GET /v1/hero-slides + +Anasayfa slider: +```json +{ + "data": [ + { + "id": 1, + "title": "Denizcilik Kariyerinize Başlayın", + "subtitle": "STCW uyumlu eğitim programları", + "button_text": "Eğitimleri İncele", + "button_url": "/egitimler", + "image": "/storage/uploads/hero-1.jpg", + "order_index": 0, + "is_active": true + } + ] +} +``` + +--- + +## Eğitim Takvimi (Schedules) + +### GET /v1/schedules + +```json +{ + "data": [ + { + "id": 1, + "course_id": 1, + "course": { "id": 1, "title": "Gemici (Birleşik) Eğitimi", "slug": "gemici-birlesik-egitimi", "..." }, + "start_date": "2026-04-07", + "end_date": "2026-05-08", + "location": "Kadıköy", + "quota": 20, + "available_seats": 8, + "is_urgent": false + } + ] +} +``` + +### GET /v1/schedules/upcoming +Yaklaşan eğitimler (start_date >= today, sıralı). + +--- + +## Menüler + +### GET /v1/menus/{location} + +Location değerleri: `header`, `footer`, `sidebar` + +```json +{ + "data": [ + { + "id": 1, + "label": "Anasayfa", + "url": "/", + "location": "header", + "type": "link", + "parent_id": null, + "order_index": 0, + "is_active": true, + "children": [] + } + ] +} +``` + +--- + +## Duyurular + +### GET /v1/announcements + +```json +{ + "data": [ + { + "id": 1, + "slug": "2026-kayitlari-basladi", + "title": "2026 Kayıtları Başladı", + "category": "duyuru", + "excerpt": "...", + "content": "<p>HTML içerik...</p>", + "image": "/storage/uploads/...", + "is_featured": true, + "meta_title": "...", + "meta_description": "...", + "published_at": "2026-03-15" + } + ] +} +``` + +--- + +## SSS (FAQs) + +### GET /v1/faqs + +```json +{ + "data": [ + { + "id": 1, + "question": "STCW belgesi nedir?", + "answer": "...", + "category": "genel", + "order_index": 0, + "is_active": true + } + ] +} +``` + +Kategoriye göre: `GET /v1/faqs/genel` + +--- + +## Lead Formu (Başvuru) + +### POST /v1/leads + +Rate limited. Request: +```json +{ + "name": "Ad Soyad", + "phone": "+90 532 ...", + "source": "web", + "target_course": "gemici-birlesik-egitimi", + "education_level": "lise", + "subject": "Eğitim başvurusu", + "message": "Bilgi almak istiyorum", + "kvkk_consent": true, + "marketing_consent": false, + "utm_source": "google", + "utm_medium": "cpc", + "utm_campaign": "denizcilik-2026" +} +``` + +--- + +## Sayfa Önizleme (Preview) + +### GET /v1/preview/{token} + +Admin panelden gönderilen önizleme. Auth yok, Next.js SSR ile fetch edilir. +10 dakika geçerli. Bulunamazsa 404. + +Response formatı `GET /v1/pages/{slug}` ile aynı: +```json +{ + "data": { + "id": 1, + "slug": "kalite-politikasi", + "title": "...", + "blocks": [...] + } +} +``` + +--- + +## Sayfa Bazlı Veri Haritası + +### Anasayfa (`/`) +| Veri | Endpoint | +|------|----------| +| Hero slider | `GET /v1/hero-slides` | +| Eğitim kartları | `GET /v1/courses?per_page=6&sort=-rating` | +| Kategoriler | `GET /v1/categories` | +| Yaklaşan eğitimler | `GET /v1/schedules/upcoming` | +| SSS | `GET /v1/faqs` | +| Site ayarları | `GET /v1/settings` (layout'tan) | + +### Eğitimler (`/egitimler`) +| Veri | Endpoint | +|------|----------| +| Eğitim listesi | `GET /v1/courses?category=guverte&per_page=12` | +| Kategoriler (filter) | `GET /v1/categories` | + +### Eğitim Detay (`/egitimler/{slug}`) +| Veri | Endpoint | +|------|----------| +| Eğitim + bloklar + takvim | `GET /v1/courses/{slug}` | + +### Kurumsal Sayfalar (`/kurumsal/{slug}`) +| Veri | Endpoint | +|------|----------| +| Sayfa + bloklar | `GET /v1/pages/{slug}` | + +### Duyurular (`/duyurular`) +| Veri | Endpoint | +|------|----------| +| Duyuru listesi | `GET /v1/announcements` | + +### İletişim (`/iletisim`) +| Veri | Endpoint | +|------|----------| +| İletişim bilgileri | `GET /v1/settings/contact` | +| Harita | `GET /v1/settings/maps` | +| Form gönderimi | `POST /v1/leads` | + +### Layout (Her Sayfa) +| Veri | Endpoint | +|------|----------| +| Header/Footer/SEO | `GET /v1/settings` | +| Menü | `GET /v1/menus/header` + `GET /v1/menus/footer` | +| Kategoriler (mega menu) | `GET /v1/categories` | + +--- + +## TypeScript Tipleri + +```ts +interface Course { + id: number; + category_id: number; + category?: Category; + slug: string; + title: string; + sub: string | null; + desc: string; + long_desc: string; + duration: string; + students: number; + rating: number; + badge: 'most_preferred' | 'popular' | null; + badge_label: string | null; + image: string | null; + price: string; + includes: string[]; + requirements: string[]; + scope: string[]; + standard: string | null; + language: string | null; + location: string | null; + blocks?: Block[]; + schedules?: Schedule[]; + meta_title: string | null; + meta_description: string | null; + created_at: string; + updated_at: string; +} + +interface Category { + id: number; + slug: string; + label: string; + desc: string | null; + image: string | null; + courses_count?: number; + menu_courses?: { title: string; slug: string }[]; +} + +interface Block { + id: number; + type: 'hero' | 'text' | 'text_image' | 'cards' | 'stats_grid' | 'cta' | 'faq' | 'gallery' | 'video' | 'testimonials' | 'html'; + content: Record<string, any>; + order_index: number; +} + +interface Schedule { + id: number; + course_id: number; + course?: Course; + start_date: string; + end_date: string; + location: string; + quota: number; + available_seats: number; + is_urgent: boolean; +} + +interface Page { + id: number; + slug: string; + title: string; + meta_title: string | null; + meta_description: string | null; + is_active: boolean; + blocks: Block[]; +} + +interface HeroSlide { + id: number; + title: string; + subtitle: string | null; + button_text: string | null; + button_url: string | null; + image: string | null; + order_index: number; +} + +interface Announcement { + id: number; + slug: string; + title: string; + category: string; + excerpt: string | null; + content: string; + image: string | null; + is_featured: boolean; + published_at: string; +} + +interface FAQ { + id: number; + question: string; + answer: string; + category: string; + order_index: number; +} + +interface Lead { + name: string; + phone: string; + source?: string; + target_course?: string; + education_level?: string; + subject?: string; + message?: string; + kvkk_consent: boolean; + marketing_consent?: boolean; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; +} + +interface MenuItem { + id: number; + label: string; + url: string; + location: string; + type: string; + parent_id: number | null; + order_index: number; + children: MenuItem[]; +} +``` + +--- + +## Image URL'leri + +Image alanları relative path döner. API base URL ile birleştir: + +```ts +const getImageUrl = (path: string | null): string | null => { + if (!path) return null; + if (path.startsWith('http')) return path; + return `${process.env.NEXT_PUBLIC_API_URL}${path}`; +}; +``` + +--- + +## Cache Stratejisi + +| Veri | ISR revalidate | +|------|---------------| +| Settings | 300s (5 dk) | +| Categories | 300s | +| Courses list | 60s | +| Course detail | 60s | +| Pages | 300s | +| Hero slides | 300s | +| Schedules | 60s | +| Announcements | 120s | +| FAQs | 300s | +| Menus | 300s | diff --git a/prompts/frontend-leads.md b/prompts/frontend-leads.md new file mode 100644 index 0000000..4ebb0bd --- /dev/null +++ b/prompts/frontend-leads.md @@ -0,0 +1,293 @@ +# Frontend (Next.js) — Lead Form Entegrasyonu + +## API Endpoint + +``` +POST /api/v1/leads +``` + +Auth yok, public endpoint. Rate limited. + +--- + +## Request Body + +```json +{ + "name": "Ad Soyad", + "phone": "+90 532 724 15 32", + "email": "ornek@email.com", + "source": "kurs_kayit", + "target_course": "gemici-birlesik-egitimi", + "education_level": "lise", + "subject": "Eğitim başvurusu", + "message": "Bilgi almak istiyorum", + "kvkk_consent": true, + "marketing_consent": false, + "utm_source": "google", + "utm_medium": "cpc", + "utm_campaign": "denizcilik-2026" +} +``` + +### Zorunlu Alanlar +| Alan | Tip | Açıklama | +|------|-----|----------| +| `name` | string | Ad Soyad (max 255) | +| `phone` | string | Telefon (max 20) | +| `source` | string | Form kaynağı (aşağıya bak) | +| `kvkk_consent` | boolean | **Zorunlu, `true` olmalı** — checkbox onaylatılmalı | + +### Opsiyonel Alanlar +| Alan | Tip | Açıklama | +|------|-----|----------| +| `email` | string | E-posta | +| `target_course` | string | Hedef eğitim slug'ı | +| `education_level` | string | Eğitim seviyesi | +| `subject` | string | Konu | +| `message` | string | Mesaj | +| `marketing_consent` | boolean | Pazarlama onayı | +| `utm_source` | string | Google Analytics UTM | +| `utm_medium` | string | — | +| `utm_campaign` | string | — | + +--- + +## Source Değerleri (Form Bazlı) + +Her form kendi `source` değerini gönderir: + +| Form | source | Zorunlu Alanlar | Opsiyonel Alanlar | +|------|--------|-----------------|-------------------| +| Kurs ön kayıt | `kurs_kayit` | name, phone, kvkk_consent | email, target_course, education_level, message | +| Danışmanlık | `danismanlik` | name, phone, kvkk_consent | email, message, target_course | +| Duyuru sidebar | `duyuru` | name, phone, kvkk_consent | email | +| İletişim | `iletisim` | name, phone, kvkk_consent | email, subject, message | +| Hero form | `hero_form` | name, phone, kvkk_consent | target_course | +| WhatsApp widget | `whatsapp_widget` | name, phone, kvkk_consent | — | + +--- + +## Response + +### Başarılı (201): +```json +{ + "success": true, + "message": "Talebiniz alınmıştır. En kısa sürede sizinle iletişime geçeceğiz." +} +``` + +### Validasyon Hatası (422): +```json +{ + "message": "KVKK metnini onaylamanız gerekmektedir.", + "errors": { + "kvkk_consent": ["KVKK metnini onaylamanız gerekmektedir."], + "name": ["Ad Soyad zorunludur."] + } +} +``` + +--- + +## Form Örnekleri + +### 1. Kurs Ön Kayıt Formu (Eğitim Detay Sayfası) + +```tsx +async function submitKursKayit(formData: FormData, courseSlug: string) { + const utm = getUTMParams(); // URL'den utm_source, utm_medium, utm_campaign + + const res = await fetch(`${API_URL}/api/v1/leads`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: formData.get('name'), + phone: formData.get('phone'), + email: formData.get('email') || undefined, + source: 'kurs_kayit', + target_course: courseSlug, + education_level: formData.get('education_level') || undefined, + message: formData.get('message') || undefined, + kvkk_consent: formData.get('kvkk') === 'on', + marketing_consent: formData.get('marketing') === 'on', + ...utm, + }), + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.message); + } + return res.json(); +} +``` + +Form alanları: +- Ad Soyad (zorunlu) +- Telefon (zorunlu) +- E-posta +- Eğitim Seviyesi (select: ilkokul, ortaokul, lise, onlisans, lisans) +- Mesaj +- [x] KVKK metnini okudum ve onaylıyorum (zorunlu checkbox) +- [ ] Kampanya ve duyurulardan haberdar olmak istiyorum + +### 2. Danışmanlık Formu + +```tsx +body: JSON.stringify({ + name: formData.get('name'), + phone: formData.get('phone'), + email: formData.get('email') || undefined, + source: 'danismanlik', + target_course: formData.get('interested_course') || undefined, + message: formData.get('message') || undefined, + kvkk_consent: true, + ...utm, +}) +``` + +### 3. İletişim Formu + +```tsx +body: JSON.stringify({ + name: formData.get('name'), + phone: formData.get('phone'), + email: formData.get('email') || undefined, + source: 'iletisim', + subject: formData.get('subject') || undefined, + message: formData.get('message'), + kvkk_consent: true, + ...utm, +}) +``` + +### 4. Duyuru Sidebar Mini Form + +```tsx +body: JSON.stringify({ + name: formData.get('name'), + phone: formData.get('phone'), + source: 'duyuru', + kvkk_consent: true, + ...utm, +}) +``` + +### 5. Hero Form (Anasayfa) + +```tsx +body: JSON.stringify({ + name: formData.get('name'), + phone: formData.get('phone'), + source: 'hero_form', + target_course: formData.get('course') || undefined, + kvkk_consent: true, + ...utm, +}) +``` + +--- + +## UTM Parametreleri + +URL'deki UTM parametrelerini otomatik olarak form submit'e ekle: + +```tsx +function getUTMParams(): Record<string, string> { + if (typeof window === 'undefined') return {}; + + const params = new URLSearchParams(window.location.search); + const utm: Record<string, string> = {}; + + for (const key of ['utm_source', 'utm_medium', 'utm_campaign']) { + const val = params.get(key); + if (val) utm[key] = val; + } + return utm; +} +``` + +UTM değerleri sayfa boyunca korunmalı (cookie veya sessionStorage): + +```tsx +// Sayfa ilk yüklendiğinde +useEffect(() => { + const utm = getUTMParams(); + if (Object.keys(utm).length > 0) { + sessionStorage.setItem('utm', JSON.stringify(utm)); + } +}, []); + +// Form submit'te +const utm = JSON.parse(sessionStorage.getItem('utm') || '{}'); +``` + +--- + +## KVKK Checkbox + +Her formda KVKK onay checkbox'ı **zorunlu**. Backend `kvkk_consent: true` olmadan kabul etmez. + +```tsx +<label className="flex items-start gap-2"> + <input type="checkbox" name="kvkk" required /> + <span className="text-sm text-gray-600"> + <a href="/kvkk" target="_blank" className="underline"> + KVKK Aydınlatma Metni + </a>'ni okudum ve onaylıyorum. + </span> +</label> +``` + +--- + +## Error Handling + +```tsx +try { + const result = await submitLead(formData); + // Başarılı — toast göster + toast.success(result.message); + form.reset(); +} catch (error) { + if (error.status === 422) { + // Validasyon hataları — form alanlarının altında göster + const { errors } = await error.json(); + setFieldErrors(errors); + } else if (error.status === 429) { + // Rate limit — çok fazla istek + toast.error('Çok fazla istek gönderildi. Lütfen biraz bekleyin.'); + } else { + toast.error('Bir hata oluştu. Lütfen tekrar deneyin.'); + } +} +``` + +--- + +## TypeScript Tipi + +```ts +interface LeadFormData { + name: string; + phone: string; + email?: string; + source: 'kurs_kayit' | 'danismanlik' | 'duyuru' | 'iletisim' | 'hero_form' | 'whatsapp_widget'; + target_course?: string; + education_level?: string; + subject?: string; + message?: string; + kvkk_consent: true; // Her zaman true olmalı + marketing_consent?: boolean; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; +} + +interface LeadResponse { + success: boolean; + message: string; +} +``` diff --git a/prompts/frontend-settings.md b/prompts/frontend-settings.md new file mode 100644 index 0000000..3f916ee --- /dev/null +++ b/prompts/frontend-settings.md @@ -0,0 +1,380 @@ +# Frontend (Next.js) — Site Ayarları Entegrasyonu + +## Genel Bakış + +Backend'den tüm site ayarları **tek bir API çağrısı** ile alınır. Ayarlar gruplara ayrılmış nested JSON formatında gelir. Bu veri layout, header, footer, SEO meta tag'leri, analytics scriptleri ve tüm sabit içerikleri besler. + +**Sensitive key'ler (API key, secret, password) public endpoint'ten filtrelenmiştir — güvenle kullanabilirsin.** + +--- + +## API Endpoints (Public — Auth Yok) + +| Method | Endpoint | Açıklama | +|--------|----------|----------| +| `GET` | `/api/v1/settings` | Tüm public ayarlar (grouped nested JSON) | +| `GET` | `/api/v1/settings/{group}` | Tek grup ayarları (flat key-value) | + +--- + +## GET /api/v1/settings — Response Formatı + +```json +{ + "general": { + "site_name": "Boğaziçi Denizcilik Eğitim Kurumu", + "site_tagline": "Türkiye'nin Köklü Denizcilik Okulu", + "site_description": "Türkiye'nin köklü denizcilik eğitim kurumu", + "logo_light": "/storage/uploads/logo-white.png", + "logo_dark": "/storage/uploads/logo-dark.png", + "favicon": "/storage/uploads/favicon.png", + "apple_touch_icon": null, + "announcement_bar_active": "true", + "announcement_bar_text": "2026 Kayıtları Devam Ediyor", + "announcement_bar_url": "/kayit", + "announcement_bar_bg_color": "#1a3e74", + "maintenance_mode": "false", + "maintenance_message": "Sitemiz bakımdadır, kısa süre içinde geri döneceğiz." + }, + "contact": { + "phone_primary": "+90 532 724 15 32", + "phone_secondary": null, + "email_info": "bilgi@bogazicidenizcilik.com", + "address_full": "Osmanağa Mah. Çuhadarağa Sk. No:21 Kadıköy/İstanbul", + "address_short": "Kadıköy, İstanbul", + "working_hours_weekday": "Hafta İçi 09:00 – 17:00", + "whatsapp_number": null, + "whatsapp_message": "Merhaba, bilgi almak istiyorum." + }, + "maps": { + "google_maps_embed_url": null, + "google_maps_place_url": null, + "latitude": "40.9876", + "longitude": "29.0234", + "map_zoom_level": "15" + }, + "social": { + "instagram_url": null, + "instagram_handle": null, + "facebook_url": null, + "youtube_url": null, + "linkedin_url": null, + "tiktok_url": null, + "twitter_url": null + }, + "seo": { + "meta_title_suffix": "| Boğaziçi Denizcilik", + "meta_title_separator": "|", + "default_meta_description": "IMO ve STCW standartlarında denizcilik eğitimi.", + "default_meta_keywords": "denizcilik kursu, STCW eğitimi, kaptan kursu", + "robots": "index, follow", + "canonical_domain": "https://bogazicidenizcilik.com", + "og_image": null, + "og_type": "website", + "og_locale": "tr_TR", + "og_site_name": "Boğaziçi Denizcilik", + "twitter_card_type": "summary_large_image", + "google_site_verification": null + }, + "analytics": { + "google_analytics_id": null, + "google_tag_manager_id": null, + "facebook_pixel_id": null, + "hotjar_id": null, + "clarity_id": null, + "crisp_website_id": null, + "custom_head_scripts": null, + "custom_body_scripts": null + }, + "header": { + "navbar_style_default": "transparent", + "cta_button_text": "Başvuru Yap", + "cta_button_url": "/kayit", + "cta_button_color": "#396cab", + "show_phone_topbar": "true", + "show_email_topbar": "true", + "show_address_topbar": "true", + "show_hours_topbar": "true", + "show_social_navbar": "true" + }, + "footer": { + "footer_description": "Türkiye'nin köklü denizcilik eğitim kurumlarından biri olarak...", + "footer_logo": null, + "copyright_text": "© 2026 Boğaziçi Denizcilik Eğitim Kurumu. Tüm hakları saklıdır.", + "footer_address": "Osmanağa Bahariye Cad. No:31 Kadıköy/İstanbul", + "footer_phone": "+90 532 724 15 32", + "footer_email": "bilgi@bogazicidenizcilik.com", + "footer_bg_color": "#0f2447", + "show_social_footer": "true" + } +} +``` + +> **Not:** `integrations` grubu public API'den dönmez (tamamı `is_public: false`). `maps.google_maps_api_key` de filtrelenmiştir. + +--- + +## GET /api/v1/settings/{group} — Tek Grup + +``` +GET /api/v1/settings/contact +``` + +```json +{ + "phone_primary": "+90 532 724 15 32", + "email_info": "bilgi@bogazicidenizcilik.com", + "address_full": "Osmanağa Mah. Çuhadarağa Sk. No:21 Kadıköy/İstanbul", + ... +} +``` + +Geçerli group değerleri: `general`, `contact`, `maps`, `social`, `seo`, `analytics`, `header`, `footer` + +--- + +## Önerilen Veri Akışı (Next.js) + +### 1. Layout-Level Fetch (Server Component) + +Ayarlar tüm sayfalarda gerekli (header, footer, SEO). Layout'ta bir kez fetch edip context/provider ile dağıt: + +```tsx +// app/layout.tsx +async function getSettings() { + const res = await fetch(`${process.env.API_URL}/api/v1/settings`, { + next: { revalidate: 300 } // 5 dk cache + }); + return res.json(); +} + +export default async function RootLayout({ children }) { + const settings = await getSettings(); + + return ( + <html> + <head> + {/* Analytics Scripts */} + {settings.analytics?.google_tag_manager_id && ( + <script>...</script> + )} + {settings.analytics?.custom_head_scripts && ( + <div dangerouslySetInnerHTML={{ __html: settings.analytics.custom_head_scripts }} /> + )} + </head> + <body> + <SettingsProvider value={settings}> + <Header settings={settings} /> + {children} + <Footer settings={settings} /> + </SettingsProvider> + {settings.analytics?.custom_body_scripts && ( + <div dangerouslySetInnerHTML={{ __html: settings.analytics.custom_body_scripts }} /> + )} + </body> + </html> + ); +} +``` + +### 2. Boolean Değerler + +Backend tüm değerleri **string** olarak döner. Boolean kontrol: + +```tsx +// Helper fonksiyon +const isEnabled = (value: string | null) => value === "true"; + +// Kullanım +{isEnabled(settings.general.announcement_bar_active) && ( + <AnnouncementBar + text={settings.general.announcement_bar_text} + url={settings.general.announcement_bar_url} + bgColor={settings.general.announcement_bar_bg_color} + /> +)} + +{isEnabled(settings.header.show_phone_topbar) && ( + <span>{settings.contact.phone_primary}</span> +)} +``` + +### 3. Image Alanları + +Image değerleri relative path olarak gelir. API base URL ile birleştir: + +```tsx +const getImageUrl = (path: string | null) => { + if (!path) return null; + if (path.startsWith('http')) return path; + return `${process.env.NEXT_PUBLIC_API_URL}${path}`; +}; + +// Kullanım +<Image src={getImageUrl(settings.general.logo_light)} alt={settings.general.site_name} /> +``` + +--- + +## Bileşen Bazlı Kullanım Haritası + +### Header / Navbar +| Ayar | Kullanım | +|------|----------| +| `general.logo_light` | Navbar logo (transparent bg) | +| `general.logo_dark` | Navbar logo (scrolled/white bg) | +| `general.announcement_bar_*` | Üst duyuru barı | +| `header.navbar_style_default` | Navbar başlangıç stili | +| `header.cta_button_text/url/color` | Sağ üst CTA butonu | +| `header.show_*_topbar` | Topbar'da göster/gizle kontrolleri | +| `header.show_social_navbar` | Sosyal medya ikonları | +| `contact.phone_primary` | Topbar telefon | +| `contact.email_info` | Topbar e-posta | +| `contact.address_short` | Topbar adres | +| `contact.working_hours_weekday` | Topbar çalışma saatleri | +| `social.*_url` | Sosyal medya ikon linkleri | + +### Footer +| Ayar | Kullanım | +|------|----------| +| `footer.footer_logo` | Footer logo | +| `footer.footer_description` | Footer açıklama metni | +| `footer.footer_address` | Adres | +| `footer.footer_phone` | Telefon | +| `footer.footer_email` | E-posta | +| `footer.copyright_text` | Copyright satırı | +| `footer.footer_bg_color` | Background rengi | +| `footer.show_social_footer` | Sosyal medya göster/gizle | +| `social.*_url` | Sosyal medya ikon linkleri | + +### SEO / Head Meta Tags +```tsx +// Her sayfa için generateMetadata +export async function generateMetadata(): Promise<Metadata> { + const settings = await getSettings(); + const seo = settings.seo; + + return { + title: { + template: `%s ${seo.meta_title_separator} ${seo.og_site_name}`, + default: settings.general.site_name, + }, + description: seo.default_meta_description, + keywords: seo.default_meta_keywords, + robots: seo.robots, + openGraph: { + type: seo.og_type, + locale: seo.og_locale, + siteName: seo.og_site_name, + images: seo.og_image ? [getImageUrl(seo.og_image)] : [], + }, + twitter: { + card: seo.twitter_card_type, + site: seo.twitter_site, + creator: seo.twitter_creator, + }, + verification: { + google: seo.google_site_verification, + other: { + 'yandex-verification': seo.yandex_verification, + 'msvalidate.01': seo.bing_site_verification, + }, + }, + }; +} +``` + +### İletişim Sayfası +| Ayar | Kullanım | +|------|----------| +| `contact.phone_primary/secondary` | Telefon numaraları | +| `contact.email_*` | E-posta adresleri | +| `contact.address_full` | Tam adres | +| `contact.working_hours_*` | Çalışma saatleri | +| `contact.whatsapp_number/message` | WhatsApp butonu | +| `maps.google_maps_embed_url` | Harita iframe | +| `maps.google_maps_place_url` | "Google Maps'te Aç" linki | +| `maps.latitude/longitude` | Marker konumu | + +### WhatsApp Floating Button +```tsx +const whatsappUrl = settings.contact.whatsapp_number + ? `https://wa.me/${settings.contact.whatsapp_number.replace(/\s/g, '')}?text=${encodeURIComponent(settings.contact.whatsapp_message || '')}` + : null; + +{whatsappUrl && <a href={whatsappUrl} target="_blank">WhatsApp</a>} +``` + +### Bakım Modu +```tsx +// middleware.ts veya layout.tsx +if (isEnabled(settings.general.maintenance_mode)) { + return <MaintenancePage message={settings.general.maintenance_message} />; +} +``` + +--- + +## Cache Stratejisi + +| Strateji | Açıklama | +|----------|----------| +| Backend | 1 saat cache (`Cache::remember`, 3600s) | +| Frontend (ISR) | `next: { revalidate: 300 }` — 5 dakika | +| Admin güncelleme sonrası | Backend cache otomatik temizlenir, frontend 5 dk içinde güncellenir | +| Acil güncelleme | Admin panelden "Cache Temizle" → frontend revalidate | + +--- + +## Null Değer Kontrolü + +Birçok ayar başlangıçta `null` olabilir. Her kullanımda null check yap: + +```tsx +// Kötü +<a href={settings.social.instagram_url}>Instagram</a> + +// İyi +{settings.social?.instagram_url && ( + <a href={settings.social.instagram_url}>Instagram</a> +)} +``` + +Sosyal medya ikonlarını dinamik render et — sadece URL'si dolu olanları göster: + +```tsx +const socialLinks = [ + { key: 'instagram_url', icon: Instagram, label: 'Instagram' }, + { key: 'facebook_url', icon: Facebook, label: 'Facebook' }, + { key: 'twitter_url', icon: Twitter, label: 'X' }, + { key: 'youtube_url', icon: Youtube, label: 'YouTube' }, + { key: 'linkedin_url', icon: Linkedin, label: 'LinkedIn' }, + { key: 'tiktok_url', icon: TikTok, label: 'TikTok' }, +]; + +{socialLinks + .filter(s => settings.social?.[s.key]) + .map(s => ( + <a key={s.key} href={settings.social[s.key]} target="_blank" aria-label={s.label}> + <s.icon /> + </a> + )) +} +``` + +--- + +## TypeScript Tipi (Önerilen) + +```ts +interface SiteSettings { + general: Record<string, string | null>; + contact: Record<string, string | null>; + maps: Record<string, string | null>; + social: Record<string, string | null>; + seo: Record<string, string | null>; + analytics: Record<string, string | null>; + header: Record<string, string | null>; + footer: Record<string, string | null>; +} +``` diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,25 @@ +<IfModule mod_rewrite.c> + <IfModule mod_negotiation.c> + Options -MultiViews -Indexes + </IfModule> + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] +</IfModule> diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/01_Gemici_Birlesik_Egitimi/01_Gemici_Birlesik_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/01_Gemici_Birlesik_Egitimi/01_Gemici_Birlesik_Egitimi_makale.docx new file mode 100644 index 0000000..13badd5 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/01_Gemici_Birlesik_Egitimi/01_Gemici_Birlesik_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/02_Gemici_Temel_Egitimi/02_Gemici_Temel_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/02_Gemici_Temel_Egitimi/02_Gemici_Temel_Egitimi_makale.docx new file mode 100644 index 0000000..a14f70a Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/02_Gemici_Temel_Egitimi/02_Gemici_Temel_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/03_Usta_Gemici_Yetistirme_Egitimi/03_Usta_Gemici_Yetistirme_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/03_Usta_Gemici_Yetistirme_Egitimi/03_Usta_Gemici_Yetistirme_Egitimi_makale.docx new file mode 100644 index 0000000..079a9e5 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/03_Usta_Gemici_Yetistirme_Egitimi/03_Usta_Gemici_Yetistirme_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/04_Uzakyol_Vardiya_Zabiti_Egitimi/04_Uzakyol_Vardiya_Zabiti_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/04_Uzakyol_Vardiya_Zabiti_Egitimi/04_Uzakyol_Vardiya_Zabiti_Egitimi_makale.docx new file mode 100644 index 0000000..e4a8dcd Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/04_Uzakyol_Vardiya_Zabiti_Egitimi/04_Uzakyol_Vardiya_Zabiti_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/05_Gemi_Guvenlik_Zabiti_Egitimi_SSO/05_Gemi_Guvenlik_Zabiti_Egitimi_SSO_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/05_Gemi_Guvenlik_Zabiti_Egitimi_SSO/05_Gemi_Guvenlik_Zabiti_Egitimi_SSO_makale.docx new file mode 100644 index 0000000..e50338b Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/05_Gemi_Guvenlik_Zabiti_Egitimi_SSO/05_Gemi_Guvenlik_Zabiti_Egitimi_SSO_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/06_Sirket_Guvenlik_Sorumlusu_Egitimi_CSO/06_Sirket_Guvenlik_Sorumlusu_Egitimi_CSO_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/06_Sirket_Guvenlik_Sorumlusu_Egitimi_CSO/06_Sirket_Guvenlik_Sorumlusu_Egitimi_CSO_makale.docx new file mode 100644 index 0000000..ba34ccc Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/06_Sirket_Guvenlik_Sorumlusu_Egitimi_CSO/06_Sirket_Guvenlik_Sorumlusu_Egitimi_CSO_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/07_Birlestirilmis_SSO_CSO_Egitimi/07_Birlestirilmis_SSO_CSO_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/07_Birlestirilmis_SSO_CSO_Egitimi/07_Birlestirilmis_SSO_CSO_Egitimi_makale.docx new file mode 100644 index 0000000..15a0aed Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/07_Birlestirilmis_SSO_CSO_Egitimi/07_Birlestirilmis_SSO_CSO_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/08_Radar_Gozlem_ve_Plotlama_Egitimi/08_Radar_Gozlem_ve_Plotlama_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/08_Radar_Gozlem_ve_Plotlama_Egitimi/08_Radar_Gozlem_ve_Plotlama_Egitimi_makale.docx new file mode 100644 index 0000000..9cd4cfe Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/08_Radar_Gozlem_ve_Plotlama_Egitimi/08_Radar_Gozlem_ve_Plotlama_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/09_Sinirli_Kaptan_Vardiya_Zabitligine_Gecis/09_Sinirli_Kaptan_Vardiya_Zabitligine_Gecis_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/09_Sinirli_Kaptan_Vardiya_Zabitligine_Gecis/09_Sinirli_Kaptan_Vardiya_Zabitligine_Gecis_makale.docx new file mode 100644 index 0000000..f247676 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/09_Sinirli_Kaptan_Vardiya_Zabitligine_Gecis/09_Sinirli_Kaptan_Vardiya_Zabitligine_Gecis_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/10_GMDSS_GOC_Egitimi/10_GMDSS_GOC_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/10_GMDSS_GOC_Egitimi/10_GMDSS_GOC_Egitimi_makale.docx new file mode 100644 index 0000000..119f35c Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/10_GMDSS_GOC_Egitimi/10_GMDSS_GOC_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/11_GMDSS_ROC_Egitimi/11_GMDSS_ROC_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/11_GMDSS_ROC_Egitimi/11_GMDSS_ROC_Egitimi_makale.docx new file mode 100644 index 0000000..a3c4656 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/11_GMDSS_ROC_Egitimi/11_GMDSS_ROC_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/12_Vardiya_Zabiti_Yetistirme_Egitimi/12_Vardiya_Zabiti_Yetistirme_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/12_Vardiya_Zabiti_Yetistirme_Egitimi/12_Vardiya_Zabiti_Yetistirme_Egitimi_makale.docx new file mode 100644 index 0000000..e0c5c03 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/12_Vardiya_Zabiti_Yetistirme_Egitimi/12_Vardiya_Zabiti_Yetistirme_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/13_ARPA_Radar_Egitimi/13_ARPA_Radar_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/13_ARPA_Radar_Egitimi/13_ARPA_Radar_Egitimi_makale.docx new file mode 100644 index 0000000..3bd099c Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/13_ARPA_Radar_Egitimi/13_ARPA_Radar_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/14_Sinirli_Vardiya_Zabiti_Egitimi/14_Sinirli_Vardiya_Zabiti_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/14_Sinirli_Vardiya_Zabiti_Egitimi/14_Sinirli_Vardiya_Zabiti_Egitimi_makale.docx new file mode 100644 index 0000000..7309d1c Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/14_Sinirli_Vardiya_Zabiti_Egitimi/14_Sinirli_Vardiya_Zabiti_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/15_Guverte_Yonetim_Duzeyi_Astsubay_Egitimi/15_Guverte_Yonetim_Duzeyi_Astsubay_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/15_Guverte_Yonetim_Duzeyi_Astsubay_Egitimi/15_Guverte_Yonetim_Duzeyi_Astsubay_Egitimi_makale.docx new file mode 100644 index 0000000..3d2f911 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/15_Guverte_Yonetim_Duzeyi_Astsubay_Egitimi/15_Guverte_Yonetim_Duzeyi_Astsubay_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/16_ECDIS_Egitimi/16_ECDIS_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/16_ECDIS_Egitimi/16_ECDIS_Egitimi_makale.docx new file mode 100644 index 0000000..e97f86b Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/16_ECDIS_Egitimi/16_ECDIS_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/17_Kopruustu_Kaynak_Yonetimi_BRM/17_Kopruustu_Kaynak_Yonetimi_BRM_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/17_Kopruustu_Kaynak_Yonetimi_BRM/17_Kopruustu_Kaynak_Yonetimi_BRM_makale.docx new file mode 100644 index 0000000..703432b Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/17_Kopruustu_Kaynak_Yonetimi_BRM/17_Kopruustu_Kaynak_Yonetimi_BRM_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/18_Balikci_Gemisi_Tayfasi_Deniz_Guvenlik_Egitimi/18_Balikci_Gemisi_Tayfasi_Deniz_Guvenlik_Egitimi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/18_Balikci_Gemisi_Tayfasi_Deniz_Guvenlik_Egitimi/18_Balikci_Gemisi_Tayfasi_Deniz_Guvenlik_Egitimi_makale.docx new file mode 100644 index 0000000..43e1c74 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Guverte_Egitimleri/18_Balikci_Gemisi_Tayfasi_Deniz_Guvenlik_Egitimi/18_Balikci_Gemisi_Tayfasi_Deniz_Guvenlik_Egitimi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/01_Yagci_Birlesik/01_Yagci_Birlesik_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/01_Yagci_Birlesik/01_Yagci_Birlesik_makale.docx new file mode 100644 index 0000000..44196a6 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/01_Yagci_Birlesik/01_Yagci_Birlesik_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/02_Yagci_Temel/02_Yagci_Temel_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/02_Yagci_Temel/02_Yagci_Temel_makale.docx new file mode 100644 index 0000000..c575185 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/02_Yagci_Temel/02_Yagci_Temel_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/03_Usta_Makine_Tayfasi/03_Usta_Makine_Tayfasi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/03_Usta_Makine_Tayfasi/03_Usta_Makine_Tayfasi_makale.docx new file mode 100644 index 0000000..165f974 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/03_Usta_Makine_Tayfasi/03_Usta_Makine_Tayfasi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/04_Ikinci_Makinist_7_02/04_Ikinci_Makinist_7_02_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/04_Ikinci_Makinist_7_02/04_Ikinci_Makinist_7_02_makale.docx new file mode 100644 index 0000000..2cda59a Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/04_Ikinci_Makinist_7_02/04_Ikinci_Makinist_7_02_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/05_Makine_Dairesi_Kaynaklari_Yonetimi_ERM/05_Makine_Dairesi_Kaynaklari_Yonetimi_ERM_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/05_Makine_Dairesi_Kaynaklari_Yonetimi_ERM/05_Makine_Dairesi_Kaynaklari_Yonetimi_ERM_makale.docx new file mode 100644 index 0000000..c6e4fa4 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/05_Makine_Dairesi_Kaynaklari_Yonetimi_ERM/05_Makine_Dairesi_Kaynaklari_Yonetimi_ERM_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/06_Yuksek_Voltaj_HV/06_Yuksek_Voltaj_HV_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/06_Yuksek_Voltaj_HV/06_Yuksek_Voltaj_HV_makale.docx new file mode 100644 index 0000000..bcbf2b7 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/06_Yuksek_Voltaj_HV/06_Yuksek_Voltaj_HV_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/07_Sinirli_Basmakinist_Tamamlama/07_Sinirli_Basmakinist_Tamamlama_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/07_Sinirli_Basmakinist_Tamamlama/07_Sinirli_Basmakinist_Tamamlama_makale.docx new file mode 100644 index 0000000..90e8c9f Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/07_Sinirli_Basmakinist_Tamamlama/07_Sinirli_Basmakinist_Tamamlama_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/08_Makine_Zabiti_Yetistirme/08_Makine_Zabiti_Yetistirme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/08_Makine_Zabiti_Yetistirme/08_Makine_Zabiti_Yetistirme_makale.docx new file mode 100644 index 0000000..9a9727c Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/08_Makine_Zabiti_Yetistirme/08_Makine_Zabiti_Yetistirme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/09_Sinirli_Makine_Zabiti_Temel/09_Sinirli_Makine_Zabiti_Temel_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/09_Sinirli_Makine_Zabiti_Temel/09_Sinirli_Makine_Zabiti_Temel_makale.docx new file mode 100644 index 0000000..1514ca6 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/09_Sinirli_Makine_Zabiti_Temel/09_Sinirli_Makine_Zabiti_Temel_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/10_Makine_Yonetim_Subay_Tamamlama/10_Makine_Yonetim_Subay_Tamamlama_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/10_Makine_Yonetim_Subay_Tamamlama/10_Makine_Yonetim_Subay_Tamamlama_makale.docx new file mode 100644 index 0000000..ba51b0d Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/10_Makine_Yonetim_Subay_Tamamlama/10_Makine_Yonetim_Subay_Tamamlama_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/11_Makine_Yonetim_Astsubay_Tamamlama/11_Makine_Yonetim_Astsubay_Tamamlama_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/11_Makine_Yonetim_Astsubay_Tamamlama/11_Makine_Yonetim_Astsubay_Tamamlama_makale.docx new file mode 100644 index 0000000..13a5a8e Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Makine_Egitimleri/11_Makine_Yonetim_Astsubay_Tamamlama/11_Makine_Yonetim_Astsubay_Tamamlama_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/01_Temel_Deniz_Emniyet_5_STCW/01_Temel_Deniz_Emniyet_5_STCW_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/01_Temel_Deniz_Emniyet_5_STCW/01_Temel_Deniz_Emniyet_5_STCW_makale.docx new file mode 100644 index 0000000..a552fad Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/01_Temel_Deniz_Emniyet_5_STCW/01_Temel_Deniz_Emniyet_5_STCW_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/02_Ileri_Yanginla_Mucadele/02_Ileri_Yanginla_Mucadele_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/02_Ileri_Yanginla_Mucadele/02_Ileri_Yanginla_Mucadele_makale.docx new file mode 100644 index 0000000..542d2af Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/02_Ileri_Yanginla_Mucadele/02_Ileri_Yanginla_Mucadele_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/03_Birlestirilmis_Tibbi_Ilkyardim_ve_Tibbi_Egitim/03_Birlestirilmis_Tibbi_Ilkyardim_ve_Tibbi_Egitim_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/03_Birlestirilmis_Tibbi_Ilkyardim_ve_Tibbi_Egitim/03_Birlestirilmis_Tibbi_Ilkyardim_ve_Tibbi_Egitim_makale.docx new file mode 100644 index 0000000..c56f346 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/03_Birlestirilmis_Tibbi_Ilkyardim_ve_Tibbi_Egitim/03_Birlestirilmis_Tibbi_Ilkyardim_ve_Tibbi_Egitim_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/04_Gemi_Guvenlik_Gorevleri/04_Gemi_Guvenlik_Gorevleri_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/04_Gemi_Guvenlik_Gorevleri/04_Gemi_Guvenlik_Gorevleri_makale.docx new file mode 100644 index 0000000..6017565 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/04_Gemi_Guvenlik_Gorevleri/04_Gemi_Guvenlik_Gorevleri_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/05_Petrol_Kimyasal_Tanker_Temel/05_Petrol_Kimyasal_Tanker_Temel_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/05_Petrol_Kimyasal_Tanker_Temel/05_Petrol_Kimyasal_Tanker_Temel_makale.docx new file mode 100644 index 0000000..6a8ebb9 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/05_Petrol_Kimyasal_Tanker_Temel/05_Petrol_Kimyasal_Tanker_Temel_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/06_Petrol_Tankeri_Ileri/06_Petrol_Tankeri_Ileri_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/06_Petrol_Tankeri_Ileri/06_Petrol_Tankeri_Ileri_makale.docx new file mode 100644 index 0000000..82bfbae Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/06_Petrol_Tankeri_Ileri/06_Petrol_Tankeri_Ileri_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/07_Yolcu_Gemisinde_Calisma/07_Yolcu_Gemisinde_Calisma_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/07_Yolcu_Gemisinde_Calisma/07_Yolcu_Gemisinde_Calisma_makale.docx new file mode 100644 index 0000000..4bdd8bb Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/07_Yolcu_Gemisinde_Calisma/07_Yolcu_Gemisinde_Calisma_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/08_Kimyasal_Tanker_Ileri/08_Kimyasal_Tanker_Ileri_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/08_Kimyasal_Tanker_Ileri/08_Kimyasal_Tanker_Ileri_makale.docx new file mode 100644 index 0000000..4968618 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/08_Kimyasal_Tanker_Ileri/08_Kimyasal_Tanker_Ileri_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/09_Gemi_Ascisi/09_Gemi_Ascisi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/09_Gemi_Ascisi/09_Gemi_Ascisi_makale.docx new file mode 100644 index 0000000..1c1bb5f Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/09_Gemi_Ascisi/09_Gemi_Ascisi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/10_Tibbi_Ilk_Yardim/10_Tibbi_Ilk_Yardim_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/10_Tibbi_Ilk_Yardim/10_Tibbi_Ilk_Yardim_makale.docx new file mode 100644 index 0000000..a373bd2 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/10_Tibbi_Ilk_Yardim/10_Tibbi_Ilk_Yardim_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/11_Tibbi_Bakim/11_Tibbi_Bakim_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/11_Tibbi_Bakim/11_Tibbi_Bakim_makale.docx new file mode 100644 index 0000000..574d4c5 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/STCW_Egitimleri/11_Tibbi_Bakim/11_Tibbi_Bakim_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/01_Liderlik_Ekip_Calismasi/01_Liderlik_Ekip_Calismasi_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/01_Liderlik_Ekip_Calismasi/01_Liderlik_Ekip_Calismasi_makale.docx new file mode 100644 index 0000000..1813327 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/01_Liderlik_Ekip_Calismasi/01_Liderlik_Ekip_Calismasi_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/02_Deniz_Cevre_Bilinci/02_Deniz_Cevre_Bilinci_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/02_Deniz_Cevre_Bilinci/02_Deniz_Cevre_Bilinci_makale.docx new file mode 100644 index 0000000..f5c10c4 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/02_Deniz_Cevre_Bilinci/02_Deniz_Cevre_Bilinci_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/03_Balast_Suyu_Yonetim/03_Balast_Suyu_Yonetim_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/03_Balast_Suyu_Yonetim/03_Balast_Suyu_Yonetim_makale.docx new file mode 100644 index 0000000..8f9b9df Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/03_Balast_Suyu_Yonetim/03_Balast_Suyu_Yonetim_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/04_Shiphandling/04_Shiphandling_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/04_Shiphandling/04_Shiphandling_makale.docx new file mode 100644 index 0000000..19e5e4b Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/04_Shiphandling/04_Shiphandling_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/05_Ship_Board_Safety_Officer/05_Ship_Board_Safety_Officer_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/05_Ship_Board_Safety_Officer/05_Ship_Board_Safety_Officer_makale.docx new file mode 100644 index 0000000..72b65cf Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/05_Ship_Board_Safety_Officer/05_Ship_Board_Safety_Officer_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/06_Ship_Security_Officer_Seminer/06_Ship_Security_Officer_Seminer_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/06_Ship_Security_Officer_Seminer/06_Ship_Security_Officer_Seminer_makale.docx new file mode 100644 index 0000000..ae92107 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/06_Ship_Security_Officer_Seminer/06_Ship_Security_Officer_Seminer_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/07_Kati_Dokme_Yukler/07_Kati_Dokme_Yukler_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/07_Kati_Dokme_Yukler/07_Kati_Dokme_Yukler_makale.docx new file mode 100644 index 0000000..c889466 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/07_Kati_Dokme_Yukler/07_Kati_Dokme_Yukler_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/08_Hazmet_Tehlikeli_Yukler/08_Hazmet_Tehlikeli_Yukler_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/08_Hazmet_Tehlikeli_Yukler/08_Hazmet_Tehlikeli_Yukler_makale.docx new file mode 100644 index 0000000..1ef1993 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/08_Hazmet_Tehlikeli_Yukler/08_Hazmet_Tehlikeli_Yukler_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/09_Cargohandling/09_Cargohandling_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/09_Cargohandling/09_Cargohandling_makale.docx new file mode 100644 index 0000000..cc5ea3b Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Seminer_Sertifikalari/09_Cargohandling/09_Cargohandling_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/01_Yat_Kaptani_149GT_Birlesik/01_Yat_Kaptani_149GT_Birlesik_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/01_Yat_Kaptani_149GT_Birlesik/01_Yat_Kaptani_149GT_Birlesik_makale.docx new file mode 100644 index 0000000..0179288 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/01_Yat_Kaptani_149GT_Birlesik/01_Yat_Kaptani_149GT_Birlesik_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/02_Yat_Kaptani_149GT_Temel/02_Yat_Kaptani_149GT_Temel_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/02_Yat_Kaptani_149GT_Temel/02_Yat_Kaptani_149GT_Temel_makale.docx new file mode 100644 index 0000000..0b57fc9 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/02_Yat_Kaptani_149GT_Temel/02_Yat_Kaptani_149GT_Temel_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/03_Yat_Kaptani_499GT_Temel/03_Yat_Kaptani_499GT_Temel_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/03_Yat_Kaptani_499GT_Temel/03_Yat_Kaptani_499GT_Temel_makale.docx new file mode 100644 index 0000000..1790618 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/03_Yat_Kaptani_499GT_Temel/03_Yat_Kaptani_499GT_Temel_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/04_Sinirsiz_Yat_Kaptanligi_Gecis/04_Sinirsiz_Yat_Kaptanligi_Gecis_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/04_Sinirsiz_Yat_Kaptanligi_Gecis/04_Sinirsiz_Yat_Kaptanligi_Gecis_makale.docx new file mode 100644 index 0000000..d517242 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yat_Kaptanligi_Egitimleri/04_Sinirsiz_Yat_Kaptanligi_Gecis/04_Sinirsiz_Yat_Kaptanligi_Gecis_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/01_Denizde_Guvenlik_Yenileme/01_Denizde_Guvenlik_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/01_Denizde_Guvenlik_Yenileme/01_Denizde_Guvenlik_Yenileme_makale.docx new file mode 100644 index 0000000..d3fd34a Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/01_Denizde_Guvenlik_Yenileme/01_Denizde_Guvenlik_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/02_GMDSS_GOC_Yenileme/02_GMDSS_GOC_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/02_GMDSS_GOC_Yenileme/02_GMDSS_GOC_Yenileme_makale.docx new file mode 100644 index 0000000..07601da Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/02_GMDSS_GOC_Yenileme/02_GMDSS_GOC_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/03_GMDSS_ROC_Yenileme/03_GMDSS_ROC_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/03_GMDSS_ROC_Yenileme/03_GMDSS_ROC_Yenileme_makale.docx new file mode 100644 index 0000000..125e74e Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/03_GMDSS_ROC_Yenileme/03_GMDSS_ROC_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/04_Guverte_Isletim_Duzeyi_Yenileme/04_Guverte_Isletim_Duzeyi_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/04_Guverte_Isletim_Duzeyi_Yenileme/04_Guverte_Isletim_Duzeyi_Yenileme_makale.docx new file mode 100644 index 0000000..c336507 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/04_Guverte_Isletim_Duzeyi_Yenileme/04_Guverte_Isletim_Duzeyi_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/05_Guverte_Yonetim_Duzeyi_Yenileme/05_Guverte_Yonetim_Duzeyi_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/05_Guverte_Yonetim_Duzeyi_Yenileme/05_Guverte_Yonetim_Duzeyi_Yenileme_makale.docx new file mode 100644 index 0000000..9a22098 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/05_Guverte_Yonetim_Duzeyi_Yenileme/05_Guverte_Yonetim_Duzeyi_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/06_Guverte_Sinirli_Isletim_Yenileme/06_Guverte_Sinirli_Isletim_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/06_Guverte_Sinirli_Isletim_Yenileme/06_Guverte_Sinirli_Isletim_Yenileme_makale.docx new file mode 100644 index 0000000..97a3374 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/06_Guverte_Sinirli_Isletim_Yenileme/06_Guverte_Sinirli_Isletim_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/07_Makine_Isletim_Duzeyi_Yenileme/07_Makine_Isletim_Duzeyi_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/07_Makine_Isletim_Duzeyi_Yenileme/07_Makine_Isletim_Duzeyi_Yenileme_makale.docx new file mode 100644 index 0000000..209bc6d Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/07_Makine_Isletim_Duzeyi_Yenileme/07_Makine_Isletim_Duzeyi_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/08_Makine_Yonetim_Duzeyi_Yenileme/08_Makine_Yonetim_Duzeyi_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/08_Makine_Yonetim_Duzeyi_Yenileme/08_Makine_Yonetim_Duzeyi_Yenileme_makale.docx new file mode 100644 index 0000000..ae769c9 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/08_Makine_Yonetim_Duzeyi_Yenileme/08_Makine_Yonetim_Duzeyi_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/09_Yat_Kaptani_149GT_Yenileme/09_Yat_Kaptani_149GT_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/09_Yat_Kaptani_149GT_Yenileme/09_Yat_Kaptani_149GT_Yenileme_makale.docx new file mode 100644 index 0000000..d487fce Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/09_Yat_Kaptani_149GT_Yenileme/09_Yat_Kaptani_149GT_Yenileme_makale.docx differ diff --git a/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/10_Yat_Kaptani_499GT_Yenileme/10_Yat_Kaptani_499GT_Yenileme_makale.docx b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/10_Yat_Kaptani_499GT_Yenileme/10_Yat_Kaptani_499GT_Yenileme_makale.docx new file mode 100644 index 0000000..e259927 Binary files /dev/null and b/public/Bogazici_Denizcilik_Egitim_Makaleleri/Yenileme_Egitimleri/10_Yat_Kaptani_499GT_Yenileme/10_Yat_Kaptani_499GT_Yenileme_makale.docx differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +<?php + +use Illuminate\Foundation\Application; +use Illuminate\Http\Request; + +define('LARAVEL_START', microtime(true)); + +// Determine if the application is in maintenance mode... +if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) { + require $maintenance; +} + +// Register the Composer autoloader... +require __DIR__.'/../vendor/autoload.php'; + +// Bootstrap Laravel and handle the request... +/** @var Application $app */ +$app = require_once __DIR__.'/../bootstrap/app.php'; + +$app->handleRequest(Request::capture()); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/uploads/announcements/.gitkeep b/public/uploads/announcements/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/categories/.gitkeep b/public/uploads/categories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/courses/.gitkeep b/public/uploads/courses/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/hero-slides/.gitkeep b/public/uploads/hero-slides/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/images/.gitkeep b/public/uploads/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/pages/.gitkeep b/public/uploads/pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/settings/.gitkeep b/public/uploads/settings/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/videos/.gitkeep b/public/uploads/videos/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..3e6abea --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,11 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/views/vendor/l5-swagger/.gitkeep b/resources/views/vendor/l5-swagger/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/vendor/l5-swagger/index.blade.php b/resources/views/vendor/l5-swagger/index.blade.php new file mode 100644 index 0000000..4f57040 --- /dev/null +++ b/resources/views/vendor/l5-swagger/index.blade.php @@ -0,0 +1,174 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>{{ $documentationTitle }} + + + + + @if(config('l5-swagger.defaults.ui.display.dark_mode')) + + @endif + + + +
+ + + + + + diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php new file mode 100644 index 0000000..b7355d7 --- /dev/null +++ b/resources/views/welcome.blade.php @@ -0,0 +1,277 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

+ + +
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- Light Mode 12 SVG --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{-- Dark Mode 12 SVG --}} + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..c5cbbba --- /dev/null +++ b/routes/api.php @@ -0,0 +1,180 @@ +group(function () { + // Categories + Route::get('categories', [CategoryController::class, 'index']); + Route::get('categories/{slug}', [CategoryController::class, 'show']); + + // Courses + Route::get('courses', [CourseController::class, 'index']); + Route::get('courses/{slug}', [CourseController::class, 'show']); + + // Schedules + Route::get('schedules', [ScheduleController::class, 'index']); + Route::get('schedules/upcoming', [ScheduleController::class, 'upcoming']); + Route::get('schedules/{id}', [ScheduleController::class, 'show']); + + // Announcements + Route::get('announcements', [AnnouncementController::class, 'index']); + Route::get('announcements/{slug}', [AnnouncementController::class, 'show']); + + // Hero Slides + Route::get('hero-slides', [HeroSlideController::class, 'index']); + + // Leads (public form submission) + Route::post('leads', [LeadController::class, 'store'])->middleware('throttle:leads'); + + // Comments (public submission) + Route::get('comments/{type}/{id}', [CommentController::class, 'index']); + Route::post('comments', [CommentController::class, 'store'])->middleware('throttle:comments'); + + // Menus + Route::get('menus/{location}', [MenuController::class, 'index']); + + // FAQs + Route::get('faqs', [FaqController::class, 'index']); + Route::get('faqs/{category}', [FaqController::class, 'index']); + + // Guide Cards + Route::get('guide-cards', [GuideCardController::class, 'index']); + + // Settings + Route::get('settings', [SettingController::class, 'index']); + Route::get('settings/{group}', [SettingController::class, 'show']); + + // Stories + Route::get('stories', [StoryController::class, 'index']); + + // Pages + Route::get('pages/{slug}', [PageController::class, 'show']); + + // Preview (public — Next.js server-side fetch) + Route::get('preview/{token}', [PreviewController::class, 'show']); + + // Sitemap + Route::get('sitemap-data', [SitemapController::class, 'index']); +}); + +/* +|-------------------------------------------------------------------------- +| Admin API Routes (Sanctum Auth) +|-------------------------------------------------------------------------- +*/ +Route::prefix('admin')->group(function () { + // Auth (public) + Route::post('login', [AuthController::class, 'login']); + + // Protected routes + Route::middleware('auth:sanctum')->group(function () { + // Auth + Route::get('me', [AuthController::class, 'me']); + Route::post('logout', [AuthController::class, 'logout']); + + // Upload + Route::post('uploads', [UploadController::class, 'store']); + + // Categories + Route::apiResource('categories', AdminCategoryController::class); + + // Courses + Route::apiResource('courses', AdminCourseController::class); + + // Course Blocks + Route::post('courses/{course}/blocks/reorder', [AdminCourseBlockController::class, 'reorder']); + Route::apiResource('courses.blocks', AdminCourseBlockController::class); + + // Schedules + Route::apiResource('schedules', AdminScheduleController::class); + + // Announcements + Route::apiResource('announcements', AdminAnnouncementController::class); + + // Hero Slides + Route::apiResource('hero-slides', AdminHeroSlideController::class); + + // Leads + Route::apiResource('leads', AdminLeadController::class)->except(['store']); + + // Menus + Route::post('menus/reorder', [AdminMenuController::class, 'reorder']); + Route::apiResource('menus', AdminMenuController::class); + + // Comments + Route::apiResource('comments', AdminCommentController::class)->except(['store']); + + // FAQs + Route::apiResource('faqs', AdminFaqController::class); + + // Guide Cards + Route::apiResource('guide-cards', AdminGuideCardController::class); + + // Settings + Route::get('settings', [AdminSettingController::class, 'index']); + Route::get('settings/group/{group}', [AdminSettingController::class, 'group']); + Route::put('settings', [AdminSettingController::class, 'update']); + Route::post('settings/clear-cache', [AdminSettingController::class, 'clearCache']); + + // Pages + Route::apiResource('pages', AdminPageController::class); + + // Page Blocks + Route::post('pages/{page}/blocks/reorder', [AdminBlockController::class, 'reorder']); + Route::apiResource('pages.blocks', AdminBlockController::class); + + // Stories + Route::apiResource('stories', AdminStoryController::class); + + // Preview + Route::post('preview', [AdminPreviewController::class, 'store']); + Route::delete('preview/{token}', [AdminPreviewController::class, 'destroy']); + + // Users + Route::apiResource('users', UserController::class); + + // Roles & Permissions + Route::get('permissions', [RoleController::class, 'permissions']); + Route::apiResource('roles', RoleController::class); + }); +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..c6be90d --- /dev/null +++ b/routes/web.php @@ -0,0 +1,14 @@ +json([ + 'success' => true, + 'message' => 'Boğaziçi Denizcilik API çalışıyor.', + 'data' => [ + 'version' => 'v1', + 'documentation' => url('/api/documentation'), + ], + ]); +}); diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh new file mode 100755 index 0000000..a559eb1 --- /dev/null +++ b/scripts/deploy-prod.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -euo pipefail + +PROJECT_DIR="/opt/projects/bogazici/corporate-api/prod/api" +ENV_SOURCE="/opt/projects/bogazici/corporate-api/prod/.env.prod" +BRANCH="main" +CONTAINER="bdc-api-prod" +COMPOSE_FILE="docker-compose.prod.yml" +UPLOADS_DIR="/opt/projects/bogazici/corporate-api/prod/uploads" + +echo "🚀 [API] Production deploy başlatılıyor..." + +cd "$PROJECT_DIR" + +sudo chown -R "$USER":www-data storage bootstrap/cache 2>/dev/null || true +sudo find storage -type d -exec chmod 2775 {} \; 2>/dev/null || true +sudo find storage -type f -exec chmod 664 {} \; 2>/dev/null || true +sudo find bootstrap/cache -type d -exec chmod 2775 {} \; 2>/dev/null || true +sudo find bootstrap/cache -type f -exec chmod 664 {} \; 2>/dev/null || true + +git fetch origin +git checkout "$BRANCH" +git reset --hard "origin/$BRANCH" +git clean -fd -e .env + +if [ ! -f "$ENV_SOURCE" ]; then + echo "❌ HATA: env dosyası yok -> $ENV_SOURCE" + exit 1 +fi + +cp "$ENV_SOURCE" .env + +if ! grep -q '^APP_KEY=base64:' .env; then + echo "❌ HATA: APP_KEY eksik veya boş -> $ENV_SOURCE" + exit 1 +fi + +sudo mkdir -p "$UPLOADS_DIR"/{images,videos,hero-slides,settings,pages,courses,announcements,categories} +sudo chown -R "$USER":www-data "$UPLOADS_DIR" 2>/dev/null || true +sudo find "$UPLOADS_DIR" -type d -exec chmod 2775 {} \; 2>/dev/null || true +sudo find "$UPLOADS_DIR" -type f -exec chmod 664 {} \; 2>/dev/null || true + +docker stop "$CONTAINER" 2>/dev/null || true +docker rm "$CONTAINER" 2>/dev/null || true +docker compose -f "$COMPOSE_FILE" down --remove-orphans 2>/dev/null || true +docker compose -f "$COMPOSE_FILE" up -d --build + +sleep 3 +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan migrate --force || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan optimize:clear || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan config:cache || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan route:cache || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan view:cache || true + +docker image prune -f 2>/dev/null || true + +echo "✅ [API] Production deploy tamamlandı" diff --git a/scripts/deploy-test.sh b/scripts/deploy-test.sh new file mode 100755 index 0000000..7d6a3aa --- /dev/null +++ b/scripts/deploy-test.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -euo pipefail + +PROJECT_DIR="/opt/projects/bogazici/corporate-api/test/api" +ENV_SOURCE="/opt/projects/bogazici/corporate-api/test/.env.test" +BRANCH="test" +CONTAINER="bdc-api-test" +COMPOSE_FILE="docker-compose.test.yml" +UPLOADS_DIR="/opt/projects/bogazici/corporate-api/test/uploads" + +echo "🚀 [API] Test deploy başlatılıyor..." + +cd "$PROJECT_DIR" + +sudo chown -R "$USER":www-data storage bootstrap/cache 2>/dev/null || true +sudo find storage -type d -exec chmod 2775 {} \; 2>/dev/null || true +sudo find storage -type f -exec chmod 664 {} \; 2>/dev/null || true +sudo find bootstrap/cache -type d -exec chmod 2775 {} \; 2>/dev/null || true +sudo find bootstrap/cache -type f -exec chmod 664 {} \; 2>/dev/null || true + +git fetch origin +git checkout "$BRANCH" +git reset --hard "origin/$BRANCH" +git clean -fd -e .env + +if [ ! -f "$ENV_SOURCE" ]; then + echo "❌ HATA: env dosyası yok -> $ENV_SOURCE" + exit 1 +fi + +cp "$ENV_SOURCE" .env + +if ! grep -q '^APP_KEY=base64:' .env; then + echo "❌ HATA: APP_KEY eksik veya boş -> $ENV_SOURCE" + exit 1 +fi + +sudo mkdir -p "$UPLOADS_DIR"/{images,videos,hero-slides,settings,pages,courses,announcements,categories} +sudo chown -R "$USER":www-data "$UPLOADS_DIR" 2>/dev/null || true +sudo find "$UPLOADS_DIR" -type d -exec chmod 2775 {} \; 2>/dev/null || true +sudo find "$UPLOADS_DIR" -type f -exec chmod 664 {} \; 2>/dev/null || true + +docker stop "$CONTAINER" 2>/dev/null || true +docker rm "$CONTAINER" 2>/dev/null || true +docker compose -f "$COMPOSE_FILE" down --remove-orphans 2>/dev/null || true +docker compose -f "$COMPOSE_FILE" up -d --build + +sleep 3 +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" composer install --no-interaction --prefer-dist --optimize-autoloader || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan migrate --force || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan optimize:clear || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan config:cache || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan route:cache || true +docker compose -f "$COMPOSE_FILE" exec -T "$CONTAINER" php artisan view:cache || true + +docker image prune -f 2>/dev/null || true + +echo "✅ [API] Test deploy tamamlandı" diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json new file mode 100644 index 0000000..92445a4 --- /dev/null +++ b/storage/api-docs/api-docs.json @@ -0,0 +1,5034 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Boğaziçi Denizcilik API", + "description": "Boğaziçi Denizcilik eğitim platformu backend API dokümantasyonu. Public (v1) ve Admin endpointlerini içerir.", + "contact": { + "name": "Boğaziçi Denizcilik", + "email": "admin@bogazicidenizcilik.com.tr" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://bogazici-api.test", + "description": "API Server" + } + ], + "paths": { + "/api/admin/announcements": { + "get": { + "tags": [ + "Admin - Announcements" + ], + "summary": "Duyuruları listele (Admin)", + "operationId": "f3617068759bd67fa6783413d808c902", + "parameters": [ + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "featured", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Duyuru listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Announcements" + ], + "summary": "Yeni duyuru oluştur", + "operationId": "85a7409f3448b7110dffac8f3eb40924", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "slug", + "category", + "content" + ], + "properties": { + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "category": { + "type": "string" + }, + "content": { + "type": "string" + }, + "excerpt": { + "type": "string" + }, + "image": { + "type": "string" + }, + "is_featured": { + "type": "boolean" + }, + "is_active": { + "type": "boolean" + }, + "published_at": { + "type": "string", + "format": "date-time" + }, + "meta_title": { + "type": "string" + }, + "meta_description": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Duyuru oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/announcements/{announcement}": { + "get": { + "tags": [ + "Admin - Announcements" + ], + "summary": "Duyuru detayı (Admin)", + "operationId": "4126902637ec62ab270e6ed7a72d06fc", + "parameters": [ + { + "name": "announcement", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Duyuru detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Announcements" + ], + "summary": "Duyuru güncelle", + "operationId": "d1da9f72269f1f615a2a90a20abe0cb0", + "parameters": [ + { + "name": "announcement", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "category": { + "type": "string" + }, + "content": { + "type": "string" + }, + "excerpt": { + "type": "string" + }, + "image": { + "type": "string" + }, + "is_featured": { + "type": "boolean" + }, + "is_active": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Duyuru güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Announcements" + ], + "summary": "Duyuru sil", + "operationId": "a29ec5c7d7b631f9488df7dc49c44ed3", + "parameters": [ + { + "name": "announcement", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Duyuru silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Admin girişi", + "description": "E-posta ve şifre ile giriş yaparak Sanctum token alır.", + "operationId": "c28cad2ba36fd99ebc986e26c83542ce", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "admin@bogazicidenizcilik.com.tr" + }, + "password": { + "type": "string", + "format": "password", + "example": "password" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Başarılı giriş", + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": { + "token": { + "type": "string" + }, + "user": { + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Geçersiz kimlik bilgileri" + }, + "422": { + "description": "Validasyon hatası" + } + } + } + }, + "/api/admin/me": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Mevcut kullanıcı bilgileri", + "description": "Oturum açmış kullanıcının bilgilerini, rollerini ve izinlerini döndürür.", + "operationId": "452e4a67283093daf1203e5a7671bbc3", + "responses": { + "200": { + "description": "Kullanıcı bilgileri" + }, + "401": { + "description": "Yetkisiz erişim" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/logout": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Çıkış yap", + "description": "Mevcut token'ı iptal eder.", + "operationId": "38d6a10488d12f5b9e6a50710eb07908", + "responses": { + "200": { + "description": "Başarıyla çıkış yapıldı" + }, + "401": { + "description": "Yetkisiz erişim" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/pages/{page}/blocks": { + "get": { + "tags": [ + "Admin - Page Blocks" + ], + "summary": "Sayfa bloklarını listele", + "operationId": "87d7b552e8588279378e8927d932eef7", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Blok listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Page Blocks" + ], + "summary": "Yeni blok oluştur", + "operationId": "bc3d4e6cb14bb865b7d00f8b294ba128", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string" + }, + "content": { + "type": "object" + }, + "order_index": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Blok oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/pages/{page}/blocks/{block}": { + "get": { + "tags": [ + "Admin - Page Blocks" + ], + "summary": "Blok detayı", + "operationId": "e35a33d8e7c0999b957b081781d2bf44", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "block", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Blok detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Page Blocks" + ], + "summary": "Blok güncelle", + "operationId": "729948978fc96a90a84dc594d2d50d62", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "block", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "type": { + "type": "string" + }, + "content": { + "type": "object" + }, + "order_index": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Blok güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Page Blocks" + ], + "summary": "Blok sil", + "operationId": "5c5f35cb571ba89b8510845fdd5b3e8a", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "block", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Blok silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/pages/{page}/blocks/reorder": { + "post": { + "tags": [ + "Admin - Page Blocks" + ], + "summary": "Blok sıralamasını güncelle", + "operationId": "c7a50e47ad18557f92332f771dc72770", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer" + }, + "order_index": { + "type": "integer" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Sıralama güncellendi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/categories": { + "get": { + "tags": [ + "Admin - Categories" + ], + "summary": "Kategorileri listele (Admin)", + "operationId": "71fce704423c89eab31adce4a23df70b", + "parameters": [ + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Kategori listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Categories" + ], + "summary": "Yeni kategori oluştur", + "operationId": "810e8b9fee1619d4229208cb942c486a", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": "string" + }, + "image": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + }, + "meta_title": { + "type": "string" + }, + "meta_description": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Kategori oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/categories/{category}": { + "get": { + "tags": [ + "Admin - Categories" + ], + "summary": "Kategori detayı (Admin)", + "operationId": "11a8aaa127a831f1b2178b277adfce70", + "parameters": [ + { + "name": "category", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Kategori detayı" + }, + "404": { + "description": "Bulunamadı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Categories" + ], + "summary": "Kategori güncelle", + "operationId": "ebbab81f1604d13a6519ce9ac4bec3ff", + "parameters": [ + { + "name": "category", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": "string" + }, + "image": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + }, + "meta_title": { + "type": "string" + }, + "meta_description": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Kategori güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Categories" + ], + "summary": "Kategori sil", + "operationId": "1c50d201211dea34943f752a3cbb8e87", + "parameters": [ + { + "name": "category", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Kategori silindi" + }, + "404": { + "description": "Bulunamadı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/comments": { + "get": { + "tags": [ + "Admin - Comments" + ], + "summary": "Yorumları listele (Admin)", + "operationId": "3138d6fb10e4082144e13e4978483c8b", + "parameters": [ + { + "name": "is_approved", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "commentable_type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Yorum listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/comments/{comment}": { + "get": { + "tags": [ + "Admin - Comments" + ], + "summary": "Yorum detayı", + "operationId": "ee77a7e44028293130b414507c92dc2a", + "parameters": [ + { + "name": "comment", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Yorum detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Comments" + ], + "summary": "Yorum güncelle (onayla/reddet)", + "operationId": "a63d783f762e0a28ff7acd242a1ac285", + "parameters": [ + { + "name": "comment", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "is_approved": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Yorum güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Comments" + ], + "summary": "Yorum sil", + "operationId": "50c1bd338ad18caccb118f3c63007957", + "parameters": [ + { + "name": "comment", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Yorum silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/courses/{course}/blocks": { + "get": { + "tags": [ + "Admin - Course Blocks" + ], + "summary": "Eğitim bloklarını listele", + "operationId": "6d3a342af704898e684b3501943ee624", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Blok listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Course Blocks" + ], + "summary": "Yeni eğitim bloğu oluştur", + "operationId": "8a900a262d0f470d312948af1f8c8040", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string" + }, + "content": { + "type": "object" + }, + "order_index": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Blok oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/courses/{course}/blocks/{block}": { + "get": { + "tags": [ + "Admin - Course Blocks" + ], + "summary": "Eğitim blok detayı", + "operationId": "d3c004034d8cbc8800d54e3e45f99ac4", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "block", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Blok detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Course Blocks" + ], + "summary": "Eğitim bloğu güncelle", + "operationId": "c1c619935e771839be6cdd65548a8a92", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "block", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "type": { + "type": "string" + }, + "content": { + "type": "object" + }, + "order_index": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Blok güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Course Blocks" + ], + "summary": "Eğitim bloğu sil", + "operationId": "034d36af32cde487ae3339366523c6e3", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "block", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Blok silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/courses/{course}/blocks/reorder": { + "post": { + "tags": [ + "Admin - Course Blocks" + ], + "summary": "Eğitim blok sıralamasını güncelle", + "operationId": "1ba704c8c6b9525abb14dce15ab1d303", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer" + }, + "order_index": { + "type": "integer" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Sıralama güncellendi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/courses": { + "get": { + "tags": [ + "Admin - Courses" + ], + "summary": "Eğitimleri listele (Admin)", + "operationId": "03c545540d1fbeac47fda1522a6dfbd8", + "parameters": [ + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Eğitim listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Courses" + ], + "summary": "Yeni eğitim oluştur", + "operationId": "165267692fcffb498f7dfac3da765f42", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "category_id", + "title", + "slug", + "desc", + "long_desc", + "duration" + ], + "properties": { + "category_id": { + "description": "Kategori ID", + "type": "integer" + }, + "slug": { + "description": "URL slug (unique)", + "type": "string" + }, + "title": { + "description": "Eğitim başlığı", + "type": "string" + }, + "sub": { + "description": "Alt başlık. Örn: STCW II/1", + "type": "string", + "nullable": true + }, + "desc": { + "description": "Kısa açıklama", + "type": "string" + }, + "long_desc": { + "description": "Detaylı açıklama", + "type": "string" + }, + "duration": { + "description": "Süre. Örn: 5 Gün", + "type": "string" + }, + "students": { + "description": "Öğrenci sayısı", + "type": "integer" + }, + "rating": { + "description": "Puan (0-5)", + "type": "number", + "format": "float" + }, + "badge": { + "description": "Rozet. Örn: Simülatör", + "type": "string", + "nullable": true + }, + "image": { + "description": "Görsel path", + "type": "string", + "nullable": true + }, + "price": { + "description": "Fiyat. Örn: 5.000 TL", + "type": "string", + "nullable": true + }, + "includes": { + "description": "Fiyata dahil olanlar", + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "requirements": { + "description": "Katılım koşulları", + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "meta_title": { + "description": "SEO Title", + "type": "string", + "nullable": true + }, + "meta_description": { + "description": "SEO Description", + "type": "string", + "nullable": true + }, + "scope": { + "description": "Eğitim kapsamı konu başlıkları", + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "standard": { + "description": "Uyum standardı. Örn: STCW / IMO Uyumlu", + "type": "string", + "nullable": true + }, + "language": { + "description": "Eğitim dili. Varsayılan: Türkçe", + "type": "string", + "nullable": true + }, + "location": { + "description": "Varsayılan lokasyon. Örn: Kadıköy, İstanbul", + "type": "string", + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Eğitim oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/courses/{course}": { + "get": { + "tags": [ + "Admin - Courses" + ], + "summary": "Eğitim detayı (Admin)", + "operationId": "8b2fa41090fe1e98c5be4e876fc943e7", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Eğitim detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Courses" + ], + "summary": "Eğitim güncelle", + "operationId": "8eb9e6e8b13f04cbd656cd66c1e735ca", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "category_id", + "title", + "slug", + "desc", + "long_desc", + "duration" + ], + "properties": { + "category_id": { + "description": "Kategori ID", + "type": "integer" + }, + "slug": { + "description": "URL slug (unique)", + "type": "string" + }, + "title": { + "description": "Eğitim başlığı", + "type": "string" + }, + "sub": { + "description": "Alt başlık", + "type": "string", + "nullable": true + }, + "desc": { + "description": "Kısa açıklama", + "type": "string" + }, + "long_desc": { + "description": "Detaylı açıklama", + "type": "string" + }, + "duration": { + "description": "Süre. Örn: 5 Gün", + "type": "string" + }, + "students": { + "description": "Öğrenci sayısı", + "type": "integer" + }, + "rating": { + "description": "Puan (0-5)", + "type": "number", + "format": "float" + }, + "badge": { + "description": "Rozet", + "type": "string", + "nullable": true + }, + "image": { + "description": "Görsel path", + "type": "string", + "nullable": true + }, + "price": { + "description": "Fiyat", + "type": "string", + "nullable": true + }, + "includes": { + "description": "Fiyata dahil olanlar", + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "requirements": { + "description": "Katılım koşulları", + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "meta_title": { + "description": "SEO Title", + "type": "string", + "nullable": true + }, + "meta_description": { + "description": "SEO Description", + "type": "string", + "nullable": true + }, + "scope": { + "description": "Eğitim kapsamı konu başlıkları", + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "standard": { + "description": "Uyum standardı", + "type": "string", + "nullable": true + }, + "language": { + "description": "Eğitim dili", + "type": "string", + "nullable": true + }, + "location": { + "description": "Varsayılan lokasyon", + "type": "string", + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Eğitim güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Courses" + ], + "summary": "Eğitim sil", + "operationId": "b3917ad34b239f50321b1c6ae08369ea", + "parameters": [ + { + "name": "course", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Eğitim silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/faqs": { + "get": { + "tags": [ + "Admin - FAQs" + ], + "summary": "SSS listele (Admin)", + "operationId": "cfcbab32cd67a2325651757c22ae2249", + "parameters": [ + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "SSS listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - FAQs" + ], + "summary": "Yeni SSS oluştur", + "operationId": "4dde384948a53bb96a395331cd2dd860", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "question", + "answer", + "category" + ], + "properties": { + "question": { + "type": "string" + }, + "answer": { + "type": "string" + }, + "category": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "SSS oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/faqs/{faq}": { + "get": { + "tags": [ + "Admin - FAQs" + ], + "summary": "SSS detayı", + "operationId": "bd10c420fa3b37cff311a1a56792da2f", + "parameters": [ + { + "name": "faq", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "SSS detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - FAQs" + ], + "summary": "SSS güncelle", + "operationId": "c4aaf3285962d8632f1270dd73b0376b", + "parameters": [ + { + "name": "faq", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "question": { + "type": "string" + }, + "answer": { + "type": "string" + }, + "category": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "SSS güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - FAQs" + ], + "summary": "SSS sil", + "operationId": "badcf7d381999eeed6e23da702440e01", + "parameters": [ + { + "name": "faq", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "SSS silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/guide-cards": { + "get": { + "tags": [ + "Admin - Guide Cards" + ], + "summary": "Rehber kartları listele (Admin)", + "operationId": "51d9063154fe9126d50b76cc7eeb3752", + "parameters": [ + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Kart listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Guide Cards" + ], + "summary": "Yeni rehber kartı oluştur", + "operationId": "2d85417066d0a2e349522754d48329b4", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "description", + "icon" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "url": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Kart oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/guide-cards/{guideCard}": { + "get": { + "tags": [ + "Admin - Guide Cards" + ], + "summary": "Rehber kart detayı", + "operationId": "642a326579cc87be64e374a693a33e09", + "parameters": [ + { + "name": "guideCard", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Kart detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Guide Cards" + ], + "summary": "Rehber kart güncelle", + "operationId": "b266ff3b70d4f14f8e8149ee919faba9", + "parameters": [ + { + "name": "guideCard", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "url": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Kart güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Guide Cards" + ], + "summary": "Rehber kart sil", + "operationId": "4d1c6ada41a9c957af8e265938d20dea", + "parameters": [ + { + "name": "guideCard", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Kart silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/hero-slides": { + "get": { + "tags": [ + "Admin - Hero Slides" + ], + "summary": "Hero slide listele (Admin)", + "operationId": "a3e0d5d84108d5fd855b0a8df4e85da6", + "parameters": [ + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Slide listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Hero Slides" + ], + "summary": "Yeni hero slide oluştur", + "operationId": "64cbfa2112a5a9517f12703119ab7ef7", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "image" + ], + "properties": { + "title": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "image": { + "type": "string" + }, + "mobile_image": { + "type": "string" + }, + "button_text": { + "type": "string" + }, + "button_url": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Slide oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/hero-slides/{heroSlide}": { + "get": { + "tags": [ + "Admin - Hero Slides" + ], + "summary": "Hero slide detayı", + "operationId": "b5b214351cfc14fd3a36ba729ace670c", + "parameters": [ + { + "name": "heroSlide", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Slide detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Hero Slides" + ], + "summary": "Hero slide güncelle", + "operationId": "21d85e8f11e916a240db9a1b60345d53", + "parameters": [ + { + "name": "heroSlide", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "image": { + "type": "string" + }, + "button_text": { + "type": "string" + }, + "button_url": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Slide güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Hero Slides" + ], + "summary": "Hero slide sil", + "operationId": "45c99667452ac71a547dd6480e5820cf", + "parameters": [ + { + "name": "heroSlide", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Slide silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/leads": { + "get": { + "tags": [ + "Admin - Leads" + ], + "summary": "Başvuruları listele (Admin)", + "operationId": "827e2e9631a50f75cd643e3645316baa", + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "source", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "is_read", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Başvuru listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/leads/{lead}": { + "get": { + "tags": [ + "Admin - Leads" + ], + "summary": "Başvuru detayı", + "operationId": "597e6322ffcc1e2206e5af28e04a15a2", + "parameters": [ + { + "name": "lead", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Başvuru detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Leads" + ], + "summary": "Başvuru güncelle", + "operationId": "51715f9de41445aa43c258202d7fb6f9", + "parameters": [ + { + "name": "lead", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "type": "string" + }, + "is_read": { + "type": "boolean" + }, + "admin_notes": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Başvuru güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Leads" + ], + "summary": "Başvuru sil", + "operationId": "da72ee6606e0c76de6b0934663c03b7f", + "parameters": [ + { + "name": "lead", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Başvuru silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/menus": { + "get": { + "tags": [ + "Admin - Menus" + ], + "summary": "Menü öğelerini listele (Admin)", + "operationId": "ac6c1d11b28a3955647836d63fd140e3", + "parameters": [ + { + "name": "location", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "Menü listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Menus" + ], + "summary": "Yeni menü öğesi oluştur", + "operationId": "f181e73829920464aabe98bb936e0c78", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "url", + "location" + ], + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + }, + "location": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "target": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Menü oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/menus/{menu}": { + "get": { + "tags": [ + "Admin - Menus" + ], + "summary": "Menü detayı", + "operationId": "229c92e3fdead223600091d8eabcdfb7", + "parameters": [ + { + "name": "menu", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Menü detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Menus" + ], + "summary": "Menü güncelle", + "operationId": "50dd0234dbe115bfc11b018c5f8e3251", + "parameters": [ + { + "name": "menu", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + }, + "location": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Menü güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Menus" + ], + "summary": "Menü sil", + "operationId": "fcac0f8f4dd79fd8c5d0b9663721af69", + "parameters": [ + { + "name": "menu", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Menü silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/menus/reorder": { + "post": { + "tags": [ + "Admin - Menus" + ], + "summary": "Menü sıralamasını güncelle", + "operationId": "fdaadcc88ca8129597fa65cf506c96c3", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer" + }, + "order_index": { + "type": "integer" + }, + "parent_id": { + "type": "integer", + "nullable": true + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Sıralama güncellendi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/pages": { + "get": { + "tags": [ + "Admin - Pages" + ], + "summary": "Sayfaları listele (Admin)", + "operationId": "6cbf05aab452d1f00614d2ba44d0ce8d", + "parameters": [ + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Sayfa listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Pages" + ], + "summary": "Yeni sayfa oluştur", + "operationId": "df071dd3329cc4093f01363e0efc9d8f", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "slug" + ], + "properties": { + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "content": { + "type": "string" + }, + "template": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "meta_title": { + "type": "string" + }, + "meta_description": { + "type": "string" + }, + "blocks": { + "type": "array", + "items": { + "properties": { + "type": { + "type": "string" + }, + "content": { + "type": "object" + }, + "order_index": { + "type": "integer" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Sayfa oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/pages/{page}": { + "get": { + "tags": [ + "Admin - Pages" + ], + "summary": "Sayfa detayı (Admin)", + "operationId": "62879d1ca7493adadc725e80809529ad", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Sayfa detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Pages" + ], + "summary": "Sayfa güncelle", + "operationId": "de6b56fb2cbe5f27878237a572f75afe", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "content": { + "type": "string" + }, + "template": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "meta_title": { + "type": "string" + }, + "meta_description": { + "type": "string" + }, + "blocks": { + "type": "array", + "items": { + "properties": { + "type": { + "type": "string" + }, + "content": { + "type": "object" + }, + "order_index": { + "type": "integer" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Sayfa güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Pages" + ], + "summary": "Sayfa sil", + "operationId": "a4bfb73ac56742e57f69c3ea34c3f996", + "parameters": [ + { + "name": "page", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Sayfa silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/preview": { + "post": { + "tags": [ + "Admin - Preview" + ], + "summary": "Önizleme oluştur", + "operationId": "352e61ce8571f2f8b9a66d09cfef54e9", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "page_id", + "blocks" + ], + "properties": { + "page_id": { + "type": "integer" + }, + "blocks": { + "type": "array", + "items": { + "properties": { + "type": { + "type": "string" + }, + "content": { + "type": "object" + }, + "order_index": { + "type": "integer" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Önizleme oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/preview/{token}": { + "delete": { + "tags": [ + "Admin - Preview" + ], + "summary": "Önizlemeyi sil", + "operationId": "f91275d50a610caf6b38b3fc4fa17d35", + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Önizleme silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/roles": { + "get": { + "tags": [ + "Admin - Roles" + ], + "summary": "Rolleri listele", + "operationId": "2af0a74e89a916dd65550cb1ff927c18", + "responses": { + "200": { + "description": "Rol listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Roles" + ], + "summary": "Yeni rol oluştur", + "operationId": "811df9a4fd01b110d9ae33803b064cd3", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "name", + "permissions" + ], + "properties": { + "name": { + "type": "string", + "example": "moderator" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "view-category", + "view-course" + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Rol oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/roles/{role}": { + "get": { + "tags": [ + "Admin - Roles" + ], + "summary": "Rol detayı", + "operationId": "98421a56365abe98dcc00ef4976daf31", + "parameters": [ + { + "name": "role", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Rol detayı" + }, + "404": { + "description": "Bulunamadı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Roles" + ], + "summary": "Rol güncelle", + "operationId": "22546d4b6474363c99fd89c71da7d97e", + "parameters": [ + { + "name": "role", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "moderator" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Rol güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Roles" + ], + "summary": "Rol sil", + "operationId": "4bac991cae74b1814ee2c4c1e2178027", + "parameters": [ + { + "name": "role", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Rol silindi" + }, + "403": { + "description": "Varsayılan roller silinemez" + }, + "404": { + "description": "Bulunamadı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/permissions": { + "get": { + "tags": [ + "Admin - Roles" + ], + "summary": "Tüm yetkileri listele", + "description": "Rol oluştururken/düzenlerken kullanılacak tüm mevcut yetkileri modül bazlı gruplandırarak döner.", + "operationId": "905006cd4f639a04a7ca71a360d4c0f7", + "responses": { + "200": { + "description": "Yetki listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/schedules": { + "get": { + "tags": [ + "Admin - Schedules" + ], + "summary": "Takvimleri listele (Admin)", + "operationId": "591c4d96ba4ec32146e1c754a4f1252b", + "parameters": [ + { + "name": "course_id", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Takvim listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Schedules" + ], + "summary": "Yeni takvim oluştur", + "operationId": "62a6df7810cffbe6c03bd5ddc144b3f4", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "course_id", + "start_date", + "location", + "quota" + ], + "properties": { + "course_id": { + "type": "integer" + }, + "start_date": { + "type": "string", + "format": "date" + }, + "end_date": { + "type": "string", + "format": "date" + }, + "location": { + "type": "string" + }, + "instructor": { + "type": "string" + }, + "quota": { + "type": "integer" + }, + "enrolled_count": { + "type": "integer" + }, + "price_override": { + "type": "number" + }, + "status": { + "type": "string" + }, + "notes": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Takvim oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/schedules/{schedule}": { + "get": { + "tags": [ + "Admin - Schedules" + ], + "summary": "Takvim detayı (Admin)", + "operationId": "fdc2f99f80d245a81512f696e7761d7c", + "parameters": [ + { + "name": "schedule", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Takvim detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Schedules" + ], + "summary": "Takvim güncelle", + "operationId": "4b90fc065b0499df369ff76a3185a18d", + "parameters": [ + { + "name": "schedule", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "course_id": { + "type": "integer" + }, + "start_date": { + "type": "string", + "format": "date" + }, + "end_date": { + "type": "string", + "format": "date" + }, + "location": { + "type": "string" + }, + "instructor": { + "type": "string" + }, + "quota": { + "type": "integer" + }, + "status": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Takvim güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Schedules" + ], + "summary": "Takvim sil", + "operationId": "2dc9aa3b201dab8a2ca77e77e6850a55", + "parameters": [ + { + "name": "schedule", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Takvim silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/settings": { + "get": { + "tags": [ + "Admin - Settings" + ], + "summary": "Tüm ayarları listele (Admin)", + "operationId": "524af03e15d7bc3114502504f91c58b7", + "responses": { + "200": { + "description": "Ayar listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Settings" + ], + "summary": "Ayarları toplu güncelle (dot notation: general.site_name)", + "operationId": "0c3c6eb6e7c69fedcf19f55431d16373", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "settings" + ], + "properties": { + "settings": { + "type": "object", + "example": "{\"general.site_name\": \"Yeni Ad\", \"contact.phone_primary\": \"+90 ...\"}" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Ayarlar güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/settings/group/{group}": { + "get": { + "tags": [ + "Admin - Settings" + ], + "summary": "Gruba göre ayarları getir", + "operationId": "2e6dba09714d593f90a2158962ec1d6b", + "parameters": [ + { + "name": "group", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Grup ayarları" + }, + "404": { + "description": "Grup bulunamadı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/settings/clear-cache": { + "post": { + "tags": [ + "Admin - Settings" + ], + "summary": "Ayar cache temizle", + "operationId": "b25d78ed8e45e6f6e1b571da66d24d94", + "responses": { + "200": { + "description": "Cache temizlendi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/stories": { + "get": { + "tags": [ + "Admin - Stories" + ], + "summary": "Tüm hikayeleri listele (Admin)", + "operationId": "963feaad34eefefc11f693e40eb3a503", + "responses": { + "200": { + "description": "Hikaye listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Stories" + ], + "summary": "Yeni hikaye oluştur", + "operationId": "ee03fccfd0c46ec5a37ad30f0c6a0344", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "content" + ], + "properties": { + "title": { + "type": "string" + }, + "badge": { + "type": "string", + "nullable": true + }, + "content": { + "type": "string" + }, + "image": { + "type": "string", + "nullable": true + }, + "cta_text": { + "type": "string", + "nullable": true + }, + "cta_url": { + "type": "string", + "nullable": true + }, + "order_index": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Hikaye oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/stories/{story}": { + "get": { + "tags": [ + "Admin - Stories" + ], + "summary": "Hikaye detayı", + "operationId": "52c22f0b12abc32d30baf1e4417d7734", + "parameters": [ + { + "name": "story", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Hikaye detayı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Stories" + ], + "summary": "Hikaye güncelle", + "operationId": "1b271c0cccf472dfeb3f5b506326c876", + "parameters": [ + { + "name": "story", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "type": "string" + }, + "badge": { + "type": "string", + "nullable": true + }, + "content": { + "type": "string" + }, + "image": { + "type": "string", + "nullable": true + }, + "cta_text": { + "type": "string", + "nullable": true + }, + "cta_url": { + "type": "string", + "nullable": true + }, + "order_index": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Hikaye güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Stories" + ], + "summary": "Hikaye sil", + "operationId": "cfec8b5ddc812c696206ddd2b5965634", + "parameters": [ + { + "name": "story", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Hikaye silindi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/upload": { + "post": { + "tags": [ + "Upload" + ], + "summary": "Dosya yükle", + "description": "Görsel dosyası yükler (max 5MB). Desteklenen formatlar: jpg, png, gif, svg, webp.", + "operationId": "0bf4da1d564a9ea318a38e3d53742ce0", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "file" + ], + "properties": { + "file": { + "description": "Görsel dosyası", + "type": "string", + "format": "binary" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Dosya yüklendi", + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": { + "path": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/users": { + "get": { + "tags": [ + "Admin - Users" + ], + "summary": "Admin kullanıcılarını listele", + "operationId": "9ded17315e13756cd593b5840cd39a26", + "parameters": [ + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "role", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Kullanıcı listesi" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "post": { + "tags": [ + "Admin - Users" + ], + "summary": "Yeni admin kullanıcı oluştur", + "operationId": "f6f90d5fbbe24f66eb2b43901e0d3f38", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "name", + "email", + "password", + "password_confirmation", + "role" + ], + "properties": { + "name": { + "type": "string", + "example": "Editör Kullanıcı" + }, + "email": { + "type": "string", + "format": "email", + "example": "editor@bogazici.com" + }, + "password": { + "type": "string", + "format": "password", + "example": "password123" + }, + "password_confirmation": { + "type": "string", + "format": "password", + "example": "password123" + }, + "role": { + "type": "string", + "example": "editor" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Kullanıcı oluşturuldu" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/admin/users/{user}": { + "get": { + "tags": [ + "Admin - Users" + ], + "summary": "Kullanıcı detayı", + "operationId": "1755e6ac1393c92a407ec78308857c0e", + "parameters": [ + { + "name": "user", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Kullanıcı detayı" + }, + "404": { + "description": "Bulunamadı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "put": { + "tags": [ + "Admin - Users" + ], + "summary": "Kullanıcı güncelle", + "operationId": "ad842a2d4b0e2fdf3756da07e54b10b0", + "parameters": [ + { + "name": "user", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string", + "format": "password" + }, + "password_confirmation": { + "type": "string", + "format": "password" + }, + "role": { + "type": "string", + "example": "editor" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Kullanıcı güncellendi" + }, + "422": { + "description": "Validasyon hatası" + } + }, + "security": [ + { + "sanctum": [] + } + ] + }, + "delete": { + "tags": [ + "Admin - Users" + ], + "summary": "Kullanıcı sil (soft delete)", + "operationId": "30b96bca56895875c78781639320f633", + "parameters": [ + { + "name": "user", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Kullanıcı silindi" + }, + "403": { + "description": "Kendini silemezsin" + }, + "404": { + "description": "Bulunamadı" + } + }, + "security": [ + { + "sanctum": [] + } + ] + } + }, + "/api/v1/announcements": { + "get": { + "tags": [ + "Announcements" + ], + "summary": "Duyuruları listele", + "operationId": "b02afaa4b0757c9fe2cb79bfdc60749e", + "parameters": [ + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "featured", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Duyuru listesi" + } + } + } + }, + "/api/v1/announcements/{slug}": { + "get": { + "tags": [ + "Announcements" + ], + "summary": "Duyuru detayı", + "operationId": "49b63b73657841b8f3d036d0869ba58c", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Duyuru detayı" + }, + "404": { + "description": "Duyuru bulunamadı" + } + } + } + }, + "/api/v1/categories": { + "get": { + "tags": [ + "Categories" + ], + "summary": "Kategorileri listele", + "description": "Tüm aktif kategorileri sayfalanmış olarak döndürür.", + "operationId": "e225c2b7eb5daf7fb16e00f4f07ff030", + "parameters": [ + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Kategori listesi" + } + } + } + }, + "/api/v1/categories/{slug}": { + "get": { + "tags": [ + "Categories" + ], + "summary": "Kategori detayı", + "description": "Slug ile kategori detayını döndürür.", + "operationId": "6b5e99ab9669011f1260b2a8fb93392e", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Kategori detayı" + }, + "404": { + "description": "Kategori bulunamadı" + } + } + } + }, + "/api/v1/comments/{type}/{id}": { + "get": { + "tags": [ + "Comments" + ], + "summary": "Onaylı yorumları getir", + "operationId": "1380d7b194b8901c7885e2ec68047603", + "parameters": [ + { + "name": "type", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "course", + "category", + "announcement" + ] + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Onaylı yorum listesi" + }, + "404": { + "description": "Geçersiz yorum tipi" + } + } + } + }, + "/api/v1/comments": { + "post": { + "tags": [ + "Comments" + ], + "summary": "Yorum gönder", + "operationId": "51f741718cabbf7ddf0e49b69f645dbe", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "body", + "author_name", + "author_email", + "commentable_type", + "commentable_id" + ], + "properties": { + "body": { + "type": "string" + }, + "author_name": { + "type": "string" + }, + "author_email": { + "type": "string", + "format": "email" + }, + "commentable_type": { + "type": "string", + "enum": [ + "course", + "category", + "announcement" + ] + }, + "commentable_id": { + "type": "integer" + }, + "rating": { + "type": "integer", + "maximum": 5, + "minimum": 1 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Yorum gönderildi" + }, + "422": { + "description": "Validasyon hatası" + } + } + } + }, + "/api/v1/courses": { + "get": { + "tags": [ + "Courses" + ], + "summary": "Eğitimleri listele", + "description": "Tüm eğitimleri sayfalanmış olarak döndürür. Kategori, arama ve sıralama filtresi destekler.", + "operationId": "3522f88b734e2061541a8dbd0b6be53c", + "parameters": [ + { + "name": "category", + "in": "query", + "description": "Kategori slug filtresi", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "Örn: -created_at, title", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Eğitim listesi" + } + } + } + }, + "/api/v1/courses/{slug}": { + "get": { + "tags": [ + "Courses" + ], + "summary": "Eğitim detayı", + "description": "Slug ile eğitim detayını döndürür.", + "operationId": "0c45ecab08c0edb745cbb12ced0b8e90", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Eğitim detayı" + }, + "404": { + "description": "Eğitim bulunamadı" + } + } + } + }, + "/api/v1/faqs/{category?}": { + "get": { + "tags": [ + "FAQs" + ], + "summary": "SSS listesi", + "description": "Tüm veya kategoriye göre filtrelenmiş SSS listesi döndürür. ?limit=6 ile anasayfa için sınırlanabilir.", + "operationId": "05fc6955e10e515e3c0f8a9f8692a5aa", + "parameters": [ + { + "name": "category", + "in": "path", + "description": "FAQ kategori filtresi", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Dönen SSS sayısını sınırla (anasayfa için)", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "SSS listesi" + }, + "404": { + "description": "Kategori bulunamadı" + } + } + } + }, + "/api/v1/guide-cards": { + "get": { + "tags": [ + "Guide Cards" + ], + "summary": "Rehber kartlarını listele", + "operationId": "9697e98cfcef9c2f7c09bf2fb461ffc6", + "responses": { + "200": { + "description": "Rehber kart listesi" + } + } + } + }, + "/api/v1/hero-slides": { + "get": { + "tags": [ + "Hero Slides" + ], + "summary": "Aktif hero slide listesi", + "operationId": "8f881c6b634a6034a79601e3798fc08a", + "responses": { + "200": { + "description": "Aktif hero slide listesi" + } + } + } + }, + "/api/v1/leads": { + "post": { + "tags": [ + "Leads" + ], + "summary": "Yeni başvuru/talep oluştur", + "operationId": "816520615fff48e26eb49ca76b38a78c", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "name", + "phone", + "source", + "kvkk_consent" + ], + "properties": { + "name": { + "description": "Ad Soyad", + "type": "string" + }, + "phone": { + "description": "Telefon", + "type": "string" + }, + "email": { + "type": "string", + "format": "email", + "nullable": true + }, + "source": { + "description": "kurs_kayit, danismanlik, duyuru, iletisim", + "type": "string" + }, + "target_course": { + "description": "Kurs slug", + "type": "string", + "nullable": true + }, + "education_level": { + "type": "string", + "nullable": true + }, + "subject": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + }, + "kvkk_consent": { + "description": "KVKK onayı (zorunlu, true olmalı)", + "type": "boolean" + }, + "marketing_consent": { + "type": "boolean", + "nullable": true + }, + "utm_source": { + "type": "string", + "nullable": true + }, + "utm_medium": { + "type": "string", + "nullable": true + }, + "utm_campaign": { + "type": "string", + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Başvuru alındı" + }, + "422": { + "description": "Validasyon hatası" + } + } + } + }, + "/api/v1/menus/{location}": { + "get": { + "tags": [ + "Menus" + ], + "summary": "Konuma göre menü öğelerini getir", + "operationId": "6559dfcc3a286a1014a87ae84ab032fc", + "parameters": [ + { + "name": "location", + "in": "path", + "description": "header, footer, mobile", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Menü listesi" + }, + "404": { + "description": "Menü konumu bulunamadı" + } + } + } + }, + "/api/v1/pages/{slug}": { + "get": { + "tags": [ + "Pages" + ], + "summary": "Sayfa detayı", + "operationId": "13c3c0449d274e4f6966bc57a6dc6087", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Sayfa detayı" + }, + "404": { + "description": "Sayfa bulunamadı" + } + } + } + }, + "/api/v1/preview/{token}": { + "get": { + "tags": [ + "Preview" + ], + "summary": "Önizleme verisini getir (public)", + "operationId": "2fe9d63e5f07638af5ef70940378cd3f", + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Önizleme verisi" + }, + "404": { + "description": "Önizleme bulunamadı veya süresi dolmuş" + } + } + } + }, + "/api/v1/schedules": { + "get": { + "tags": [ + "Schedules" + ], + "summary": "Eğitim takvimini listele", + "operationId": "b1c4b2b699913ab9a2a9755e851a4a48", + "parameters": [ + { + "name": "course_id", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Takvim listesi" + } + } + } + }, + "/api/v1/schedules/upcoming": { + "get": { + "tags": [ + "Schedules" + ], + "summary": "Yaklaşan eğitimleri listele", + "operationId": "08cb28c6491a171608bbd02de0aa7025", + "responses": { + "200": { + "description": "Yaklaşan eğitimler" + } + } + } + }, + "/api/v1/schedules/{id}": { + "get": { + "tags": [ + "Schedules" + ], + "summary": "Takvim detayı", + "operationId": "799eca0d264ba539216a4dd07cd8447b", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Takvim detayı" + }, + "404": { + "description": "Takvim bulunamadı" + } + } + } + }, + "/api/v1/settings": { + "get": { + "tags": [ + "Settings" + ], + "summary": "Tüm site ayarlarını getir (group bazlı nested)", + "description": "Return all public settings grouped by group name.", + "operationId": "0dd8f3513d25a61df64210ec5b698722", + "responses": { + "200": { + "description": "Ayarlar listesi" + } + } + } + }, + "/api/v1/settings/{group}": { + "get": { + "tags": [ + "Settings" + ], + "summary": "Tek grup ayarlarını getir", + "description": "Return public settings for a single group.", + "operationId": "f7dd44d43bc4b684e67b3905a81fd6e1", + "parameters": [ + { + "name": "group", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Grup ayarları" + }, + "404": { + "description": "Grup bulunamadı" + } + } + } + }, + "/api/v1/sitemap-data": { + "get": { + "tags": [ + "Sitemap" + ], + "summary": "SEO sitemap verisi", + "description": "Yayındaki tüm kurs, duyuru ve sayfa slug+updated_at bilgilerini döner.", + "operationId": "0bf0e832809b20c97cede40aa1be8e76", + "responses": { + "200": { + "description": "Sitemap verisi" + } + } + } + }, + "/api/v1/stories": { + "get": { + "tags": [ + "Stories" + ], + "summary": "Aktif hikayeleri listele", + "operationId": "c3b775e95dc619bc709034aa863d2a47", + "responses": { + "200": { + "description": "Hikaye listesi" + } + } + } + } + }, + "components": { + "securitySchemes": { + "sanctum": { + "type": "apiKey", + "description": "Enter token in format (Bearer )", + "name": "Authorization", + "in": "header" + } + } + }, + "tags": [ + { + "name": "Auth", + "description": "Kimlik doğrulama işlemleri" + }, + { + "name": "Upload", + "description": "Dosya yükleme" + }, + { + "name": "Categories", + "description": "Kategori işlemleri" + }, + { + "name": "Courses", + "description": "Eğitim işlemleri" + }, + { + "name": "Schedules", + "description": "Eğitim takvimi" + }, + { + "name": "Announcements", + "description": "Duyuru ve haberler" + }, + { + "name": "Hero Slides", + "description": "Ana sayfa slider" + }, + { + "name": "Leads", + "description": "İletişim talepleri" + }, + { + "name": "Menus", + "description": "Menü yönetimi" + }, + { + "name": "Comments", + "description": "Yorum yönetimi" + }, + { + "name": "FAQs", + "description": "Sıkça sorulan sorular" + }, + { + "name": "Guide Cards", + "description": "Rehber kartları" + }, + { + "name": "Settings", + "description": "Site ayarları" + }, + { + "name": "Pages", + "description": "Sayfa yönetimi" + }, + { + "name": "Admin - Categories", + "description": "Admin: Kategori CRUD" + }, + { + "name": "Admin - Courses", + "description": "Admin: Eğitim CRUD" + }, + { + "name": "Admin - Schedules", + "description": "Admin: Takvim CRUD" + }, + { + "name": "Admin - Announcements", + "description": "Admin: Duyuru CRUD" + }, + { + "name": "Admin - Hero Slides", + "description": "Admin: Hero Slide CRUD" + }, + { + "name": "Admin - Leads", + "description": "Admin: Başvuru yönetimi" + }, + { + "name": "Admin - Menus", + "description": "Admin: Menü CRUD" + }, + { + "name": "Admin - Comments", + "description": "Admin: Yorum yönetimi" + }, + { + "name": "Admin - FAQs", + "description": "Admin: SSS CRUD" + }, + { + "name": "Admin - Guide Cards", + "description": "Admin: Rehber kartları CRUD" + }, + { + "name": "Admin - Settings", + "description": "Admin: Ayar yönetimi" + }, + { + "name": "Admin - Pages", + "description": "Admin: Sayfa CRUD" + }, + { + "name": "Admin - Page Blocks", + "description": "Admin - Page Blocks" + }, + { + "name": "Admin - Course Blocks", + "description": "Admin - Course Blocks" + }, + { + "name": "Admin - Preview", + "description": "Admin - Preview" + }, + { + "name": "Admin - Roles", + "description": "Admin - Roles" + }, + { + "name": "Admin - Stories", + "description": "Admin - Stories" + }, + { + "name": "Admin - Users", + "description": "Admin - Users" + }, + { + "name": "Preview", + "description": "Preview" + }, + { + "name": "Sitemap", + "description": "Sitemap" + }, + { + "name": "Stories", + "description": "Stories" + } + ] +} \ No newline at end of file diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8fdc86b --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..3834758 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,49 @@ +extend(TestCase::class) + // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + ->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +toBeTrue(); +}); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..f35b4e7 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +});