9 Commits

Author SHA1 Message Date
2034814939 Revert "Update project README"
All checks were successful
Tests / Laravel tests (pull_request) Successful in 11m57s
This reverts commit 6c5d0e4a9b.
2026-06-03 23:00:05 +02:00
6c5d0e4a9b Update project README
All checks were successful
Tests / Laravel tests (pull_request) Successful in 11m52s
2026-06-03 22:52:32 +02:00
886e3993fc Enforce PHP and Blade formatting
All checks were successful
Tests / Laravel tests (pull_request) Successful in 12m39s
2026-06-03 22:22:39 +02:00
aab8d33b8d Merge pull request 'Add Larastan level 7 analysis' (#3) from bugfixes/add-larastan-level-7 into main
All checks were successful
Tests / Laravel tests (push) Successful in 2m20s
Reviewed-on: #3
2026-06-03 22:18:09 +02:00
eb9c8796de Add Larastan level 7 analysis
All checks were successful
Tests / Laravel tests (pull_request) Successful in 2m36s
2026-06-03 22:09:33 +02:00
53c4823b22 Merge pull request 'Fix controller cleanup issues' (#2) from bugfixes/controller-cleanup-and-personalia-fixes into main
All checks were successful
Tests / Laravel tests (push) Successful in 4m15s
Reviewed-on: #2
2026-06-03 22:02:27 +02:00
fe47b79a25 Fix controller cleanup issues
All checks were successful
Tests / Laravel tests (pull_request) Successful in 3m24s
2026-06-03 21:57:10 +02:00
27449eabf0 Merge pull request 'feat: include testing, factories, git workflow for testing.' (#1) from feature/add-php-unit-tests into main
All checks were successful
Tests / Laravel tests (push) Successful in 2m7s
Reviewed-on: #1
2026-06-03 21:27:33 +02:00
9df6c0ab46 feat: include testing, factories, git workflow for testing.
All checks were successful
Tests / Laravel tests (pull_request) Successful in 3m31s
2026-06-03 21:23:39 +02:00
83 changed files with 2027 additions and 392 deletions

View File

@@ -0,0 +1,52 @@
name: Tests
on:
pull_request:
push:
branches:
- main
jobs:
php-tests:
name: Laravel tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: bcmath, exif, fileinfo, gd, mbstring, pdo_sqlite, sqlite3, zip
coverage: none
- name: Install Composer dependencies
uses: ramsey/composer-install@v3
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: Install NPM dependencies
run: npm ci
- name: Prepare application
run: |
cp .env.example .env
php artisan key:generate --ansi
- name: Check PHP formatting
run: composer format:check
- name: Check Blade formatting
run: npm run format:check
- name: Run test suite
run: php artisan test
- name: Run static analysis
run: composer analyse

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
@@ -14,12 +15,18 @@ class VerifyEmailController extends Controller
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
$user = $request->user();
if (! $user instanceof MustVerifyEmail) {
abort(403);
}
if ($user->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
if ($user->markEmailAsVerified()) {
event(new Verified($user));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');

View File

@@ -4,23 +4,25 @@ namespace App\Http\Controllers;
use App\Http\Requests\EducationRequest;
use App\Models\Education;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EducationController extends Controller
{
public function index()
public function index(): View
{
$educations = Education::with('media')->latest()->get();
return view('educations.index', compact('educations'));
}
public function create()
public function create(): View
{
return view('educations.create');
}
public function store(EducationRequest $request)
public function store(EducationRequest $request): RedirectResponse
{
$education = Education::create($request->validated());
@@ -29,17 +31,12 @@ class EducationController extends Controller
return redirect()->route('educations.index')->with('success', 'Opleiding toegevoegd.');
}
public function show(Education $education)
{
return view('educations.show', compact('education'));
}
public function edit(Education $education)
public function edit(Education $education): View
{
return view('educations.edit', compact('education'));
}
public function update(EducationRequest $request, Education $education)
public function update(EducationRequest $request, Education $education): RedirectResponse
{
$education->update($request->validated());
@@ -48,7 +45,7 @@ class EducationController extends Controller
return redirect()->route('educations.index')->with('success', 'Opleiding bijgewerkt.');
}
public function destroy(Education $education)
public function destroy(Education $education): RedirectResponse
{
$education->clearMediaCollection('image');
$education->delete();

View File

@@ -8,11 +8,13 @@ use App\Models\Education;
use App\Models\Personalia;
use App\Models\Skill;
use App\Models\WorkExperience;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class FrontendController extends Controller
{
public function index()
public function index(): View
{
$skills = Skill::all()->groupBy('type');
@@ -23,21 +25,20 @@ class FrontendController extends Controller
return view('welcome', compact('skills', 'personalia', 'education', 'experience'));
}
public function getPersonalia($id)
public function getPersonalia(Personalia $personalia): JsonResponse
{
$item = Personalia::findOrFail($id);
NotifyTelegramAboutPersonaliaClick::dispatch(
$item,
$personalia,
request()->ip(),
request()->userAgent()
);
return response()->json([
'value' => $item->value,
'value' => $personalia->value,
]);
}
public function message(Request $request)
public function message(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',

View File

@@ -2,25 +2,26 @@
namespace App\Http\Controllers;
use App\Models\Personalia;
use Illuminate\Http\Request;
use App\Http\Requests\PersonaliaRequest;
use App\Models\Personalia;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PersonaliaController extends Controller
{
public function index()
public function index(): View
{
$personalia = Personalia::all();
return view('personalia.index', compact('personalia'));
}
public function create()
public function create(): View
{
return view('personalia.create');
}
public function store(PersonaliaRequest $request)
public function store(PersonaliaRequest $request): RedirectResponse
{
$validated = $request->validated();
@@ -32,13 +33,13 @@ class PersonaliaController extends Controller
return redirect()->route('personalia.index')->with('success', 'Persoonlijk item toegevoegd.');
}
public function edit(Personalia $personalium)
public function edit(Personalia $personalium): View
{
return view('personalia.edit', ['personalia' => $personalium]);
}
public function update(Request $request, Personalia $personalium)
public function update(PersonaliaRequest $request, Personalia $personalium): RedirectResponse
{
$validated = $request->validated();
@@ -46,12 +47,13 @@ class PersonaliaController extends Controller
...$validated,
'hidden' => $request->boolean('hidden'),
]);
return redirect()->route('personalia.index')->with('success', 'Persoonlijk item bijgewerkt.');
}
public function destroy(Personalia $personalia)
public function destroy(Personalia $personalium): RedirectResponse
{
$personalia->delete();
$personalium->delete();
return redirect()->route('personalia.index')->with('success', 'Persoonlijk item verwijderd.');
}

View File

@@ -2,25 +2,26 @@
namespace App\Http\Controllers;
use App\Models\Skill;
use Illuminate\Http\Request;
use App\Http\Requests\SkillRequest;
use App\Models\Skill;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class SkillController extends Controller
{
public function index()
public function index(): View
{
$skills = Skill::latest()->get();
return view('skills.index', compact('skills'));
}
public function create()
public function create(): View
{
return view('skills.create');
}
public function store(SkillRequest $request)
public function store(SkillRequest $request): RedirectResponse
{
$skill = Skill::create($request->validated());
@@ -31,17 +32,12 @@ class SkillController extends Controller
return redirect()->route('skills.index')->with('success', 'Vaardigheid toegevoegd.');
}
public function show(Skill $skill)
{
return view('skills.show', compact('skill'));
}
public function edit(Skill $skill)
public function edit(Skill $skill): View
{
return view('skills.edit', compact('skill'));
}
public function update(SkillRequest $request, Skill $skill)
public function update(SkillRequest $request, Skill $skill): RedirectResponse
{
$skill->update($request->validated());
@@ -53,7 +49,7 @@ class SkillController extends Controller
return redirect()->route('skills.index')->with('success', 'Vaardigheid bijgewerkt.');
}
public function destroy(Skill $skill)
public function destroy(Skill $skill): RedirectResponse
{
$skill->clearMediaCollection('image');
$skill->delete();

View File

@@ -2,26 +2,26 @@
namespace App\Http\Controllers;
use App\Models\WorkExperience;
use Illuminate\Http\Request;
use App\Http\Requests\WorkExperienceRequest;
use App\Models\WorkExperience;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class WorkExperienceController extends Controller
{
public function index()
public function index(): View
{
$experiences = WorkExperience::with('media')->latest()->get();
return view('work_experiences.index', compact('experiences'));
}
public function create()
public function create(): View
{
return view('work_experiences.create');
}
public function store(WorkExperienceRequest $request)
public function store(WorkExperienceRequest $request): RedirectResponse
{
$experience = WorkExperience::create($request->validated());
@@ -32,17 +32,12 @@ class WorkExperienceController extends Controller
return redirect()->route('work-experiences.index')->with('success', 'Ervaring toegevoegd.');
}
public function show(WorkExperience $workExperience)
{
return view('work_experiences.show', compact('workExperience'));
}
public function edit(WorkExperience $workExperience)
public function edit(WorkExperience $workExperience): View
{
return view('work_experiences.edit', compact('workExperience'));
}
public function update(WorkExperienceRequest $request, WorkExperience $workExperience)
public function update(WorkExperienceRequest $request, WorkExperience $workExperience): RedirectResponse
{
$workExperience->update($request->validated());
@@ -54,7 +49,7 @@ class WorkExperienceController extends Controller
return redirect()->route('work-experiences.index')->with('success', 'Ervaring bijgewerkt.');
}
public function destroy(WorkExperience $workExperience)
public function destroy(WorkExperience $workExperience): RedirectResponse
{
$workExperience->clearMediaCollection('image');
$workExperience->delete();

View File

@@ -11,6 +11,9 @@ class EducationRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -23,6 +26,9 @@ class EducationRequest extends FormRequest
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [

View File

@@ -11,6 +11,9 @@ class PersonaliaRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -21,6 +24,9 @@ class PersonaliaRequest extends FormRequest
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
@@ -28,5 +34,4 @@ class PersonaliaRequest extends FormRequest
'value.required' => 'Een waarde is verplicht.',
];
}
}

View File

@@ -11,6 +11,9 @@ class SkillRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -21,6 +24,10 @@ class SkillRequest extends FormRequest
'type' => ['required', 'in:rating,tag,other'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
@@ -29,5 +36,4 @@ class SkillRequest extends FormRequest
'type.in' => 'Het type moet rating, tag of other zijn.',
];
}
}

View File

@@ -11,6 +11,9 @@ class WorkExperienceRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -23,6 +26,9 @@ class WorkExperienceRequest extends FormRequest
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [

View File

@@ -21,9 +21,9 @@ class NotifyTelegramAboutContactMessage implements ShouldQueue
protected string $userAgent;
protected string $email;
protected ?string $email;
protected string $phone;
protected ?string $phone;
public function __construct(string $name, string $message, string $ip, string $userAgent, ?string $email = null, ?string $phone = null)
{
@@ -35,7 +35,7 @@ class NotifyTelegramAboutContactMessage implements ShouldQueue
$this->phone = $phone;
}
public function handle()
public function handle(): void
{
$email = $this->email ?? '';
$phone = $this->phone ?? '';

View File

@@ -14,27 +14,30 @@ class NotifyTelegramAboutPersonaliaClick implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $personalia;
protected Personalia $personalia;
protected $ip;
protected ?string $ip;
protected $userAgent;
protected ?string $userAgent;
public function __construct(Personalia $personalia, $ip, $userAgent)
public function __construct(Personalia $personalia, ?string $ip, ?string $userAgent)
{
$this->personalia = $personalia;
$this->ip = $ip;
$this->userAgent = $userAgent;
}
public function handle()
public function handle(): void
{
$ip = $this->ip ?? '';
$userAgent = $this->userAgent ?? '';
$message = <<<TEXT
👤 *Persoonlijke gegevens bekeken*
Naam: {$this->personalia->value}
IP: {$this->ip}
User Agent: `{$this->userAgent}`
IP: {$ip}
User Agent: `{$userAgent}`
📅 Tijdstip: *{now()->format('d-m-Y H:i')}*
TEXT;

View File

@@ -2,12 +2,17 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Education extends Model implements HasMedia
{
/** @use HasFactory<\Database\Factories\EducationFactory> */
use HasFactory;
use InteractsWithMedia;
protected $table = 'education';
@@ -20,12 +25,12 @@ class Education extends Model implements HasMedia
'beschrijving',
];
public function image()
public function image(): ?Media
{
return $this->getFirstMedia('image');
}
public function imageUrl()
public function imageUrl(): ?string
{
return $this->image() ? $this->image()->getUrl() : null;
}

View File

@@ -2,10 +2,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Personalia extends Model
{
/** @use HasFactory<\Database\Factories\PersonaliaFactory> */
use HasFactory;
protected $fillable = ['key', 'value', 'hidden', 'icon'];
protected $table = 'personalia';
@@ -13,14 +17,4 @@ class Personalia extends Model
protected $casts = [
'hidden' => 'boolean',
];
public function image()
{
return $this->getFirstMedia('image');
}
public function imageUrl()
{
return $this->image() ? $this->image()->getUrl() : null;
}
}

View File

@@ -2,22 +2,27 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Skill extends Model implements HasMedia
{
/** @use HasFactory<\Database\Factories\SkillFactory> */
use HasFactory;
use InteractsWithMedia;
protected $fillable = ['title', 'description', 'rating', 'type'];
public function image()
public function image(): ?Media
{
return $this->getFirstMedia('image');
}
public function imageUrl()
public function imageUrl(): ?string
{
return $this->image() ? $this->image()->getUrl() : null;
}

View File

@@ -2,11 +2,12 @@
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
class User extends Authenticatable implements MustVerifyEmail
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;

View File

@@ -2,12 +2,17 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class WorkExperience extends Model implements HasMedia
{
/** @use HasFactory<\Database\Factories\WorkExperienceFactory> */
use HasFactory;
use InteractsWithMedia;
protected $fillable = [
@@ -18,12 +23,12 @@ class WorkExperience extends Model implements HasMedia
'beschrijving',
];
public function image()
public function image(): ?Media
{
return $this->getFirstMedia('image');
}
public function imageUrl()
public function imageUrl(): ?string
{
return $this->image() ? $this->image()->getUrl() : null;
}

View File

@@ -13,6 +13,7 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"larastan/larastan": "^3.10",
"laravel/breeze": "^2.3",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.13",
@@ -57,6 +58,15 @@
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
],
"analyse": [
"phpstan analyse --memory-limit=1G"
],
"format": [
"pint"
],
"format:check": [
"pint --test"
]
},
"extra": {

199
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0d27ba8c7d653ba6d0cbd49b2eec5d2d",
"content-hash": "2f5e7cf541f786348723b475b25a79c5",
"packages": [
{
"name": "brick/math",
@@ -6685,6 +6685,47 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
{
"name": "iamcal/sql-parser",
"version": "v0.7",
"source": {
"type": "git",
"url": "https://github.com/iamcal/SQLParser.git",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8",
"shasum": ""
},
"require-dev": {
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "^5|^6|^7|^8|^9"
},
"type": "library",
"autoload": {
"psr-4": {
"iamcal\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Cal Henderson",
"email": "cal@iamcal.com"
}
],
"description": "MySQL schema parser",
"support": {
"issues": "https://github.com/iamcal/SQLParser/issues",
"source": "https://github.com/iamcal/SQLParser/tree/v0.7"
},
"time": "2026-01-28T22:20:33+00:00"
},
{
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
@@ -6745,6 +6786,96 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "larastan/larastan",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "2970f83398154178a739609c244577267c7ee8eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/2970f83398154178a739609c244577267c7ee8eb",
"reference": "2970f83398154178a739609c244577267c7ee8eb",
"shasum": ""
},
"require": {
"ext-json": "*",
"iamcal/sql-parser": "^0.7.0",
"illuminate/console": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/container": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/database": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/http": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
"php": "^8.2",
"phpstan/phpstan": "^2.2.0"
},
"require-dev": {
"doctrine/coding-standard": "^14",
"laravel/framework": "^11.44.2 || ^12.7.2 || ^13",
"mockery/mockery": "^1.6.12",
"nikic/php-parser": "^5.4",
"orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11",
"orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11",
"phpstan/phpstan-deprecation-rules": "^2.0.1",
"phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8 || ^13.1.8"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
"phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Larastan\\Larastan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Can Vural",
"email": "can9119@gmail.com"
}
],
"description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
"keywords": [
"PHPStan",
"code analyse",
"code analysis",
"larastan",
"laravel",
"package",
"php",
"static analysis"
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.10.0"
},
"funding": [
{
"url": "https://github.com/canvural",
"type": "github"
}
],
"time": "2026-05-28T08:00:58+00:00"
},
{
"name": "laravel/breeze",
"version": "v2.3.7",
@@ -7994,6 +8125,70 @@
},
"time": "2025-02-19T13:28:12+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.2.1",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660",
"reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ondřej Mirtes"
},
{
"name": "Markus Staab"
},
{
"name": "Vincent Langlet"
}
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2026-05-28T14:44:12+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "11.0.10",
@@ -9611,5 +9806,5 @@
"php": "^8.2"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Education>
*/
class EducationFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$startDate = fake()->dateTimeBetween('-8 years', '-2 years');
return [
'opleiding' => fake()->randomElement(['HBO-ICT', 'Software Engineering', 'Applicatieontwikkeling']),
'instituut' => fake()->company(),
'startdatum' => $startDate->format('Y-m-d'),
'einddatum' => fake()->dateTimeBetween($startDate, 'now')->format('Y-m-d'),
'beschrijving' => fake()->sentence(),
];
}
public function current(): static
{
return $this->state(fn (array $attributes) => [
'einddatum' => null,
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Personalia>
*/
class PersonaliaFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'key' => fake()->randomElement(['Email', 'Telefoon', 'Website', 'Locatie']),
'value' => fake()->word(),
'hidden' => false,
'icon' => fake()->randomElement(['fa-solid fa-envelope', 'fa-solid fa-phone', 'fa-solid fa-globe']),
];
}
public function hidden(): static
{
return $this->state(fn (array $attributes) => [
'hidden' => true,
]);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Skill>
*/
class SkillFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'title' => fake()->randomElement(['Laravel', 'PHP', 'Docker', 'Tailwind CSS']),
'description' => fake()->sentence(),
'rating' => fake()->numberBetween(1, 10),
'type' => fake()->randomElement(['rating', 'tag', 'other']),
];
}
public function rating(): static
{
return $this->state(fn (array $attributes) => [
'type' => 'rating',
'rating' => fake()->numberBetween(1, 10),
]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WorkExperience>
*/
class WorkExperienceFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$startDate = fake()->dateTimeBetween('-8 years', '-1 year');
return [
'werkgever' => fake()->company(),
'functie' => fake()->jobTitle(),
'startdatum' => $startDate->format('Y-m-d'),
'einddatum' => fake()->dateTimeBetween($startDate, 'now')->format('Y-m-d'),
'beschrijving' => fake()->sentence(),
];
}
public function current(): static
{
return $this->state(fn (array $attributes) => [
'einddatum' => null,
]);
}
}

View File

@@ -9,14 +9,14 @@ return new class extends Migration
/**
* Run the migrations.
*/
public function up()
public function up(): void
{
Schema::table('skills', function (Blueprint $table) {
$table->string('type')->default('rating')->after('id');
});
}
public function down()
public function down(): void
{
Schema::table('skills', function (Blueprint $table) {
$table->dropColumn('type');

600
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.8.2",
"blade-formatter": "^1.44.4",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.31",
@@ -47,6 +48,19 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.7.tgz",
"integrity": "sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-js-pure": "^3.48.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@@ -594,6 +608,13 @@
"node": ">= 8"
}
},
"node_modules/@one-ini/wasm": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
"dev": true,
"license": "MIT"
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -605,6 +626,20 @@
"node": ">=14"
}
},
"node_modules/@prettier/plugin-php": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.24.0.tgz",
"integrity": "sha512-x9l65fCE/pgoET6RQowgdgG8Xmzs44z6j6Hhg3coINCyCw9JBGJ5ZzMR2XHAM2jmAdbJAIgqB6cUn4/3W3XLTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"linguist-languages": "^8.0.0",
"php-parser": "^3.2.5"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
@@ -885,6 +920,21 @@
"win32"
]
},
"node_modules/@shufo/tailwindcss-class-sorter": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@shufo/tailwindcss-class-sorter/-/tailwindcss-class-sorter-3.0.1.tgz",
"integrity": "sha512-y9SMobvwElX2G6vdg4odJ6UL6hu/o5RlMsdwEeDLGaqHU3BLSw9CeitGgBus6kadjjDdT2wseG0Tl5yXWdc4UQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
"object-hash": "^3.0.0",
"tailwindcss": "^3.3.2"
},
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@@ -1213,6 +1263,50 @@
"dev": true,
"license": "MIT"
},
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/aigle": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/aigle/-/aigle-1.14.1.tgz",
"integrity": "sha512-bCmQ65CEebspmpbWFs6ab3S27TNyVH1b5MledX8KoiGxUhsJmPUUGpaoSijhwawNnq5Lt8jbcq7Z7gUAD0nuTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"aigle-core": "^1.0.0"
}
},
"node_modules/aigle-core": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/aigle-core/-/aigle-core-1.0.0.tgz",
"integrity": "sha512-uGFWPumk5DLvYnUphNnff+kWC8VeAnjPbbU8ovsSHflKXGX77SD7cAN/aSBCLX3xnoJAM9KdtRgxUygRnSSu7A==",
"dev": true,
"license": "MIT"
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/alpinejs": {
"version": "3.14.9",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.9.tgz",
@@ -1357,6 +1451,126 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/blade-formatter": {
"version": "1.44.4",
"resolved": "https://registry.npmjs.org/blade-formatter/-/blade-formatter-1.44.4.tgz",
"integrity": "sha512-+A7hdXQwIZ21+qWXQPgz+hJKtnjG2iN0Ay2m/E/4zQ5cvdFykTjXXGqae91ZmcKez1x0CSdS80WNObamTwJgFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@prettier/plugin-php": "^0.24.0",
"@shufo/tailwindcss-class-sorter": "3.0.1",
"aigle": "^1.14.1",
"ajv": "^8.9.0",
"chalk": "^4.1.0",
"concat-stream": "^2.0.0",
"detect-indent": "^6.0.0",
"find-config": "^1.0.0",
"glob": "^13.0.0",
"html-attribute-sorter": "^0.4.3",
"ignore": "^6.0.0",
"js-beautify": "^1.15.4",
"lodash": "^4.17.19",
"php-parser": "3.5.0",
"prettier": "^3.2.5",
"string-replace-async": "^2.0.0",
"tailwindcss": "^3.1.8",
"vscode-oniguruma": "1.7.0",
"vscode-textmate": "^7.0.1",
"xregexp": "^5.0.1",
"yargs": "^17.3.1"
},
"bin": {
"blade-formatter": "bin/blade-formatter.cjs"
},
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/blade-formatter/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/blade-formatter/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/blade-formatter/node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
"path-scurry": "^2.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/blade-formatter/node_modules/lru-cache": {
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/blade-formatter/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/blade-formatter/node_modules/path-scurry": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -1413,6 +1627,13 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1657,6 +1878,22 @@
"node": ">= 6"
}
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"dev": true,
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concurrently": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
@@ -1683,6 +1920,29 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"node_modules/core-js-pure": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz",
"integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1721,6 +1981,16 @@
"node": ">=0.4.0"
}
},
"node_modules/detect-indent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -1767,6 +2037,35 @@
"dev": true,
"license": "MIT"
},
"node_modules/editorconfig": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
"integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@one-ini/wasm": "0.1.1",
"commander": "^10.0.0",
"minimatch": "^9.0.1",
"semver": "^7.5.3"
},
"bin": {
"editorconfig": "bin/editorconfig"
},
"engines": {
"node": ">=14"
}
},
"node_modules/editorconfig/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.170",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz",
@@ -1895,6 +2194,13 @@
"node": ">=6"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -1925,6 +2231,23 @@
"node": ">= 6"
}
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -1948,6 +2271,19 @@
"node": ">=8"
}
},
"node_modules/find-config": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/find-config/-/find-config-1.0.0.tgz",
"integrity": "sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==",
"dev": true,
"license": "MIT",
"dependencies": {
"user-home": "^2.0.0"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -2197,6 +2533,40 @@
"node": ">= 0.4"
}
},
"node_modules/html-attribute-sorter": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/html-attribute-sorter/-/html-attribute-sorter-0.4.3.tgz",
"integrity": "sha512-HWSvaXJki44tg0uR1t+j5pRdUVpNiZcJaoB/PFhss/YoAw9cxUDLCpIBbLWQmKjBQfWk91P6LaRnredEyabrDw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/ignore": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz",
"integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC"
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -2302,6 +2672,42 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-beautify": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
"integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
"dev": true,
"license": "MIT",
"dependencies": {
"config-chain": "^1.1.13",
"editorconfig": "^1.0.4",
"glob": "^10.4.2",
"js-cookie": "^3.0.5",
"nopt": "^7.2.1"
},
"bin": {
"css-beautify": "js/bin/css-beautify.js",
"html-beautify": "js/bin/html-beautify.js",
"js-beautify": "js/bin/js-beautify.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/js-cookie": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/laravel-vite-plugin": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz",
@@ -2581,6 +2987,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/linguist-languages": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-8.2.0.tgz",
"integrity": "sha512-KCUUH9x97QWYU0SXOCGxUrZR6cSfuQrMhABB7L/0I8N0LXOeaKe7+RZs7FAwvWCV2qKfZ4Wv1luLq4OfMezSJg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -2689,11 +3102,11 @@
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -2765,6 +3178,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/nopt": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
"dev": true,
"license": "ISC",
"dependencies": {
"abbrev": "^2.0.0"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -2805,6 +3234,16 @@
"node": ">= 6"
}
},
"node_modules/os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -2846,6 +3285,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/php-parser": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.5.0.tgz",
"integrity": "sha512-EHdzSckQNP86jQRCEsMYhs+YzS4BfvfxnyhvzHVhVRoRUGEMFi8f3xKfuS9xdChBazZSyvb10SZbqhYQLGBcQg==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3036,6 +3482,29 @@
"dev": true,
"license": "MIT"
},
"node_modules/prettier": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true,
"license": "ISC"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3074,6 +3543,21 @@
"pify": "^2.3.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -3097,6 +3581,16 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -3203,6 +3697,40 @@
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3268,6 +3796,26 @@
"integrity": "sha512-WO+ns7BYZqGS4jWVTg5JNhIvNV4LGbUtNTSck4zAkWRQzA1IfxwIkMGc0BbdGy4PGIjK7kKo5CZcN6Sd5dHVlw==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-replace-async": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/string-replace-async/-/string-replace-async-2.0.0.tgz",
"integrity": "sha512-AHMupZscUiDh07F1QziX7PLoB1DQ/pzu19vc8Xa8LwZcgnOXaw7yCgBuSYrxVEfaM2d8scc3Gtp+i+QJZV+spw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -3605,6 +4153,13 @@
"dev": true,
"license": "0BSD"
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -3636,6 +4191,19 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/user-home": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz",
"integrity": "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"os-homedir": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3757,6 +4325,20 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vscode-oniguruma": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
"integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
"dev": true,
"license": "MIT"
},
"node_modules/vscode-textmate": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-7.0.4.tgz",
"integrity": "sha512-9hJp0xL7HW1Q5OgGe03NACo7yiCTMEk3WU/rtKXUbncLtdg6rVVNJnHwD88UhbIYU2KoxY0Dih0x+kIsmUKn2A==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3868,6 +4450,16 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/xregexp": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.2.tgz",
"integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime-corejs3": "^7.26.9"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@@ -4,7 +4,9 @@
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
"dev": "vite",
"format": "blade-formatter --write \"resources/views/**/*.blade.php\"",
"format:check": "blade-formatter --check-formatted \"resources/views/**/*.blade.php\""
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.2",
@@ -12,6 +14,7 @@
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.8.2",
"blade-formatter": "^1.44.4",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.31",

