feat: complete user authentication, admin guard, customer account dashboard and branded HTML emails

This commit is contained in:
2026-11-01 13:55:45 +01:00
parent b6ee6938cb
commit ee89da5e61
37 changed files with 1895 additions and 22 deletions
+2 -2
View File
@@ -53,8 +53,8 @@ MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="info@jouwdriedee.nl"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="Jouwdriedee.nl"
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
@@ -0,0 +1,69 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class AdminCredentialsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:admin-credentials {--reset : Generate a new random password for the admin}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Toon of herstel de beheerder (admin) inloggegevens';
/**
* Execute the console command.
*/
public function handle(): int
{
$email = 'info@jouwdriedee.nl';
$admin = User::where('email', $email)->first();
if (! $admin || $this->option('reset')) {
$newPassword = Str::password(16);
$admin = User::updateOrCreate(
['email' => $email],
[
'name' => 'Jouwdriedee Beheer',
'password' => Hash::make($newPassword),
'is_admin' => true,
'email_verified_at' => now(),
]
);
$this->info('Admin gebruiker succesvol aangemaakt/bijgewerkt met een nieuw wachtwoord:');
$this->table(['Veld', 'Waarde'], [
['E-mailadres', $email],
['Nieuw Wachtwoord', $newPassword],
['Admin Status', 'Ja (is_admin = true)'],
]);
return self::SUCCESS;
}
$this->info("Admin gebruiker '{$email}' bestaat reeds in het systeem.");
$this->table(['Veld', 'Waarde'], [
['E-mailadres', $email],
['Naam', $admin->name],
['Admin Status', $admin->isAdmin() ? 'Ja' : 'Nee'],
['Aangemaakt op', $admin->created_at->format('d-m-Y H:i')],
]);
$this->line("Tip: Draai 'php artisan app:admin-credentials --reset' om een nieuw random wachtwoord te genereren.");
return self::SUCCESS;
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
class AccountController extends Controller
{
/**
* Display the customer account dashboard & order history.
*/
public function index(): View
{
$user = Auth::user();
$orders = Order::where('user_id', $user->id)
->orWhere('customer_email', $user->email)
->with('items')
->latest()
->get();
return view('account.index', compact('user', 'orders'));
}
/**
* Update customer profile info.
*/
public function updateProfile(Request $request): RedirectResponse
{
$user = Auth::user();
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
]);
$user->update($validated);
return back()->with('success', 'Jouw profielgegevens zijn succesvol bijgewerkt.');
}
/**
* Update customer password.
*/
public function updatePassword(Request $request): RedirectResponse
{
$user = Auth::user();
$validated = $request->validate([
'current_password' => 'required|current_password',
'password' => 'required|string|min:8|confirmed',
], [
'current_password.current_password' => 'Het huidige wachtwoord is onjuist.',
'password.min' => 'Het nieuwe wachtwoord moet minimaal 8 tekens lang zijn.',
'password.confirmed' => 'De nieuwe wachtwoorden komen niet met elkaar overeen.',
]);
$user->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('success', 'Jouw wachtwoord is succesvol gewijzigd.');
}
}
@@ -46,4 +46,3 @@ class CategoryController extends Controller
->with('success', "Categorie '{$category->name}' is succesvol bijgewerkt!"); ->with('success', "Categorie '{$category->name}' is succesvol bijgewerkt!");
} }
} }
@@ -133,4 +133,3 @@ class ContentController extends Controller
->with('success', 'Alle teksten en lay-out instellingen zijn hersteld naar de standaarden.'); ->with('success', 'Alle teksten en lay-out instellingen zijn hersteld naar de standaarden.');
} }
} }
@@ -51,4 +51,3 @@ class OrderController extends Controller
->with('success', "Bestelling #{$order->order_number} status is gewijzigd naar '{$validated['status']}'!"); ->with('success', "Bestelling #{$order->order_number} status is gewijzigd naar '{$validated['status']}'!");
} }
} }
@@ -132,4 +132,3 @@ class ProductController extends Controller
->with('success', "Product '{$name}' is verwijderd."); ->with('success', "Product '{$name}' is verwijderd.");
} }
} }
@@ -0,0 +1,124 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Mail\PasswordResetMail;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Throwable;
class ForgotPasswordController extends Controller
{
/**
* Display the form to request a password reset link.
*/
public function showLinkRequestForm(): View
{
return view('auth.forgot-password');
}
/**
* Send a reset link to the given user.
*/
public function sendResetLinkEmail(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
$user = User::where('email', $request->input('email'))->first();
// Always show friendly message to prevent email enumeration
if (! $user) {
return back()->with('success', 'Als dit e-mailadres bij ons bekend is, hebben we zojuist een herstellink verstuurd.');
}
$token = Str::random(64);
DB::table('password_reset_tokens')->updateOrInsert(
['email' => $user->email],
[
'token' => Hash::make($token),
'created_at' => Carbon::now(),
]
);
$resetUrl = route('password.reset', [
'token' => $token,
'email' => $user->email,
]);
try {
Mail::to($user->email)->send(new PasswordResetMail($user, $resetUrl));
} catch (Throwable $e) {
report($e);
}
return back()->with('success', 'Als dit e-mailadres bij ons bekend is, hebben we zojuist een herstellink verstuurd.');
}
/**
* Display the password reset view for the given token.
*/
public function showResetForm(Request $request, string $token): View
{
return view('auth.reset-password', [
'token' => $token,
'email' => $request->input('email'),
]);
}
/**
* Reset the given user's password.
*/
public function reset(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required|string',
'email' => 'required|email',
'password' => 'required|string|min:8|confirmed',
], [
'password.min' => 'Het nieuwe wachtwoord moet minimaal 8 tekens lang zijn.',
'password.confirmed' => 'De wachtwoorden komen niet met elkaar overeen.',
]);
$record = DB::table('password_reset_tokens')
->where('email', $request->input('email'))
->first();
if (! $record || ! Hash::check($request->input('token'), $record->token)) {
throw ValidationException::withMessages([
'email' => ['Deze herstellink is ongeldig of verlopen.'],
]);
}
if (Carbon::parse($record->created_at)->addMinutes(60)->isPast()) {
DB::table('password_reset_tokens')->where('email', $request->input('email'))->delete();
throw ValidationException::withMessages([
'email' => ['Deze herstellink is verlopen. Vraag alsjeblieft een nieuwe link aan.'],
]);
}
$user = User::where('email', $request->input('email'))->firstOrFail();
$user->update([
'password' => Hash::make($request->input('password')),
]);
DB::table('password_reset_tokens')->where('email', $request->input('email'))->delete();
Auth::login($user);
return redirect()->route('account.index')
->with('success', 'Jouw wachtwoord is succesvol gewijzigd! Je bent direct ingelogd.');
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class LoginController extends Controller
{
/**
* Show the login form.
*/
public function showLoginForm(): View|RedirectResponse
{
if (Auth::check()) {
return Auth::user()->isAdmin()
? redirect()->route('admin.content.index')
: redirect()->route('account.index');
}
return view('auth.login');
}
/**
* Handle a login request.
*/
public function login(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
$remember = $request->boolean('remember');
if (Auth::attempt($credentials, $remember)) {
$request->session()->regenerate();
$user = Auth::user();
if ($user->isAdmin()) {
return redirect()->intended(route('admin.content.index'))
->with('success', "Welkom terug, {$user->name}! Je bent ingelogd als beheerder.");
}
return redirect()->intended(route('account.index'))
->with('success', "Welkom terug, {$user->name}!");
}
throw ValidationException::withMessages([
'email' => ['Deze inloggegevens komen niet overeen met onze gegevens.'],
]);
}
/**
* Log the user out.
*/
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('home')
->with('success', 'Je bent succesvol uitgelogd.');
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\View\View;
use Throwable;
class RegisterController extends Controller
{
/**
* Show the registration form.
*/
public function showRegistrationForm(): View|RedirectResponse
{
if (Auth::check()) {
return redirect()->route('account.index');
}
return view('auth.register');
}
/**
* Handle an incoming registration request.
*/
public function register(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
], [
'name.required' => 'Vul alsjeblieft je naam in.',
'email.required' => 'Vul alsjeblieft een geldig e-mailadres in.',
'email.unique' => 'Er bestaat al een account met dit e-mailadres.',
'password.required' => 'Kies een veilig wachtwoord.',
'password.min' => 'Het wachtwoord moet minimaal 8 tekens lang zijn.',
'password.confirmed' => 'De twee wachtwoorden komen niet met elkaar overeen.',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'is_admin' => false,
]);
// Stuur welkomstmail
try {
Mail::to($user->email)->send(new WelcomeMail($user));
} catch (Throwable $e) {
report($e);
}
Auth::login($user);
return redirect()->route('account.index')
->with('success', "Welkom bij Jouwdriedee.nl, {$user->name}! Je account is aangemaakt en er is een bevestiging gestuurd naar {$user->email}.");
}
}
@@ -2,9 +2,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Mail\OrderConfirmationMail;
use App\Models\Order; use App\Models\Order;
use App\Models\OrderItem; use App\Models\OrderItem;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CheckoutController extends Controller class CheckoutController extends Controller
@@ -59,6 +61,7 @@ class CheckoutController extends Controller
$orderNumber = 'J3D-'.date('Y').'-'.strtoupper(Str::random(6)); $orderNumber = 'J3D-'.date('Y').'-'.strtoupper(Str::random(6));
$order = Order::create([ $order = Order::create([
'user_id' => auth()->id(),
'order_number' => $orderNumber, 'order_number' => $orderNumber,
'customer_name' => $validated['customer_name'], 'customer_name' => $validated['customer_name'],
'customer_email' => $validated['customer_email'], 'customer_email' => $validated['customer_email'],
@@ -100,6 +103,13 @@ class CheckoutController extends Controller
]); ]);
} }
// Stuur mooie bestelbevestiging e-mail
try {
Mail::to($order->customer_email)->send(new OrderConfirmationMail($order->load('items')));
} catch (\Throwable $e) {
report($e);
}
// Leeg winkelwagen na succesvolle plaatsing // Leeg winkelwagen na succesvolle plaatsing
session()->forget('cart'); session()->forget('cart');
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsAdmin
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
if (! auth()->check()) {
return redirect()->guest(route('login'))
->with('error', 'Log eerst in met je beheerder-account om toegang te krijgen tot het admin gedeelte.');
}
if (! auth()->user()->isAdmin()) {
return redirect()->route('home')
->with('error', 'Je hebt geen beheerderstoegang tot deze pagina.');
}
return $next($request);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Mail;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderConfirmationMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public Order $order) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Bestelbevestiging #'.$this->order->order_number.' - Jouwdriedee.nl',
);
}
public function content(): Content
{
return new Content(
view: 'emails.order-confirmation',
);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class PasswordResetMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public User $user,
public string $resetUrl
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Wachtwoord opnieuw instellen voor Jouwdriedee.nl',
);
}
public function content(): Content
{
return new Content(
view: 'emails.password-reset',
);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WelcomeMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public User $user) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welkom bij Jouwdriedee.nl! 🎉',
);
}
public function content(): Content
{
return new Content(
view: 'emails.welcome',
);
}
}
+7
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
class Order extends Model class Order extends Model
@@ -11,6 +12,7 @@ class Order extends Model
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'user_id',
'order_number', 'order_number',
'customer_name', 'customer_name',
'customer_email', 'customer_email',
@@ -40,6 +42,11 @@ class Order extends Model
return $this->hasMany(OrderItem::class); return $this->hasMany(OrderItem::class);
} }
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function getFormattedTotalAttribute(): string public function getFormattedTotalAttribute(): string
{ {
return '€ '.number_format($this->total, 2, ',', '.'); return '€ '.number_format($this->total, 2, ',', '.');
+21 -2
View File
@@ -2,15 +2,15 @@
namespace App\Models; namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory; use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password'])] #[Fillable(['name', 'email', 'password', 'is_admin'])]
#[Hidden(['password', 'remember_token'])] #[Hidden(['password', 'remember_token'])]
class User extends Authenticatable class User extends Authenticatable
{ {
@@ -27,6 +27,25 @@ class User extends Authenticatable
return [ return [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'is_admin' => 'boolean',
]; ];
} }
/**
* Check if the user has administrative privileges.
*/
public function isAdmin(): bool
{
return (bool) $this->is_admin;
}
/**
* Orders placed by this user.
*
* @return HasMany<Order, $this>
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
} }
-1
View File
@@ -8,4 +8,3 @@ if (! function_exists('setting')) {
return Setting::get($key, $default); return Setting::get($key, $default);
} }
} }
+4
View File
@@ -1,5 +1,6 @@
<?php <?php
use App\Http\Middleware\EnsureUserIsAdmin;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
@@ -13,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*'); $middleware->trustProxies(at: '*');
$middleware->alias([
'admin' => EnsureUserIsAdmin::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen( $exceptions->shouldRenderJsonWhen(
+2 -2
View File
@@ -111,8 +111,8 @@ return [
*/ */
'from' => [ 'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 'address' => env('MAIL_FROM_ADDRESS', 'info@jouwdriedee.nl'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Jouwdriedee.nl')),
], ],
]; ];
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false)->after('password');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->after('id')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('user_id');
});
}
};
+12
View File
@@ -6,12 +6,24 @@ use App\Models\Category;
use App\Models\Color; use App\Models\Color;
use App\Models\Material; use App\Models\Material;
use App\Models\Product; use App\Models\Product;
use App\Models\User;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
// 0. Hoofdbeheerder (Admin) met sterk willekeurig wachtwoord
User::updateOrCreate(
['email' => 'info@jouwdriedee.nl'],
[
'name' => 'Jouwdriedee Beheer',
'password' => Hash::make('Jw3D!x9#QmK7$vR2'),
'is_admin' => true,
'email_verified_at' => now(),
]
);
// 1. Materialen // 1. Materialen
$materials = [ $materials = [
[ [
+222
View File
@@ -0,0 +1,222 @@
@extends('layouts.app')
@section('title', 'Mijn Account | Jouwdriedee.nl')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12 space-y-8" x-data="{ tab: 'orders' }">
<!-- Account Welcome Banner -->
<div class="p-6 sm:p-8 rounded-3xl bg-gradient-to-r from-amber-500/10 via-rose-500/10 to-indigo-500/10 border border-slate-200/80 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div class="flex items-center space-x-4">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-tr from-amber-400 to-rose-400 flex items-center justify-center text-white font-heading font-black text-2xl shadow-xs">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<div>
<div class="flex items-center space-x-2">
<h1 class="font-heading text-xl sm:text-2xl font-black text-slate-900">
Hallo, {{ $user->name }}!
</h1>
@if($user->isAdmin())
<span class="px-2.5 py-0.5 rounded-full bg-slate-900 text-amber-400 text-[10px] font-black uppercase tracking-wider">
Admin Beheerder
</span>
@endif
</div>
<p class="text-xs text-slate-500 mt-0.5">
{{ $user->email }} &bull; Lid sinds {{ $user->created_at->format('d-m-Y') }}
</p>
</div>
</div>
<div class="flex items-center space-x-2.5 self-stretch sm:self-auto justify-end">
@if($user->isAdmin())
<a href="{{ route('admin.content.index') }}" class="px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-amber-300 font-black text-xs transition flex items-center space-x-1.5 shadow-xs">
<i data-lucide="shield" class="w-4 h-4"></i>
<span>Admin Dashboard</span>
</a>
@endif
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit" class="px-4 py-2 rounded-xl bg-white hover:bg-rose-50 text-rose-600 border border-slate-200 text-xs font-bold transition flex items-center space-x-1.5 cursor-pointer shadow-2xs">
<i data-lucide="log-out" class="w-3.5 h-3.5"></i>
<span>Uitloggen</span>
</button>
</form>
</div>
</div>
<!-- Navigation Tabs -->
<div class="flex items-center space-x-2 border-b border-slate-200 text-xs font-bold pb-2">
<button
type="button"
@click="tab = 'orders'"
:class="tab === 'orders' ? 'bg-amber-400 text-slate-950 font-black' : 'bg-white text-slate-600 hover:bg-slate-100'"
class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 cursor-pointer"
>
<i data-lucide="package" class="w-4 h-4"></i>
<span>Mijn Bestellingen ({{ $orders->count() }})</span>
</button>
<button
type="button"
@click="tab = 'profile'"
:class="tab === 'profile' ? 'bg-amber-400 text-slate-950 font-black' : 'bg-white text-slate-600 hover:bg-slate-100'"
class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 cursor-pointer"
>
<i data-lucide="user-cog" class="w-4 h-4"></i>
<span>Profiel & Beveiliging</span>
</button>
</div>
<!-- TAB 1: BESTELLINGEN -->
<div x-show="tab === 'orders'" class="space-y-4">
@if($orders->isEmpty())
<div class="p-12 text-center rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-3">
<div class="w-14 h-14 rounded-2xl bg-amber-50 text-amber-600 flex items-center justify-center mx-auto text-2xl">
🛍️
</div>
<h3 class="font-heading text-lg font-black text-slate-900">Nog geen bestellingen geplaatst</h3>
<p class="text-xs text-slate-500 max-w-sm mx-auto">
Je hebt nog geen 3D prints besteld. Ontdek onze fidget toys, Pokémon figuren en popculture modellen!
</p>
<div class="pt-2">
<a href="{{ route('catalog') }}" class="inline-flex items-center space-x-2 px-5 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 text-xs font-black transition">
<span>Winkelassortiment bekijken</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a>
</div>
</div>
@else
<div class="space-y-4">
@foreach($orders as $order)
<div class="p-5 sm:p-6 rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<!-- Header regel -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 gap-2">
<div>
<span class="font-mono font-bold text-xs text-slate-900">#{{ $order->order_number }}</span>
<span class="text-xs text-slate-400 ml-2">&bull; {{ $order->created_at->format('d-m-Y om H:i') }}</span>
</div>
<div class="flex items-center space-x-3">
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase
@if($order->status === 'completed' || $order->status === 'shipped' || $order->status === 'verzonden') bg-emerald-100 text-emerald-800
@elseif($order->status === 'printed' || $order->status === 'processing' || $order->status === 'in_print') bg-sky-100 text-sky-800
@elseif($order->status === 'cancelled' || $order->status === 'geannuleerd') bg-rose-100 text-rose-800
@else bg-amber-100 text-amber-900 @endif
">
{{ str_replace('_', ' ', $order->status) }}
</span>
<span class="font-heading font-black text-slate-900 text-sm">
{{ $order->formatted_total }}
</span>
</div>
</div>
<!-- Items -->
<div class="divide-y divide-slate-100">
@foreach($order->items as $item)
<div class="py-2.5 flex items-start justify-between text-xs">
<div>
<div class="font-bold text-slate-900">{{ $item->product_name }}</div>
<div class="text-[11px] text-slate-500 mt-0.5">
@if($item->material) Materiaal: <span class="font-semibold">{{ strtoupper($item->material) }}</span> &bull; @endif
@if($item->color) Kleur: <span class="font-semibold">{{ $item->color }}</span> &bull; @endif
Schaal: {{ $item->scale ?? '100%' }}
</div>
</div>
<div class="text-right">
<span class="font-semibold text-slate-600">{{ $item->quantity }}x</span>
<span class="font-bold text-slate-900 ml-1">&euro; {{ number_format($item->total_price, 2, ',', '.') }}</span>
</div>
</div>
@endforeach
</div>
<!-- Bezorging adres -->
<div class="pt-3 border-t border-slate-100 flex items-center justify-between text-[11px] text-slate-500">
<span>Bezorgadres: {{ $order->shipping_street }} {{ $order->shipping_number }}, {{ $order->shipping_postal_code }} {{ $order->shipping_city }}</span>
<span class="font-bold text-emerald-600 flex items-center space-x-1">
<i data-lucide="truck" class="w-3.5 h-3.5"></i>
<span>PostNL</span>
</span>
</div>
</div>
@endforeach
</div>
@endif
</div>
<!-- TAB 2: PROFIEL & BEVEILIGING -->
<div x-show="tab === 'profile'" class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Profielgegevens -->
<div class="p-6 rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2">
<i data-lucide="user" class="w-4 h-4 text-amber-500"></i>
<span>Persoonlijke gegevens</span>
</h3>
<form action="{{ route('account.profile') }}" method="POST" class="space-y-4">
@csrf
@method('PUT')
<div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-700">Naam</label>
<input type="text" name="name" value="{{ old('name', $user->name) }}" required class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
</div>
<div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-700">E-mailadres</label>
<input type="email" name="email" value="{{ old('email', $user->email) }}" required class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
</div>
<div class="pt-2">
<button type="submit" class="px-5 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition cursor-pointer">
Gegevens Opslaan
</button>
</div>
</form>
</div>
<!-- Wachtwoord Wijzigen -->
<div class="p-6 rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2">
<i data-lucide="lock" class="w-4 h-4 text-rose-500"></i>
<span>Wachtwoord wijzigen</span>
</h3>
<form action="{{ route('account.password') }}" method="POST" class="space-y-4">
@csrf
@method('PUT')
<div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-700">Huidig wachtwoord</label>
<input type="password" name="current_password" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
</div>
<div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label>
<input type="password" name="password" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
</div>
<div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-700">Bevestig nieuw wachtwoord</label>
<input type="password" name="password_confirmation" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
</div>
<div class="pt-2">
<button type="submit" class="px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-black text-xs transition cursor-pointer">
Wachtwoord Wijzigen
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,63 @@
@extends('layouts.app')
@section('title', 'Wachtwoord Vergeten | Jouwdriedee.nl')
@section('content')
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6">
<!-- Header -->
<div class="text-center space-y-2">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-indigo-100 text-indigo-900 shadow-2xs mb-1">
<i data-lucide="key-round" class="w-6 h-6 text-indigo-600"></i>
</div>
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Wachtwoord vergeten</h1>
<p class="text-xs text-slate-500">Vul je e-mailadres in en ontvang een beveiligde herstellink</p>
</div>
<!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
<form action="{{ route('password.email') }}" method="POST" class="space-y-4">
@csrf
<!-- E-mailadres -->
<div class="space-y-1.5">
<label for="email" class="block text-xs font-bold text-slate-700">Jouw geregistreerde e-mailadres</label>
<div class="relative">
<input
type="email"
name="email"
id="email"
value="{{ old('email') }}"
required
autofocus
placeholder="jouw@email.nl"
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('email') border-rose-400 @enderror"
>
<i data-lucide="mail" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div>
@error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Submit Button -->
<div class="pt-2">
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<i data-lucide="send" class="w-4 h-4"></i>
<span>Herstellink Versturen</span>
</button>
</div>
</form>
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600">
<a href="{{ route('login') }}" class="text-rose-600 hover:text-rose-700 underline inline-flex items-center space-x-1">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar inloggen</span>
</a>
</div>
</div>
</div>
</div>
@endsection
+95
View File
@@ -0,0 +1,95 @@
@extends('layouts.app')
@section('title', 'Inloggen | Jouwdriedee.nl')
@section('content')
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6">
<!-- Header -->
<div class="text-center space-y-2">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-amber-100 text-amber-900 shadow-2xs mb-1">
<i data-lucide="log-in" class="w-6 h-6 text-amber-600"></i>
</div>
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Welkom terug</h1>
<p class="text-xs text-slate-500">Log in met jouw e-mailadres en wachtwoord</p>
</div>
<!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
<form action="{{ route('login.submit') }}" method="POST" class="space-y-4">
@csrf
<!-- E-mailadres -->
<div class="space-y-1.5">
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres</label>
<div class="relative">
<input
type="email"
name="email"
id="email"
value="{{ old('email') }}"
required
autofocus
placeholder="jouw@email.nl"
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('email') border-rose-400 @enderror"
>
<i data-lucide="mail" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div>
@error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Wachtwoord -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<label for="password" class="block text-xs font-bold text-slate-700">Wachtwoord</label>
<a href="{{ route('password.request') }}" class="text-[11px] font-bold text-rose-600 hover:text-rose-700 transition">
Wachtwoord vergeten?
</a>
</div>
<div class="relative">
<input
type="password"
name="password"
id="password"
required
placeholder="••••••••"
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('password') border-rose-400 @enderror"
>
<i data-lucide="lock" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div>
@error('password')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Onthoud mij -->
<div class="flex items-center justify-between pt-1">
<label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" name="remember" value="1" class="w-4 h-4 rounded text-amber-500 focus:ring-amber-400 border-slate-300">
<span class="text-xs font-bold text-slate-600">Onthoud mij op dit apparaat</span>
</label>
</div>
<!-- Submit Button -->
<div class="pt-2">
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<span>Inloggen</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i>
</button>
</div>
</form>
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600">
<span>Nog geen account?</span>
<a href="{{ route('register') }}" class="text-rose-600 hover:text-rose-700 underline ml-1">
Direct registreren
</a>
</div>
</div>
</div>
</div>
@endsection
+118
View File
@@ -0,0 +1,118 @@
@extends('layouts.app')
@section('title', 'Registreren | Jouwdriedee.nl')
@section('content')
<div class="min-h-[75vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6">
<!-- Header -->
<div class="text-center space-y-2">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-rose-100 text-rose-900 shadow-2xs mb-1">
<i data-lucide="user-plus" class="w-6 h-6 text-rose-600"></i>
</div>
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Account aanmaken</h1>
<p class="text-xs text-slate-500">Meld je aan om bestellingen te volgen en sneller te bestellen</p>
</div>
<!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
<form action="{{ route('register.submit') }}" method="POST" class="space-y-4">
@csrf
<!-- Naam -->
<div class="space-y-1.5">
<label for="name" class="block text-xs font-bold text-slate-700">Voor- en achternaam *</label>
<div class="relative">
<input
type="text"
name="name"
id="name"
value="{{ old('name') }}"
required
autofocus
placeholder="bijv. Jan Jansen"
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('name') border-rose-400 @enderror"
>
<i data-lucide="user" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div>
@error('name')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- E-mailadres -->
<div class="space-y-1.5">
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres *</label>
<div class="relative">
<input
type="email"
name="email"
id="email"
value="{{ old('email') }}"
required
placeholder="jouw@email.nl"
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('email') border-rose-400 @enderror"
>
<i data-lucide="mail" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div>
@error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Wachtwoord -->
<div class="space-y-1.5">
<label for="password" class="block text-xs font-bold text-slate-700">Wachtwoord (minimaal 8 tekens) *</label>
<div class="relative">
<input
type="password"
name="password"
id="password"
required
placeholder="••••••••"
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('password') border-rose-400 @enderror"
>
<i data-lucide="lock" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div>
@error('password')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Bevestig Wachtwoord -->
<div class="space-y-1.5">
<label for="password_confirmation" class="block text-xs font-bold text-slate-700">Bevestig wachtwoord *</label>
<div class="relative">
<input
type="password"
name="password_confirmation"
id="password_confirmation"
required
placeholder="••••••••"
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400"
>
<i data-lucide="shield-check" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div>
</div>
<!-- Submit Button -->
<div class="pt-2">
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<span>Account Aanmaken</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i>
</button>
</div>
</form>
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600">
<span>Heb je al een account?</span>
<a href="{{ route('login') }}" class="text-rose-600 hover:text-rose-700 underline ml-1">
Hier inloggen
</a>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,83 @@
@extends('layouts.app')
@section('title', 'Nieuw Wachtwoord Instellen | Jouwdriedee.nl')
@section('content')
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6">
<!-- Header -->
<div class="text-center space-y-2">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-amber-100 text-amber-900 shadow-2xs mb-1">
<i data-lucide="lock" class="w-6 h-6 text-amber-600"></i>
</div>
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Nieuw wachtwoord</h1>
<p class="text-xs text-slate-500">Kies een nieuw en veilig wachtwoord voor jouw account</p>
</div>
<!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
<form action="{{ route('password.update') }}" method="POST" class="space-y-4">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<!-- E-mailadres -->
<div class="space-y-1.5">
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres</label>
<input
type="email"
name="email"
id="email"
value="{{ old('email', $email) }}"
required
readonly
class="w-full bg-slate-100 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-600 font-semibold cursor-not-allowed"
>
@error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Nieuw Wachtwoord -->
<div class="space-y-1.5">
<label for="password" class="block text-xs font-bold text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label>
<input
type="password"
name="password"
id="password"
required
autofocus
placeholder="••••••••"
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('password') border-rose-400 @enderror"
>
@error('password')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Bevestig Nieuw Wachtwoord -->
<div class="space-y-1.5">
<label for="password_confirmation" class="block text-xs font-bold text-slate-700">Bevestig nieuw wachtwoord</label>
<input
type="password"
name="password_confirmation"
id="password_confirmation"
required
placeholder="••••••••"
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400"
>
</div>
<!-- Submit Button -->
<div class="pt-2">
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Wachtwoord Opslaan & Inloggen</span>
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Jouwdriedee.nl')</title>
<style>
/* Base styles */
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; outline: none; text-decoration: none; }
body { margin: 0; padding: 0; width: 100% !important; background-color: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
/* Interactive elements */
.btn-primary {
display: inline-block;
background-color: #FBBF24;
color: #0F172A !important;
font-weight: 800;
font-size: 14px;
padding: 12px 28px;
border-radius: 12px;
text-decoration: none;
text-align: center;
}
.btn-primary:hover {
background-color: #F59E0B;
}
</style>
</head>
<body style="margin: 0; padding: 0; background-color: #F8FAFC; color: #1E293B;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #F8FAFC; padding: 30px 10px;">
<tr>
<td align="center">
<!-- Outer Container (Max 600px) -->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px; background-color: #FFFFFF; border-radius: 20px; overflow: hidden; border: 1px solid #E2E8F0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);">
<!-- Top Colorful Accent Gradient Bar -->
<tr>
<td height="6" style="background: linear-gradient(90deg, #F43F5E 0%, #F59E0B 50%, #38BDF8 100%);"></td>
</tr>
<!-- Header with Logo -->
<tr>
<td align="center" style="padding: 28px 24px 20px 24px; border-bottom: 1px solid #F1F5F9; background-color: #FFFFFF;">
<a href="{{ config('app.url') }}" target="_blank" style="text-decoration: none; display: inline-block;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<!-- Isometric 3D Logo Mark representation in SVG/colors -->
<td align="center" valign="middle" style="background: linear-gradient(135deg, #F59E0B 0%, #F43F5E 50%, #6366F1 100%); border-radius: 12px; width: 40px; height: 40px; text-align: center; color: #FFFFFF; font-weight: 900; font-size: 18px; line-height: 40px;">
3D
</td>
<td valign="middle" style="padding-left: 12px; text-align: left;">
<span style="font-size: 22px; font-weight: 900; color: #0F172A; letter-spacing: -0.5px;">Jouw<span style="color: #F43F5E;">driedee</span><span style="background-color: #6366F1; color: #FFFFFF; font-size: 10px; font-weight: 900; padding: 2px 5px; border-radius: 4px; margin-left: 4px; vertical-align: top;">.NL</span></span>
<span style="display: block; font-size: 11px; font-weight: 700; color: #64748B;">Nederlandse 3D Printstudio &amp; Webshop</span>
</td>
</tr>
</table>
</a>
</td>
</tr>
<!-- Main Email Content Slot -->
<tr>
<td style="padding: 32px 32px 28px 32px;">
@yield('content')
</td>
</tr>
<!-- Footer Info & Trust Badges -->
<tr>
<td style="background-color: #0F172A; padding: 28px 32px; color: #94A3B8; text-align: center; font-size: 12px; line-height: 1.6;">
<div style="font-weight: 800; color: #F1F5F9; font-size: 13px; margin-bottom: 6px;">
Jouwdriedee.nl &bull; Hoogwaardige 3D Prints &amp; Collectibles
</div>
<div style="color: #94A3B8; margin-bottom: 14px;">
Geprint met precisie op Bambu Lab printers met 100% veilig Bio-PLA+ en PETG filament.
</div>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom: 16px; border-top: 1px solid #1E293B; padding-top: 14px;">
<tr>
<td align="center" style="color: #F59E0B; font-size: 11px; font-weight: 700;">
&bull; Vragen of speciale wensen? Stuur een e-mail naar <a href="mailto:info@jouwdriedee.nl" style="color: #FBBF24; text-decoration: underline;">info@jouwdriedee.nl</a> &bull;
</td>
</tr>
</table>
<div style="font-size: 11px; color: #64748B;">
&copy; {{ date('Y') }} Jouwdriedee.nl. Alle rechten voorbehouden.
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,104 @@
@extends('emails.layouts.master')
@section('title', 'Bestelbevestiging #' . $order->order_number . ' | Jouwdriedee.nl')
@section('content')
<div style="text-align: left;">
<div style="display: inline-block; background-color: #D1FAE5; color: #065F46; font-size: 11px; font-weight: 800; padding: 4px 10px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px;">
✓ Bestelling Ontvangen
</div>
<h1 style="color: #0F172A; font-size: 22px; font-weight: 900; margin: 0 0 10px 0; line-height: 1.3;">
Bedankt voor je bestelling, {{ $order->customer_name }}!
</h1>
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin: 0 0 18px 0;">
We hebben je bestelling <strong>#{{ $order->order_number }}</strong> in goede orde ontvangen. Onze 3D-printers worden voorbereid en jouw items worden met zorg en precisie geprint.
</p>
<!-- Bestelde Items Overzicht -->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin: 20px 0; border: 1px solid #E2E8F0; border-radius: 14px; overflow: hidden;">
<tr style="background-color: #F8FAFC; border-bottom: 1px solid #E2E8F0;">
<th align="left" style="padding: 12px 16px; font-size: 11px; font-weight: 800; text-transform: uppercase; color: #64748B;">Item</th>
<th align="center" style="padding: 12px 16px; font-size: 11px; font-weight: 800; text-transform: uppercase; color: #64748B;">Aantal</th>
<th align="right" style="padding: 12px 16px; font-size: 11px; font-weight: 800; text-transform: uppercase; color: #64748B;">Prijs</th>
</tr>
@foreach($order->items as $item)
<tr style="border-bottom: 1px solid #F1F5F9;">
<td style="padding: 14px 16px;">
<div style="font-weight: 800; font-size: 13px; color: #0F172A;">
{{ $item->product_name }}
</div>
<div style="font-size: 11px; color: #64748B; margin-top: 3px;">
@if($item->material) Materiaal: <strong>{{ strtoupper($item->material) }}</strong> &bull; @endif
@if($item->color) Kleur: <strong>{{ $item->color }}</strong> &bull; @endif
Schaal: {{ $item->scale ?? '100%' }}
</div>
</td>
<td align="center" style="padding: 14px 16px; font-size: 13px; font-weight: 700; color: #334155;">
{{ $item->quantity }}x
</td>
<td align="right" style="padding: 14px 16px; font-size: 13px; font-weight: 800; color: #0F172A;">
&euro; {{ number_format($item->total_price, 2, ',', '.') }}
</td>
</tr>
@endforeach
<!-- Subtotaal & Verzending -->
<tr style="background-color: #FAFAFA;">
<td colspan="2" style="padding: 10px 16px; font-size: 12px; color: #64748B;">Subtotaal</td>
<td align="right" style="padding: 10px 16px; font-size: 12px; font-weight: 700; color: #334155;">
&euro; {{ number_format($order->subtotal, 2, ',', '.') }}
</td>
</tr>
<tr style="background-color: #FAFAFA;">
<td colspan="2" style="padding: 10px 16px; font-size: 12px; color: #64748B;">Verzendkosten (PostNL)</td>
<td align="right" style="padding: 10px 16px; font-size: 12px; font-weight: 700; color: #334155;">
{{ $order->shipping_cost > 0 ? '€ ' . number_format($order->shipping_cost, 2, ',', '.') : 'Gratis' }}
</td>
</tr>
<tr style="background-color: #F1F5F9; border-top: 2px solid #E2E8F0;">
<td colspan="2" style="padding: 12px 16px; font-size: 14px; font-weight: 900; color: #0F172A;">Totaal incl. btw</td>
<td align="right" style="padding: 12px 16px; font-size: 16px; font-weight: 900; color: #D97706;">
{{ $order->formatted_total }}
</td>
</tr>
</table>
<!-- Afleveradres -->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 14px; margin-bottom: 24px; padding: 16px;">
<tr>
<td>
<div style="font-weight: 800; font-size: 12px; text-transform: uppercase; color: #64748B; margin-bottom: 6px;">
📦 Afleveradres:
</div>
<div style="font-size: 13px; color: #1E293B; line-height: 1.5;">
<strong>{{ $order->customer_name }}</strong><br>
{{ $order->shipping_street }} {{ $order->shipping_number }}<br>
{{ $order->shipping_postal_code }} {{ $order->shipping_city }}<br>
{{ $order->shipping_country ?? 'Nederland' }}
</div>
</td>
</tr>
</table>
<!-- Wat gebeurt er nu? -->
<div style="background-color: #EFF6FF; border: 1px solid #BFDBFE; border-radius: 12px; padding: 14px; font-size: 12px; color: #1E40AF; line-height: 1.5; margin-bottom: 20px;">
<strong>Wat gebeurt er nu?</strong><br>
1. We printen jouw model op hoge resolutie.<br>
2. We controleren de scharnieren en details met de hand.<br>
3. Je ontvangt een verzendbevestiging zodra het pakketje is overgedragen aan PostNL (binnen 2-3 werkdagen).
</div>
@auth
<div style="text-align: center; margin: 24px 0 10px 0;">
<a href="{{ route('account.index') }}" class="btn-primary" style="display: inline-block; background-color: #FBBF24; color: #0F172A !important; font-weight: 800; font-size: 13px; padding: 12px 24px; border-radius: 12px; text-decoration: none;">
Volg Bestelling in Mijn Account &rarr;
</a>
</div>
@endauth
</div>
@endsection
@@ -0,0 +1,37 @@
@extends('emails.layouts.master')
@section('title', 'Wachtwoord Opnieuw Instellen | Jouwdriedee.nl')
@section('content')
<div style="text-align: left;">
<div style="display: inline-block; background-color: #E0E7FF; color: #3730A3; font-size: 11px; font-weight: 800; padding: 4px 10px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px;">
🔒 Beveiliging
</div>
<h1 style="color: #0F172A; font-size: 22px; font-weight: 900; margin: 0 0 14px 0; line-height: 1.3;">
Wachtwoord opnieuw instellen
</h1>
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin: 0 0 16px 0;">
Hallo {{ $user->name }},<br>
We hebben een verzoek ontvangen om het wachtwoord van jouw account bij <strong>Jouwdriedee.nl</strong> te resetten. Klik op onderstaande knop om een nieuw wachtwoord te kiezen:
</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ $resetUrl }}" class="btn-primary" style="display: inline-block; background-color: #FBBF24; color: #0F172A !important; font-weight: 800; font-size: 14px; padding: 14px 32px; border-radius: 12px; text-decoration: none; box-shadow: 0 4px 12px rgba(251, 191, 36, 0.3);">
Wachtwoord Opnieuw Instellen &rarr;
</a>
</div>
<div style="background-color: #FFFBEB; border: 1px solid #FDE68A; border-radius: 12px; padding: 14px; margin-top: 24px; font-size: 12px; color: #92400E; line-height: 1.5;">
<strong>Let op:</strong> Deze link is om veiligheidsredenen slechts <strong>60 minuten</strong> geldig. Heb je zelf geen wachtwoordreset aangevraagd? Dan kun je deze e-mail veilig negeren. Jouw huidige wachtwoord blijft ongewijzigd.
</div>
<p style="color: #94A3B8; font-size: 11px; line-height: 1.4; margin-top: 24px; word-break: break-all;">
Werkt de knop niet? Kopieer en plak de volgende URL direct in je browser:<br>
<a href="{{ $resetUrl }}" style="color: #6366F1;">{{ $resetUrl }}</a>
</p>
</div>
@endsection
+51
View File
@@ -0,0 +1,51 @@
@extends('emails.layouts.master')
@section('title', 'Welkom bij Jouwdriedee.nl!')
@section('content')
<div style="text-align: left;">
<div style="display: inline-block; background-color: #FEF3C7; color: #92400E; font-size: 11px; font-weight: 800; padding: 4px 10px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px;">
🎉 Account Aangemaakt
</div>
<h1 style="color: #0F172A; font-size: 22px; font-weight: 900; margin: 0 0 14px 0; line-height: 1.3;">
Welkom bij Jouwdriedee, {{ $user->name }}!
</h1>
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin: 0 0 16px 0;">
Leuk dat je een account hebt aangemaakt bij onze 3D printstudio. Vanaf nu kun je eenvoudig jouw bestellingen volgen, favoriete modellen bewaren en nóg sneller afrekenen.
</p>
<!-- Highlight Box -->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 14px; margin: 20px 0; padding: 18px;">
<tr>
<td>
<div style="font-weight: 800; font-size: 13px; color: #0F172A; margin-bottom: 8px;">
Wat kun je verwachten van onze 3D prints?
</div>
<div style="font-size: 12px; color: #475569; line-height: 1.6;">
&bull; <strong>Haarscherpe Bambu Lab kwaliteit:</strong> Laagdiktes vanaf 0.12 mm voor soepele scharnieren.<br>
&bull; <strong>16+ Kleurvariaties:</strong> Van zijdeglans Silk tot levendige fluor en pastel tinten.<br>
&bull; <strong>Duurzaam PLA+ filament:</strong> 100% biologisch afbreekbaar en reukloos.
</div>
</td>
</tr>
</table>
<!-- Call to Actions -->
<div style="text-align: center; margin: 28px 0 20px 0;">
<a href="{{ route('catalog') }}" class="btn-primary" style="display: inline-block; background-color: #FBBF24; color: #0F172A !important; font-weight: 800; font-size: 14px; padding: 12px 28px; border-radius: 12px; text-decoration: none; margin-right: 8px;">
Bekijk de Collectie &rarr;
</a>
<a href="{{ route('account.index') }}" style="display: inline-block; background-color: #F1F5F9; color: #334155 !important; font-weight: 700; font-size: 13px; padding: 12px 20px; border-radius: 12px; text-decoration: none;">
Mijn Account
</a>
</div>
<p style="color: #64748B; font-size: 12px; line-height: 1.5; margin: 20px 0 0 0; border-top: 1px solid #F1F5F9; padding-top: 16px;">
Heb je een vraag over een print of wil je iets op maat laten maken? Neem gerust contact op met ons via <a href="mailto:info@jouwdriedee.nl" style="color: #F59E0B; font-weight: 700;">info@jouwdriedee.nl</a>.
</p>
</div>
@endsection
+73 -1
View File
@@ -102,7 +102,7 @@
$cartItems = is_array(session('cart')) ? session('cart') : []; $cartItems = is_array(session('cart')) ? session('cart') : [];
$cartCount = array_sum(array_column($cartItems, 'quantity')); $cartCount = array_sum(array_column($cartItems, 'quantity'));
@endphp @endphp
<a href="{{ route('cart.index') }}" class="px-4 py-2 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs flex items-center space-x-2 transition shadow-xs"> <a href="{{ route('cart.index') }}" class="px-3.5 sm:px-4 py-2 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs flex items-center space-x-1.5 sm:space-x-2 transition shadow-xs">
<i data-lucide="shopping-bag" class="w-4 h-4 text-slate-900"></i> <i data-lucide="shopping-bag" class="w-4 h-4 text-slate-900"></i>
<span class="hidden sm:inline">Winkelwagen</span> <span class="hidden sm:inline">Winkelwagen</span>
@if($cartCount > 0) @if($cartCount > 0)
@@ -112,6 +112,43 @@
@endif @endif
</a> </a>
<!-- User Account / Login Button -->
@guest
<a href="{{ route('login') }}" class="px-3 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold text-xs flex items-center space-x-1.5 transition">
<i data-lucide="user" class="w-4 h-4 text-slate-700"></i>
<span class="hidden sm:inline">Inloggen</span>
</a>
@else
<div class="relative" x-data="{ userMenu: false }">
<button @click="userMenu = !userMenu" type="button" class="px-3 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-900 font-black text-xs flex items-center space-x-1.5 transition cursor-pointer">
<i data-lucide="user-check" class="w-4 h-4 text-amber-500"></i>
<span class="hidden sm:inline">{{ Str::limit(auth()->user()->name, 12) }}</span>
<i data-lucide="chevron-down" class="w-3 h-3 text-slate-500"></i>
</button>
<div x-show="userMenu" @click.away="userMenu = false" x-cloak class="absolute right-0 mt-2 w-48 rounded-2xl bg-white border border-slate-200 shadow-xl py-2 z-50 text-xs font-bold text-slate-700">
<a href="{{ route('account.index') }}" class="flex items-center space-x-2 px-4 py-2 hover:bg-slate-50 text-slate-800">
<i data-lucide="user-circle" class="w-4 h-4 text-slate-500"></i>
<span>Mijn Account</span>
</a>
@if(auth()->user()->isAdmin())
<a href="{{ route('admin.content.index') }}" class="flex items-center space-x-2 px-4 py-2 hover:bg-amber-50 text-amber-900 font-black">
<i data-lucide="shield" class="w-4 h-4 text-amber-500"></i>
<span>Admin Beheer</span>
</a>
@endif
<div class="border-t border-slate-100 my-1"></div>
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit" class="w-full text-left flex items-center space-x-2 px-4 py-2 hover:bg-rose-50 text-rose-600 cursor-pointer">
<i data-lucide="log-out" class="w-4 h-4"></i>
<span>Uitloggen</span>
</button>
</form>
</div>
</div>
@endguest
<!-- Mobile Menu Trigger --> <!-- Mobile Menu Trigger -->
<button @click="mobileMenuOpen = !mobileMenuOpen" type="button" class="lg:hidden p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 transition"> <button @click="mobileMenuOpen = !mobileMenuOpen" type="button" class="lg:hidden p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 transition">
<i data-lucide="menu" class="w-5 h-5"></i> <i data-lucide="menu" class="w-5 h-5"></i>
@@ -150,6 +187,41 @@
<a href="{{ route('catalog') }}" class="px-3.5 py-2.5 rounded-xl hover:bg-slate-50 flex items-center justify-between text-slate-700"> <a href="{{ route('catalog') }}" class="px-3.5 py-2.5 rounded-xl hover:bg-slate-50 flex items-center justify-between text-slate-700">
<span>Alle Prints Bekijken</span> <span>Alle Prints Bekijken</span>
</a> </a>
<div class="border-t border-slate-100 pt-2 mt-1">
@guest
<div class="grid grid-cols-2 gap-2">
<a href="{{ route('login') }}" class="py-2.5 px-3 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 text-center font-bold text-xs flex items-center justify-center space-x-1.5">
<i data-lucide="log-in" class="w-3.5 h-3.5"></i>
<span>Inloggen</span>
</a>
<a href="{{ route('register') }}" class="py-2.5 px-3 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 text-center font-black text-xs flex items-center justify-center space-x-1.5">
<i data-lucide="user-plus" class="w-3.5 h-3.5"></i>
<span>Registreren</span>
</a>
</div>
@else
<div class="space-y-1">
<a href="{{ route('account.index') }}" class="px-3.5 py-2.5 rounded-xl hover:bg-slate-50 flex items-center justify-between text-slate-900 font-bold">
<span>Mijn Account ({{ auth()->user()->name }})</span>
<i data-lucide="user" class="w-4 h-4 text-amber-500"></i>
</a>
@if(auth()->user()->isAdmin())
<a href="{{ route('admin.content.index') }}" class="px-3.5 py-2.5 rounded-xl bg-amber-50 hover:bg-amber-100 flex items-center justify-between text-amber-950 font-black">
<span>Admin Beheer</span>
<i data-lucide="shield" class="w-4 h-4 text-amber-600"></i>
</a>
@endif
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit" class="w-full text-left px-3.5 py-2.5 rounded-xl hover:bg-rose-50 text-rose-600 font-bold flex items-center justify-between">
<span>Uitloggen</span>
<i data-lucide="log-out" class="w-4 h-4"></i>
</button>
</form>
</div>
@endguest
</div>
</div> </div>
</div> </div>
</header> </header>
+32 -2
View File
@@ -1,10 +1,14 @@
<?php <?php
use App\Http\Controllers\AccountController;
use App\Http\Controllers\Admin\CategoryController as AdminCategoryController; use App\Http\Controllers\Admin\CategoryController as AdminCategoryController;
use App\Http\Controllers\Admin\ContentController; use App\Http\Controllers\Admin\ContentController;
use App\Http\Controllers\Admin\MakerWorldController; use App\Http\Controllers\Admin\MakerWorldController;
use App\Http\Controllers\Admin\OrderController as AdminOrderController; use App\Http\Controllers\Admin\OrderController as AdminOrderController;
use App\Http\Controllers\Admin\ProductController as AdminProductController; use App\Http\Controllers\Admin\ProductController as AdminProductController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\CartController; use App\Http\Controllers\CartController;
use App\Http\Controllers\CheckoutController; use App\Http\Controllers\CheckoutController;
use App\Http\Controllers\ShopController; use App\Http\Controllers\ShopController;
@@ -77,8 +81,34 @@ Route::get('/afrekenen', [CheckoutController::class, 'index'])->name('checkout.i
Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store'); Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store');
Route::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success'); Route::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success');
// Admin & CMS Beheer // Authenticatie & Klantenportaal
Route::prefix('admin')->name('admin.')->group(function () { Route::middleware('guest')->group(function () {
Route::get('/inloggen', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit');
Route::redirect('/login', '/inloggen');
Route::get('/registreren', [RegisterController::class, 'showRegistrationForm'])->name('register');
Route::post('/registreren', [RegisterController::class, 'register'])->name('register.submit');
Route::redirect('/register', '/registreren');
Route::get('/wachtwoord-vergeten', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/wachtwoord-vergeten', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/wachtwoord-resetten/{token}', [ForgotPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/wachtwoord-resetten', [ForgotPasswordController::class, 'reset'])->name('password.update');
});
Route::middleware('auth')->group(function () {
Route::post('/uitloggen', [LoginController::class, 'logout'])->name('logout');
Route::get('/uitloggen', [LoginController::class, 'logout']); // Fallback voor GET uitloggen
// Klantendashboard / Mijn Account
Route::get('/mijn-account', [AccountController::class, 'index'])->name('account.index');
Route::put('/mijn-account/profiel', [AccountController::class, 'updateProfile'])->name('account.profile');
Route::put('/mijn-account/wachtwoord', [AccountController::class, 'updatePassword'])->name('account.password');
});
// Admin & CMS Beheer (Beveiligd: alleen ingelogde admins)
Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(function () {
Route::redirect('/', '/admin/content'); Route::redirect('/', '/admin/content');
// Layout, Stijl & Teksten CMS // Layout, Stijl & Teksten CMS
+45 -8
View File
@@ -6,6 +6,7 @@ use App\Models\Category;
use App\Models\Order; use App\Models\Order;
use App\Models\Product; use App\Models\Product;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@@ -13,10 +14,46 @@ class AdminContentTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_admin_content_editor_loads_successfully(): void protected User $admin;
protected function setUp(): void
{
parent::setUp();
$this->admin = User::create([
'name' => 'Admin Jouwdriedee',
'email' => 'info@jouwdriedee.nl',
'password' => bcrypt('Jw3D!x9#QmK7$vR2'),
'is_admin' => true,
]);
}
public function test_guest_is_redirected_to_login_when_accessing_admin(): void
{ {
$response = $this->get(route('admin.content.index')); $response = $this->get(route('admin.content.index'));
$response->assertRedirect(route('login'));
}
public function test_regular_customer_cannot_access_admin(): void
{
$customer = User::create([
'name' => 'Klant Piet',
'email' => 'piet@example.com',
'password' => bcrypt('secret1234'),
'is_admin' => false,
]);
$response = $this->actingAs($customer)->get(route('admin.content.index'));
$response->assertRedirect(route('home'));
$response->assertSessionHas('error');
}
public function test_admin_content_editor_loads_successfully(): void
{
$response = $this->actingAs($this->admin)->get(route('admin.content.index'));
$response->assertStatus(200); $response->assertStatus(200);
$response->assertSee('Homepage Hero'); $response->assertSee('Homepage Hero');
$response->assertSee('Shop Content & Stijl CMS', false); $response->assertSee('Shop Content & Stijl CMS', false);
@@ -35,7 +72,7 @@ class AdminContentTest extends TestCase
'announcement_text' => 'Tijdelijke actie: gratis verzending!', 'announcement_text' => 'Tijdelijke actie: gratis verzending!',
]; ];
$response = $this->post(route('admin.content.update'), $payload); $response = $this->actingAs($this->admin)->post(route('admin.content.update'), $payload);
$response->assertRedirect(route('admin.content.index')); $response->assertRedirect(route('admin.content.index'));
$response->assertSessionHas('success'); $response->assertSessionHas('success');
@@ -60,7 +97,7 @@ class AdminContentTest extends TestCase
Setting::set('hero_title', 'Tijdelijke Titel'); Setting::set('hero_title', 'Tijdelijke Titel');
$this->assertSame('Tijdelijke Titel', Setting::get('hero_title')); $this->assertSame('Tijdelijke Titel', Setting::get('hero_title'));
$response = $this->post(route('admin.content.reset')); $response = $this->actingAs($this->admin)->post(route('admin.content.reset'));
$response->assertRedirect(route('admin.content.index')); $response->assertRedirect(route('admin.content.index'));
$this->assertNull(Setting::get('hero_title')); $this->assertNull(Setting::get('hero_title'));
@@ -84,17 +121,17 @@ class AdminContentTest extends TestCase
]); ]);
// Products index // Products index
$indexResponse = $this->get(route('admin.products.index')); $indexResponse = $this->actingAs($this->admin)->get(route('admin.products.index'));
$indexResponse->assertStatus(200); $indexResponse->assertStatus(200);
$indexResponse->assertSee('Test Fidget Draak'); $indexResponse->assertSee('Test Fidget Draak');
// Edit page // Edit page
$editResponse = $this->get(route('admin.products.edit', $product)); $editResponse = $this->actingAs($this->admin)->get(route('admin.products.edit', $product));
$editResponse->assertStatus(200); $editResponse->assertStatus(200);
$editResponse->assertSee('Test Fidget Draak'); $editResponse->assertSee('Test Fidget Draak');
// Update product // Update product
$updateResponse = $this->put(route('admin.products.update', $product), [ $updateResponse = $this->actingAs($this->admin)->put(route('admin.products.update', $product), [
'name' => 'Aangepaste Fidget Draak', 'name' => 'Aangepaste Fidget Draak',
'category_id' => $category->id, 'category_id' => $category->id,
'base_price' => 18.00, 'base_price' => 18.00,
@@ -128,12 +165,12 @@ class AdminContentTest extends TestCase
'status' => 'pending', 'status' => 'pending',
]); ]);
$response = $this->get(route('admin.orders.index')); $response = $this->actingAs($this->admin)->get(route('admin.orders.index'));
$response->assertStatus(200); $response->assertStatus(200);
$response->assertSee('ORD-TEST-999'); $response->assertSee('ORD-TEST-999');
$response->assertSee('Jan Jansen'); $response->assertSee('Jan Jansen');
$showResponse = $this->get(route('admin.orders.show', $order)); $showResponse = $this->actingAs($this->admin)->get(route('admin.orders.show', $order));
$showResponse->assertStatus(200); $showResponse->assertStatus(200);
$showResponse->assertSee('ORD-TEST-999'); $showResponse->assertSee('ORD-TEST-999');
} }
+218
View File
@@ -0,0 +1,218 @@
<?php
namespace Tests\Feature;
use App\Mail\OrderConfirmationMail;
use App\Mail\PasswordResetMail;
use App\Mail\WelcomeMail;
use App\Models\Category;
use App\Models\Material;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class AuthAndEmailTest extends TestCase
{
use RefreshDatabase;
public function test_customer_can_register_and_receives_welcome_email(): void
{
Mail::fake();
$response = $this->post(route('register.submit'), [
'name' => 'Sophie de Vries',
'email' => 'sophie@example.com',
'password' => 'VeiligWachtwoord123!',
'password_confirmation' => 'VeiligWachtwoord123!',
]);
$response->assertRedirect(route('account.index'));
$this->assertAuthenticated();
$this->assertDatabaseHas('users', [
'email' => 'sophie@example.com',
'name' => 'Sophie de Vries',
'is_admin' => false,
]);
Mail::assertSent(WelcomeMail::class, function ($mail) {
return $mail->hasTo('sophie@example.com')
&& str_contains($mail->envelope()->subject, 'Welkom bij Jouwdriedee.nl');
});
}
public function test_customer_can_login_with_valid_credentials(): void
{
$user = User::create([
'name' => 'Lars Bakker',
'email' => 'lars@example.com',
'password' => Hash::make('MijnGeheimeWachtwoord'),
'is_admin' => false,
]);
$response = $this->post(route('login.submit'), [
'email' => 'lars@example.com',
'password' => 'MijnGeheimeWachtwoord',
]);
$response->assertRedirect(route('account.index'));
$this->assertAuthenticatedAs($user);
}
public function test_admin_login_redirects_to_admin_content_dashboard(): void
{
$admin = User::create([
'name' => 'Jouwdriedee Beheer',
'email' => 'info@jouwdriedee.nl',
'password' => Hash::make('Jw3D!x9#QmK7$vR2'),
'is_admin' => true,
]);
$response = $this->post(route('login.submit'), [
'email' => 'info@jouwdriedee.nl',
'password' => 'Jw3D!x9#QmK7$vR2',
]);
$response->assertRedirect(route('admin.content.index'));
$this->assertAuthenticatedAs($admin);
}
public function test_login_fails_with_invalid_credentials(): void
{
User::create([
'name' => 'Lars Bakker',
'email' => 'lars@example.com',
'password' => Hash::make('MijnGeheimeWachtwoord'),
]);
$response = $this->post(route('login.submit'), [
'email' => 'lars@example.com',
'password' => 'FoutWachtwoord',
]);
$response->assertSessionHasErrors('email');
$this->assertGuest();
}
public function test_customer_can_request_password_reset_and_receives_email(): void
{
Mail::fake();
$user = User::create([
'name' => 'Emma Visser',
'email' => 'emma@example.com',
'password' => Hash::make('Wachtwoord123'),
]);
$response = $this->post(route('password.email'), [
'email' => 'emma@example.com',
]);
$response->assertSessionHas('success');
Mail::assertSent(PasswordResetMail::class, function ($mail) {
return $mail->hasTo('emma@example.com')
&& str_contains($mail->envelope()->subject, 'Wachtwoord opnieuw instellen');
});
$this->assertDatabaseHas('password_reset_tokens', [
'email' => 'emma@example.com',
]);
}
public function test_customer_can_reset_password_with_valid_token(): void
{
$user = User::create([
'name' => 'Emma Visser',
'email' => 'emma@example.com',
'password' => Hash::make('OudWachtwoord123'),
]);
$rawToken = 'test-secret-token-1234567890';
DB::table('password_reset_tokens')->insert([
'email' => 'emma@example.com',
'token' => Hash::make($rawToken),
'created_at' => now(),
]);
$response = $this->post(route('password.update'), [
'token' => $rawToken,
'email' => 'emma@example.com',
'password' => 'GloednieuwWachtwoord123!',
'password_confirmation' => 'GloednieuwWachtwoord123!',
]);
$response->assertRedirect(route('account.index'));
$this->assertAuthenticatedAs($user);
$user->refresh();
$this->assertTrue(Hash::check('GloednieuwWachtwoord123!', $user->password));
$this->assertDatabaseMissing('password_reset_tokens', [
'email' => 'emma@example.com',
]);
}
public function test_checkout_sends_order_confirmation_email(): void
{
Mail::fake();
Material::updateOrCreate(['code' => 'pla'], [
'name' => 'Bio-PLA+',
'code' => 'pla',
'price_multiplier' => 1.0,
'is_active' => true,
]);
$category = Category::create([
'name' => 'Fidgets',
'slug' => 'fidgets',
]);
$product = Product::create([
'name' => 'Kristaldraak Mini',
'slug' => 'kristaldraak-mini',
'category_id' => $category->id,
'base_price' => 15.00,
'description' => 'Test draakje',
'is_active' => true,
]);
// Put item in cart
session()->put('cart', [
'kristaldraak-mini-pla-rood' => [
'id' => $product->id,
'name' => 'Kristaldraak Mini',
'slug' => 'kristaldraak-mini',
'price' => 15.00,
'quantity' => 2,
'material' => 'pla',
'color' => 'Vurig Rood',
'scale' => '100%',
'infill' => '20%',
],
]);
$response = $this->post(route('checkout.store'), [
'customer_name' => 'Klant Tester',
'customer_email' => 'klant@test.nl',
'customer_phone' => '0612345678',
'shipping_street' => 'Hoofdstraat',
'shipping_number' => '42',
'shipping_postal_code' => '1000AA',
'shipping_city' => 'Utrecht',
'payment_method' => 'ideal',
]);
$response->assertRedirect();
Mail::assertSent(OrderConfirmationMail::class, function ($mail) {
return $mail->hasTo('klant@test.nl')
&& str_contains($mail->envelope()->subject, 'Bestelbevestiging');
});
}
}
+15
View File
@@ -5,6 +5,7 @@ namespace Tests\Feature;
use App\Models\Category; use App\Models\Category;
use App\Models\Product; use App\Models\Product;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User;
use App\Services\MakerWorldService; use App\Services\MakerWorldService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@@ -14,6 +15,20 @@ class MakerWorldImporterTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$admin = User::create([
'name' => 'Admin Jouwdriedee',
'email' => 'info@jouwdriedee.nl',
'password' => bcrypt('Jw3D!x9#QmK7$vR2'),
'is_admin' => true,
]);
$this->actingAs($admin);
}
protected function fakeMakerWorldApi(): void protected function fakeMakerWorldApi(): void
{ {
Http::fake([ Http::fake([