feat: complete user authentication, admin guard, customer account dashboard and branded HTML emails
This commit is contained in:
+2
-2
@@ -53,8 +53,8 @@ MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
MAIL_FROM_ADDRESS="info@jouwdriedee.nl"
|
||||
MAIL_FROM_NAME="Jouwdriedee.nl"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,4 +133,3 @@ class ContentController extends Controller
|
||||
->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']}'!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,4 +132,3 @@ class ProductController extends Controller
|
||||
->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;
|
||||
|
||||
use App\Mail\OrderConfirmationMail;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CheckoutController extends Controller
|
||||
@@ -59,6 +61,7 @@ class CheckoutController extends Controller
|
||||
$orderNumber = 'J3D-'.date('Y').'-'.strtoupper(Str::random(6));
|
||||
|
||||
$order = Order::create([
|
||||
'user_id' => auth()->id(),
|
||||
'order_number' => $orderNumber,
|
||||
'customer_name' => $validated['customer_name'],
|
||||
'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
|
||||
session()->forget('cart');
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Order extends Model
|
||||
@@ -11,6 +12,7 @@ class Order extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'order_number',
|
||||
'customer_name',
|
||||
'customer_email',
|
||||
@@ -40,6 +42,11 @@ class Order extends Model
|
||||
return $this->hasMany(OrderItem::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function getFormattedTotalAttribute(): string
|
||||
{
|
||||
return '€ '.number_format($this->total, 2, ',', '.');
|
||||
|
||||
+21
-2
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Fillable(['name', 'email', 'password', 'is_admin'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
@@ -27,6 +27,25 @@ class User extends Authenticatable
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,3 @@ if (! function_exists('setting')) {
|
||||
return Setting::get($key, $default);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\EnsureUserIsAdmin;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
@@ -13,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->alias([
|
||||
'admin' => EnsureUserIsAdmin::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
|
||||
+2
-2
@@ -111,8 +111,8 @@ return [
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'info@jouwdriedee.nl'),
|
||||
'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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -6,12 +6,24 @@ use App\Models\Category;
|
||||
use App\Models\Color;
|
||||
use App\Models\Material;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
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
|
||||
$materials = [
|
||||
[
|
||||
|
||||
@@ -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 }} • 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">• {{ $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> • @endif
|
||||
@if($item->color) Kleur: <span class="font-semibold">{{ $item->color }}</span> • @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">€ {{ 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
|
||||
@@ -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
|
||||
@@ -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 & 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 • Hoogwaardige 3D Prints & 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;">
|
||||
• 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> •
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="font-size: 11px; color: #64748B;">
|
||||
© {{ 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> • @endif
|
||||
@if($item->color) Kleur: <strong>{{ $item->color }}</strong> • @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;">
|
||||
€ {{ 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;">
|
||||
€ {{ 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 →
|
||||
</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 →
|
||||
</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
|
||||
@@ -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;">
|
||||
• <strong>Haarscherpe Bambu Lab kwaliteit:</strong> Laagdiktes vanaf 0.12 mm voor soepele scharnieren.<br>
|
||||
• <strong>16+ Kleurvariaties:</strong> Van zijdeglans Silk tot levendige fluor en pastel tinten.<br>
|
||||
• <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 →
|
||||
</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
|
||||
@@ -102,7 +102,7 @@
|
||||
$cartItems = is_array(session('cart')) ? session('cart') : [];
|
||||
$cartCount = array_sum(array_column($cartItems, 'quantity'));
|
||||
@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>
|
||||
<span class="hidden sm:inline">Winkelwagen</span>
|
||||
@if($cartCount > 0)
|
||||
@@ -112,6 +112,43 @@
|
||||
@endif
|
||||
</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 -->
|
||||
<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>
|
||||
@@ -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">
|
||||
<span>Alle Prints Bekijken</span>
|
||||
</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>
|
||||
</header>
|
||||
|
||||
+32
-2
@@ -1,10 +1,14 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\AccountController;
|
||||
use App\Http\Controllers\Admin\CategoryController as AdminCategoryController;
|
||||
use App\Http\Controllers\Admin\ContentController;
|
||||
use App\Http\Controllers\Admin\MakerWorldController;
|
||||
use App\Http\Controllers\Admin\OrderController as AdminOrderController;
|
||||
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\CheckoutController;
|
||||
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::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success');
|
||||
|
||||
// Admin & CMS Beheer
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
// Authenticatie & Klantenportaal
|
||||
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');
|
||||
|
||||
// Layout, Stijl & Teksten CMS
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Category;
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -13,10 +14,46 @@ class AdminContentTest extends TestCase
|
||||
{
|
||||
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->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->assertSee('Homepage Hero');
|
||||
$response->assertSee('Shop Content & Stijl CMS', false);
|
||||
@@ -35,7 +72,7 @@ class AdminContentTest extends TestCase
|
||||
'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->assertSessionHas('success');
|
||||
@@ -60,7 +97,7 @@ class AdminContentTest extends TestCase
|
||||
Setting::set('hero_title', 'Tijdelijke Titel');
|
||||
$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'));
|
||||
|
||||
$this->assertNull(Setting::get('hero_title'));
|
||||
@@ -84,17 +121,17 @@ class AdminContentTest extends TestCase
|
||||
]);
|
||||
|
||||
// Products index
|
||||
$indexResponse = $this->get(route('admin.products.index'));
|
||||
$indexResponse = $this->actingAs($this->admin)->get(route('admin.products.index'));
|
||||
$indexResponse->assertStatus(200);
|
||||
$indexResponse->assertSee('Test Fidget Draak');
|
||||
|
||||
// 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->assertSee('Test Fidget Draak');
|
||||
|
||||
// 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',
|
||||
'category_id' => $category->id,
|
||||
'base_price' => 18.00,
|
||||
@@ -128,12 +165,12 @@ class AdminContentTest extends TestCase
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('admin.orders.index'));
|
||||
$response = $this->actingAs($this->admin)->get(route('admin.orders.index'));
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('ORD-TEST-999');
|
||||
$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->assertSee('ORD-TEST-999');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Feature;
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\MakerWorldService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -14,6 +15,20 @@ class MakerWorldImporterTest extends TestCase
|
||||
{
|
||||
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
|
||||
{
|
||||
Http::fake([
|
||||
|
||||
Reference in New Issue
Block a user