11
phpstan.neon Normal file
View File

@@ -0,0 +1,11 @@
includes:
- vendor/larastan/larastan/extension.neon
parameters:
level: 7
paths:
- app
- database
- routes
tmpDir: storage/framework/phpstan

View File

@@ -22,7 +22,7 @@
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_DATABASE" value="testing"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" id="svg2" xml:space="preserve" viewBox="0 0 406.66666 473.33334" sodipodi:docname="Beeldmerk.ai"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"/><sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="640" inkscape:window-height="480" id="namedview4"/><g id="g10" inkscape:groupmode="layer" inkscape:label="Beeldmerk" transform="matrix(1.3333333,0,0,-1.3333333,0,473.33333)"><g id="g12" transform="translate(192.9405,297.8561)"><path d="M 0,0 3.661,-204.853 95.917,-260.941 95.291,47.32 Z" style="fill:#009000;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path14"/></g><g id="g16" transform="translate(287.7484,238.091)"><path d="M 0,0 -183.898,-87.583 -275.154,-31.495 1.239,108.114 Z" style="fill:#00ab00;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path18"/></g><g id="g20" transform="translate(172.7637,52.3648)"><path d="M 0,0 -93.429,-43.886 -96.399,59 -2.97,102.886 Z" style="fill:#ee0004;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path22"/></g><g id="g24" transform="translate(169.794,155.2513)"><path d="m 0,0 2.97,-102.886 -93.429,-43.887" style="fill:#d8000e;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path26"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -10,10 +10,8 @@
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required
autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>

