Compare commits
7 Commits
feature/ad
...
featute/re
| Author | SHA1 | Date | |
|---|---|---|---|
| 38943743aa | |||
| 866118a86f | |||
| aab8d33b8d | |||
| eb9c8796de | |||
| 53c4823b22 | |||
| fe47b79a25 | |||
| 27449eabf0 |
@@ -32,3 +32,6 @@ jobs:
|
||||
|
||||
- name: Run test suite
|
||||
run: php artisan test
|
||||
|
||||
- name: Run static analysis
|
||||
run: composer analyse
|
||||
|
||||
123
README.md
123
README.md
@@ -1,79 +1,106 @@
|
||||
# Interactieve Laravel CV Applicatie
|
||||
# CV Roberto
|
||||
|
||||
Deze Laravel-applicatie is gebouwd als mijn interactieve en dynamische cv. In plaats van een statisch pdf-bestand, kun je hier real-time mijn:
|
||||
Dit project is mijn interactieve CV en tegelijk een voorbeeld van hoe ik een Laravel-app opzet.
|
||||
|
||||
- **Vaardigheden** (met beoordeling en iconen)
|
||||
- **Werkervaring**
|
||||
- **Opleidingen**
|
||||
- **Persoonlijke gegevens**
|
||||
- **Tags & kernkwaliteiten**
|
||||
Live: [cv.robert.ooo](https://cv.robert.ooo)
|
||||
|
||||
zien — inclusief slimme automatisering, logging en Telegram-notificaties voor recruiterinteracties.
|
||||
## Wat zit erin
|
||||
|
||||
---
|
||||
- Publieke CV-pagina met werkervaring, opleidingen, skills en personalia.
|
||||
- Adminomgeving om CV-data te beheren.
|
||||
- Afbeeldingsuploads via Spatie Media Library.
|
||||
- Contactformulier met queue job voor Telegram-notificaties.
|
||||
- Klikbare verborgen personalia, zodat bots de waarde niet direct in de HTML zien.
|
||||
- Feature tests voor de belangrijkste controllerflows.
|
||||
- CI-checks voor tests, Larastan en formatting.
|
||||
|
||||
## 🧰 Techniek & Stack
|
||||
## Stack
|
||||
|
||||
- **Framework:** Laravel 12
|
||||
- **Frontend:** Tailwind CSS, Blade
|
||||
- **DevOps-integraties:** Telegram alerts, Healthchecks, custom logging
|
||||
- **CI/CD-ready:** Ondersteuning voor deploy hooks en jobs
|
||||
- **Overige tools:** Docker, Nginx, Git, Redis, Cron, Promtail, Grafana
|
||||
- Laravel 12
|
||||
- PHP 8.2+
|
||||
- Blade
|
||||
- Tailwind CSS
|
||||
- Pest
|
||||
- Larastan level 7
|
||||
- Laravel Pint
|
||||
- Blade Formatter
|
||||
- Docker / Laravel Sail
|
||||
|
||||
---
|
||||
## Kwaliteitschecks
|
||||
|
||||
## ⚙️ Installatie
|
||||
|
||||
1. **Clone deze repo:**
|
||||
Deze checks horen groen te zijn voordat een merge logisch is:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roberto-guagliardo/cv-app.git
|
||||
cd cv-app
|
||||
composer test
|
||||
composer analyse
|
||||
composer format:check
|
||||
npm run format:check
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Installeer dependencies:**
|
||||
In de workflow worden dezelfde checks afgedwongen:
|
||||
|
||||
```bash
|
||||
composer install
|
||||
npm install && npm run build
|
||||
```
|
||||
- PHPUnit/Pest feature tests
|
||||
- Larastan op level 7
|
||||
- PHP formatting via Pint
|
||||
- Blade formatting via Blade Formatter
|
||||
|
||||
3. **Maak .env aan en configureer je database, storage en Telegram:**
|
||||
Skipped tests zijn alleen acceptabel als dat bewust is, zoals disabled registratieflows. Larastan en formatting moeten gewoon groen zijn.
|
||||
|
||||
## Lokaal draaien
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
composer install
|
||||
npm ci
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
4. **Voer migraties & seeders uit:**
|
||||
|
||||
```bash
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
5. **Geniet van de app:**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
---
|
||||
Met Sail:
|
||||
|
||||
## 🌐 Live Demo
|
||||
```bash
|
||||
cp .env.example .env
|
||||
./vendor/bin/sail up -d
|
||||
./vendor/bin/sail artisan key:generate
|
||||
./vendor/bin/sail artisan migrate --seed
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
Wil je zien hoe het eruitziet? Bekijk mijn live cv op:
|
||||
Voor de admin login kun je in `.env` deze waardes zetten en daarna opnieuw seeden:
|
||||
|
||||
➡️ [cv.robert.ooo](https://cv.robert.ooo)
|
||||
```env
|
||||
ADMIN_NAME="Admin"
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=changeme123
|
||||
```
|
||||
|
||||
---
|
||||
## Development
|
||||
|
||||
## 📊 Bijdragen
|
||||
PHP formatter:
|
||||
|
||||
Deze applicatie is persoonlijk en niet bedoeld voor publieke bijdragen, maar voel je vrij om de structuur of ideeën te gebruiken voor je eigen showcase-app.
|
||||
```bash
|
||||
composer format
|
||||
composer format:check
|
||||
```
|
||||
|
||||
---
|
||||
Blade formatter:
|
||||
|
||||
## 💌 Contact
|
||||
```bash
|
||||
npm run format
|
||||
npm run format:check
|
||||
```
|
||||
|
||||
Wil je mij benaderen voor een samenwerking of opdracht?
|
||||
Gebruik het contactformulier op de site of stuur direct een bericht via [Telegram](https://t.me/robertguagliardo).
|
||||
Tests en analyse:
|
||||
|
||||
```bash
|
||||
composer test
|
||||
composer analyse
|
||||
```
|
||||
|
||||
## Contact
|
||||
|
||||
Gebruik het contactformulier op de site of stuur me een bericht via [Telegram](https://t.me/robertguagliardo).
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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.',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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 ?? '–';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,10 +6,13 @@ 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';
|
||||
@@ -22,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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Personalia extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\PersonaliaFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['key', 'value', 'hidden', 'icon'];
|
||||
@@ -16,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,23 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,10 +6,13 @@ 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 = [
|
||||
@@ -20,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;
|
||||
}
|
||||
|
||||
@@ -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,9 @@
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"analyse": [
|
||||
"phpstan analyse --memory-limit=1G"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
|
||||
199
composer.lock
generated
199
composer.lock
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
*/
|
||||
class EducationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$startDate = fake()->dateTimeBetween('-8 years', '-2 years');
|
||||
|
||||
@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
*/
|
||||
class PersonaliaFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
*/
|
||||
class SkillFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
*/
|
||||
class WorkExperienceFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$startDate = fake()->dateTimeBetween('-8 years', '-1 year');
|
||||
|
||||
@@ -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');
|
||||
|
||||
11
phpstan.neon
Normal file
11
phpstan.neon
Normal file
@@ -0,0 +1,11 @@
|
||||
includes:
|
||||
- vendor/larastan/larastan/extension.neon
|
||||
|
||||
parameters:
|
||||
level: 7
|
||||
paths:
|
||||
- app
|
||||
- database
|
||||
- routes
|
||||
|
||||
tmpDir: storage/framework/phpstan
|
||||
2
resources/images/sitiweb.svg
Normal file
2
resources/images/sitiweb.svg
Normal 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 |
@@ -134,7 +134,7 @@
|
||||
© {{ 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>
|
||||
|
||||
@@ -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']);
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ test('the homepage shows the public cv data', function () {
|
||||
->assertViewHas('personalia', fn ($personalia) => $personalia->contains($personalium))
|
||||
->assertViewHas('education', fn ($educations) => $educations->contains($education))
|
||||
->assertViewHas('experience', fn ($experiences) => $experiences->contains($experience));
|
||||
})->skip('Homepage currently depends on missing public/storage/sitiweb.svg.');
|
||||
});
|
||||
|
||||
test('a hidden personalia value can be requested and the click is queued for notification', function () {
|
||||
Queue::fake();
|
||||
|
||||
@@ -88,11 +88,12 @@ test('an authenticated user can update personalia', function () {
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('personalia.index'));
|
||||
|
||||
expect($personalium->refresh())
|
||||
->value->toBe('new@example.com')
|
||||
->hidden->toBeFalse()
|
||||
->icon->toBe('fa-regular fa-envelope');
|
||||
})->skip('PersonaliaController::update currently uses Request instead of PersonaliaRequest.');
|
||||
$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();
|
||||
@@ -104,4 +105,4 @@ test('an authenticated user can delete personalia', function () {
|
||||
->assertRedirect(route('personalia.index'));
|
||||
|
||||
$this->assertDatabaseMissing('personalia', ['id' => $personalium->id]);
|
||||
})->skip('PersonaliaController::destroy currently does not match the resource route parameter binding.');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user