View File

@@ -12,7 +12,8 @@
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required
autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>

View File

@@ -8,7 +8,8 @@
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required
autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
@@ -16,10 +17,8 @@
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required
autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
@@ -27,14 +26,17 @@
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
<input id="remember_me" type="checkbox"
class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
name="remember">
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif

View File

@@ -5,14 +5,16 @@
<!-- Name -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required
autofocus autocomplete="name" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')"
required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
@@ -20,10 +22,8 @@
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="new-password" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required
autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
@@ -32,15 +32,15 @@
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-text-input id="password_confirmation" class="block mt-1 w-full" type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}">
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>

View File

@@ -8,14 +8,16 @@
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required
autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required
autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
@@ -23,9 +25,8 @@
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-text-input id="password_confirmation" class="block mt-1 w-full" type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>

View File

@@ -23,7 +23,8 @@
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
<button type="submit"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
{{ __('Log Out') }}
</button>
</form>

View File

@@ -1,3 +1,4 @@
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
<path
d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z" />
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,3 +1,4 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
<button
{{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -1 +1,2 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
<a
{{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@@ -1,16 +1,16 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700'])
@php
$alignmentClasses = match ($align) {
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
'top' => 'origin-top',
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
};
$alignmentClasses = match ($align) {
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
'top' => 'origin-top',
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
};
$width = match ($width) {
'48' => 'w-48',
default => $width,
};
$width = match ($width) {
'48' => 'w-48',
default => $width,
};
@endphp
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
@@ -18,16 +18,11 @@ $width = match ($width) {
{{ $trigger }}
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div x-show="open" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;" @click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
{{ $content }}
</div>

View File

@@ -1,78 +1,57 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@props(['name', 'show' => false, 'maxWidth' => '2xl'])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
<div x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => !el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) - 1 },
}" x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null" x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false" x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()" x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" style="display: {{ $show ? 'block' : 'none' }};">
<div x-show="show" class="fixed inset-0 transform transition-all" x-on:click="show = false"
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
</div>
<div
x-show="show"
<div x-show="show"
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
{{ $slot }}
</div>
</div>

View File

@@ -1,7 +1,8 @@
@props(['active'])
@php
$classes = ($active ?? false)
$classes =
$active ?? false
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
@endphp

View File

@@ -1,3 +1,4 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
<button
{{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -1,7 +1,8 @@
@props(['active'])
@php
$classes = ($active ?? false)
$classes =
$active ?? false
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
@endphp

View File

@@ -1,3 +1,4 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
<button
{{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -1,3 +1,4 @@
@props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) }}>
<input @disabled($disabled)
{{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) }}>

View File

@@ -6,6 +6,6 @@
</x-slot>
{{ __("You're logged in!") }}
{{ __("You're logged in!") }}
</x-app-layout>

View File

@@ -1,33 +1,45 @@
<div class="space-y-4">
<div>
<label for="opleiding" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Opleiding</label>
<input type="text" id="opleiding" name="opleiding" value="{{ old('opleiding', $education->opleiding ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<input type="text" id="opleiding" name="opleiding" value="{{ old('opleiding', $education->opleiding ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div>
<label for="instituut" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Instituut</label>
<input type="text" id="instituut" name="instituut" value="{{ old('instituut', $education->instituut ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<input type="text" id="instituut" name="instituut"
value="{{ old('instituut', $education->instituut ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="startdatum" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Startdatum</label>
<input type="date" id="startdatum" name="startdatum" value="{{ old('startdatum', $education->startdatum ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<label for="startdatum"
class="block text-sm font-medium text-gray-700 dark:text-gray-200">Startdatum</label>
<input type="date" id="startdatum" name="startdatum"
value="{{ old('startdatum', $education->startdatum ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div>
<label for="einddatum" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Einddatum</label>
<input type="date" id="einddatum" name="einddatum" value="{{ old('einddatum', $education->einddatum ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<input type="date" id="einddatum" name="einddatum"
value="{{ old('einddatum', $education->einddatum ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
</div>
<div>
<label for="beschrijving" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Beschrijving</label>
<textarea id="beschrijving" name="beschrijving" rows="4" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">{{ old('beschrijving', $education->beschrijving ?? '') }}</textarea>
<label for="beschrijving"
class="block text-sm font-medium text-gray-700 dark:text-gray-200">Beschrijving</label>
<textarea id="beschrijving" name="beschrijving" rows="4"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">{{ old('beschrijving', $education->beschrijving ?? '') }}</textarea>
</div>
<div>
<label for="afbeelding" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Afbeelding (optioneel)</label>
<input type="file" id="afbeelding" name="afbeelding" class="mt-1 block w-full text-sm text-gray-500 file:bg-gray-100 file:border file:border-gray-300 file:rounded file:px-4 file:py-2 dark:file:bg-gray-800 dark:file:text-gray-300 dark:file:border-gray-600">
<label for="afbeelding" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Afbeelding
(optioneel)</label>
<input type="file" id="afbeelding" name="afbeelding"
class="mt-1 block w-full text-sm text-gray-500 file:bg-gray-100 file:border file:border-gray-300 file:rounded file:px-4 file:py-2 dark:file:bg-gray-800 dark:file:text-gray-300 dark:file:border-gray-600">
</div>
</div>

View File

@@ -4,7 +4,8 @@
<form method="POST" action="{{ route('educations.store') }}" enctype="multipart/form-data">
@csrf
@include('educations._form')
<button type="submit" class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
<button type="submit"
class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
</form>
</div>
</x-app-layout>

View File

@@ -5,7 +5,8 @@
@csrf
@method('PUT')
@include('educations._form')
<button type="submit" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</button>
<button type="submit"
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</button>
</form>
</div>
</x-app-layout>

View File

@@ -2,13 +2,15 @@
<div class="p-6">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-4">Opleidingen</h2>
<a href="{{ route('educations.create') }}" class="mb-4 inline-block bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">Nieuwe opleiding</a>
<a href="{{ route('educations.create') }}"
class="mb-4 inline-block bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">Nieuwe opleiding</a>
@foreach ($educations as $education)
<div class="mb-4 p-4 bg-white dark:bg-gray-800 shadow rounded">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">{{ $education->opleiding }}</h3>
<p class="text-gray-600 dark:text-gray-300">{{ $education->instituut }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $education->startdatum }} {{ $education->einddatum ?? 'heden' }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $education->startdatum }}
{{ $education->einddatum ?? 'heden' }}</p>
<p class="mt-2 text-gray-700 dark:text-gray-300">{{ $education->beschrijving }}</p>
@if ($education->image())
@@ -16,9 +18,11 @@
@endif
<div class="mt-4 space-x-2">
<a href="{{ route('educations.edit', $education) }}" class="px-3 py-1 bg-green-400 text-white rounded hover:bg-blue-600">Bewerk</a>
<a href="{{ route('educations.edit', $education) }}"
class="px-3 py-1 bg-green-400 text-white rounded hover:bg-blue-600">Bewerk</a>
<form action="{{ route('educations.destroy', $education) }}" method="POST" class="inline-block" onsubmit="return confirm('Weet je zeker dat je dit wilt verwijderen?')">
<form action="{{ route('educations.destroy', $education) }}" method="POST" class="inline-block"
onsubmit="return confirm('Weet je zeker dat je dit wilt verwijderen?')">
@csrf
@method('DELETE')
<button class="px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700">Verwijder</button>

View File

@@ -1,21 +1,23 @@
<div class="p-4 bg-white dark:bg-gray-800 rounded shadow">
<h3 class="text-lg font-bold text-gray-800 dark:text-white">{{ $skill->title }}</h3>
@if($skill->type === 'rating')
@if ($skill->type === 'rating')
<p class="text-sm text-gray-600 dark:text-gray-300 mb-2">Beoordeling: {{ $skill->rating }}/10</p>
@endif
@if($skill->description)
@if ($skill->description)
<p class="text-sm text-gray-600 dark:text-gray-300">{{ $skill->description }}</p>
@endif
@if ($skill->getFirstMediaUrl('image'))
<img src="{{ $skill->getFirstMediaUrl('image') }}" alt="{{ $skill->title }}" class="mt-2 max-w-full h-32 object-contain rounded">
<img src="{{ $skill->getFirstMediaUrl('image') }}" alt="{{ $skill->title }}"
class="mt-2 max-w-full h-32 object-contain rounded">
@endif
<div class="mt-4 flex justify-between">
<a href="{{ route('skills.edit', $skill) }}" class="text-blue-600 hover:underline">Bewerken</a>
<form action="{{ route('skills.destroy', $skill) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit wilt verwijderen?')">
<form action="{{ route('skills.destroy', $skill) }}" method="POST"
onsubmit="return confirm('Weet je zeker dat je dit wilt verwijderen?')">
@csrf
@method('DELETE')
<button class="text-red-600 hover:underline">Verwijderen</button>

View File

@@ -57,7 +57,7 @@
@if ($skill->getFirstMediaUrl('image'))
<img src="{{ $skill->getFirstMediaUrl('image') }}" alt="{{ $skill->title }}"
class="w-10 h-10 object-contain mb-1 hover:scale-125 transition hover:shadow-lg" >
class="w-10 h-10 object-contain mb-1 hover:scale-125 transition hover:shadow-lg">
@endif
<span class="text-xs text-center text-gray-700 dark:text-gray-300">{{ $skill->title }}</span>
</div>

View File

@@ -43,7 +43,7 @@
</div>
</main>
</div>
@stack('scripts')
@stack('scripts')
</body>
</html>

View File

@@ -1,30 +1,34 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
</body>
<div
class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
</body>
</html>

View File

@@ -35,12 +35,16 @@
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
<button
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</div>
</button>
@@ -71,7 +75,7 @@
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
@@ -82,10 +86,14 @@
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
<button @click="open = ! open"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16" />
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
@@ -93,7 +101,7 @@
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
@@ -117,7 +125,7 @@
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>

View File

@@ -1,22 +1,30 @@
<div class="space-y-6">
<div>
<label for="key" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Label (bv. Geboortedatum)</label>
<input type="text" id="key" name="key" value="{{ old('key', $personalia->key ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<label for="key" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Label (bv.
Geboortedatum)</label>
<input type="text" id="key" name="key" value="{{ old('key', $personalia->key ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div>
<label for="value" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Waarde (bv. 12 maart 1988)</label>
<input type="text" id="value" name="value" value="{{ old('value', $personalia->value ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<label for="value" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Waarde (bv. 12 maart
1988)</label>
<input type="text" id="value" name="value" value="{{ old('value', $personalia->value ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div>
<label for="icon" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Font Awesome Icon (optioneel)</label>
<input type="text" id="icon" name="icon" value="{{ old('icon', $personalia->icon ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded" placeholder="fa-solid fa-user">
<label for="icon" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Font Awesome Icon
(optioneel)</label>
<input type="text" id="icon" name="icon" value="{{ old('icon', $personalia->icon ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded"
placeholder="fa-solid fa-user">
<p class="text-xs text-gray-500 mt-1">Gebruik een Font Awesome class zoals <code>fa-solid fa-user</code>.</p>
</div>
<div class="flex items-center space-x-2">
<input type="checkbox" id="hidden" name="hidden" value="1" {{ old('hidden', $personalia->hidden ?? false) ? 'checked' : '' }} class="form-checkbox text-blue-600">
<input type="checkbox" id="hidden" name="hidden" value="1"
{{ old('hidden', $personalia->hidden ?? false) ? 'checked' : '' }} class="form-checkbox text-blue-600">
<label for="hidden" class="text-gray-700 dark:text-gray-200">Verbergen voor publiek</label>
</div>
</div>

View File

@@ -4,7 +4,8 @@
<form method="POST" action="{{ route('personalia.store') }}">
@csrf
@include('personalia._form')
<button type="submit" class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
<button type="submit"
class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
</form>
</div>
</x-app-layout>

View File

@@ -1,11 +1,12 @@
<x-app-layout>
<div class="p-6">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-4">Persoonlijk item bewerken</h2>
<form method="POST" action="{{ route('personalia.update', ['personalium' =>$personalia]) }}">
<form method="POST" action="{{ route('personalia.update', ['personalium' => $personalia]) }}">
@csrf
@method('PUT')
@include('personalia._form')
<button type="submit" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</button>
<button type="submit"
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</button>
</form>
</div>
</x-app-layout>

View File

@@ -1,7 +1,8 @@
<x-app-layout>
<div class="p-6">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-4">Personalia</h2>
<a href="{{ route('personalia.create') }}" class="mb-4 inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Nieuw item toevoegen</a>
<a href="{{ route('personalia.create') }}"
class="mb-4 inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Nieuw item toevoegen</a>
<div class="space-y-4">
@foreach ($personalia as $item)
@@ -19,8 +20,10 @@
</div>
</div>
<div class="flex space-x-2">
<a href="{{ route('personalia.edit', $item) }}" class="text-blue-600 hover:underline">Bewerken</a>
<form action="{{ route('personalia.destroy', $item) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit item wilt verwijderen?')">
<a href="{{ route('personalia.edit', $item) }}"
class="text-blue-600 hover:underline">Bewerken</a>
<form action="{{ route('personalia.destroy', $item) }}" method="POST"
onsubmit="return confirm('Weet je zeker dat je dit item wilt verwijderen?')">
@csrf
@method('DELETE')
<button class="text-red-600 hover:underline">Verwijderen</button>

View File

@@ -9,10 +9,8 @@
</p>
</header>
<x-danger-button
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
>{{ __('Delete Account') }}</x-danger-button>
<x-danger-button x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">{{ __('Delete Account') }}</x-danger-button>
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
@@ -30,13 +28,8 @@
<div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-text-input id="password" name="password" type="password" class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}" />
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div>

View File

@@ -15,19 +15,22 @@
<div>
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-text-input id="update_password_current_password" name="current_password" type="password"
class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password" :value="__('New Password')" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full"
autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password"
class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
</div>
@@ -35,13 +38,8 @@
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'password-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p>
<p x-data="{ show: true }" x-show="show" x-transition x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400">{{ __('Saved.') }}</p>
@endif
</div>
</form>

View File

@@ -19,21 +19,24 @@
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)"
required autofocus autocomplete="name" />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)"
required autocomplete="username" />
<x-input-error class="mt-2" :messages="$errors->get('email')" />
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && !$user->hasVerifiedEmail())
<div>
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
{{ __('Your email address is unverified.') }}
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
<button form="send-verification"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@@ -51,13 +54,8 @@
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'profile-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p>
<p x-data="{ show: true }" x-show="show" x-transition x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400">{{ __('Saved.') }}</p>
@endif
</div>
</form>

View File

@@ -1,57 +1,62 @@
<div class="space-y-6">
<div>
<label for="type" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Type</label>
<select id="type" name="type" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<option value="rating" @selected(old('type', $skill->type ?? 'rating') === 'rating')>Rating</option>
<option value="tag" @selected(old('type', $skill->type ?? '') === 'tag')>Tag</option>
<option value="other" @selected(old('type', $skill->type ?? '') === 'other')>Overig</option>
</select>
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Type</label>
<select id="type" name="type"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<option value="rating" @selected(old('type', $skill->type ?? 'rating') === 'rating')>Rating</option>
<option value="tag" @selected(old('type', $skill->type ?? '') === 'tag')>Tag</option>
<option value="other" @selected(old('type', $skill->type ?? '') === 'other')>Overig</option>
</select>
</div>
<div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Titel</label>
<input type="text" id="title" name="title" value="{{ old('title', $skill->title ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<input type="text" id="title" name="title" value="{{ old('title', $skill->title ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div id="description-field">
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Beschrijving</label>
<textarea id="description" name="description" rows="4" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">{{ old('description', $skill->description ?? '') }}</textarea>
<textarea id="description" name="description" rows="4"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">{{ old('description', $skill->description ?? '') }}</textarea>
</div>
<div id="rating-field">
<label for="rating" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Beoordeling (110)</label>
<input type="number" id="rating" name="rating" min="1" max="10" value="{{ old('rating', $skill->rating ?? 5) }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<label for="rating" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Beoordeling
(110)</label>
<input type="number" id="rating" name="rating" min="1" max="10"
value="{{ old('rating', $skill->rating ?? 5) }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div id="image-field">
<label for="image" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Afbeelding</label>
<input type="file" id="image" name="image" class="mt-1 text-gray-800 dark:text-gray-100">
@if (!empty($skill) && $skill->getFirstMediaUrl('image'))
<img src="{{ $skill->getFirstMediaUrl('image') }}" class="mt-2 max-w-xs rounded">
@endif
</div>
<div id="image-field">
<label for="image" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Afbeelding</label>
<input type="file" id="image" name="image" class="mt-1 text-gray-800 dark:text-gray-100">
@if (!empty($skill) && $skill->getFirstMediaUrl('image'))
<img src="{{ $skill->getFirstMediaUrl('image') }}" class="mt-2 max-w-xs rounded">
@endif
</div>
</div>
@push('scripts')
<script>
function toggleFields() {
const type = document.getElementById('type').value;
<script>
function toggleFields() {
const type = document.getElementById('type').value;
// Rating alleen tonen bij type = rating
document.getElementById('rating-field').style.display = (type === 'rating') ? 'block' : 'none';
// Rating alleen tonen bij type = rating
document.getElementById('rating-field').style.display = (type === 'rating') ? 'block' : 'none';
// Image tonen bij rating en other
document.getElementById('image-field').style.display = (type === 'rating' || type === 'other') ? 'block' :
'none';
// Image tonen bij rating en other
document.getElementById('image-field').style.display = (type === 'rating' || type === 'other') ? 'block' : 'none';
// Beschrijving alleen tonen bij rating
document.getElementById('description-field').style.display = (type === 'rating') ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('type').addEventListener('change', toggleFields);
toggleFields(); // initial
});
</script>
// Beschrijving alleen tonen bij rating
document.getElementById('description-field').style.display = (type === 'rating') ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('type').addEventListener('change', toggleFields);
toggleFields(); // initial
});
</script>
@endpush

View File

@@ -4,7 +4,8 @@
<form method="POST" action="{{ route('skills.store') }}" enctype="multipart/form-data">
@csrf
@include('skills._form')
<button type="submit" class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
<button type="submit"
class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
</form>
</div>
</x-app-layout>

View File

@@ -5,7 +5,8 @@
@csrf
@method('PUT')
@include('skills._form')
<button type="submit" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</button>
<button type="submit"
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</button>
</form>
</div>
</x-app-layout>

View File

@@ -2,7 +2,8 @@
<div class="p-6 space-y-10">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white">Vaardigheden</h2>
<a href="{{ route('skills.create') }}" class="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
<a href="{{ route('skills.create') }}"
class="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Nieuwe vaardigheid
</a>
@@ -11,7 +12,7 @@
@endphp
{{-- Rating --}}
@if($groupedSkills->has('rating'))
@if ($groupedSkills->has('rating'))
<div>
<h3 class="text-xl font-bold text-gray-700 dark:text-gray-200 mb-4">Ratings</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -23,12 +24,13 @@
@endif
{{-- Tags --}}
@if($groupedSkills->has('tag'))
@if ($groupedSkills->has('tag'))
<div>
<h3 class="text-xl font-bold text-gray-700 dark:text-gray-200 mb-4">Tags</h3>
<div class="flex flex-wrap gap-2">
@foreach ($groupedSkills['tag'] as $skill)
<span class="inline-block bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-100 text-sm px-3 py-1 rounded">
<span
class="inline-block bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-100 text-sm px-3 py-1 rounded">
{{ $skill->title }}
</span>
@endforeach
@@ -37,7 +39,7 @@
@endif
{{-- Overig --}}
@if($groupedSkills->has('other'))
@if ($groupedSkills->has('other'))
<div>
<h3 class="text-xl font-bold text-gray-700 dark:text-gray-200 mb-4">Overige vaardigheden</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Laravel</title>
@@ -134,7 +134,7 @@
&copy; {{ date('Y') }} Roberto Guagliardo. Alle rechten voorbehouden.
</footer>
<div id="custom-cursor">
{!! file_get_contents(public_path('storage/sitiweb.svg')) !!}
{!! file_get_contents(resource_path('images/sitiweb.svg')) !!}
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>

View File

@@ -4,24 +4,33 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="werkgever" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Werkgever</label>
<input type="text" id="werkgever" name="werkgever" value="{{ old('werkgever', $workExperience->werkgever ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<input type="text" id="werkgever" name="werkgever"
value="{{ old('werkgever', $workExperience->werkgever ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div>
<label for="functie" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Functie</label>
<input type="text" id="functie" name="functie" value="{{ old('functie', $workExperience->functie ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<input type="text" id="functie" name="functie"
value="{{ old('functie', $workExperience->functie ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="startdatum" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Startdatum</label>
<input type="date" id="startdatum" name="startdatum" value="{{ old('startdatum', $workExperience->startdatum ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<label for="startdatum"
class="block text-sm font-medium text-gray-700 dark:text-gray-200">Startdatum</label>
<input type="date" id="startdatum" name="startdatum"
value="{{ old('startdatum', $workExperience->startdatum ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
<div id="einddatum-container">
<label for="einddatum" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Einddatum</label>
<input type="date" id="einddatum" name="einddatum" value="{{ old('einddatum', $workExperience->einddatum ?? '') }}" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
<input type="date" id="einddatum" name="einddatum"
value="{{ old('einddatum', $workExperience->einddatum ?? '') }}"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">
</div>
</div>
@@ -35,8 +44,10 @@
<div>
<label for="beschrijving" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Beschrijving</label>
<textarea id="beschrijving" name="beschrijving" rows="4" class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">{{ old('beschrijving', $workExperience->beschrijving ?? '') }}</textarea>
<label for="beschrijving"
class="block text-sm font-medium text-gray-700 dark:text-gray-200">Beschrijving</label>
<textarea id="beschrijving" name="beschrijving" rows="4"
class="mt-1 w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded">{{ old('beschrijving', $workExperience->beschrijving ?? '') }}</textarea>
</div>
<div>
@@ -61,7 +72,7 @@
}
// Init bij load
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function() {
toggleEinddatum();
});
</script>

View File

@@ -5,7 +5,8 @@
<form method="POST" action="{{ route('work-experiences.store') }}" enctype="multipart/form-data">
@csrf
@include('work_experiences._form')
<button type="submit" class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
<button type="submit"
class="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Opslaan</button>
</form>
</div>
</x-app-layout>

View File

@@ -2,11 +2,13 @@
<x-app-layout>
<div class="p-6">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-4">Werkervaring Bewerken</h2>
<form method="POST" action="{{ route('work-experiences.update', $workExperience) }}" enctype="multipart/form-data">
<form method="POST" action="{{ route('work-experiences.update', $workExperience) }}"
enctype="multipart/form-data">
@csrf
@method('PUT')
@include('work_experiences._form')
<x-primary-button type="submit" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</x-primary-button>
<x-primary-button type="submit"
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Bijwerken</x-primary-button>
</form>
</div>
</x-app-layout>

View File

@@ -3,7 +3,8 @@
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white">Werkervaring</h2>
<a href="{{ route('work-experiences.create') }}" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">+ Nieuw</a>
<a href="{{ route('work-experiences.create') }}"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">+ Nieuw</a>
</div>
@if (session('success'))
@@ -15,16 +16,20 @@
<div class="p-4 bg-white dark:bg-gray-800 rounded shadow">
<div class="flex justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ $experience->functie }} bij {{ $experience->werkgever }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-300">{{ $experience->startdatum }} {{ $experience->einddatum ?? 'heden' }}</p>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ $experience->functie }}
bij {{ $experience->werkgever }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-300">{{ $experience->startdatum }}
{{ $experience->einddatum ?? 'heden' }}</p>
<p class="mt-2 text-gray-700 dark:text-gray-200">{{ $experience->beschrijving }}</p>
</div>
<div class="flex gap-2">
<a href="{{ route('work-experiences.edit', $experience) }}" class="text-sitiweb-green hover:underline">Bewerken</a>
<a href="{{ route('work-experiences.edit', $experience) }}"
class="text-sitiweb-green hover:underline">Bewerken</a>
<form method="POST" action="{{ route('work-experiences.destroy', $experience) }}">
@csrf
@method('DELETE')
<button type="submit" class="text-red-500 hover:underline" onclick="return confirm('Weet je zeker dat je dit wilt verwijderen?')">Verwijderen</button>
<button type="submit" class="text-red-500 hover:underline"
onclick="return confirm('Weet je zeker dat je dit wilt verwijderen?')">Verwijderen</button>
</form>
</div>
</div>

View File

@@ -8,10 +8,10 @@ use App\Http\Controllers\SkillController;
use Illuminate\Support\Facades\Route;
Route::get('/', [FrontendController::class, 'index'])->name('home');
Route::get('/dashboard', function () {
Route::get('/dashboard', function (): \Illuminate\View\View {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/getPersonalia/{id}', [FrontendController::class, 'getPersonalia'])->name('personalia');
Route::get('/getPersonalia/{personalia}', [FrontendController::class, 'getPersonalia'])->name('personalia');
Route::post('/contact', [FrontendController::class, 'message'])->name('contact');
Route::middleware('auth')->group(function () {
@@ -19,10 +19,10 @@ Route::middleware('auth')->group(function () {
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::resource('work-experiences', \App\Http\Controllers\WorkExperienceController::class);
Route::resource('skills', SkillController::class);
Route::resource('personalia', PersonaliaController::class);
Route::resource('educations', EducationController::class);
Route::resource('work-experiences', \App\Http\Controllers\WorkExperienceController::class)->except(['show']);
Route::resource('skills', SkillController::class)->except(['show']);
Route::resource('personalia', PersonaliaController::class)->except(['show']);
Route::resource('educations', EducationController::class)->except(['show']);
});

View File

@@ -4,7 +4,7 @@ test('registration screen can be rendered', function () {
$response = $this->get('/register');
$response->assertStatus(200);
});
})->skip('Registration routes are disabled for this application.');
test('new users can register', function () {
$response = $this->post('/register', [
@@ -16,4 +16,4 @@ test('new users can register', function () {
$this->assertAuthenticated();
$response->assertRedirect(route('dashboard', absolute: false));
});
})->skip('Registration routes are disabled for this application.');

View File

@@ -0,0 +1,110 @@
<?php
use App\Models\Education;
use App\Models\User;
use Illuminate\Http\UploadedFile;
test('guests cannot manage educations', function () {
$education = Education::factory()->create();
$this->get(route('educations.index'))->assertRedirect(route('login'));
$this->get(route('educations.create'))->assertRedirect(route('login'));
$this->post(route('educations.store'), [])->assertRedirect(route('login'));
$this->get(route('educations.edit', $education))->assertRedirect(route('login'));
$this->patch(route('educations.update', $education), [])->assertRedirect(route('login'));
$this->delete(route('educations.destroy', $education))->assertRedirect(route('login'));
});
test('an authenticated user can view the education overview', function () {
$user = User::factory()->create();
$education = Education::factory()->create();
$this->actingAs($user)
->get(route('educations.index'))
->assertOk()
->assertViewIs('educations.index')
->assertViewHas('educations', fn ($educations) => $educations->contains($education));
});
test('an authenticated user can create an education with an image', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('educations.store'), [
'opleiding' => 'HBO-ICT',
'instituut' => 'Hogeschool Utrecht',
'startdatum' => '2020-09-01',
'einddatum' => '2024-07-01',
'beschrijving' => 'Software engineering en web development.',
'afbeelding' => UploadedFile::fake()->image('education.jpg'),
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('educations.index'));
$education = Education::where('opleiding', 'HBO-ICT')->firstOrFail();
$this->assertDatabaseHas('education', [
'id' => $education->id,
'instituut' => 'Hogeschool Utrecht',
]);
$this->assertDatabaseHas('media', [
'model_type' => Education::class,
'model_id' => $education->id,
'collection_name' => 'image',
]);
});
test('an authenticated user can update an education and replace its image', function () {
$user = User::factory()->create();
$education = Education::factory()->current()->create([
'opleiding' => 'HBO-ICT',
'instituut' => 'Hogeschool Utrecht',
'startdatum' => '2020-09-01',
]);
$education
->addMedia(UploadedFile::fake()->image('old-education.jpg'))
->toMediaCollection('image');
$response = $this->actingAs($user)->patch(route('educations.update', $education), [
'opleiding' => 'Software Engineering',
'instituut' => 'Avans Hogeschool',
'startdatum' => '2021-09-01',
'einddatum' => '2025-07-01',
'beschrijving' => 'Verdieping in backend development.',
'afbeelding' => UploadedFile::fake()->image('new-education.jpg'),
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('educations.index'));
expect($education->refresh())
->opleiding->toBe('Software Engineering')
->instituut->toBe('Avans Hogeschool')
->getMedia('image')->toHaveCount(1);
});
test('an authenticated user can delete an education and its image', function () {
$user = User::factory()->create();
$education = Education::factory()->current()->create();
$education
->addMedia(UploadedFile::fake()->image('education.jpg'))
->toMediaCollection('image');
$this->actingAs($user)
->delete(route('educations.destroy', $education))
->assertRedirect(route('educations.index'));
$this->assertDatabaseMissing('education', ['id' => $education->id]);
$this->assertDatabaseMissing('media', [
'model_type' => Education::class,
'model_id' => $education->id,
]);
});

View File

@@ -0,0 +1,101 @@
<?php
use App\Jobs\NotifyTelegramAboutContactMessage;
use App\Jobs\NotifyTelegramAboutPersonaliaClick;
use App\Models\Education;
use App\Models\Personalia;
use App\Models\Skill;
use App\Models\WorkExperience;
use Illuminate\Support\Facades\Queue;
test('the homepage shows the public cv data', function () {
$skill = Skill::factory()->rating()->create([
'title' => 'Laravel',
'description' => 'Framework expertise',
'rating' => 8,
]);
$personalium = Personalia::factory()->hidden()->create([
'key' => 'Email',
'value' => 'roberto@example.com',
'icon' => 'fa-solid fa-envelope',
]);
$education = Education::factory()->create([
'opleiding' => 'HBO-ICT',
'instituut' => 'Hogeschool Utrecht',
'startdatum' => '2020-09-01',
'einddatum' => '2024-07-01',
]);
$experience = WorkExperience::factory()->current()->create([
'werkgever' => 'Acme',
'functie' => 'Developer',
'startdatum' => '2022-01-01',
]);
$this->get(route('home'))
->assertOk()
->assertViewIs('welcome')
->assertViewHas('skills', fn ($skills) => $skills->get('rating')->contains($skill))
->assertViewHas('personalia', fn ($personalia) => $personalia->contains($personalium))
->assertViewHas('education', fn ($educations) => $educations->contains($education))
->assertViewHas('experience', fn ($experiences) => $experiences->contains($experience));
});
test('a hidden personalia value can be requested and the click is queued for notification', function () {
Queue::fake();
$personalium = Personalia::factory()->hidden()->create([
'key' => 'Email',
'value' => 'roberto@example.com',
'icon' => 'fa-solid fa-envelope',
]);
$this->withHeader('User-Agent', 'Pest Browser')
->getJson(route('personalia', $personalium))
->assertOk()
->assertJson([
'value' => 'roberto@example.com',
]);
Queue::assertPushed(NotifyTelegramAboutPersonaliaClick::class);
});
test('requesting unknown personalia returns not found', function () {
Queue::fake();
$this->getJson(route('personalia', 999))->assertNotFound();
Queue::assertNotPushed(NotifyTelegramAboutPersonaliaClick::class);
});
test('a contact message can be submitted and is queued for notification', function () {
Queue::fake();
$this->withHeader('User-Agent', 'Pest Browser')
->postJson(route('contact'), [
'name' => 'Roberto',
'email' => 'roberto@example.com',
'phone' => '+31612345678',
'message' => 'Hoi, ik wil graag contact opnemen.',
])
->assertOk()
->assertJson([
'status' => 'success',
]);
Queue::assertPushed(NotifyTelegramAboutContactMessage::class);
});
test('a contact message requires a name and message', function () {
Queue::fake();
$this->postJson(route('contact'), [
'email' => 'not-an-email',
])
->assertUnprocessable()
->assertJsonValidationErrors(['name', 'message', 'email']);
Queue::assertNotPushed(NotifyTelegramAboutContactMessage::class);
});

View File

@@ -0,0 +1,108 @@
<?php
use App\Models\Personalia;
use App\Models\User;
test('guests cannot manage personalia', function () {
$personalium = Personalia::factory()->hidden()->create();
$this->get(route('personalia.index'))->assertRedirect(route('login'));
$this->get(route('personalia.create'))->assertRedirect(route('login'));
$this->post(route('personalia.store'), [])->assertRedirect(route('login'));
$this->get(route('personalia.edit', $personalium))->assertRedirect(route('login'));
$this->patch(route('personalia.update', $personalium), [])->assertRedirect(route('login'));
$this->delete(route('personalia.destroy', $personalium))->assertRedirect(route('login'));
});
test('an authenticated user can view the personalia overview', function () {
$user = User::factory()->create();
$personalium = Personalia::factory()->hidden()->create();
$this->actingAs($user)
->get(route('personalia.index'))
->assertOk()
->assertViewIs('personalia.index')
->assertViewHas('personalia', fn ($personalia) => $personalia->contains($personalium));
});
test('an authenticated user can create visible personalia', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('personalia.store'), [
'key' => 'Website',
'value' => 'https://example.com',
'icon' => 'fa-solid fa-globe',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('personalia.index'));
$this->assertDatabaseHas('personalia', [
'key' => 'Website',
'value' => 'https://example.com',
'hidden' => false,
'icon' => 'fa-solid fa-globe',
]);
});
test('an authenticated user can create hidden personalia', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('personalia.store'), [
'key' => 'Telefoon',
'value' => '+31612345678',
'hidden' => '1',
'icon' => 'fa-solid fa-phone',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('personalia.index'));
$this->assertDatabaseHas('personalia', [
'key' => 'Telefoon',
'value' => '+31612345678',
'hidden' => true,
'icon' => 'fa-solid fa-phone',
]);
});
test('an authenticated user can update personalia', function () {
$user = User::factory()->create();
$personalium = Personalia::factory()->hidden()->create([
'key' => 'Email',
'value' => 'old@example.com',
'icon' => 'fa-solid fa-envelope',
]);
$response = $this->actingAs($user)->patch(route('personalia.update', $personalium), [
'key' => 'Email',
'value' => 'new@example.com',
'icon' => 'fa-regular fa-envelope',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('personalia.index'));
$personalium->refresh();
expect($personalium->value)->toBe('new@example.com');
expect($personalium->hidden)->toBeFalse();
expect($personalium->icon)->toBe('fa-regular fa-envelope');
});
test('an authenticated user can delete personalia', function () {
$user = User::factory()->create();
$personalium = Personalia::factory()->hidden()->create();
$this->actingAs($user)
->delete(route('personalia.destroy', $personalium))
->assertRedirect(route('personalia.index'));
$this->assertDatabaseMissing('personalia', ['id' => $personalium->id]);
});

View File

@@ -0,0 +1,19 @@
<?php
use App\Models\User;
test('guests cannot access profile management routes', function () {
$this->get(route('profile.edit'))->assertRedirect(route('login'));
$this->patch(route('profile.update'), [])->assertRedirect(route('login'));
$this->delete(route('profile.destroy'), [])->assertRedirect(route('login'));
});
test('the profile edit page receives the authenticated user', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('profile.edit'))
->assertOk()
->assertViewIs('profile.edit')
->assertViewHas('user', fn (User $viewUser) => $viewUser->is($user));
});

View File

@@ -0,0 +1,110 @@
<?php
use App\Models\Skill;
use App\Models\User;
use Illuminate\Http\UploadedFile;
test('guests cannot manage skills', function () {
$skill = Skill::factory()->rating()->create();
$this->get(route('skills.index'))->assertRedirect(route('login'));
$this->get(route('skills.create'))->assertRedirect(route('login'));
$this->post(route('skills.store'), [])->assertRedirect(route('login'));
$this->get(route('skills.edit', $skill))->assertRedirect(route('login'));
$this->patch(route('skills.update', $skill), [])->assertRedirect(route('login'));
$this->delete(route('skills.destroy', $skill))->assertRedirect(route('login'));
});
test('an authenticated user can view the skill overview', function () {
$user = User::factory()->create();
$skill = Skill::factory()->rating()->create();
$this->actingAs($user)
->get(route('skills.index'))
->assertOk()
->assertViewIs('skills.index')
->assertViewHas('skills', fn ($skills) => $skills->contains($skill));
});
test('an authenticated user can create a skill with an image', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('skills.store'), [
'title' => 'Laravel',
'description' => 'Framework expertise',
'rating' => 8,
'type' => 'rating',
'image' => UploadedFile::fake()->image('skill.jpg'),
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('skills.index'));
$skill = Skill::where('title', 'Laravel')->firstOrFail();
$this->assertDatabaseHas('skills', [
'id' => $skill->id,
'rating' => 8,
'type' => 'rating',
]);
$this->assertDatabaseHas('media', [
'model_type' => Skill::class,
'model_id' => $skill->id,
'collection_name' => 'image',
'disk' => 'public',
]);
});
test('an authenticated user can update a skill and replace its image', function () {
$user = User::factory()->create();
$skill = Skill::factory()->rating()->create([
'title' => 'Laravel',
'description' => 'Framework expertise',
'rating' => 8,
]);
$skill
->addMedia(UploadedFile::fake()->image('old-skill.jpg'))
->toMediaCollection('image', 'public');
$response = $this->actingAs($user)->patch(route('skills.update', $skill), [
'title' => 'PHP',
'description' => 'Backend expertise',
'rating' => 9,
'type' => 'rating',
'image' => UploadedFile::fake()->image('new-skill.jpg'),
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('skills.index'));
expect($skill->refresh())
->title->toBe('PHP')
->rating->toBe(9)
->getMedia('image')->toHaveCount(1);
});
test('an authenticated user can delete a skill and its image', function () {
$user = User::factory()->create();
$skill = Skill::factory()->rating()->create();
$skill
->addMedia(UploadedFile::fake()->image('skill.jpg'))
->toMediaCollection('image', 'public');
$this->actingAs($user)
->delete(route('skills.destroy', $skill))
->assertRedirect(route('skills.index'));
$this->assertDatabaseMissing('skills', ['id' => $skill->id]);
$this->assertDatabaseMissing('media', [
'model_type' => Skill::class,
'model_id' => $skill->id,
]);
});

View File

@@ -0,0 +1,110 @@
<?php
use App\Models\User;
use App\Models\WorkExperience;
use Illuminate\Http\UploadedFile;
test('guests cannot manage work experiences', function () {
$experience = WorkExperience::factory()->create();
$this->get(route('work-experiences.index'))->assertRedirect(route('login'));
$this->get(route('work-experiences.create'))->assertRedirect(route('login'));
$this->post(route('work-experiences.store'), [])->assertRedirect(route('login'));
$this->get(route('work-experiences.edit', $experience))->assertRedirect(route('login'));
$this->patch(route('work-experiences.update', $experience), [])->assertRedirect(route('login'));
$this->delete(route('work-experiences.destroy', $experience))->assertRedirect(route('login'));
});
test('an authenticated user can view the work experience overview', function () {
$user = User::factory()->create();
$experience = WorkExperience::factory()->create();
$this->actingAs($user)
->get(route('work-experiences.index'))
->assertOk()
->assertViewIs('work_experiences.index')
->assertViewHas('experiences', fn ($experiences) => $experiences->contains($experience));
});
test('an authenticated user can create a work experience with an image', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('work-experiences.store'), [
'werkgever' => 'Acme',
'functie' => 'Laravel Developer',
'startdatum' => '2022-01-01',
'einddatum' => null,
'beschrijving' => 'Bouwde maatwerkapplicaties.',
'afbeelding' => UploadedFile::fake()->image('experience.jpg'),
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('work-experiences.index'));
$experience = WorkExperience::where('werkgever', 'Acme')->firstOrFail();
$this->assertDatabaseHas('work_experiences', [
'id' => $experience->id,
'functie' => 'Laravel Developer',
]);
$this->assertDatabaseHas('media', [
'model_type' => WorkExperience::class,
'model_id' => $experience->id,
'collection_name' => 'image',
]);
});
test('an authenticated user can update a work experience and replace its image', function () {
$user = User::factory()->create();
$experience = WorkExperience::factory()->current()->create([
'werkgever' => 'Acme',
'functie' => 'Developer',
'startdatum' => '2022-01-01',
]);
$experience
->addMedia(UploadedFile::fake()->image('old-experience.jpg'))
->toMediaCollection('image');
$response = $this->actingAs($user)->patch(route('work-experiences.update', $experience), [
'werkgever' => 'Globex',
'functie' => 'Senior Laravel Developer',
'startdatum' => '2023-01-01',
'einddatum' => null,
'beschrijving' => 'Leidde backend development.',
'afbeelding' => UploadedFile::fake()->image('new-experience.jpg'),
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('work-experiences.index'));
expect($experience->refresh())
->werkgever->toBe('Globex')
->functie->toBe('Senior Laravel Developer')
->getMedia('image')->toHaveCount(1);
});
test('an authenticated user can delete a work experience and its image', function () {
$user = User::factory()->create();
$experience = WorkExperience::factory()->current()->create();
$experience
->addMedia(UploadedFile::fake()->image('experience.jpg'))
->toMediaCollection('image');
$this->actingAs($user)
->delete(route('work-experiences.destroy', $experience))
->assertRedirect(route('work-experiences.index'));
$this->assertDatabaseMissing('work_experiences', ['id' => $experience->id]);
$this->assertDatabaseMissing('media', [
'model_type' => WorkExperience::class,
'model_id' => $experience->id,
]);
});

View File

@@ -1,7 +0,0 @@
<?php
it('returns a successful response', function () {
$response = $this->get('/');
$response->assertStatus(200);
});

View File

@@ -12,7 +12,7 @@ test('profile page is displayed', function () {
$response->assertOk();
});
test('profile information can be updated', function () {
test('profile information can be updated without changing email verification status', function () {
$user = User::factory()->create();
$response = $this
@@ -30,7 +30,7 @@ test('profile information can be updated', function () {
$this->assertSame('Test User', $user->name);
$this->assertSame('test@example.com', $user->email);
$this->assertNull($user->email_verified_at);
$this->assertNotNull($user->email_verified_at);
});
test('email verification status is unchanged when the email address is unchanged', function () {

View File

@@ -3,8 +3,15 @@
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Storage;
abstract class TestCase extends BaseTestCase
{
//
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Storage::fake('public');
}
}