Compare commits

..

21 Commits

Author SHA1 Message Date
NiekRabelink 1290456378 Update SPF guidance in admin dashboard to include SpamExperts IPv4 and IPv6 subnets 2026-11-02 12:51:30 +01:00
NiekRabelink 3ddcf1d238 Add whitespace-nowrap and strict shrink-0 to admin nav items to prevent any text wrapping or overlap 2026-11-02 12:44:08 +01:00
NiekRabelink 07258420af Add full multipart HTML document and plain-text alternate part to eliminate SpamAssassin penalty 2026-11-02 12:39:42 +01:00
NiekRabelink 17b16ddd9f Add DKIM active status to admin email verification dashboard 2026-11-02 11:53:39 +01:00
NiekRabelink 72c0feb1f3 Fix EHLO domain for Versio Exim anti-spoofing and upgrade test email to formatted HTML 2026-11-02 11:39:56 +01:00
NiekRabelink ab1d46e0e3 Update admin email tab to display verified active DNS SPF and DMARC status instead of troubleshooting notice 2026-11-02 11:34:48 +01:00
NiekRabelink 1635b0c10a SchemaHelper 2026-11-02 11:13:00 +01:00
NiekRabelink 560a6ae20b Fix timezone to Europe/Amsterdam, fix 500 error on communication and customer pages with bulletproof schema helper, and redesign admin header to compact grouped dropdowns 2026-11-02 11:05:28 +01:00
NiekRabelink da9992e060 Controller fix 2026-11-02 10:40:56 +01:00
NiekRabelink cf55f3c9f9 Fix 500 error on admin access: make navigation layout resilient and auto-run pending migrations on admin entry 2026-11-02 10:38:22 +01:00
NiekRabelink 44c440222a Fix admin content tab retention on test mail & save, add SMTP encryption support, and provide Gmail DNS deliverability diagnostic guidance 2026-11-02 10:02:53 +01:00
NiekRabelink 9fd39bd7d6 Add customer contact inquiry system, automated receipt emails, admin communication inbox, and customer profile management 2026-11-02 09:56:24 +01:00
NiekRabelink 387241f971 Blade update 2026-11-02 09:23:40 +01:00
NiekRabelink c222434f3c Complete redesign: modern, fresh, subtle Scandinavian tech studio look & feel for webshop and admin 2026-11-02 08:17:25 +01:00
NiekRabelink 9deef06169 ContentTest 2026-11-01 21:34:33 +01:00
NiekRabelink e0592212e1 Redesign logo & storefront, light subtle admin theme, and 3D print cost calculator 2026-11-01 21:23:53 +01:00
NiekRabelink 0c95c7d120 fix: enable cron daemon in Dockerfile, speed up sync interval to every 5 minutes and add opportunistic auto-sync check 2026-11-01 17:02:01 +01:00
NiekRabelink ef19221b62 security: add HTTP security headers, rate limiting on sensitive routes, and file upload extension validation 2026-11-01 16:46:03 +01:00
NiekRabelink 2d44647f94 Offline blade 2026-11-01 16:43:16 +01:00
NiekRabelink b3add6f0c2 feat: add webshop offline maintenance mode with admin toggle and red maintenance screen 2026-11-01 16:28:30 +01:00
NiekRabelink 4994d4a8ef feat: replace raw image path input with interactive visual gallery and multi-image manager in admin and shop 2026-11-01 16:24:12 +01:00
68 changed files with 7085 additions and 2700 deletions
+1
View File
@@ -3,6 +3,7 @@ APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=false APP_DEBUG=false
APP_URL=https://jouwdriedee.nl APP_URL=https://jouwdriedee.nl
APP_TIMEZONE=Europe/Amsterdam
APP_LOCALE=nl APP_LOCALE=nl
APP_FALLBACK_LOCALE=nl APP_FALLBACK_LOCALE=nl
+6 -3
View File
@@ -41,12 +41,15 @@ COPY --from=node_builder /app/public/build /app/public/build
RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
# Zorg voor de juiste opslagpermissies en database bestand # Zorg voor de juiste opslagpermissies en database bestand
RUN mkdir -p /app/storage /app/bootstrap/cache /app/database \ RUN mkdir -p /app/storage /app/bootstrap/cache /app/database /app/storage/logs \
&& touch /app/database/database.sqlite \ && touch /app/database/database.sqlite /app/storage/logs/cron.log \
&& chown -R www-data:www-data /app/storage /app/bootstrap/cache /app/database \ && chown -R www-data:www-data /app/storage /app/bootstrap/cache /app/database \
&& chmod -R 775 /app/storage /app/bootstrap/cache /app/database && chmod -R 775 /app/storage /app/bootstrap/cache /app/database
# Configureer crontab voor Laravel Scheduler (draait elke minuut php artisan schedule:run)
RUN echo "* * * * * cd /app && php artisan schedule:run >> /app/storage/logs/cron.log 2>&1" > /etc/crontabs/root
EXPOSE 80 EXPOSE 80
CMD ["sh", "-c", "php artisan storage:link && mkdir -p /app/database /app/storage/framework/cache /app/storage/framework/sessions /app/storage/framework/views /app/storage/logs && touch /app/database/database.sqlite && chmod -R 777 /app/database /app/storage && php artisan migrate --force && php artisan db:seed --force && frankenphp run --config /etc/caddy/Caddyfile"] CMD ["sh", "-c", "php artisan storage:link && mkdir -p /app/database /app/storage/framework/cache /app/storage/framework/sessions /app/storage/framework/views /app/storage/logs && touch /app/database/database.sqlite /app/storage/logs/cron.log && chmod -R 777 /app/database /app/storage && php artisan migrate --force && php artisan db:seed --force && crond -b -l 2 && frankenphp run --config /etc/caddy/Caddyfile"]
+12 -1
View File
@@ -2,11 +2,13 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Communication;
use App\Models\Order; use App\Models\Order;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\View\View; use Illuminate\View\View;
@@ -25,7 +27,16 @@ class AccountController extends Controller
->latest() ->latest()
->get(); ->get();
return view('account.index', compact('user', 'orders')); $communications = collect();
if (Schema::hasTable('communications')) {
$communications = Communication::where('user_id', $user->id)
->orWhere('email', strtolower($user->email))
->with('replies')
->latest()
->get();
}
return view('account.index', compact('user', 'orders', 'communications'));
} }
/** /**
@@ -0,0 +1,142 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Mail\ContactInquiryReplyMail;
use App\Models\Communication;
use App\Models\CommunicationReply;
use App\Services\DatabaseSchemaHelper;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\View\View;
class CommunicationController extends Controller
{
/**
* Display a listing of all customer communications / inquiries.
*/
public function index(Request $request): View
{
DatabaseSchemaHelper::ensureCommunicationsTables();
$query = Communication::with(['user', 'order', 'replies'])->latest();
// Status filter
if ($request->filled('status') && $request->input('status') !== 'all') {
$query->where('status', $request->input('status'));
}
// Search query
if ($request->filled('q')) {
$search = trim($request->input('q'));
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('subject', 'like', "%{$search}%")
->orWhere('message', 'like', "%{$search}%");
});
}
$communications = $query->paginate(20)->withQueryString();
$counts = [
'all' => Communication::count(),
'new' => Communication::where('status', 'new')->count(),
'in_progress' => Communication::where('status', 'in_progress')->count(),
'resolved' => Communication::where('status', 'resolved')->count(),
'closed' => Communication::where('status', 'closed')->count(),
];
return view('admin.communications.index', [
'communications' => $communications,
'counts' => $counts,
'currentStatus' => $request->input('status', 'all'),
'search' => $request->input('q', ''),
]);
}
/**
* Display the specified communication thread.
*/
public function show(Communication $communication): View
{
$communication->load(['user.orders', 'order.items', 'replies.user']);
// Mark as in_progress if still new when viewed by admin
if ($communication->status === 'new') {
$communication->update(['status' => 'in_progress']);
}
return view('admin.communications.show', [
'communication' => $communication,
]);
}
/**
* Update the status and/or admin notes for this communication.
*/
public function updateStatus(Request $request, Communication $communication): RedirectResponse
{
$validated = $request->validate([
'status' => 'required|string|in:new,in_progress,resolved,closed',
'admin_notes' => 'nullable|string|max:5000',
]);
$communication->update($validated);
return back()->with('success', 'Status van het bericht is bijgewerkt.');
}
/**
* Send a reply to the customer from the admin dashboard.
*/
public function sendReply(Request $request, Communication $communication): RedirectResponse
{
$validated = $request->validate([
'message' => 'required|string|min:5|max:5000',
], [
'message.required' => 'Vul alstublieft een antwoordbericht in.',
'message.min' => 'Het antwoordbericht moet minimaal 5 tekens bevatten.',
]);
$reply = CommunicationReply::create([
'communication_id' => $communication->id,
'user_id' => Auth::id(),
'is_admin_reply' => true,
'message' => trim($validated['message']),
]);
$communication->update([
'status' => 'resolved',
'last_replied_at' => now(),
]);
// Send email to customer from info@jouwdriedee.nl
try {
Mail::to($communication->email)->send(new ContactInquiryReplyMail($communication, $reply));
} catch (\Throwable $e) {
Log::error('Fout bij verzenden van admin reactie naar klant: '.$e->getMessage(), [
'communication_id' => $communication->id,
'email' => $communication->email,
]);
return back()->with('warning', 'Het antwoord is opgeslagen in de database, maar er trad een fout op bij het verzenden van de e-mail: '.$e->getMessage());
}
return back()->with('success', 'Jouw antwoord is verzonden naar '.$communication->email.' en toegevoegd aan het communicatiedossier.');
}
/**
* Remove the specified communication from storage.
*/
public function destroy(Communication $communication): RedirectResponse
{
$communication->delete();
return redirect()->route('admin.communications.index')->with('success', 'Klantbericht is verwijderd.');
}
}
@@ -85,6 +85,19 @@ class ContentController extends Controller
'footer_shipping_text', 'footer_shipping_text',
'footer_copyright', 'footer_copyright',
'footer_payment_text', 'footer_payment_text',
// Offline / Maintenance Mode
'shop_offline_title',
'shop_offline_message',
// 3D Print Kostprijscalculator Tarieven
'calc_filament_price_kg',
'calc_electricity_kwh',
'calc_printer_wattage',
'calc_machine_hourly_rate',
'calc_handling_flat',
'calc_default_markup',
'calc_failure_rate',
]; ];
// Handle checkbox toggles that aren't present if unchecked // Handle checkbox toggles that aren't present if unchecked
@@ -92,6 +105,10 @@ class ContentController extends Controller
Setting::set('announcement_enabled', '0'); Setting::set('announcement_enabled', '0');
} }
if ($request->has('shop_offline_submitted')) {
Setting::set('shop_offline', $request->boolean('shop_offline') ? '1' : '0');
}
foreach ($fields as $field) { foreach ($fields as $field) {
if ($request->has($field)) { if ($request->has($field)) {
$value = $request->input($field); $value = $request->input($field);
@@ -101,8 +118,10 @@ class ContentController extends Controller
Setting::flushCache(); Setting::flushCache();
return redirect()->route('admin.content.index') $tab = $request->input('active_tab');
->with('success', 'Layout en teksten zijn direct opgeslagen en live bijgewerkt in de webshop!'); $redirect = $tab ? redirect()->route('admin.content.index', ['tab' => $tab]) : redirect()->route('admin.content.index');
return $redirect->with('success', 'Layout en teksten zijn direct opgeslagen en live bijgewerkt in de webshop!');
} }
/** /**
@@ -147,25 +166,80 @@ class ContentController extends Controller
$recipient = $validated['test_email']; $recipient = $validated['test_email'];
$defaultMailer = config('mail.default'); $defaultMailer = config('mail.default');
$fromAddress = config('mail.from.address'); $fromAddress = config('mail.from.address');
$fromName = config('mail.from.name', 'Jouwdriedee.nl');
$smtpHost = config('mail.mailers.smtp.host');
$smtpPort = config('mail.mailers.smtp.port');
try { try {
Mail::raw("Dit is een test e-mail vanaf Jouwdriedee.nl om de e-mail bezorging naar {$recipient} te controleren.\n\nActieve mailer: {$defaultMailer}\nAfzender: {$fromAddress}\nDatum: ".now()->format('d-m-Y H:i:s'), function ($message) use ($recipient) { $now = now()->format('d-m-Y H:i:s');
$htmlContent = '<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test E-mail • Jouwdriedee.nl</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; background-color: #f8fafc; margin: 0; padding: 24px;">
<div style="max-width: 600px; margin: 0 auto; background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; padding: 24px;">
<h2 style="margin: 0 0 8px 0; color: #0f172a; font-size: 20px;">Test E-mail • Jouwdriedee.nl</h2>
<p style="color: #64748b; font-size: 13px; margin: 0 0 16px 0;">Deze e-mail bevestigt dat de e-mailverbinding succesvol tot stand is gebracht.</p>
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; font-size: 13px; color: #334155; line-height: 1.6;">
<p style="margin: 0 0 8px 0;"><strong>Ontvanger:</strong> '.e($recipient).'</p>
<p style="margin: 0 0 8px 0;"><strong>Verzendende Driver:</strong> '.e($defaultMailer).'</p>
<p style="margin: 0 0 8px 0;"><strong>SMTP Server:</strong> '.e($smtpHost).':'.e($smtpPort).'</p>
<p style="margin: 0 0 8px 0;"><strong>Afzender:</strong> '.e($fromName).' &lt;'.e($fromAddress).'&gt;</p>
<p style="margin: 0;"><strong>Tijdstip:</strong> '.$now.'</p>
</div>
<p style="color: #94a3b8; font-size: 12px; margin-top: 20px; border-top: 1px solid #f1f5f9; padding-top: 12px;">Jouwdriedee.nl • 3D Printstudio &amp; Webshop</p>
</div>
</body>
</html>';
$plainContent = "Test E-mail • Jouwdriedee.nl\n\n"
."Deze e-mail bevestigt dat de e-mailverbinding succesvol tot stand is gebracht.\n\n"
."Ontvanger: {$recipient}\n"
."Verzendende Driver: {$defaultMailer}\n"
."SMTP Server: {$smtpHost}:{$smtpPort}\n"
."Afzender: {$fromName} <{$fromAddress}>\n"
."Tijdstip: {$now}\n\n"
.'Jouwdriedee.nl • 3D Printstudio & Webshop';
Mail::send([], [], function ($message) use ($recipient, $fromAddress, $fromName, $htmlContent, $plainContent) {
$message->to($recipient) $message->to($recipient)
->subject('Test E-mail vanaf Jouwdriedee.nl'); ->from($fromAddress, $fromName)
->subject('Test E-mail vanaf Jouwdriedee.nl')
->html($htmlContent)
->text($plainContent);
}); });
if ($defaultMailer === 'log') { if ($defaultMailer === 'log') {
return redirect()->back() return redirect()->route('admin.content.index', ['tab' => 'email'])
->with('warning', "De testmail is weggeschreven naar de serverlogs (MAIL_MAILER staat op 'log'). Er is GEEN echte e-mail naar {$recipient} verzonden. Stel op Coolify geldige SMTP-gegevens in (bijv. Brevo, Resend of je hosting SMTP) om echte e-mails te bezorgen."); ->with('warning', "De testmail is weggeschreven naar de serverlogs (MAIL_MAILER staat op 'log'). Er is GEEN echte e-mail naar {$recipient} verzonden. Stel op Coolify geldige SMTP-gegevens in (bijv. Brevo, Resend of je hosting SMTP) om echte e-mails te bezorgen.");
} }
return redirect()->back() return redirect()->route('admin.content.index', ['tab' => 'email'])
->with('success', "Test e-mail succesvol verzonden via driver '{$defaultMailer}' naar {$recipient}! Controleer je inbox en eventueel je spambox."); ->with('success', "Test e-mail succesvol overhandigd aan de SMTP-server ({$smtpHost}:{$smtpPort}) voor {$recipient}! Controleer zowel je inbox als je Spam/Ongewenst map.");
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Test mail error: '.$e->getMessage(), ['trace' => $e->getTraceAsString()]); Log::error('Test mail error: '.$e->getMessage(), ['trace' => $e->getTraceAsString()]);
return redirect()->back() return redirect()->route('admin.content.index', ['tab' => 'email'])
->with('error', "Fout bij verzenden van e-mail via {$defaultMailer}: ".$e->getMessage()); ->with('error', "Fout bij verzenden van e-mail via {$defaultMailer}: ".$e->getMessage());
} }
} }
/**
* Toggle the webshop offline / online status.
*/
public function toggleOffline(Request $request): RedirectResponse
{
$current = Setting::get('shop_offline', '0') === '1';
$new = ! $current;
Setting::set('shop_offline', $new ? '1' : '0');
Setting::flushCache();
$statusText = $new ? 'OFFLINE gezet' : 'weer ONLINE gezet';
return redirect()->route('admin.content.index', ['tab' => 'offline'])
->with('success', "De webshop is direct {$statusText}!");
}
} }
@@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Communication;
use App\Models\Order;
use App\Models\User;
use App\Services\DatabaseSchemaHelper;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\View\View;
class CustomerController extends Controller
{
/**
* Display a listing of registered customers and guest buyers.
*/
public function index(Request $request): View
{
DatabaseSchemaHelper::ensureCommunicationsTables();
$search = trim($request->input('q', ''));
// Query registered non-admin users
$usersQuery = User::where('is_admin', false);
if ($search !== '') {
$usersQuery->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
$registeredUsers = $usersQuery->withCount(['orders', 'communications'])->latest()->get();
// Also discover guest customers who placed orders or sent inquiries without an account
$registeredEmails = User::pluck('email')->map(fn ($e) => strtolower($e))->toArray();
$guestOrderEmails = Order::whereNull('user_id')
->select('customer_email', 'customer_name', 'customer_phone', 'created_at')
->latest()
->get()
->groupBy(fn ($item) => strtolower($item->customer_email));
$guestInquiryEmails = Communication::whereNull('user_id')
->select('email', 'name', 'phone', 'created_at')
->latest()
->get()
->groupBy(fn ($item) => strtolower($item->email));
$guestCustomers = collect();
$allGuestEmails = $guestOrderEmails->keys()->merge($guestInquiryEmails->keys())->unique();
foreach ($allGuestEmails as $email) {
if (in_array(strtolower($email), $registeredEmails)) {
continue;
}
if ($search !== '' && ! str_contains(strtolower($email), strtolower($search))) {
continue;
}
$orderGroup = $guestOrderEmails->get($email, collect());
$inquiryGroup = $guestInquiryEmails->get($email, collect());
$latestOrder = $orderGroup->first();
$latestInquiry = $inquiryGroup->first();
$name = $latestOrder ? $latestOrder->customer_name : ($latestInquiry ? $latestInquiry->name : 'Gast');
$phone = $latestOrder ? $latestOrder->customer_phone : ($latestInquiry ? $latestInquiry->phone : null);
$createdAt = $latestOrder ? $latestOrder->created_at : ($latestInquiry ? $latestInquiry->created_at : now());
$guestCustomers->push((object) [
'id' => null,
'is_registered' => false,
'name' => $name,
'email' => $email,
'phone' => $phone,
'orders_count' => $orderGroup->count(),
'communications_count' => $inquiryGroup->count(),
'created_at' => $createdAt,
]);
}
// Map registered users into unified customer object
$registeredCustomers = $registeredUsers->map(function ($u) {
return (object) [
'id' => $u->id,
'is_registered' => true,
'name' => $u->name,
'email' => $u->email,
'phone' => null,
'orders_count' => $u->orders_count,
'communications_count' => $u->communications_count,
'created_at' => $u->created_at,
];
});
$allCustomers = $registeredCustomers->concat($guestCustomers)->sortByDesc('created_at')->values();
// Manual pagination for the unified list
$perPage = 20;
$page = LengthAwarePaginator::resolveCurrentPage('page');
$paginatedCustomers = new LengthAwarePaginator(
$allCustomers->forPage($page, $perPage),
$allCustomers->count(),
$perPage,
$page,
['path' => route('admin.customers.index'), 'query' => $request->query()]
);
return view('admin.customers.index', [
'customers' => $paginatedCustomers,
'totalRegistered' => $registeredUsers->count(),
'totalGuests' => $guestCustomers->count(),
'search' => $search,
]);
}
/**
* Display a specific customer's profile, including their full communication dossier and orders.
*/
public function show(string $identifier): View
{
$user = null;
$email = null;
if (is_numeric($identifier)) {
$user = User::find($identifier);
if ($user) {
$email = strtolower($user->email);
}
} else {
$email = strtolower(urldecode($identifier));
$user = User::where('email', $email)->first();
}
if (! $user && ! $email) {
abort(404, 'Klant niet gevonden.');
}
// Fetch all communications for this customer (by user_id OR by email)
$communicationsQuery = Communication::with(['replies', 'order'])->latest();
if ($user) {
$communicationsQuery->where(function ($q) use ($user, $email) {
$q->where('user_id', $user->id)
->orWhere('email', $email);
});
} else {
$communicationsQuery->where('email', $email);
}
$communications = $communicationsQuery->get();
// Fetch all orders for this customer (by user_id OR by email)
$ordersQuery = Order::with('items')->latest();
if ($user) {
$ordersQuery->where(function ($q) use ($user, $email) {
$q->where('user_id', $user->id)
->orWhere('customer_email', $email);
});
} else {
$ordersQuery->where('customer_email', $email);
}
$orders = $ordersQuery->get();
$customerName = $user ? $user->name : ($orders->first()?->customer_name ?? $communications->first()?->name ?? 'Klant');
$customerPhone = $orders->first()?->customer_phone ?? $communications->first()?->phone ?? null;
$totalSpent = $orders->sum('total');
return view('admin.customers.show', [
'user' => $user,
'email' => $email,
'customerName' => $customerName,
'customerPhone' => $customerPhone,
'totalSpent' => $totalSpent,
'communications' => $communications,
'orders' => $orders,
]);
}
}
@@ -10,6 +10,7 @@ use App\Services\MakerWorldService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View; use Illuminate\View\View;
class MakerWorldController extends Controller class MakerWorldController extends Controller
@@ -31,6 +32,22 @@ class MakerWorldController extends Controller
$apiCollections = []; $apiCollections = [];
if ($tab === 'collections') { if ($tab === 'collections') {
// Check of actieve collecties toe zijn aan synchronisatie (> 5 minuten geleden of nog nooit gesynchroniseerd)
$dueCollections = MakerWorldCollection::where('is_active_sync', true)
->where(function ($q) {
$q->whereNull('last_synced_at')
->orWhere('last_synced_at', '<=', now()->subMinutes(5));
})
->get();
foreach ($dueCollections as $dueCol) {
try {
$this->makerWorld->syncCollection($dueCol);
} catch (\Throwable $e) {
Log::warning("Auto-sync error for collection #{$dueCol->id}: {$e->getMessage()}");
}
}
if ($isAuthenticated) { if ($isAuthenticated) {
$apiCollections = $this->makerWorld->getFavoritesCollections(); $apiCollections = $this->makerWorld->getFavoritesCollections();
} }
@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Category; use App\Models\Category;
use App\Models\Product; use App\Models\Product;
use App\Services\PriceCalculatorService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -84,7 +86,9 @@ class ProductController extends Controller
'base_price' => 'required|numeric|min:0.01', 'base_price' => 'required|numeric|min:0.01',
'short_description' => 'nullable|string|max:500', 'short_description' => 'nullable|string|max:500',
'description' => 'required|string', 'description' => 'required|string',
'image_url' => 'nullable|string|max:500', 'image_url' => 'nullable|string|max:2048',
'gallery_images' => 'nullable|array',
'gallery_images.*' => 'nullable|string|max:2048',
'dimensions' => 'nullable|string|max:100', 'dimensions' => 'nullable|string|max:100',
'weight_grams' => 'nullable|integer|min:0', 'weight_grams' => 'nullable|integer|min:0',
'print_time_hours' => 'nullable|numeric|min:0', 'print_time_hours' => 'nullable|numeric|min:0',
@@ -95,6 +99,7 @@ class ProductController extends Controller
$validated['slug'] = Str::slug($validated['name']); $validated['slug'] = Str::slug($validated['name']);
$validated['is_featured'] = $request->boolean('is_featured'); $validated['is_featured'] = $request->boolean('is_featured');
$validated['is_active'] = $request->boolean('is_active'); $validated['is_active'] = $request->boolean('is_active');
$validated['gallery_images'] = $this->cleanGalleryImages($request->input('gallery_images', []), $validated['image_url'] ?? null);
$validated['available_materials'] = ['pla', 'petg', 'silk', 'tpu']; $validated['available_materials'] = ['pla', 'petg', 'silk', 'tpu'];
$validated['available_colors'] = [ $validated['available_colors'] = [
'Zon Geel (Pikachu)', 'Vurig Rood (Charizard)', 'Hemelsblauw (Squirtle)', 'Zon Geel (Pikachu)', 'Vurig Rood (Charizard)', 'Hemelsblauw (Squirtle)',
@@ -129,7 +134,9 @@ class ProductController extends Controller
'base_price' => 'required|numeric|min:0.01', 'base_price' => 'required|numeric|min:0.01',
'short_description' => 'nullable|string|max:500', 'short_description' => 'nullable|string|max:500',
'description' => 'required|string', 'description' => 'required|string',
'image_url' => 'nullable|string|max:500', 'image_url' => 'nullable|string|max:2048',
'gallery_images' => 'nullable|array',
'gallery_images.*' => 'nullable|string|max:2048',
'dimensions' => 'nullable|string|max:100', 'dimensions' => 'nullable|string|max:100',
'weight_grams' => 'nullable|integer|min:0', 'weight_grams' => 'nullable|integer|min:0',
'print_time_hours' => 'nullable|numeric|min:0', 'print_time_hours' => 'nullable|numeric|min:0',
@@ -139,6 +146,7 @@ class ProductController extends Controller
$validated['is_featured'] = $request->boolean('is_featured'); $validated['is_featured'] = $request->boolean('is_featured');
$validated['is_active'] = $request->boolean('is_active'); $validated['is_active'] = $request->boolean('is_active');
$validated['gallery_images'] = $this->cleanGalleryImages($request->input('gallery_images', []), $validated['image_url'] ?? null);
$product->update($validated); $product->update($validated);
@@ -146,6 +154,62 @@ class ProductController extends Controller
->with('success', "Product '{$product->name}' is succesvol bijgewerkt!"); ->with('success', "Product '{$product->name}' is succesvol bijgewerkt!");
} }
/**
* Handle image upload for product gallery / cover.
*/
public function uploadImage(Request $request): JsonResponse
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg,webp,gif|max:8192',
]);
$path = $request->file('image')->store('products', 'public');
$url = '/storage/'.$path;
return response()->json([
'success' => true,
'url' => $url,
]);
}
/**
* Calculate 3D printing cost price and suggested retail price.
*/
public function calculatePrice(Request $request, PriceCalculatorService $calculator): JsonResponse
{
$weight = (float) $request->input('weight_grams', 80);
$hours = (float) $request->input('print_time_hours', 4.0);
$colors = (int) $request->input('color_count', 1);
$material = (string) $request->input('material', 'pla');
$markup = $request->filled('markup_percent') ? (float) $request->input('markup_percent') : null;
$result = $calculator->calculate($weight, $hours, $colors, $material, $markup);
return response()->json([
'success' => true,
'data' => $result,
]);
}
/**
* Sanitize and filter gallery images.
*
* @param array<int, mixed> $images
* @return array<int, string>
*/
protected function cleanGalleryImages(array $images, ?string $coverUrl = null): array
{
$clean = [];
foreach ($images as $img) {
$img = trim((string) $img);
if ($img !== '' && $img !== $coverUrl && ! in_array($img, $clean, true)) {
$clean[] = $img;
}
}
return $clean;
}
/** /**
* Remove the specified product from storage. * Remove the specified product from storage.
*/ */
+1 -1
View File
@@ -94,7 +94,7 @@ class CartController extends Controller
$validated = $request->validate([ $validated = $request->validate([
'model_name' => 'nullable|string|max:255', 'model_name' => 'nullable|string|max:255',
'model_url' => 'nullable|url|max:1000', 'model_url' => 'nullable|url|max:1000',
'model_file' => 'nullable|file|max:51200', // 50MB 'model_file' => 'nullable|file|extensions:stl,3mf,obj,step,stp,zip|max:51200', // 50MB
'size' => 'required|in:klein,medium,groot', 'size' => 'required|in:klein,medium,groot',
'material' => 'required|string', 'material' => 'required|string',
'color' => 'required|string', 'color' => 'required|string',
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers;
use App\Mail\ContactInquiryConfirmationMail;
use App\Mail\ContactInquiryReceivedMail;
use App\Models\Communication;
use App\Models\Order;
use App\Models\Setting;
use App\Services\DatabaseSchemaHelper;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\View\View;
class ContactController extends Controller
{
/**
* Show the contact / question form.
*/
public function index(): View
{
$user = Auth::user();
$userOrders = $user ? $user->orders()->latest()->take(10)->get() : collect();
return view('contact.index', [
'user' => $user,
'userOrders' => $userOrders,
]);
}
/**
* Handle the submission of a question / contact inquiry.
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:150',
'email' => 'required|email|max:150',
'phone' => 'nullable|string|max:40',
'subject' => 'required|string|max:200',
'order_number' => 'nullable|string|max:50',
'message' => 'required|string|min:10|max:5000',
], [
'name.required' => 'Vul alstublieft jouw naam in.',
'email.required' => 'Vul alstublieft een geldig e-mailadres in.',
'email.email' => 'Het ingevoerde e-mailadres is niet geldig.',
'subject.required' => 'Vul alstublieft het onderwerp van jouw vraag in.',
'message.required' => 'Vul alstublieft jouw bericht of vraag in.',
'message.min' => 'Het bericht moet minimaal 10 tekens bevatten.',
]);
$orderId = null;
if (! empty($validated['order_number'])) {
$cleanNumber = ltrim(trim($validated['order_number']), '#');
$foundOrder = Order::where('order_number', $cleanNumber)->first();
if ($foundOrder) {
$orderId = $foundOrder->id;
}
}
DatabaseSchemaHelper::ensureCommunicationsTables();
$communication = Communication::create([
'user_id' => Auth::id(),
'order_id' => $orderId,
'name' => trim($validated['name']),
'email' => strtolower(trim($validated['email'])),
'phone' => ! empty($validated['phone']) ? trim($validated['phone']) : null,
'subject' => trim($validated['subject']),
'message' => trim($validated['message']),
'status' => 'new',
'ip_address' => $request->ip(),
'user_agent' => substr((string) $request->userAgent(), 0, 500),
]);
// 1. Send incoming mail to webshop management (info@jouwdrieedee.nl / info@jouwdriedee.nl)
try {
$shopInbox = Setting::get('contact_email', 'info@jouwdrieedee.nl');
// Ensure delivery to both info@jouwdrieedee.nl and fallback info@jouwdriedee.nl
$recipients = array_unique([$shopInbox, 'info@jouwdrieedee.nl', 'info@jouwdriedee.nl']);
Mail::to($recipients)->send(new ContactInquiryReceivedMail($communication));
} catch (\Throwable $e) {
Log::error('Fout bij versturen van klantvraag naar webshop beheer: '.$e->getMessage(), [
'communication_id' => $communication->id,
]);
}
// 2. Send afschrift receipt from (noreply@jouwdriedee.nl) to the customer
try {
Mail::to($communication->email)->send(new ContactInquiryConfirmationMail($communication));
} catch (\Throwable $e) {
Log::error('Fout bij versturen van afschrift naar klant: '.$e->getMessage(), [
'communication_id' => $communication->id,
'email' => $communication->email,
]);
}
return redirect()->route('contact.index')->with(
'success',
'Bedankt voor je vraag! We hebben jouw bericht ontvangen. Er is een afschrift verzonden naar '.$communication->email.'.'
);
}
}
+1 -1
View File
@@ -106,7 +106,7 @@ class ShopController extends Controller
'preferred_material' => 'nullable|string|max:50', 'preferred_material' => 'nullable|string|max:50',
'preferred_color' => 'nullable|string|max:50', 'preferred_color' => 'nullable|string|max:50',
'dimensions' => 'nullable|string|max:100', 'dimensions' => 'nullable|string|max:100',
'model_file' => 'nullable|file|max:51200', // max 50MB (stl, step, obj, zip) 'model_file' => 'nullable|file|extensions:stl,3mf,obj,step,stp,zip|max:51200', // max 50MB (stl, step, obj, zip)
]); ]);
$filePath = null; $filePath = null;
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AddSecurityHeaders
{
/**
* Handle an incoming request and append modern security headers.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
return $response;
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Middleware;
use App\Models\Setting;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckWebshopOffline
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// If the webshop is not offline, continue normally
if (Setting::get('shop_offline', '0') !== '1') {
return $next($request);
}
// If the user is authenticated as an admin, allow full access
if ($request->user() && $request->user()->isAdmin()) {
return $next($request);
}
// Keep admin, auth, health check and asset routes accessible
if ($this->isExemptRoute($request)) {
return $next($request);
}
// Otherwise, render the large red offline maintenance screen
$title = Setting::get('shop_offline_title', 'Aan deze webshop wordt hard gewerkt! 🛠️');
$message = Setting::get('shop_offline_message', 'Onze 3D printers worden momenteel opnieuw gekalibreerd en we bouwen aan nieuwe toffe prints en functies. We zijn heel snel weer online!');
return response()->view('errors.offline', [
'offlineTitle' => $title,
'offlineMessage' => $message,
], 503);
}
/**
* Check if the current route is exempt from offline mode.
*/
protected function isExemptRoute(Request $request): bool
{
$exemptPatterns = [
'admin',
'admin/*',
'inloggen',
'login',
'uitloggen',
'logout',
'registreren',
'register',
'wachtwoord-*',
'up',
'health-debug',
'images/*',
'storage/*',
'build/*',
'favicon.ico',
];
foreach ($exemptPatterns as $pattern) {
if ($request->is($pattern)) {
return true;
}
}
return false;
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Services\DatabaseSchemaHelper;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -23,6 +24,10 @@ class EnsureUserIsAdmin
->with('error', 'Je hebt geen beheerderstoegang tot deze pagina.'); ->with('error', 'Je hebt geen beheerderstoegang tot deze pagina.');
} }
// Auto-migrate check: Ensure that new database tables (like communications) are migrated
// automatically when an admin enters the admin panel, avoiding 500 errors on Coolify/production.
DatabaseSchemaHelper::ensureCommunicationsTables();
return $next($request); return $next($request);
} }
} }
@@ -0,0 +1,33 @@
<?php
namespace App\Mail;
use App\Models\Communication;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ContactInquiryConfirmationMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public Communication $communication) {}
public function envelope(): Envelope
{
return new Envelope(
from: new Address('noreply@jouwdriedee.nl', 'Jouwdriedee.nl'),
subject: 'Afschrift: Jouw vraag aan Jouwdriedee.nl - '.$this->communication->subject,
);
}
public function content(): Content
{
return new Content(
view: 'emails.contact-inquiry-confirmation',
);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Mail;
use App\Models\Communication;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ContactInquiryReceivedMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public Communication $communication) {}
public function envelope(): Envelope
{
return new Envelope(
subject: '[Nieuwe Klantvraag] #'.$this->communication->id.': '.$this->communication->subject,
replyTo: [
new Address($this->communication->email, $this->communication->name),
],
);
}
public function content(): Content
{
return new Content(
view: 'emails.contact-inquiry-received',
);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Mail;
use App\Models\Communication;
use App\Models\CommunicationReply;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ContactInquiryReplyMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Communication $communication,
public CommunicationReply $reply
) {}
public function envelope(): Envelope
{
return new Envelope(
from: new Address('info@jouwdriedee.nl', 'Jouwdriedee.nl Klantenservice'),
replyTo: [
new Address('info@jouwdriedee.nl', 'Jouwdriedee.nl Klantenservice'),
],
subject: 'Reactie op jouw vraag: '.$this->communication->subject.' - Jouwdriedee.nl',
);
}
public function content(): Content
{
return new Content(
view: 'emails.contact-inquiry-reply',
);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
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 Communication extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'order_id',
'name',
'email',
'phone',
'subject',
'message',
'status',
'admin_notes',
'ip_address',
'user_agent',
'last_replied_at',
];
protected $casts = [
'last_replied_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function replies(): HasMany
{
return $this->hasMany(CommunicationReply::class)->oldest();
}
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
'new' => 'Nieuw',
'in_progress' => 'In behandeling',
'resolved' => 'Beantwoord',
'closed' => 'Gesloten',
default => ucfirst($this->status),
};
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CommunicationReply extends Model
{
use HasFactory;
protected $fillable = [
'communication_id',
'user_id',
'is_admin_reply',
'message',
];
protected $casts = [
'is_admin_reply' => 'boolean',
];
public function communication(): BelongsTo
{
return $this->belongsTo(Communication::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+10
View File
@@ -48,4 +48,14 @@ class User extends Authenticatable
{ {
return $this->hasMany(Order::class); return $this->hasMany(Order::class);
} }
/**
* Communications / questions submitted by this user.
*
* @return HasMany<Communication, $this>
*/
public function communications(): HasMany
{
return $this->hasMany(Communication::class);
}
} }
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Services;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
class DatabaseSchemaHelper
{
/**
* Ensure the communications and communication_replies tables exist.
* If missing, cleanly migrate or create them so the admin panel never throws a 500 error.
*/
public static function ensureCommunicationsTables(): void
{
try {
if (Schema::hasTable('communications') && Schema::hasTable('communication_replies')) {
return;
}
// If tables are missing but migration was recorded as ran, reset the migration record
if (Schema::hasTable('migrations')) {
DB::table('migrations')->where('migration', 'like', '%create_communications_table%')->delete();
}
Artisan::call('migrate', ['--force' => true]);
// Direct DDL Fallback if artisan migrate didn't create them
if (! Schema::hasTable('communications')) {
Schema::create('communications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('order_id')->nullable()->constrained('orders')->nullOnDelete();
$table->string('name');
$table->string('email')->index();
$table->string('phone')->nullable();
$table->string('subject');
$table->text('message');
$table->string('status')->default('new')->index();
$table->text('admin_notes')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->timestamp('last_replied_at')->nullable();
$table->timestamps();
});
}
if (! Schema::hasTable('communication_replies')) {
Schema::create('communication_replies', function (Blueprint $table) {
$table->id();
$table->foreignId('communication_id')->constrained('communications')->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->boolean('is_admin_reply')->default(true);
$table->text('message');
$table->timestamps();
});
}
} catch (\Throwable $e) {
Log::error('ensureCommunicationsTables error: '.$e->getMessage(), ['trace' => $e->getTraceAsString()]);
}
}
}
+15 -15
View File
@@ -653,19 +653,14 @@ class MakerWorldService
$dimensions = is_array($design['dimensions']) ? implode(' x ', $design['dimensions']) : (string) $design['dimensions']; $dimensions = is_array($design['dimensions']) ? implode(' x ', $design['dimensions']) : (string) $design['dimensions'];
} }
// Calculate a smart suggested retail price // Calculate a smart suggested retail price using the 3D Printing Cost Calculator
// Base starting price of 9.95, + ~0.08 per gram + 1.25 per print hour $colorCount = 1;
$suggestedPrice = 14.95; if (! empty($profile['filaments']) && is_array($profile['filaments'])) {
if ($weightGrams || $printTimeHours) { $colorCount = max(1, min(4, count($profile['filaments'])));
$cost = 5.0;
if ($weightGrams) {
$cost += ($weightGrams * 0.07);
}
if ($printTimeHours) {
$cost += ($printTimeHours * 1.50);
}
$suggestedPrice = round(max(9.95, $cost * 1.6), 2);
} }
$calculator = app(PriceCalculatorService::class);
$calcResult = $calculator->calculate($weightGrams ?: 85, $printTimeHours ?: 4.0, $colorCount);
$suggestedPrice = $calcResult['suggested_price'];
// Creator and MakerWorld URL // Creator and MakerWorld URL
$authorName = $design['designCreator']['name'] ?? $design['designCreator']['handle'] ?? 'MakerWorld Creator'; $authorName = $design['designCreator']['name'] ?? $design['designCreator']['handle'] ?? 'MakerWorld Creator';
@@ -780,9 +775,14 @@ class MakerWorldService
if (! $existingProduct->makerworld_collection_id) { if (! $existingProduct->makerworld_collection_id) {
$updates['makerworld_collection_id'] = $collection->id; $updates['makerworld_collection_id'] = $collection->id;
} }
if (empty($existingProduct->gallery_images) && ! empty($design['designExtension']['design_pictures'])) { if (empty($existingProduct->gallery_images)) {
$formatted = $this->formatDesignForImport($design); $full = (! empty($design['designExtension']['design_pictures']))
$updates['gallery_images'] = $formatted['gallery_images']; ? $design
: ($this->getDesignDetails($designId) ?? $design);
$formatted = $this->formatDesignForImport($full);
if (! empty($formatted['gallery_images'])) {
$updates['gallery_images'] = $formatted['gallery_images'];
}
} }
if (empty($existingProduct->print_info)) { if (empty($existingProduct->print_info)) {
$formatted = $this->formatDesignForImport($design); $formatted = $this->formatDesignForImport($design);
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace App\Services;
use App\Models\Setting;
class PriceCalculatorService
{
/**
* Purge/waste multipliers depending on the number of AMS/filament colors.
*/
public const COLOR_WASTE_MULTIPLIERS = [
1 => 1.00, // 0% extra waste for single-color
2 => 1.15, // +15% purge tower & poop waste
3 => 1.25, // +25% purge tower & poop waste
4 => 1.35, // +35% purge tower & poop waste
];
/**
* Material base multipliers relative to standard PLA.
*/
public const MATERIAL_MULTIPLIERS = [
'pla' => 1.00,
'petg' => 1.20,
'silk' => 1.30,
'tpu' => 1.40,
];
/**
* Calculate comprehensive 3D printing costs and suggested retail price.
*
* @param float|int|null $weightGrams Model weight in grams
* @param float|int|null $printTimeHours Printing duration in hours
* @param int $colorCount Number of filament colors (1 to 4)
* @param string $material Material code ('pla', 'petg', 'silk', 'tpu')
* @param float|null $customMarkupPercent Optional override for markup percentage (e.g. 60%)
* @return array<string, mixed> Detailed calculation breakdown
*/
public function calculate(
float|int|null $weightGrams = 80,
float|int|null $printTimeHours = 4.0,
int $colorCount = 1,
string $material = 'pla',
?float $customMarkupPercent = null
): array {
$weight = max(1.0, (float) ($weightGrams ?: 80));
$hours = max(0.2, (float) ($printTimeHours ?: 4.0));
$colors = max(1, min(4, $colorCount));
$matCode = strtolower(trim($material)) ?: 'pla';
// 1. Settings / Rates configuration
$filamentPriceKg = (float) Setting::get('calc_filament_price_kg', 20.00); // € 20 / kg
$electricityPriceKwh = (float) Setting::get('calc_electricity_kwh', 0.35); // € 0.35 / kWh
$printerWattage = (int) Setting::get('calc_printer_wattage', 150); // 150 W average Bambu Lab
$machineHourlyRate = (float) Setting::get('calc_machine_hourly_rate', 0.50); // € 0.50 / uur afschrijving & nozzle/bed slijtage
$handlingFlat = (float) Setting::get('calc_handling_flat', 1.50); // € 1.50 voorbereiding, supports & verpakking
$failureRate = (float) Setting::get('calc_failure_rate', 5.0); // 5% faalkans opslag
$markupPercent = $customMarkupPercent !== null ? $customMarkupPercent : (float) Setting::get('calc_default_markup', 65.0); // 65% winstmarge
$vatPercent = 21.0; // 21% btw
// 2. Multipliers
$colorWasteMult = self::COLOR_WASTE_MULTIPLIERS[$colors] ?? 1.00;
$matMult = self::MATERIAL_MULTIPLIERS[$matCode] ?? 1.00;
// 3. Material cost (includes multi-color purge tower waste)
$effectiveWeight = round($weight * $colorWasteMult, 1);
$materialCost = ($effectiveWeight / 1000.0) * ($filamentPriceKg * $matMult);
// 4. Electricity cost
$kw = $printerWattage / 1000.0;
$kwhUsed = $hours * $kw;
$electricityCost = $kwhUsed * $electricityPriceKwh;
// 5. Machine wear & depreciation
$machineCost = $hours * $machineHourlyRate;
// 6. Multi-color handling fee (extra setup for loading AMS spools)
$colorHandlingCost = max(0, $colors - 1) * 0.50;
// 7. Handling / labor
$handlingCost = $handlingFlat + $colorHandlingCost;
// 8. Direct production cost
$subtotalCost = $materialCost + $electricityCost + $machineCost + $handlingCost;
// 9. Failure rate risk buffer
$failureCost = $subtotalCost * ($failureRate / 100.0);
// 10. Net cost price (direct production cost + risk)
$totalCostPrice = $subtotalCost + $failureCost;
// 11. Margin & Retail price excl. VAT
$profitAmount = $totalCostPrice * ($markupPercent / 100.0);
$priceExclVat = $totalCostPrice + $profitAmount;
// 12. VAT (21%) & Consumer price
$vatAmount = $priceExclVat * ($vatPercent / 100.0);
$consumerPriceExact = $priceExclVat + $vatAmount;
// 13. Smart commercial rounding (.95 / .50)
$suggestedRetailPrice = $this->smartRound($consumerPriceExact);
return [
'inputs' => [
'weight_grams' => $weight,
'print_time_hours' => $hours,
'color_count' => $colors,
'material' => $matCode,
'markup_percent' => $markupPercent,
],
'rates' => [
'filament_price_kg' => $filamentPriceKg,
'electricity_price_kwh' => $electricityPriceKwh,
'machine_hourly_rate' => $machineHourlyRate,
'handling_flat' => $handlingFlat,
'failure_rate_percent' => $failureRate,
],
'breakdown' => [
'effective_weight_grams' => $effectiveWeight,
'purge_waste_grams' => round($effectiveWeight - $weight, 1),
'material_cost' => round($materialCost, 2),
'electricity_cost' => round($electricityCost, 2),
'machine_cost' => round($machineCost, 2),
'handling_cost' => round($handlingCost, 2),
'failure_cost' => round($failureCost, 2),
'total_cost_price' => round($totalCostPrice, 2),
'profit_amount' => round($profitAmount, 2),
'price_excl_vat' => round($priceExclVat, 2),
'vat_amount' => round($vatAmount, 2),
'price_incl_vat_exact' => round($consumerPriceExact, 2),
],
'suggested_price' => $suggestedRetailPrice,
'suggested_price_formatted' => '€ '.number_format($suggestedRetailPrice, 2, ',', '.'),
];
}
/**
* Smart commercial rounding to appealing .95 or .50 price points.
*/
protected function smartRound(float $rawPrice): float
{
// Enforce minimum price of 6.95
if ($rawPrice <= 7.00) {
return 6.95;
}
$floor = floor($rawPrice);
$fraction = $rawPrice - $floor;
if ($fraction <= 0.25) {
$rounded = $floor - 0.05; // e.g. 14.10 -> 13.95 or 14.00 -> 13.95, but keep at least floor
return max(6.95, $floor < $rawPrice ? $floor + 0.50 : $floor - 0.05);
} elseif ($fraction <= 0.70) {
return $floor + 0.50; // e.g. 14.40 -> 14.50
} else {
return $floor + 0.95; // e.g. 14.80 -> 14.95
}
}
}
+6
View File
@@ -1,5 +1,7 @@
<?php <?php
use App\Http\Middleware\AddSecurityHeaders;
use App\Http\Middleware\CheckWebshopOffline;
use App\Http\Middleware\EnsureUserIsAdmin; use App\Http\Middleware\EnsureUserIsAdmin;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
@@ -14,6 +16,10 @@ return Application::configure(basePath: dirname(__DIR__))
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*'); $middleware->trustProxies(at: '*');
$middleware->web(append: [
AddSecurityHeaders::class,
CheckWebshopOffline::class,
]);
$middleware->alias([ $middleware->alias([
'admin' => EnsureUserIsAdmin::class, 'admin' => EnsureUserIsAdmin::class,
]); ]);
+1 -1
View File
@@ -65,7 +65,7 @@ return [
| |
*/ */
'timezone' => 'UTC', 'timezone' => env('APP_TIMEZONE', 'Europe/Amsterdam'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
+9 -1
View File
@@ -43,10 +43,18 @@ return [
'url' => env('MAIL_URL'), 'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'), 'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525), 'port' => env('MAIL_PORT', 2525),
'encryption' => env('MAIL_ENCRYPTION'),
'username' => env('MAIL_USERNAME'), 'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'), 'password' => env('MAIL_PASSWORD'),
'timeout' => null, 'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 'local_domain' => env('MAIL_EHLO_DOMAIN', (function () {
$host = parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST);
if (! $host || $host === 'localhost' || $host === '127.0.0.1') {
return null;
}
return str_starts_with($host, 'mail.') ? $host : 'mail.'.$host;
})()),
], ],
'ses' => [ 'ses' => [
@@ -0,0 +1,53 @@
<?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
{
if (! Schema::hasTable('communications')) {
Schema::create('communications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('order_id')->nullable()->constrained('orders')->nullOnDelete();
$table->string('name');
$table->string('email')->index();
$table->string('phone')->nullable();
$table->string('subject');
$table->text('message');
$table->string('status')->default('new')->index(); // new, in_progress, resolved, closed
$table->text('admin_notes')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->timestamp('last_replied_at')->nullable();
$table->timestamps();
});
}
if (! Schema::hasTable('communication_replies')) {
Schema::create('communication_replies', function (Blueprint $table) {
$table->id();
$table->foreignId('communication_id')->constrained('communications')->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->boolean('is_admin_reply')->default(true);
$table->text('message');
$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('communication_replies');
Schema::dropIfExists('communications');
}
};
+22 -61
View File
@@ -1,70 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<defs> <defs>
<!-- Top Slice Gradients --> <linearGradient id="icoTop" x1="0%" y1="0%" x2="100%" y2="100%">
<linearGradient id="icoTopFace" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#38bdf8" />
<stop offset="0%" stop-color="#38BDF8" /> <stop offset="100%" stop-color="#0284c7" />
<stop offset="100%" stop-color="#0284C7" />
</linearGradient> </linearGradient>
<linearGradient id="icoTopRight" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="icoLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0284C7" /> <stop offset="0%" stop-color="#6366f1" />
<stop offset="100%" stop-color="#0369A1" /> <stop offset="100%" stop-color="#4338ca" />
</linearGradient> </linearGradient>
<linearGradient id="icoTopLeft" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="icoRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0EA5E9" /> <stop offset="0%" stop-color="#0ea5e9" />
<stop offset="100%" stop-color="#0284C7" /> <stop offset="100%" stop-color="#0369a1" />
</linearGradient>
<!-- Mid Slice Gradients -->
<linearGradient id="icoMidFace" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F43F5E" />
<stop offset="100%" stop-color="#E11D48" />
</linearGradient>
<linearGradient id="icoMidRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#E11D48" />
<stop offset="100%" stop-color="#BE123C" />
</linearGradient>
<linearGradient id="icoMidLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FB7185" />
<stop offset="100%" stop-color="#E11D48" />
</linearGradient>
<!-- Bot Slice Gradients -->
<linearGradient id="icoBotFace" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FBBF24" />
<stop offset="100%" stop-color="#F59E0B" />
</linearGradient>
<linearGradient id="icoBotRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F59E0B" />
<stop offset="100%" stop-color="#D97706" />
</linearGradient>
<linearGradient id="icoBotLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FCD34D" />
<stop offset="100%" stop-color="#F59E0B" />
</linearGradient> </linearGradient>
</defs> </defs>
<g transform="translate(4, 2)"> <!-- Background rounded squircle with subtle border -->
<!-- Bottom Layer (Amber) --> <rect x="2" y="2" width="44" height="44" rx="12" fill="#0f172a" />
<g transform="translate(0, 22)">
<polygon points="22,6 40,16 22,26 4,16" fill="url(#icoBotFace)" opacity="0.9" />
<polygon points="4,16 22,26 22,34 4,24" fill="url(#icoBotLeft)" />
<polygon points="22,26 40,16 40,24 22,34" fill="url(#icoBotRight)" />
</g>
<!-- Middle Layer (Rose Pink) --> <!-- Modern Geometric 3D Prism Mark -->
<g transform="translate(0, 11)"> <g transform="translate(6, 6)">
<polygon points="22,6 40,16 22,26 4,16" fill="url(#icoMidFace)" opacity="0.95" /> <!-- Top Face -->
<polygon points="4,16 22,26 22,34 4,24" fill="url(#icoMidLeft)" /> <path d="M18 6 L31 13.5 L18 21 L5 13.5 Z" fill="url(#icoTop)" />
<polygon points="22,26 40,16 40,24 22,34" fill="url(#icoMidRight)" /> <!-- Left Face -->
</g> <path d="M5 15.5 L17 22.5 L17 34 L5 27 Z" fill="url(#icoLeft)" />
<!-- Right Face -->
<!-- Top Layer (Cyan / Sky) --> <path d="M19 22.5 L31 15.5 L31 27 L19 34 Z" fill="url(#icoRight)" />
<g transform="translate(0, 0)"> <!-- Precision Core Light Accent -->
<polygon points="22,6 40,16 22,26 4,16" fill="url(#icoTopFace)" /> <circle cx="18" cy="21" r="2" fill="#ffffff" opacity="0.95" />
<polygon points="4,16 22,26 22,34 4,24" fill="url(#icoTopLeft)" />
<polygon points="22,26 40,16 40,24 22,34" fill="url(#icoTopRight)" />
<circle cx="22" cy="16" r="3" fill="#FFFFFF" opacity="0.95" />
</g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+35 -94
View File
@@ -1,105 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 56" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 48" fill="none" class="w-auto h-full">
<defs> <defs>
<!-- Top Slice Gradients --> <!-- Modern high-end gradients -->
<linearGradient id="topFace" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="facetTop" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#38BDF8" /> <stop offset="0%" stop-color="#38bdf8" />
<stop offset="100%" stop-color="#0284C7" /> <stop offset="100%" stop-color="#0284c7" />
</linearGradient> </linearGradient>
<linearGradient id="topRight" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="facetLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0284C7" /> <stop offset="0%" stop-color="#6366f1" />
<stop offset="100%" stop-color="#0369A1" /> <stop offset="100%" stop-color="#4338ca" />
</linearGradient> </linearGradient>
<linearGradient id="topLeft" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="facetRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0EA5E9" /> <stop offset="0%" stop-color="#0ea5e9" />
<stop offset="100%" stop-color="#0284C7" /> <stop offset="100%" stop-color="#0369a1" />
</linearGradient> </linearGradient>
<linearGradient id="accentDot" x1="0%" y1="0%" x2="100%" y2="100%">
<!-- Mid Slice Gradients --> <stop offset="0%" stop-color="#38bdf8" />
<linearGradient id="midFace" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="100%" stop-color="#6366f1" />
<stop offset="0%" stop-color="#F43F5E" />
<stop offset="100%" stop-color="#E11D48" />
</linearGradient>
<linearGradient id="midRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#E11D48" />
<stop offset="100%" stop-color="#BE123C" />
</linearGradient>
<linearGradient id="midLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FB7185" />
<stop offset="100%" stop-color="#E11D48" />
</linearGradient>
<!-- Bot Slice Gradients -->
<linearGradient id="botFace" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FBBF24" />
<stop offset="100%" stop-color="#F59E0B" />
</linearGradient>
<linearGradient id="botRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F59E0B" />
<stop offset="100%" stop-color="#D97706" />
</linearGradient>
<linearGradient id="botLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FCD34D" />
<stop offset="100%" stop-color="#F59E0B" />
</linearGradient>
<!-- Wordmark Gradient -->
<linearGradient id="wordmarkGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#F43F5E" />
<stop offset="50%" stop-color="#8B5CF6" />
<stop offset="100%" stop-color="#06B6D4" />
</linearGradient>
<linearGradient id="badgeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6366F1" />
<stop offset="100%" stop-color="#4F46E5" />
</linearGradient> </linearGradient>
</defs> </defs>
<!-- 3D Layered Isometric Cube Mark --> <!-- Modern Geometric 3D Prism Mark -->
<g transform="translate(6, 4)"> <g transform="translate(6, 6)">
<!-- Bottom Layer (Amber) --> <!-- Top Face (Soft Isometric Diamond) -->
<g transform="translate(0, 22)"> <path d="M18 4 L30 11 L18 18 L6 11 Z" fill="url(#facetTop)" />
<!-- Top surface of bottom layer --> <!-- Left Face (Dimension with 3D print layer notch) -->
<polygon points="22,6 40,16 22,26 4,16" fill="url(#botFace)" opacity="0.9" /> <path d="M6 13 L17 19.5 L17 31 L6 24.5 Z" fill="url(#facetLeft)" />
<!-- Left side of bottom layer --> <!-- Right Face -->
<polygon points="4,16 22,26 22,34 4,24" fill="url(#botLeft)" /> <path d="M19 19.5 L30 13 L30 24.5 L19 31 Z" fill="url(#facetRight)" />
<!-- Right side of bottom layer --> <!-- Precision Core Light Accent -->
<polygon points="22,26 40,16 40,24 22,34" fill="url(#botRight)" /> <circle cx="18" cy="18" r="1.75" fill="#ffffff" opacity="0.95" />
</g>
<!-- Middle Layer (Rose Pink) -->
<g transform="translate(0, 11)">
<!-- Top surface of middle layer -->
<polygon points="22,6 40,16 22,26 4,16" fill="url(#midFace)" opacity="0.95" />
<!-- Left side of middle layer -->
<polygon points="4,16 22,26 22,34 4,24" fill="url(#midLeft)" />
<!-- Right side of middle layer -->
<polygon points="22,26 40,16 40,24 22,34" fill="url(#midRight)" />
</g>
<!-- Top Layer (Cyan / Sky) -->
<g transform="translate(0, 0)">
<!-- Top surface of top layer -->
<polygon points="22,6 40,16 22,26 4,16" fill="url(#topFace)" />
<!-- Left side of top layer -->
<polygon points="4,16 22,26 22,34 4,24" fill="url(#topLeft)" />
<!-- Right side of top layer -->
<polygon points="22,26 40,16 40,24 22,34" fill="url(#topRight)" />
<!-- Accent Sparkle/Nozzle Dot on Top Face -->
<circle cx="22" cy="16" r="3" fill="#FFFFFF" opacity="0.9" />
</g>
</g> </g>
<!-- Wordmark Text --> <!-- Modern Clean Typography -->
<g transform="translate(62, 38)"> <g transform="translate(50, 31)">
<!-- "Jouw" in Bold Slate --> <!-- 'jouw' in clean, light-medium neutral font -->
<text x="0" y="0" font-family="'Fredoka', system-ui, sans-serif" font-weight="700" font-size="28" fill="#0F172A" letter-spacing="-0.5">Jouw</text> <text font-family="'Plus Jakarta Sans', system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#0f172a" letter-spacing="-0.5">jouw</text>
<!-- 'driedee' in confident bold -->
<!-- "driedee" in Vibrant Gradient --> <text x="56" font-family="'Plus Jakarta Sans', system-ui, -apple-system, sans-serif" font-size="22" font-weight="700" fill="#0f172a" letter-spacing="-0.5">driedee</text>
<text x="74" y="0" font-family="'Fredoka', system-ui, sans-serif" font-weight="700" font-size="28" fill="url(#wordmarkGrad)" letter-spacing="-0.5">driedee</text> <!-- Vibrant Modern Dot -->
<circle cx="137.5" cy="-5.5" r="3" fill="url(#accentDot)" />
<!-- ".nl" Badge --> <!-- '.nl' micro pill badge -->
<rect x="168" y="-21" width="28" height="16" rx="5" fill="url(#badgeGrad)" /> <rect x="145" y="-17" width="24" height="15" rx="4" fill="#f1f5f9" />
<text x="173" y="-9" font-family="'Nunito', system-ui, sans-serif" font-weight="800" font-size="10" fill="#FFFFFF" letter-spacing="0.5">.NL</text> <text x="149" y="-6" font-family="'Plus Jakarta Sans', system-ui, -apple-system, sans-serif" font-size="10" font-weight="700" fill="#475569" letter-spacing="0.2">nl</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

+21 -16
View File
@@ -4,8 +4,8 @@
@source '../../storage/framework/views/*.php'; @source '../../storage/framework/views/*.php';
@theme { @theme {
--font-heading: 'Fredoka', ui-rounded, 'Nunito', system-ui, sans-serif; --font-heading: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-sans: 'Nunito', system-ui, sans-serif; --font-sans: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
} }
@layer utilities { @layer utilities {
@@ -13,22 +13,27 @@
font-family: var(--font-heading); font-family: var(--font-heading);
} }
/* Refined, smooth playful button */ /* Modern subtle glass effect */
.playful-btn { .glass-header {
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); background-color: rgba(255, 255, 255, 0.85);
} backdrop-filter: blur(12px);
.playful-btn:hover { -webkit-backdrop-filter: blur(12px);
transform: translateY(-2px);
}
.playful-btn:active {
transform: translateY(1px);
} }
/* Modern playful card */ /* Clean modern micro-elevation */
.playful-card { .card-hover {
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.25s ease;
} }
.playful-card:hover { .card-hover:hover {
transform: translateY(-3px); transform: translateY(-2px);
box-shadow: 0 12px 24px -10px rgba(15, 23, 42, 0.08);
}
/* Refined button transition */
.btn-press {
transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-press:active {
transform: scale(0.98);
} }
} }
+174 -79
View File
@@ -3,22 +3,22 @@
@section('title', 'Mijn Account | Jouwdriedee.nl') @section('title', 'Mijn Account | Jouwdriedee.nl')
@section('content') @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' }"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12 space-y-6" x-data="{ tab: 'orders' }">
<!-- Account Welcome Banner --> <!-- 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="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/80 shadow-xs 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="flex items-center space-x-3.5">
<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"> <div class="w-12 h-12 rounded-xl bg-slate-900 text-white flex items-center justify-center font-bold text-base shadow-xs">
{{ strtoupper(substr($user->name, 0, 1)) }} {{ strtoupper(substr($user->name, 0, 1)) }}
</div> </div>
<div> <div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<h1 class="font-heading text-xl sm:text-2xl font-black text-slate-900"> <h1 class="text-xl font-bold text-slate-900">
Hallo, {{ $user->name }}! {{ $user->name }}
</h1> </h1>
@if($user->isAdmin()) @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"> <span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 text-[10px] font-semibold border border-slate-200">
Admin Beheerder Beheerder
</span> </span>
@endif @endif
</div> </div>
@@ -28,17 +28,17 @@
</div> </div>
</div> </div>
<div class="flex items-center space-x-2.5 self-stretch sm:self-auto justify-end"> <div class="flex items-center space-x-2 self-stretch sm:self-auto justify-end">
@if($user->isAdmin()) @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"> <a href="{{ route('admin.content.index') }}" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center space-x-1.5 shadow-xs">
<i data-lucide="shield" class="w-4 h-4"></i> <i data-lucide="shield" class="w-3.5 h-3.5"></i>
<span>Admin Dashboard</span> <span>Admin Dashboard</span>
</a> </a>
@endif @endif
<form action="{{ route('logout') }}" method="POST"> <form action="{{ route('logout') }}" method="POST">
@csrf @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"> <button type="submit" class="px-3.5 py-2 rounded-xl bg-white hover:bg-slate-50 text-slate-600 hover:text-slate-900 border border-slate-200 text-xs font-medium transition flex items-center space-x-1.5 cursor-pointer">
<i data-lucide="log-out" class="w-3.5 h-3.5"></i> <i data-lucide="log-out" class="w-3.5 h-3.5"></i>
<span>Uitloggen</span> <span>Uitloggen</span>
</button> </button>
@@ -47,69 +47,85 @@
</div> </div>
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div class="flex items-center space-x-2 border-b border-slate-200 text-xs font-bold pb-2"> <div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl max-w-max text-xs font-medium">
<button <button
type="button" type="button"
@click="tab = 'orders'" @click="tab = 'orders'"
:class="tab === 'orders' ? 'bg-amber-400 text-slate-950 font-black' : 'bg-white text-slate-600 hover:bg-slate-100'" :class="tab === 'orders' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 cursor-pointer" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
> >
<i data-lucide="package" class="w-4 h-4"></i> <i data-lucide="package" class="w-3.5 h-3.5"></i>
<span>Mijn Bestellingen ({{ $orders->count() }})</span> <span>Mijn Bestellingen</span>
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
{{ $orders->count() }}
</span>
</button>
<button
type="button"
@click="tab = 'messages'"
:class="tab === 'messages' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
>
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
<span>Mijn Vragen</span>
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
{{ $communications->count() }}
</span>
</button> </button>
<button <button
type="button" type="button"
@click="tab = 'profile'" @click="tab = 'profile'"
:class="tab === 'profile' ? 'bg-amber-400 text-slate-950 font-black' : 'bg-white text-slate-600 hover:bg-slate-100'" :class="tab === 'profile' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 cursor-pointer" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
> >
<i data-lucide="user-cog" class="w-4 h-4"></i> <i data-lucide="user-cog" class="w-3.5 h-3.5"></i>
<span>Profiel & Beveiliging</span> <span>Profiel & Beveiliging</span>
</button> </button>
</div> </div>
<!-- TAB 1: BESTELLINGEN --> <!-- TAB 1: BESTELLINGEN -->
<div x-show="tab === 'orders'" class="space-y-4"> <div x-show="tab === 'orders'" class="space-y-3">
@if($orders->isEmpty()) @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="p-10 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-2.5">
<div class="w-14 h-14 rounded-2xl bg-amber-50 text-amber-600 flex items-center justify-center mx-auto text-2xl"> <div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto">
🛍️ <i data-lucide="shopping-bag" class="w-5 h-5"></i>
</div> </div>
<h3 class="font-heading text-lg font-black text-slate-900">Nog geen bestellingen geplaatst</h3> <h3 class="text-sm font-semibold text-slate-900">Nog geen bestellingen</h3>
<p class="text-xs text-slate-500 max-w-sm mx-auto"> <p class="text-xs text-slate-500 max-w-xs mx-auto">
Je hebt nog geen 3D prints besteld. Ontdek onze fidget toys, Pokémon figuren en popculture modellen! Je hebt nog geen 3D prints besteld. Bekijk onze studio collectie!
</p> </p>
<div class="pt-2"> <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"> <a href="{{ route('catalog') }}" class="inline-flex items-center space-x-1.5 px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition shadow-xs">
<span>Winkelassortiment bekijken</span> <span>Collectie bekijken</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-right" class="w-3 h-3"></i>
</a> </a>
</div> </div>
</div> </div>
@else @else
<div class="space-y-4"> <div class="space-y-3">
@foreach($orders as $order) @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"> <div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-2xs space-y-3">
<!-- Header regel --> <!-- 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 class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 gap-2">
<div> <div>
<span class="font-mono font-bold text-xs text-slate-900">#{{ $order->order_number }}</span> <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> <span class="text-xs text-slate-400 ml-2">&bull; {{ $order->created_at->format('d-m-Y H:i') }}</span>
</div> </div>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase <span class="px-2 py-0.5 rounded-full text-[10px] font-semibold
@if($order->status === 'completed' || $order->status === 'shipped' || $order->status === 'verzonden') bg-emerald-100 text-emerald-800 @if($order->status === 'completed' || $order->status === 'shipped') bg-emerald-50 text-emerald-700 border border-emerald-200/60
@elseif($order->status === 'printed' || $order->status === 'processing' || $order->status === 'in_print') bg-sky-100 text-sky-800 @elseif($order->status === 'printed' || $order->status === 'processing') bg-sky-50 text-sky-700 border border-sky-200/60
@elseif($order->status === 'cancelled' || $order->status === 'geannuleerd') bg-rose-100 text-rose-800 @elseif($order->status === 'cancelled') bg-rose-50 text-rose-700 border border-rose-200/60
@else bg-amber-100 text-amber-900 @endif @else bg-amber-50 text-amber-700 border border-amber-200/60 @endif
"> ">
{{ str_replace('_', ' ', $order->status) }} {{ $order->status_label }}
</span> </span>
<span class="font-heading font-black text-slate-900 text-sm"> <span class="font-bold text-slate-900 text-xs">
{{ $order->formatted_total }} {{ $order->formatted_total }}
</span> </span>
</div> </div>
@@ -118,28 +134,28 @@
<!-- Items --> <!-- Items -->
<div class="divide-y divide-slate-100"> <div class="divide-y divide-slate-100">
@foreach($order->items as $item) @foreach($order->items as $item)
<div class="py-2.5 flex items-start justify-between text-xs"> <div class="py-2 flex items-start justify-between text-xs">
<div> <div>
<div class="font-bold text-slate-900">{{ $item->product_name }}</div> <div class="font-medium text-slate-900">{{ $item->product_name }}</div>
<div class="text-[11px] text-slate-500 mt-0.5"> <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->material) {{ strtoupper($item->material) }} &bull; @endif
@if($item->color) Kleur: <span class="font-semibold">{{ $item->color }}</span> &bull; @endif @if($item->color) {{ $item->color }} &bull; @endif
Schaal: {{ $item->scale ?? '100%' }} {{ $item->scale ?? '100%' }}
</div> </div>
</div> </div>
<div class="text-right"> <div class="text-right">
<span class="font-semibold text-slate-600">{{ $item->quantity }}x</span> <span class="text-slate-500">{{ $item->quantity }}x</span>
<span class="font-bold text-slate-900 ml-1">&euro; {{ number_format($item->total_price, 2, ',', '.') }}</span> <span class="font-semibold text-slate-900 ml-1">&euro; {{ number_format($item->total_price, 2, ',', '.') }}</span>
</div> </div>
</div> </div>
@endforeach @endforeach
</div> </div>
<!-- Bezorging adres --> <!-- Bezorging adres -->
<div class="pt-3 border-t border-slate-100 flex items-center justify-between text-[11px] text-slate-500"> <div class="pt-2.5 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>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"> <span class="font-medium text-slate-700 flex items-center space-x-1">
<i data-lucide="truck" class="w-3.5 h-3.5"></i> <i data-lucide="truck" class="w-3.5 h-3.5 text-slate-400"></i>
<span>PostNL</span> <span>PostNL</span>
</span> </span>
</div> </div>
@@ -150,32 +166,112 @@
@endif @endif
</div> </div>
<!-- TAB: MIJN VRAGEN & COMMUNICATIE -->
<div x-show="tab === 'messages'" class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-sm font-bold text-slate-900">Jouw gestelde vragen aan Jouwdriedee.nl</h2>
<p class="text-xs text-slate-500">Volg hier jouw berichten en antwoorden van onze printstudio.</p>
</div>
<a href="{{ route('contact.index') }}" class="px-3.5 py-1.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
<span>Nieuwe vraag stellen</span>
</a>
</div>
@if($communications->isEmpty())
<div class="p-10 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-2.5">
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto">
<i data-lucide="message-square" class="w-5 h-5"></i>
</div>
<h3 class="text-sm font-semibold text-slate-900">Nog geen vragen gesteld</h3>
<p class="text-xs text-slate-500 max-w-xs mx-auto">
Heb je een vraag over een model, levering of materiaal? Stel hem gerust!
</p>
<div class="pt-2">
<a href="{{ route('contact.index') }}" class="inline-flex items-center space-x-1.5 px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition shadow-xs">
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
<span>Vraag stellen aan de shop</span>
</a>
</div>
</div>
@else
<div class="space-y-3">
@foreach($communications as $comm)
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-2xs space-y-3">
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-2.5 border-b border-slate-100 gap-2">
<div>
<span class="font-mono text-xs text-slate-400">#{{ $comm->id }}</span>
<h3 class="font-bold text-slate-900 text-xs sm:text-sm inline ml-1">{{ $comm->subject }}</h3>
<span class="text-xs text-slate-400 ml-2">&bull; {{ $comm->created_at->format('d-m-Y om H:i') }}</span>
</div>
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold self-start sm:self-auto
@if($comm->status === 'new') bg-sky-50 text-sky-700 border border-sky-200/60
@elseif($comm->status === 'in_progress') bg-amber-50 text-amber-700 border border-amber-200/60
@elseif($comm->status === 'resolved') bg-emerald-50 text-emerald-700 border border-emerald-200/60
@else bg-slate-100 text-slate-600 border border-slate-200/60 @endif">
{{ $comm->status_label }}
</span>
</div>
<!-- Oorspronkelijke vraag -->
<div class="text-xs text-slate-700 leading-relaxed bg-slate-50 p-3 rounded-xl border border-slate-100">
<span class="font-semibold text-slate-900 block text-[11px] mb-1">Jouw vraag:</span>
{{ $comm->message }}
</div>
<!-- Eventuele antwoorden van de shop -->
@if($comm->replies->isNotEmpty())
<div class="space-y-2 pt-1">
<span class="text-[11px] font-semibold text-emerald-700 block">Antwoord van Jouwdriedee.nl:</span>
@foreach($comm->replies as $reply)
<div class="p-3 rounded-xl bg-emerald-50/60 border border-emerald-200/70 text-xs text-slate-800 space-y-1">
<div class="flex items-center justify-between text-[10px] text-emerald-700 font-medium">
<span>Studio Specialist</span>
<span>{{ $reply->created_at->format('d-m-Y H:i') }}</span>
</div>
<div class="whitespace-pre-wrap">{{ $reply->message }}</div>
</div>
@endforeach
</div>
@else
<div class="text-[11px] text-slate-400 italic">
We hebben jouw vraag in behandeling. Je ontvangt zo snel mogelijk antwoord via e-mail.
</div>
@endif
</div>
@endforeach
</div>
@endif
</div>
<!-- TAB 2: PROFIEL & BEVEILIGING --> <!-- TAB 2: PROFIEL & BEVEILIGING -->
<div x-show="tab === 'profile'" class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div x-show="tab === 'profile'" class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- Profielgegevens --> <!-- Profielgegevens -->
<div class="p-6 rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3.5">
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-1.5">
<i data-lucide="user" class="w-4 h-4 text-amber-500"></i> <i data-lucide="user" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Persoonlijke gegevens</span> <span>Persoonlijke gegevens</span>
</h3> </h3>
<form action="{{ route('account.profile') }}" method="POST" class="space-y-4"> <form action="{{ route('account.profile') }}" method="POST" class="space-y-3 pt-1">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700">Naam</label> <label class="block text-xs font-medium 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"> <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 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700">E-mailadres</label> <label class="block text-xs font-medium 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"> <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 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="pt-2"> <div class="pt-1">
<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"> <button type="submit" class="px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition shadow-xs cursor-pointer">
Gegevens Opslaan Gegevens Opslaan
</button> </button>
</div> </div>
@@ -183,33 +279,33 @@
</div> </div>
<!-- Wachtwoord Wijzigen --> <!-- Wachtwoord Wijzigen -->
<div class="p-6 rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3.5">
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-1.5">
<i data-lucide="lock" class="w-4 h-4 text-rose-500"></i> <i data-lucide="lock" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Wachtwoord wijzigen</span> <span>Wachtwoord wijzigen</span>
</h3> </h3>
<form action="{{ route('account.password') }}" method="POST" class="space-y-4"> <form action="{{ route('account.password') }}" method="POST" class="space-y-3 pt-1">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700">Huidig wachtwoord</label> <label class="block text-xs font-medium 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"> <input type="password" name="current_password" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label> <label class="block text-xs font-medium 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"> <input type="password" name="password" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700">Bevestig nieuw wachtwoord</label> <label class="block text-xs font-medium 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"> <input type="password" name="password_confirmation" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="pt-2"> <div class="pt-1">
<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"> <button type="submit" class="px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition shadow-xs cursor-pointer">
Wachtwoord Wijzigen Wachtwoord Wijzigen
</button> </button>
</div> </div>
@@ -220,4 +316,3 @@
</div> </div>
@endsection @endsection
+23 -24
View File
@@ -6,58 +6,57 @@
<div class="space-y-6 max-w-xl mx-auto"> <div class="space-y-6 max-w-xl mx-auto">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between pb-6 border-b border-slate-800"> <div class="flex items-center justify-between pb-6 border-b border-slate-200/80">
<div> <div>
<a href="{{ route('admin.categories.index') }}" class="text-xs text-amber-400 font-bold hover:underline inline-flex items-center space-x-1 mb-1"> <a href="{{ route('admin.categories.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1 transition">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar Categorieën</span> <span>Terug naar Categorieën</span>
</a> </a>
<h1 class="font-heading text-2xl font-black text-white">Categorie Bewerken: {{ $category->name }}</h1> <h1 class="text-2xl font-bold tracking-tight text-slate-900">Categorie Bewerken</h1>
<p class="text-xs text-slate-500 mt-0.5">{{ $category->name }}</p>
</div> </div>
</div> </div>
<!-- Edit Form --> <!-- Edit Form -->
<form action="{{ route('admin.categories.update', $category) }}" method="POST" class="space-y-6"> <form action="{{ route('admin.categories.update', $category) }}" method="POST" class="space-y-5">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-300">Categorienaam *</label> <label class="block text-xs font-medium text-slate-700">Categorienaam *</label>
<input type="text" name="name" value="{{ old('name', $category->name) }}" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white font-bold focus:outline-none focus:ring-2 focus:ring-amber-400"> <input type="text" name="name" value="{{ old('name', $category->name) }}" required class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-300">Omschrijving</label> <label class="block text-xs font-medium text-slate-700">Omschrijving</label>
<textarea name="description" rows="3" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2 text-xs text-white leading-relaxed">{{ old('description', $category->description) }}</textarea> <textarea name="description" rows="3" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 leading-relaxed focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">{{ old('description', $category->description) }}</textarea>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-300">Lucide Icoon</label> <label class="block text-xs font-medium text-slate-700">Lucide Icoon</label>
<input type="text" name="icon" value="{{ old('icon', $category->icon) }}" placeholder="sparkles, zap, film..." class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="text" name="icon" value="{{ old('icon', $category->icon) }}" placeholder="sparkles, zap, film..." class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-300">Volgorde</label> <label class="block text-xs font-medium text-slate-700">Volgorde</label>
<input type="number" name="sort_order" value="{{ old('sort_order', $category->sort_order) }}" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="number" name="sort_order" value="{{ old('sort_order', $category->sort_order) }}" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center justify-end space-x-3"> <div class="flex items-center justify-end space-x-2.5">
<a href="{{ route('admin.categories.index') }}" class="px-4 py-2.5 rounded-xl bg-slate-800 text-slate-300 hover:text-white text-xs font-bold transition"> <a href="{{ route('admin.categories.index') }}" class="px-4 py-2 rounded-xl bg-white hover:bg-slate-50 border border-slate-200 text-slate-700 text-xs font-medium transition">
Annuleren Annuleren
</a> </a>
<button type="submit" class="px-6 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition flex items-center space-x-1.5"> <button type="submit" class="px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition shadow-xs cursor-pointer">
<i data-lucide="save" class="w-4 h-4"></i> Wijzigingen Opslaan
<span>Opslaan</span>
</button> </button>
</div> </div>
</form> </form>
</div> </div>
@endsection @endsection
@@ -6,41 +6,48 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between pb-6 border-b border-slate-800"> <div class="flex items-center justify-between pb-6 border-b border-slate-200/80">
<div> <div>
<div class="inline-flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1"> <div class="inline-flex items-center space-x-1.5 text-xs text-slate-500 font-medium mb-1">
<i data-lucide="folder-tree" class="w-4 h-4"></i> <i data-lucide="folder-tree" class="w-3.5 h-3.5"></i>
<span>Winkelstructuur</span> <span>Winkelstructuur</span>
</div> </div>
<h1 class="font-heading text-2xl sm:text-3xl font-black text-white">Categorieën Beheer</h1> <h1 class="text-2xl font-bold tracking-tight text-slate-900">Categorieën</h1>
<p class="text-xs sm:text-sm text-slate-400 mt-1">Beheer categorie titels, omschrijvingen en sortering.</p> <p class="text-xs text-slate-500 mt-1">Beheer categorietitels, omschrijvingen en sortering van de webshop.</p>
</div> </div>
</div> </div>
@if(session('success'))
<div class="p-3.5 rounded-xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-center space-x-2.5">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 shrink-0"></i>
<span class="font-medium">{{ session('success') }}</span>
</div>
@endif
<!-- Categories Grid --> <!-- Categories Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-5">
@foreach($categories as $category) @foreach($categories as $category)
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 flex flex-col justify-between space-y-4"> <div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-2xs hover:border-slate-300 transition flex flex-col justify-between space-y-4">
<div class="space-y-3"> <div class="space-y-2.5">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="px-2.5 py-1 rounded-lg bg-amber-400/20 text-amber-400 font-mono text-xs font-black"> <span class="px-2 py-0.5 rounded-md bg-slate-100 text-slate-700 font-mono text-[11px]">
/catalogus?category={{ $category->slug }} /catalogus?category={{ $category->slug }}
</span> </span>
<span class="text-xs text-slate-400 font-bold"> <span class="text-xs text-slate-500 font-medium">
{{ $category->products_count }} producten {{ $category->products_count }} producten
</span> </span>
</div> </div>
<h3 class="font-heading text-lg font-black text-white">{{ $category->name }}</h3> <h3 class="text-base font-bold text-slate-900">{{ $category->name }}</h3>
<p class="text-xs text-slate-400 leading-relaxed font-normal">{{ $category->description }}</p> <p class="text-xs text-slate-500 leading-relaxed font-normal">{{ $category->description }}</p>
</div> </div>
<div class="pt-4 border-t border-slate-800 flex items-center justify-between"> <div class="pt-3.5 border-t border-slate-100 flex items-center justify-between">
<a href="{{ route('catalog', ['category' => $category->slug]) }}" target="_blank" class="text-xs text-slate-400 hover:text-white flex items-center space-x-1"> <a href="{{ route('catalog', ['category' => $category->slug]) }}" target="_blank" class="text-xs text-slate-500 hover:text-slate-900 flex items-center space-x-1 transition">
<span>Bekijk in shop</span> <span>Bekijk in shop</span>
<i data-lucide="external-link" class="w-3 h-3"></i> <i data-lucide="external-link" class="w-3 h-3"></i>
</a> </a>
<a href="{{ route('admin.categories.edit', $category) }}" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-amber-400 text-slate-200 hover:text-slate-950 text-xs font-bold transition flex items-center space-x-1"> <a href="{{ route('admin.categories.edit', $category) }}" class="px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition flex items-center space-x-1.5">
<i data-lucide="pencil" class="w-3 h-3"></i> <i data-lucide="pencil" class="w-3 h-3"></i>
<span>Bewerken</span> <span>Bewerken</span>
</a> </a>
@@ -51,4 +58,3 @@
</div> </div>
@endsection @endsection
@@ -0,0 +1,223 @@
@extends('layouts.admin')
@section('title', 'Communicatie & Klantvragen | Jouwdriedee Beheer')
@section('content')
<div class="space-y-6">
<!-- Header Section -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
<div>
<div class="inline-flex items-center space-x-1.5 text-xs text-slate-500 font-medium mb-1">
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
<span>Klantenservice &amp; Inquiries</span>
</div>
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Communicatie ({{ $counts['all'] }})</h1>
<p class="text-xs text-slate-500 mt-1">
Overzicht van binnengekomen vragen via de webshop, afschriften en verzonden reacties.
</p>
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('admin.customers.index') }}" class="px-3.5 py-2 rounded-xl bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
<i data-lucide="users" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Klantprofielen</span>
</a>
<a href="{{ route('contact.index') }}" target="_blank" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
<i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Open Formulier</span>
</a>
</div>
</div>
<!-- Flash message -->
@if(session('success'))
<div class="p-3.5 rounded-xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-center space-x-2.5">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 shrink-0"></i>
<span class="font-medium">{{ session('success') }}</span>
</div>
@endif
<!-- Filter & Search Bar -->
<div class="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
<!-- Status Tabs -->
<div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl max-w-max text-xs font-medium overflow-x-auto">
<a href="{{ route('admin.communications.index', ['status' => 'all', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'all' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span>Alle</span>
<span class="px-1.5 py-0.2 rounded-md text-[10px] {{ $currentStatus === 'all' ? 'bg-slate-100 text-slate-700' : 'bg-slate-200/60 text-slate-600' }}">
{{ $counts['all'] }}
</span>
</a>
<a href="{{ route('admin.communications.index', ['status' => 'new', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'new' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span class="w-1.5 h-1.5 rounded-full bg-sky-500 {{ $counts['new'] > 0 ? 'animate-pulse' : '' }}"></span>
<span>Nieuw</span>
@if($counts['new'] > 0)
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-sky-100 text-sky-800 font-bold">
{{ $counts['new'] }}
</span>
@endif
</a>
<a href="{{ route('admin.communications.index', ['status' => 'in_progress', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'in_progress' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span>In behandeling</span>
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-600">
{{ $counts['in_progress'] }}
</span>
</a>
<a href="{{ route('admin.communications.index', ['status' => 'resolved', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'resolved' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span>Beantwoord</span>
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-600">
{{ $counts['resolved'] }}
</span>
</a>
<a href="{{ route('admin.communications.index', ['status' => 'closed', 'q' => $search]) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentStatus === 'closed' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span>Gesloten</span>
</a>
</div>
<!-- Search Form -->
<form action="{{ route('admin.communications.index') }}" method="GET" class="flex items-center space-x-2">
<input type="hidden" name="status" value="{{ $currentStatus }}">
<div class="relative w-full sm:w-64">
<input
type="text"
name="q"
value="{{ $search }}"
placeholder="Zoek op naam, e-mail..."
class="w-full pl-8 pr-3 py-1.5 bg-white border border-slate-200/80 rounded-xl text-xs text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-900 focus:border-slate-900"
>
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
</div>
@if($search)
<a href="{{ route('admin.communications.index', ['status' => $currentStatus]) }}" class="px-2.5 py-1.5 text-xs text-slate-500 hover:text-slate-800 transition">
Wissen
</a>
@endif
</form>
</div>
<!-- Communications Table Card -->
<div class="rounded-2xl bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-slate-700">
<thead class="bg-slate-50/80 text-[11px] uppercase tracking-wider text-slate-500 font-semibold border-b border-slate-200/70">
<tr>
<th class="py-3 px-4 w-20">ID</th>
<th class="py-3 px-4 w-40">Datum &amp; Tijd</th>
<th class="py-3 px-4">Klant</th>
<th class="py-3 px-4">Onderwerp &amp; Vraag</th>
<th class="py-3 px-4 w-32">Status</th>
<th class="py-3 px-4 text-right w-28">Actie</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 font-medium">
@forelse($communications as $comm)
<tr class="hover:bg-slate-50/60 transition {{ $comm->status === 'new' ? 'bg-sky-50/30 font-semibold' : '' }}">
<!-- ID -->
<td class="py-3.5 px-4 font-mono text-slate-500">
#{{ $comm->id }}
</td>
<!-- Datum -->
<td class="py-3.5 px-4 text-slate-500 whitespace-nowrap">
<div>{{ $comm->created_at->format('d-m-Y') }}</div>
<div class="text-[10px] text-slate-400 font-mono">{{ $comm->created_at->format('H:i') }}</div>
</td>
<!-- Klant -->
<td class="py-3.5 px-4">
<div class="flex items-center space-x-2">
<a href="{{ route('admin.customers.show', $comm->user_id ?? $comm->email) }}" class="font-semibold text-slate-900 hover:underline">
{{ $comm->name }}
</a>
@if($comm->user_id)
<span class="px-1.5 py-0.2 rounded bg-slate-100 text-slate-600 text-[10px] font-normal" title="Geregistreerde klant">
Account
</span>
@else
<span class="px-1.5 py-0.2 rounded bg-slate-50 text-slate-400 text-[10px] font-normal" title="Gastbezoeker">
Gast
</span>
@endif
</div>
<div class="text-[11px] text-slate-400 font-normal">
{{ $comm->email }}
</div>
@if($comm->order)
<div class="text-[10px] text-slate-500 mt-0.5">
Betreft order:
<a href="{{ route('admin.orders.show', $comm->order) }}" class="font-mono text-indigo-600 hover:underline">
#{{ $comm->order->order_number }}
</a>
</div>
@endif
</td>
<!-- Onderwerp & Snippet -->
<td class="py-3.5 px-4">
<a href="{{ route('admin.communications.show', $comm) }}" class="font-bold text-slate-900 hover:text-slate-700 block truncate max-w-md">
{{ $comm->subject }}
</a>
<p class="text-[11px] text-slate-500 font-normal line-clamp-1 max-w-md mt-0.5">
{{ $comm->message }}
</p>
@if($comm->replies->isNotEmpty())
<span class="inline-flex items-center space-x-1 text-[10px] text-emerald-700 font-semibold mt-1">
<i data-lucide="check" class="w-3 h-3"></i>
<span>{{ $comm->replies->count() }} reactie(s) verzonden</span>
</span>
@endif
</td>
<!-- Status Badge -->
<td class="py-3.5 px-4 whitespace-nowrap">
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-semibold
@if($comm->status === 'new') bg-sky-50 text-sky-700 border border-sky-200/80
@elseif($comm->status === 'in_progress') bg-amber-50 text-amber-700 border border-amber-200/80
@elseif($comm->status === 'resolved') bg-emerald-50 text-emerald-700 border border-emerald-200/80
@else bg-slate-100 text-slate-600 border border-slate-200/60
@endif">
{{ $comm->status_label }}
</span>
</td>
<!-- Actie -->
<td class="py-3.5 px-4 text-right whitespace-nowrap">
<a href="{{ route('admin.communications.show', $comm) }}" class="inline-flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition">
<span>Bekijken</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="py-12 text-center text-slate-400">
<div class="w-10 h-10 rounded-xl bg-slate-100 flex items-center justify-center text-slate-400 mx-auto mb-2">
<i data-lucide="inbox" class="w-5 h-5"></i>
</div>
<p class="text-xs font-medium text-slate-600">Geen berichten gevonden</p>
<p class="text-[11px] text-slate-400 mt-0.5">Vragen van klanten via het contactformulier verschijnen hier direct.</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($communications->hasPages())
<div class="p-4 border-t border-slate-100">
{{ $communications->links() }}
</div>
@endif
</div>
</div>
@endsection
@@ -0,0 +1,283 @@
@extends('layouts.admin')
@section('title', 'Klantvraag #' . $communication->id . ': ' . $communication->subject . ' | Jouwdriedee Beheer')
@section('content')
<div class="space-y-6 max-w-6xl mx-auto">
<!-- Header Section -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
<div>
<a href="{{ route('admin.communications.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1 transition">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar Communicatie overzicht</span>
</a>
<div class="flex items-center space-x-2 mt-0.5">
<span class="font-mono text-xs text-slate-400">#{{ $communication->id }}</span>
<h1 class="text-xl sm:text-2xl font-bold tracking-tight text-slate-900">{{ $communication->subject }}</h1>
</div>
<p class="text-xs text-slate-500 mt-0.5">
Ontvangen op {{ $communication->created_at->format('d-m-Y om H:i') }} &bull; Afschrift verzonden naar klant
</p>
</div>
<div class="flex items-center space-x-2">
<!-- Delete Communication -->
<form action="{{ route('admin.communications.destroy', $communication) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit klantbericht wilt verwijderen?');">
@csrf
@method('DELETE')
<button type="submit" class="px-3 py-1.5 rounded-lg text-slate-400 hover:text-rose-600 hover:bg-rose-50 border border-transparent hover:border-rose-200 text-xs font-medium transition cursor-pointer flex items-center space-x-1">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Verwijderen</span>
</button>
</form>
</div>
</div>
<!-- Flash message -->
@if(session('success'))
<div class="p-3.5 rounded-xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-center space-x-2.5">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 shrink-0"></i>
<span class="font-medium">{{ session('success') }}</span>
</div>
@endif
@if(session('warning'))
<div class="p-3.5 rounded-xl bg-amber-50 border border-amber-200/60 text-amber-800 text-xs flex items-center space-x-2.5">
<i data-lucide="alert-triangle" class="w-4 h-4 text-amber-600 shrink-0"></i>
<span class="font-medium">{{ session('warning') }}</span>
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<!-- Linkerkolom: Bericht Thread & Antwoordformulier -->
<div class="lg:col-span-8 space-y-6">
<!-- Oorspronkelijke vraag van de klant -->
<div class="bg-white rounded-2xl border border-slate-200/80 shadow-xs p-6 space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-100">
<div class="flex items-center space-x-3">
<div class="w-9 h-9 rounded-xl bg-slate-900 text-white flex items-center justify-center font-bold text-xs">
{{ strtoupper(substr($communication->name, 0, 1)) }}
</div>
<div>
<div class="flex items-center space-x-2">
<span class="font-bold text-slate-900 text-xs">{{ $communication->name }}</span>
<span class="text-[10px] text-slate-400">&lt;{{ $communication->email }}&gt;</span>
</div>
<span class="text-[11px] text-slate-400">Oorspronkelijk bericht</span>
</div>
</div>
<span class="text-[11px] text-slate-400 font-mono">
{{ $communication->created_at->format('d-m-Y H:i:s') }}
</span>
</div>
<!-- Het daadwerkelijke bericht -->
<div class="text-xs sm:text-sm text-slate-800 leading-relaxed whitespace-pre-wrap font-normal p-4 rounded-xl bg-slate-50 border border-slate-200/60">
{{ $communication->message }}
</div>
@if($communication->ip_address)
<div class="text-[10px] text-slate-400 flex items-center space-x-3 pt-1">
<span>IP-adres: {{ $communication->ip_address }}</span>
<span>&bull;</span>
<span>Afschrift verstuurd naar: {{ $communication->email }}</span>
</div>
@endif
</div>
<!-- Reactie Geschiedenis (Replies) -->
@if($communication->replies->isNotEmpty())
<div class="space-y-4">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="corner-down-right" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Eerdere Reacties ({{ $communication->replies->count() }})</span>
</h3>
@foreach($communication->replies as $reply)
<div class="bg-white rounded-2xl border border-emerald-200/80 shadow-xs p-5 space-y-3">
<div class="flex items-center justify-between pb-2 border-b border-slate-100">
<div class="flex items-center space-x-2.5">
<div class="w-7 h-7 rounded-lg bg-emerald-600 text-white flex items-center justify-center font-bold text-[10px]">
{{ $reply->user ? strtoupper(substr($reply->user->name, 0, 1)) : 'J3D' }}
</div>
<div>
<span class="font-semibold text-slate-900 text-xs">{{ $reply->user?->name ?? 'Jouwdriedee.nl Beheer' }}</span>
<span class="text-[10px] text-emerald-700 font-medium ml-1.5 px-1.5 py-0.2 rounded bg-emerald-50">Verzonden via info@jouwdriedee.nl</span>
</div>
</div>
<span class="text-[11px] text-slate-400 font-mono">
{{ $reply->created_at->format('d-m-Y H:i') }}
</span>
</div>
<div class="text-xs sm:text-sm text-slate-800 leading-relaxed whitespace-pre-wrap">
{{ $reply->message }}
</div>
</div>
@endforeach
</div>
@endif
<!-- Direct Antwoord Formulier -->
<div class="bg-white rounded-2xl border border-slate-200/80 shadow-xs p-6 space-y-4">
<div>
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="reply" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Beantwoord deze vraag direct</span>
</h3>
<p class="text-xs text-slate-500 mt-0.5">
Jouw antwoord wordt als e-mail verzonden vanuit <code class="text-slate-800 font-mono bg-slate-100 px-1 py-0.2 rounded">info@jouwdriedee.nl</code> naar <strong class="text-slate-800">{{ $communication->email }}</strong>.
</p>
</div>
<form action="{{ route('admin.communications.reply', $communication) }}" method="POST" class="space-y-3">
@csrf
<div>
<textarea
name="message"
rows="5"
required
placeholder="Typ jouw reactie aan {{ $communication->name }}..."
class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3.5 text-xs sm:text-sm text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 leading-relaxed"
></textarea>
</div>
<div class="flex items-center justify-between pt-1">
<div class="text-[11px] text-slate-400 flex items-center space-x-1.5">
<i data-lucide="mail-check" class="w-3.5 h-3.5 text-emerald-600"></i>
<span>Status wordt na verzending automatisch 'Beantwoord'</span>
</div>
<button type="submit" class="px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center space-x-2 shadow-xs cursor-pointer">
<i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Verstuur Reactie naar Klant</span>
</button>
</div>
</form>
</div>
</div>
<!-- Rechterkolom: Klantprofiel Info & Status Paneel -->
<div class="lg:col-span-4 space-y-5">
<!-- Status & Notities Box -->
<div class="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-4">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Status &amp; Interne Notities</h3>
<form action="{{ route('admin.communications.status', $communication) }}" method="POST" class="space-y-3">
@csrf
<div class="space-y-1">
<label class="block text-xs font-medium text-slate-700">Dossier Status</label>
<select name="status" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs font-semibold text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
<option value="new" {{ $communication->status === 'new' ? 'selected' : '' }}>Nieuw</option>
<option value="in_progress" {{ $communication->status === 'in_progress' ? 'selected' : '' }}>In behandeling</option>
<option value="resolved" {{ $communication->status === 'resolved' ? 'selected' : '' }}>Beantwoord / Opgelost</option>
<option value="closed" {{ $communication->status === 'closed' ? 'selected' : '' }}>Gesloten</option>
</select>
</div>
<div class="space-y-1">
<label class="block text-xs font-medium text-slate-700">Interne beheerder notitie</label>
<textarea name="admin_notes" rows="3" placeholder="Alleen zichtbaar voor beheerders..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">{{ old('admin_notes', $communication->admin_notes) }}</textarea>
</div>
<button type="submit" class="w-full py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 text-xs font-medium transition cursor-pointer">
Status &amp; Notitie Opslaan
</button>
</form>
</div>
<!-- Klantprofiel Kaart -->
<div class="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3.5">
<div class="flex items-center justify-between">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Klantgegevens</h3>
<a href="{{ route('admin.customers.show', $communication->user_id ?? $communication->email) }}" class="text-[11px] font-semibold text-slate-900 underline hover:text-slate-700">
Volledig Profiel &rarr;
</a>
</div>
<div class="text-xs space-y-2 text-slate-600">
<div>
<span class="text-slate-400 block text-[10px]">Naam</span>
<a href="{{ route('admin.customers.show', $communication->user_id ?? $communication->email) }}" class="font-semibold text-slate-900 hover:underline">
{{ $communication->name }}
</a>
</div>
<div>
<span class="text-slate-400 block text-[10px]">E-mailadres</span>
<a href="mailto:{{ $communication->email }}" class="text-slate-900 hover:underline font-mono">
{{ $communication->email }}
</a>
</div>
@if($communication->phone)
<div>
<span class="text-slate-400 block text-[10px]">Telefoonnummer</span>
<a href="tel:{{ $communication->phone }}" class="text-slate-900 hover:underline font-mono">
{{ $communication->phone }}
</a>
</div>
@endif
<div>
<span class="text-slate-400 block text-[10px]">Account status</span>
@if($communication->user)
<span class="inline-flex items-center space-x-1 text-emerald-700 font-semibold">
<i data-lucide="check" class="w-3 h-3"></i>
<span>Geregistreerd account (#{{ $communication->user->id }})</span>
</span>
@else
<span class="text-slate-500">Gastbezoeker (geen account)</span>
@endif
</div>
</div>
<div class="pt-2 border-t border-slate-100">
<a href="{{ route('admin.customers.show', $communication->user_id ?? $communication->email) }}" class="w-full py-2 px-3 rounded-xl bg-slate-50 hover:bg-slate-100 text-slate-800 text-xs font-medium transition flex items-center justify-center space-x-1.5">
<i data-lucide="user" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Bekijk alle communicatie van deze klant</span>
</a>
</div>
</div>
<!-- Gekoppelde Bestelling (indien van toepassing) -->
@if($communication->order)
<div class="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Betreft Bestelling</h3>
<a href="{{ route('admin.orders.show', $communication->order) }}" class="text-[11px] font-semibold text-indigo-600 underline">
Order details &rarr;
</a>
</div>
<div class="text-xs space-y-1.5 text-slate-600">
<div>
<span class="text-slate-400 block text-[10px]">Ordernummer</span>
<span class="font-mono font-bold text-slate-900">#{{ $communication->order->order_number }}</span>
</div>
<div>
<span class="text-slate-400 block text-[10px]">Orderdatum</span>
<span>{{ $communication->order->created_at->format('d-m-Y H:i') }}</span>
</div>
<div>
<span class="text-slate-400 block text-[10px]">Bedrag &amp; Status</span>
<span class="font-bold text-slate-900">{{ $communication->order->formatted_total }}</span>
<span class="ml-1 text-[10px] px-1.5 py-0.2 rounded bg-slate-100 font-semibold">{{ $communication->order->status }}</span>
</div>
</div>
</div>
@endif
</div>
</div>
</div>
@endsection
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,151 @@
@extends('layouts.admin')
@section('title', 'Klanten & Profielen | Jouwdriedee Beheer')
@section('content')
<div class="space-y-6">
<!-- Header Section -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
<div>
<div class="inline-flex items-center space-x-1.5 text-xs text-slate-500 font-medium mb-1">
<i data-lucide="users" class="w-3.5 h-3.5"></i>
<span>Relatiebeheer &amp; Accounts</span>
</div>
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Klanten &amp; Profielen</h1>
<p class="text-xs text-slate-500 mt-1">
Overzicht van geregistreerde klanten ({{ $totalRegistered }}) en gastkopers ({{ $totalGuests }}).
</p>
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('admin.communications.index') }}" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
<i data-lucide="message-square" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Communicatie Inbox</span>
</a>
</div>
</div>
<!-- Search Bar -->
<div class="flex items-center justify-between">
<form action="{{ route('admin.customers.index') }}" method="GET" class="flex items-center space-x-2 w-full sm:w-80">
<div class="relative w-full">
<input
type="text"
name="q"
value="{{ $search }}"
placeholder="Zoek klant op naam of e-mail..."
class="w-full pl-8 pr-3 py-2 bg-white border border-slate-200/80 rounded-xl text-xs text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-900 focus:border-slate-900"
>
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
</div>
@if($search)
<a href="{{ route('admin.customers.index') }}" class="px-2.5 py-2 text-xs text-slate-500 hover:text-slate-800 transition">
Wissen
</a>
@endif
</form>
</div>
<!-- Customers Table Card -->
<div class="rounded-2xl bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-slate-700">
<thead class="bg-slate-50/80 text-[11px] uppercase tracking-wider text-slate-500 font-semibold border-b border-slate-200/70">
<tr>
<th class="py-3 px-4">Klant</th>
<th class="py-3 px-4">Type</th>
<th class="py-3 px-4">Bestellingen</th>
<th class="py-3 px-4">Communicatie / Vragen</th>
<th class="py-3 px-4">Actief sinds</th>
<th class="py-3 px-4 text-right">Profiel</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 font-medium">
@forelse($customers as $customer)
<tr class="hover:bg-slate-50/60 transition">
<!-- Naam & E-mail -->
<td class="py-3.5 px-4">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-xl bg-slate-900 text-white flex items-center justify-center font-bold text-xs shrink-0">
{{ strtoupper(substr($customer->name, 0, 1)) }}
</div>
<div>
<a href="{{ route('admin.customers.show', $customer->id ?? $customer->email) }}" class="font-bold text-slate-900 hover:underline">
{{ $customer->name }}
</a>
<div class="text-[11px] text-slate-400 font-normal">
{{ $customer->email }}
</div>
</div>
</div>
</td>
<!-- Type -->
<td class="py-3.5 px-4">
@if($customer->is_registered)
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200/60">
Geregistreerd account
</span>
@else
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-100 text-slate-600 border border-slate-200/60">
Gastkoper
</span>
@endif
</td>
<!-- Bestellingen -->
<td class="py-3.5 px-4">
<span class="px-2 py-0.5 rounded-md bg-slate-100 text-slate-700 text-[11px] font-medium">
{{ $customer->orders_count }} order(s)
</span>
</td>
<!-- Communicatie -->
<td class="py-3.5 px-4">
@if($customer->communications_count > 0)
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md bg-indigo-50 text-indigo-700 text-[11px] font-semibold border border-indigo-200/60">
<i data-lucide="message-square" class="w-3 h-3"></i>
<span>{{ $customer->communications_count }} bericht(en)</span>
</span>
@else
<span class="text-slate-400 text-[11px]">0 berichten</span>
@endif
</td>
<!-- Actief sinds -->
<td class="py-3.5 px-4 text-slate-500 whitespace-nowrap">
{{ $customer->created_at instanceof \Carbon\Carbon ? $customer->created_at->format('d-m-Y') : date('d-m-Y', strtotime((string)$customer->created_at)) }}
</td>
<!-- Actie -->
<td class="py-3.5 px-4 text-right whitespace-nowrap">
<a href="{{ route('admin.customers.show', $customer->id ?? $customer->email) }}" class="inline-flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition">
<span>Bekijk profiel</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="py-12 text-center text-slate-400">
Geen klanten gevonden die voldoen aan de zoekopdracht.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($customers->hasPages())
<div class="p-4 border-t border-slate-100">
{{ $customers->links() }}
</div>
@endif
</div>
</div>
@endsection
@@ -0,0 +1,239 @@
@extends('layouts.admin')
@section('title', 'Klantprofiel: ' . $customerName . ' | Jouwdriedee Beheer')
@section('content')
<div class="space-y-6 max-w-6xl mx-auto" x-data="{ tab: 'communications' }">
<!-- Header Section -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/80 gap-4">
<div>
<a href="{{ route('admin.customers.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1 transition">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar Klantenoverzicht</span>
</a>
<div class="flex items-center space-x-2 mt-0.5">
<h1 class="text-2xl font-bold tracking-tight text-slate-900">{{ $customerName }}</h1>
@if($user)
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200/60">
Geregistreerd account (#{{ $user->id }})
</span>
@else
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-100 text-slate-600 border border-slate-200/60">
Gastkoper
</span>
@endif
</div>
<p class="text-xs text-slate-500 mt-0.5">
{{ $email }} @if($customerPhone) &bull; {{ $customerPhone }} @endif
</p>
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('admin.communications.index') }}" class="px-3.5 py-2 rounded-xl bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
<i data-lucide="inbox" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Alle Communicatie</span>
</a>
<a href="mailto:{{ $email }}" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-xs">
<i data-lucide="mail" class="w-3.5 h-3.5 text-slate-400"></i>
<span>E-mail Sturen</span>
</a>
</div>
</div>
<!-- Quick Stats Cards -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="p-4 rounded-2xl bg-white border border-slate-200/80 shadow-2xs flex items-center justify-between">
<div>
<span class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">Communicatie Dossier</span>
<span class="text-xl font-bold text-slate-900 mt-0.5 block">{{ $communications->count() }} vragen / berichten</span>
</div>
<div class="w-10 h-10 rounded-xl bg-indigo-50 text-indigo-600 flex items-center justify-center">
<i data-lucide="message-square" class="w-5 h-5"></i>
</div>
</div>
<div class="p-4 rounded-2xl bg-white border border-slate-200/80 shadow-2xs flex items-center justify-between">
<div>
<span class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">Bestellingen</span>
<span class="text-xl font-bold text-slate-900 mt-0.5 block">{{ $orders->count() }} orders</span>
</div>
<div class="w-10 h-10 rounded-xl bg-sky-50 text-sky-600 flex items-center justify-center">
<i data-lucide="shopping-cart" class="w-5 h-5"></i>
</div>
</div>
<div class="p-4 rounded-2xl bg-white border border-slate-200/80 shadow-2xs flex items-center justify-between">
<div>
<span class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block">Totale Omzet</span>
<span class="text-xl font-bold text-slate-900 mt-0.5 block">&euro; {{ number_format($totalSpent, 2, ',', '.') }}</span>
</div>
<div class="w-10 h-10 rounded-xl bg-emerald-50 text-emerald-600 flex items-center justify-center">
<i data-lucide="receipt" class="w-5 h-5"></i>
</div>
</div>
</div>
<!-- Segmented Navigation Tabs -->
<div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl max-w-max text-xs font-medium">
<button
type="button"
@click="tab = 'communications'"
:class="tab === 'communications' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
class="px-3.5 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
>
<i data-lucide="message-square" class="w-3.5 h-3.5"></i>
<span>Communicatie &amp; Vragen</span>
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
{{ $communications->count() }}
</span>
</button>
<button
type="button"
@click="tab = 'orders'"
:class="tab === 'orders' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900'"
class="px-3.5 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer"
>
<i data-lucide="shopping-bag" class="w-3.5 h-3.5"></i>
<span>Bestelgeschiedenis</span>
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-200/60 text-slate-700">
{{ $orders->count() }}
</span>
</button>
</div>
<!-- TAB 1: COMMUNICATIE & VRAGEN BINNEN DIT KLANTPROFIEL -->
<div x-show="tab === 'communications'" class="space-y-4">
@if($communications->isEmpty())
<div class="p-12 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-2">
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-1">
<i data-lucide="message-square-off" class="w-5 h-5"></i>
</div>
<h3 class="text-sm font-semibold text-slate-800">Geen communicatie gevonden</h3>
<p class="text-xs text-slate-500 max-w-sm mx-auto">
Deze klant heeft nog geen vragen gesteld via het contactformulier.
</p>
</div>
@else
<div class="space-y-3">
@foreach($communications as $comm)
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-2xs space-y-3 hover:border-slate-300 transition">
<!-- Header regel -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-2.5 border-b border-slate-100 gap-2">
<div class="flex items-center space-x-2">
<span class="font-mono text-xs text-slate-400">#{{ $comm->id }}</span>
<h3 class="font-bold text-slate-900 text-sm">{{ $comm->subject }}</h3>
</div>
<div class="flex items-center space-x-3">
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-semibold
@if($comm->status === 'new') bg-sky-50 text-sky-700 border border-sky-200/80
@elseif($comm->status === 'in_progress') bg-amber-50 text-amber-700 border border-amber-200/80
@elseif($comm->status === 'resolved') bg-emerald-50 text-emerald-700 border border-emerald-200/80
@else bg-slate-100 text-slate-600 border border-slate-200/60
@endif">
{{ $comm->status_label }}
</span>
<span class="text-xs text-slate-400 font-mono">
{{ $comm->created_at->format('d-m-Y H:i') }}
</span>
</div>
</div>
<!-- Vraag tekst -->
<div class="text-xs text-slate-700 leading-relaxed bg-slate-50 p-3 rounded-xl border border-slate-100">
{{ $comm->message }}
</div>
<!-- Footer info & link naar communicatiedossier -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pt-1 gap-2 text-xs">
<div class="flex items-center space-x-3 text-slate-500">
@if($comm->order)
<span>Betreft: <a href="{{ route('admin.orders.show', $comm->order) }}" class="font-mono text-indigo-600 hover:underline">#{{ $comm->order->order_number }}</a></span>
<span>&bull;</span>
@endif
<span>{{ $comm->replies->count() }} antwoord(en) verzonden</span>
</div>
<a href="{{ route('admin.communications.show', $comm) }}" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition shadow-xs self-start sm:self-auto">
<span>Open Bericht &amp; Beantwoord</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a>
</div>
</div>
@endforeach
</div>
@endif
</div>
<!-- TAB 2: BESTELGESCHIEDENIS BINNEN DIT KLANTPROFIEL -->
<div x-show="tab === 'orders'" class="space-y-4">
@if($orders->isEmpty())
<div class="p-12 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-2">
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-1">
<i data-lucide="shopping-cart" class="w-5 h-5"></i>
</div>
<h3 class="text-sm font-semibold text-slate-800">Nog geen bestellingen</h3>
<p class="text-xs text-slate-500 max-w-sm mx-auto">
Deze klant heeft nog geen geplaatste orders in het systeem.
</p>
</div>
@else
<div class="rounded-2xl bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
<table class="w-full text-left text-xs text-slate-700">
<thead class="bg-slate-50/80 text-[11px] uppercase tracking-wider text-slate-500 font-semibold border-b border-slate-200/70">
<tr>
<th class="py-3 px-4">Order #</th>
<th class="py-3 px-4">Datum</th>
<th class="py-3 px-4">Items</th>
<th class="py-3 px-4">Totaal</th>
<th class="py-3 px-4">Status</th>
<th class="py-3 px-4 text-right">Details</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 font-medium">
@foreach($orders as $order)
<tr class="hover:bg-slate-50/60 transition">
<td class="py-3.5 px-4 font-mono font-bold text-slate-900">
{{ $order->order_number }}
</td>
<td class="py-3.5 px-4 text-slate-500">
{{ $order->created_at->format('d-m-Y H:i') }}
</td>
<td class="py-3.5 px-4">
{{ $order->items->count() }} prints
</td>
<td class="py-3.5 px-4 font-bold text-slate-900">
{{ $order->formatted_total }}
</td>
<td class="py-3.5 px-4">
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold
@if($order->status === 'completed' || $order->status === 'shipped') bg-emerald-50 text-emerald-700 border border-emerald-200/60
@elseif($order->status === 'printed' || $order->status === 'processing') bg-sky-50 text-sky-700 border border-sky-200/60
@elseif($order->status === 'cancelled') bg-rose-50 text-rose-700 border border-rose-200/60
@else bg-amber-50 text-amber-700 border border-amber-200/60
@endif">
{{ $order->status_label }}
</span>
</td>
<td class="py-3.5 px-4 text-right">
<a href="{{ route('admin.orders.show', $order) }}" class="inline-flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition">
<span>Order bekijken</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</div>
@endsection
File diff suppressed because it is too large Load Diff
+309 -354
View File
@@ -3,386 +3,342 @@
@section('title', 'Artikel Beheer (Geïmporteerde Modellen) | Jouwdriedee.nl Beheer') @section('title', 'Artikel Beheer (Geïmporteerde Modellen) | Jouwdriedee.nl Beheer')
@section('content') @section('content')
<div class="py-8 bg-slate-950 text-slate-100 min-h-screen" x-data="artikelBeheerApp()"> <div class="space-y-6" x-data="artikelBeheerApp()">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
<!-- Top Header & Breadcrumb --> <!-- Top Header & Breadcrumb -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 pb-6 border-b border-slate-800"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 pb-6 border-b border-slate-200/60">
<div> <div>
<div class="flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1"> <span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 block mb-1">MakerWorld Modellen</span>
<span class="inline-block w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse"></span> <h1 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight">Artikel Beheer (Staging)</h1>
<span>Staging & Curation</span> <p class="mt-1 text-xs sm:text-sm text-slate-500">
<span>&bull;</span> Beoordeel binnengehaalde modellen. Prijzen zijn automatisch berekend op basis van gewicht, printduur en kleuren via de 3D-calculator.
<span>Geïmporteerde Modellen</span> </p>
</div> </div>
<h1 class="text-2xl sm:text-3xl font-black text-white flex items-center space-x-3">
<span class="p-2 bg-gradient-to-tr from-amber-400 to-orange-500 rounded-2xl text-slate-950 shadow-lg shadow-amber-400/20"> <!-- Header Quick Actions -->
<i data-lucide="layers" class="w-6 h-6"></i> <div class="flex items-center space-x-2.5 self-start md:self-auto">
</span> <a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-3.5 py-2 rounded-lg bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
<span>Artikel Beheer</span> <i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-slate-400"></i>
</h1> <span>Collecties & Sync</span>
<p class="mt-1 text-sm text-slate-400"> </a>
Beoordeel automatisch binnengehaalde MakerWorld modellen. Stel de gewenste categorie en verkoopprijs in en publiceer ze met 1 klik naar de webshop!
</p> <a href="{{ route('admin.products.index') }}" class="px-3.5 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition flex items-center space-x-1.5 shadow-2xs">
<i data-lucide="box" class="w-3.5 h-3.5"></i>
<span>Actieve Producten</span>
</a>
</div>
</div>
<!-- Status Filter Tabs (Sleek Segmented Control) -->
<div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl w-fit overflow-x-auto text-xs">
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'draft'])) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'draft' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span>Wacht op Beoordeling</span>
<span class="px-1.5 py-0.2 rounded-full text-[10px] font-semibold {{ ($tab ?? 'draft') === 'draft' ? 'bg-amber-100 text-amber-800' : 'bg-slate-200 text-slate-600' }}">
{{ $countDraft ?? 0 }}
</span>
</a>
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'published'])) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'published' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Gepubliceerd</span>
<span class="text-[11px] text-slate-400">({{ $countPublished ?? 0 }})</span>
</a>
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'all'])) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'all' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span>Alle Geïmporteerd</span>
<span class="text-[11px] text-slate-400">({{ $countAll ?? 0 }})</span>
</a>
</div>
<!-- Filter & Search Toolbar -->
<div class="bg-white rounded-xl p-3 border border-slate-200/60 shadow-2xs flex flex-col md:flex-row md:items-center justify-between gap-3">
<form method="GET" action="{{ route('admin.makerworld.staging') }}" class="flex flex-wrap items-center gap-2.5 flex-1">
<div class="relative min-w-[220px] flex-1">
<input
type="text"
name="q"
value="{{ request('q') }}"
placeholder="Zoek op titel of ID..."
class="w-full pl-8 pr-3 py-1.5 bg-slate-50 border border-slate-200/80 rounded-lg text-slate-800 text-xs placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400 focus:bg-white transition"
>
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
</div> </div>
<!-- Header Quick Actions --> <div class="w-48">
<div class="flex items-center space-x-3"> <select name="collection_id" onchange="this.form.submit()" class="w-full px-2.5 py-1.5 bg-slate-50 border border-slate-200/80 rounded-lg text-slate-700 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400">
<a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-4 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-200 text-xs font-bold transition flex items-center space-x-2 border border-slate-800"> <option value="">Alle collecties</option>
<i data-lucide="refresh-cw" class="w-4 h-4 text-amber-400"></i> @foreach($collections as $col)
<span>Collecties & Auto-Sync</span> <option value="{{ $col->id }}" {{ request('collection_id') == $col->id ? 'selected' : '' }}>
</a> {{ $col->name }} ({{ $col->items_count }})
</option>
@endforeach
</select>
</div>
<a href="{{ route('admin.products.index') }}" class="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-bold transition flex items-center space-x-2 border border-slate-700"> <button type="submit" class="px-3 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition">
<i data-lucide="box" class="w-4 h-4"></i> Filter
<span>Actieve Producten</span> </button>
@if(request()->hasAny(['q', 'collection_id']))
<a href="{{ route('admin.makerworld.staging') }}" class="text-xs text-rose-600 hover:underline">
Wissen
</a>
@endif
</form>
<div class="text-xs text-slate-500 font-medium">
<span>{{ $totalStaged }} concepten wachten op review</span>
</div>
</div>
<!-- Bulk Selection Bar -->
<div x-show="selectedIds.length > 0" x-cloak class="p-3.5 rounded-xl bg-slate-900 text-white flex flex-col md:flex-row items-center justify-between gap-3 shadow-md">
<div class="flex items-center space-x-2 text-xs font-medium">
<i data-lucide="check-square" class="w-4 h-4 text-emerald-400"></i>
<span x-text="selectedIds.length + ' geselecteerd'"></span>
</div>
<form action="{{ route('admin.makerworld.staging.bulk-publish') }}" method="POST" class="flex flex-wrap items-center gap-2.5">
@csrf
<template x-for="id in selectedIds" :key="id">
<input type="hidden" name="product_ids[]" :value="id">
</template>
<div class="flex items-center space-x-1.5">
<label class="text-[11px] text-slate-300">Categorie:</label>
<select name="category_id" required class="px-2.5 py-1 bg-slate-800 text-white rounded-md text-xs border border-slate-700">
@foreach($categories as $cat)
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
@endforeach
</select>
</div>
<div class="flex items-center space-x-1.5">
<label class="text-[11px] text-slate-300">Marge %:</label>
<input type="number" name="custom_markup" placeholder="40" class="w-14 px-2 py-1 bg-slate-800 text-white rounded-md text-xs border border-slate-700">
</div>
<button type="submit" class="px-3 py-1 bg-white hover:bg-slate-100 text-slate-900 font-semibold text-xs rounded-md transition flex items-center space-x-1 cursor-pointer">
<i data-lucide="check" class="w-3.5 h-3.5"></i>
<span>Publiceer Selectie</span>
</button>
</form>
<form action="{{ route('admin.makerworld.staging.bulk-reject') }}" method="POST" onsubmit="return confirm('Weet je zeker dat je de geselecteerde concepten wilt verwijderen?');">
@csrf
<template x-for="id in selectedIds" :key="id">
<input type="hidden" name="product_ids[]" :value="id">
</template>
<button type="submit" class="px-2.5 py-1 bg-rose-600 hover:bg-rose-700 text-white font-medium text-xs rounded-md transition flex items-center space-x-1 cursor-pointer">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Verwijderen</span>
</button>
</form>
</div>
<!-- Select All Trigger -->
@if($stagedProducts->isNotEmpty())
<div class="flex items-center justify-between text-xs text-slate-400 px-1">
<button type="button" @click="toggleSelectAll()" class="hover:text-slate-800 font-medium transition flex items-center space-x-1.5 cursor-pointer">
<i data-lucide="check" class="w-3.5 h-3.5"></i>
<span x-text="selectedIds.length === {{ $stagedProducts->count() }} ? 'Deselecteer alles' : 'Selecteer alle {{ $stagedProducts->count() }} op deze pagina'"></span>
</button>
<span>Pagina {{ $stagedProducts->currentPage() }} van {{ $stagedProducts->lastPage() }}</span>
</div>
@endif
<!-- Staged Products Grid -->
@if($stagedProducts->isEmpty())
<div class="text-center py-16 bg-white rounded-2xl border border-slate-200/60 p-8 shadow-2xs">
<div class="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-slate-600 mx-auto mb-3">
<i data-lucide="check-circle" class="w-6 h-6 text-emerald-500"></i>
</div>
<h3 class="text-base font-semibold text-slate-900">Geen concepten in de wachtrij</h3>
<p class="text-xs text-slate-500 max-w-sm mx-auto mt-1">
Alle binnengehaalde modellen zijn beoordeeld en gepubliceerd, of er zijn nog geen nieuwe modellen gesynchroniseerd.
</p>
<div class="mt-4">
<a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-4 py-2 rounded-lg bg-slate-900 text-white font-medium text-xs transition">
Bekijk Collecties & Auto-Sync
</a> </a>
</div> </div>
</div> </div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
@foreach($stagedProducts as $product)
@php
$printInfo = $product->print_info ?? [];
@endphp
<div class="bg-white rounded-xl border border-slate-200/60 overflow-hidden flex flex-col justify-between shadow-2xs hover:border-slate-300 transition relative">
<!-- Notification Alerts --> <div>
@if(session('success'))
<div class="p-4 rounded-2xl bg-emerald-950/60 border border-emerald-500/40 text-emerald-200 text-sm flex items-center space-x-3">
<i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-400 shrink-0"></i>
<div class="font-bold">{{ session('success') }}</div>
</div>
@endif
@if(session('error'))
<div class="p-4 rounded-2xl bg-rose-950/60 border border-rose-500/40 text-rose-200 text-sm flex items-center space-x-3">
<i data-lucide="x-circle" class="w-5 h-5 text-rose-400 shrink-0"></i>
<div class="font-bold">{{ session('error') }}</div>
</div>
@endif
<!-- Status Filter Tabs -->
<div class="flex items-center space-x-2 border-b border-slate-800 pb-2 overflow-x-auto text-xs font-bold">
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'draft'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'draft' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>Wacht op Beoordeling (Concepten)</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'draft') === 'draft' ? 'bg-slate-950 text-amber-400 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $countDraft ?? 0 }}
</span>
</a>
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'published'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'published' ? 'bg-emerald-500 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
<span>Reeds Gepubliceerd in Webshop</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'draft') === 'published' ? 'bg-slate-950 text-emerald-400 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $countPublished ?? 0 }}
</span>
</a>
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'all'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'all' ? 'bg-indigo-600 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<i data-lucide="download-cloud" class="w-3.5 h-3.5"></i>
<span>Alle Binnengehaalde Modellen</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'draft') === 'all' ? 'bg-slate-950 text-indigo-300 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $countAll ?? 0 }}
</span>
</a>
</div>
<!-- Filter & Search Toolbar -->
<div class="bg-slate-900 rounded-3xl p-4 sm:p-5 border border-slate-800 flex flex-col md:flex-row md:items-center justify-between gap-4">
<form method="GET" action="{{ route('admin.makerworld.staging') }}" class="flex flex-wrap items-center gap-3 flex-1">
<!-- Search input -->
<div class="relative min-w-[240px] flex-1">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-500">
<i data-lucide="search" class="w-4 h-4"></i>
</div>
<input
type="text"
name="q"
value="{{ request('q') }}"
placeholder="Zoek op titel, maker of model-ID..."
class="w-full pl-9 pr-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white text-xs placeholder-slate-500 focus:ring-amber-400"
>
</div>
<!-- Collection dropdown filter -->
<div class="w-48">
<select name="collection_id" onchange="this.form.submit()" class="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white text-xs font-bold focus:ring-amber-400">
<option value="">Alle collecties</option>
@foreach($collections as $col)
<option value="{{ $col->id }}" {{ request('collection_id') == $col->id ? 'selected' : '' }}>
{{ $col->name }} ({{ $col->items_count }})
</option>
@endforeach
</select>
</div>
<button type="submit" class="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold transition">
Filteren
</button>
@if(request()->hasAny(['q', 'collection_id']))
<a href="{{ route('admin.makerworld.staging') }}" class="text-xs text-rose-400 hover:underline font-bold">
Filters wissen
</a>
@endif
</form>
<div class="flex items-center space-x-3 text-xs">
<span class="px-3 py-1.5 rounded-xl bg-amber-400/10 text-amber-300 border border-amber-400/30 font-black">
{{ $totalStaged }} concepten wachten op beoordeling
</span>
</div>
</div>
<!-- Bulk Selection Action Bar -->
<div x-show="selectedIds.length > 0" x-cloak class="p-4 rounded-2xl bg-amber-400 text-slate-950 flex flex-col md:flex-row items-center justify-between gap-4 shadow-xl">
<div class="flex items-center space-x-2 font-black text-xs uppercase tracking-wider">
<i data-lucide="check-square" class="w-5 h-5"></i>
<span x-text="selectedIds.length + ' artikelen geselecteerd'"></span>
</div>
<form action="{{ route('admin.makerworld.staging.bulk-publish') }}" method="POST" class="flex flex-wrap items-center gap-3">
@csrf
<template x-for="id in selectedIds" :key="id">
<input type="hidden" name="product_ids[]" :value="id">
</template>
<div class="flex items-center space-x-2">
<label class="text-xs font-black uppercase">Categorie:</label>
<select name="category_id" required class="px-3 py-1.5 bg-slate-950 text-white rounded-xl text-xs font-bold">
@foreach($categories as $cat)
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
@endforeach
</select>
</div>
<div class="flex items-center space-x-2">
<label class="text-xs font-black uppercase">Prijs-marge %:</label>
<input type="number" name="custom_markup" placeholder="0" class="w-16 px-2 py-1.5 bg-slate-950 text-white rounded-xl text-xs font-bold">
</div>
<button type="submit" class="px-4 py-2 bg-slate-950 text-amber-400 hover:bg-slate-900 font-black text-xs rounded-xl shadow-md transition flex items-center space-x-1.5">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Publiceer Selectie naar Webshop</span>
</button>
</form>
<form action="{{ route('admin.makerworld.staging.bulk-reject') }}" method="POST" onsubmit="return confirm('Weet je zeker dat je de geselecteerde concepten wilt verwijderen?');">
@csrf
<template x-for="id in selectedIds" :key="id">
<input type="hidden" name="product_ids[]" :value="id">
</template>
<button type="submit" class="px-3 py-2 bg-rose-600 hover:bg-rose-700 text-white font-bold text-xs rounded-xl transition flex items-center space-x-1">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Verwijderen</span>
</button>
</form>
</div>
<!-- Select All Trigger -->
@if($stagedProducts->isNotEmpty())
<div class="flex items-center justify-between text-xs text-slate-400 px-2">
<button type="button" @click="toggleSelectAll()" class="hover:text-amber-400 font-bold transition flex items-center space-x-1.5">
<i data-lucide="check" class="w-4 h-4"></i>
<span x-text="selectedIds.length === {{ $stagedProducts->count() }} ? 'Deselecteer alles' : 'Selecteer alle {{ $stagedProducts->count() }} modellen op deze pagina'"></span>
</button>
<span>Pagina {{ $stagedProducts->currentPage() }} van {{ $stagedProducts->lastPage() }}</span>
</div>
@endif
<!-- Staged Products Grid -->
@if($stagedProducts->isEmpty())
<div class="text-center py-20 bg-slate-900/60 rounded-3xl border border-slate-800 p-8">
<div class="w-20 h-20 rounded-3xl bg-slate-800 flex items-center justify-center text-slate-400 mx-auto mb-4">
<i data-lucide="sparkles" class="w-10 h-10 text-amber-400"></i>
</div>
<h3 class="text-xl font-black text-white">Geen concepten in de wachtrij!</h3>
<p class="text-xs text-slate-400 max-w-md mx-auto mt-2">
Alle binnengehaalde modellen zijn al beoordeeld en gepubliceerd, of er zijn nog geen nieuwe modellen toegevoegd aan je gevolgde collecties.
</p>
<div class="mt-6 flex items-center justify-center space-x-3">
<a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-5 py-2.5 rounded-xl bg-amber-400 text-slate-950 font-black text-xs uppercase tracking-wider shadow-lg shadow-amber-400/20 transition">
Bekijk Collecties & Auto-Sync
</a>
</div>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($stagedProducts as $product)
@php
$printInfo = $product->print_info ?? [];
@endphp
<div class="bg-slate-900 rounded-3xl border border-slate-800 overflow-hidden flex flex-col justify-between shadow-lg hover:border-slate-700 transition relative">
<!-- Top Image Preview & Gallery --> <!-- Top Image Preview & Gallery -->
<div> <div class="relative aspect-4/3 bg-slate-50 overflow-hidden group">
<div class="relative aspect-4/3 bg-slate-950 overflow-hidden group"> <img src="{{ $product->image_url }}" alt="{{ $product->name }}" class="w-full h-full object-cover">
<img src="{{ $product->image_url }}" alt="{{ $product->name }}" class="w-full h-full object-cover">
<!-- Selection Checkbox --> <!-- Selection Checkbox -->
<div class="absolute top-3 left-3 z-10"> <div class="absolute top-2.5 left-2.5 z-10">
<label class="flex items-center justify-center w-7 h-7 rounded-xl bg-slate-900/90 backdrop-blur border border-white/20 cursor-pointer shadow-md hover:scale-105 transition"> <label class="flex items-center justify-center w-6 h-6 rounded-md bg-white/95 backdrop-blur border border-slate-300 cursor-pointer shadow-2xs hover:scale-105 transition">
<input type="checkbox" value="{{ $product->id }}" x-model="selectedIds" class="w-4 h-4 text-amber-500 rounded focus:ring-0"> <input type="checkbox" value="{{ $product->id }}" x-model="selectedIds" class="w-3.5 h-3.5 text-slate-900 rounded focus:ring-0">
</label> </label>
</div>
<!-- Draft Badge -->
<div class="absolute top-3 right-3 flex flex-col items-end gap-1.5">
<span class="px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider bg-amber-400 text-slate-950 shadow-md">
Concept
</span>
@if($product->makerworldCollection)
<span class="px-2 py-0.5 rounded-md text-[9px] font-bold bg-slate-950/80 text-amber-300 border border-amber-400/20 backdrop-blur truncate max-w-[150px]">
{{ $product->makerworldCollection->name }}
</span>
@endif
</div>
<!-- MakerWorld Link -->
<a href="https://makerworld.com/nl/models/{{ $product->makerworld_id }}" target="_blank" class="absolute bottom-3 right-3 p-2 rounded-xl bg-slate-950/80 hover:bg-slate-900 text-slate-300 hover:text-white border border-white/10 backdrop-blur transition" title="Bekijk origineel op MakerWorld">
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
</a>
</div> </div>
<!-- Photo Thumbnails Strip (All Pulled Pictures) --> <!-- Draft Badge -->
@if(!empty($product->gallery_images) && count($product->gallery_images) > 0) <div class="absolute top-2.5 right-2.5 flex flex-col items-end gap-1">
<div class="px-4 py-2 bg-slate-950 border-b border-slate-800/80 flex items-center space-x-2 overflow-x-auto"> <span class="px-2 py-0.5 rounded-md text-[10px] font-semibold bg-white/95 backdrop-blur text-slate-800 border border-slate-200 shadow-2xs">
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-wider shrink-0">Foto's:</span> Concept
<img src="{{ $product->image_url }}" alt="" class="w-9 h-9 rounded-lg object-cover border-2 border-amber-400 shrink-0"> </span>
@foreach(array_slice($product->gallery_images, 0, 6) as $galleryPic) @if($product->makerworldCollection)
<img src="{{ $galleryPic }}" alt="" class="w-9 h-9 rounded-lg object-cover border border-slate-800 shrink-0 hover:border-slate-600 transition" loading="lazy"> <span class="px-2 py-0.5 rounded-md text-[9px] font-medium bg-slate-900/80 text-white backdrop-blur truncate max-w-[140px]">
@endforeach {{ $product->makerworldCollection->name }}
@if(count($product->gallery_images) > 6)
<span class="text-[10px] text-slate-400 font-bold shrink-0">+{{ count($product->gallery_images) - 6 }} meer</span>
@endif
</div>
@endif
<!-- Print Specifications Strip -->
<div class="grid grid-cols-3 gap-1 px-4 py-2.5 bg-slate-950/40 border-b border-slate-800/60 text-center text-xs">
<div>
<span class="text-[10px] font-bold text-slate-500 block uppercase">Printtijd</span>
<span class="font-black text-slate-200">
{{ $product->print_time_hours ? $product->print_time_hours . ' uur' : '-' }}
</span> </span>
</div> @endif
<div>
<span class="text-[10px] font-bold text-slate-500 block uppercase">Gewicht</span>
<span class="font-black text-slate-200">
{{ $product->weight_grams ? $product->weight_grams . ' gram' : '-' }}
</span>
</div>
<div>
<span class="text-[10px] font-bold text-amber-400 block uppercase">Adviesprijs</span>
<span class="font-black text-amber-400">
{{ $product->formatted_price }}
</span>
</div>
</div> </div>
<!-- Meta Details --> <!-- MakerWorld Link -->
<div class="p-4 space-y-2"> <a href="https://makerworld.com/nl/models/{{ $product->makerworld_id }}" target="_blank" class="absolute bottom-2.5 right-2.5 p-1.5 rounded-md bg-white/90 hover:bg-white text-slate-700 border border-slate-200 shadow-2xs backdrop-blur transition" title="Bekijk origineel op MakerWorld">
<div class="text-[11px] text-slate-400 flex items-center justify-between"> <i data-lucide="external-link" class="w-3.5 h-3.5"></i>
<span class="truncate">Maker: <strong class="text-slate-300">{{ $product->makerworld_author ?? 'MakerWorld Maker' }}</strong></span> </a>
<span class="font-mono text-slate-500 text-[10px]">#{{ $product->makerworld_id }}</span> </div>
</div>
<!-- Photo Thumbnails Strip -->
@if(!empty($product->gallery_images) && count($product->gallery_images) > 0)
<div class="px-3 py-1.5 bg-slate-50 border-b border-slate-100 flex items-center space-x-1.5 overflow-x-auto">
<span class="text-[9px] uppercase font-semibold text-slate-400 shrink-0">Foto's:</span>
<img src="{{ $product->image_url }}" alt="" class="w-7 h-7 rounded-md object-cover border border-slate-900 shrink-0">
@foreach(array_slice($product->gallery_images, 0, 5) as $galleryPic)
<img src="{{ $galleryPic }}" alt="" class="w-7 h-7 rounded-md object-cover border border-slate-200 shrink-0 hover:border-slate-400 transition" loading="lazy">
@endforeach
@if(count($product->gallery_images) > 5)
<span class="text-[9px] text-slate-400 font-medium shrink-0">+{{ count($product->gallery_images) - 5 }}</span>
@endif
</div>
@endif
<!-- Specs Strip -->
<div class="grid grid-cols-3 gap-1 px-3 py-2 bg-slate-50/50 border-b border-slate-100 text-center text-xs">
<div>
<span class="text-[9px] uppercase font-medium text-slate-400 block">Printtijd</span>
<span class="font-semibold text-slate-700">
{{ $product->print_time_hours ? $product->print_time_hours . ' u' : '-' }}
</span>
</div>
<div>
<span class="text-[9px] uppercase font-medium text-slate-400 block">Gewicht</span>
<span class="font-semibold text-slate-700">
{{ $product->weight_grams ? $product->weight_grams . ' g' : '-' }}
</span>
</div>
<div>
<span class="text-[9px] uppercase font-semibold text-slate-500 block">Adviesprijs</span>
<span class="font-bold text-slate-900 font-heading">
{{ $product->formatted_price }}
</span>
</div> </div>
</div> </div>
@if($product->status === 'published') <div class="p-3">
<!-- Already Published Info Card --> <div class="text-[11px] text-slate-500 flex items-center justify-between">
<div class="p-4 bg-slate-950 border-t border-slate-800 space-y-3"> <span class="truncate">Maker: <strong class="text-slate-700">{{ $product->makerworld_author ?? 'MakerWorld' }}</strong></span>
<div class="p-3 rounded-2xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 text-xs flex items-center justify-between"> <span class="font-mono text-slate-400 text-[10px]">#{{ $product->makerworld_id }}</span>
<div class="flex items-center space-x-1.5 font-bold"> </div>
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-400"></i> </div>
<span>Gepubliceerd in Webshop</span> </div>
</div>
<span class="font-black text-white">{{ $product->formatted_price }}</span> @if($product->status === 'published')
<div class="p-3 bg-slate-50/80 border-t border-slate-100 space-y-2">
<div class="p-2 rounded-lg bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-center justify-between">
<div class="flex items-center space-x-1.5 font-medium">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600"></i>
<span>Gepubliceerd</span>
</div> </div>
<div class="flex items-center space-x-2"> <span class="font-bold text-slate-900">{{ $product->formatted_price }}</span>
<a href="{{ route('admin.products.edit', $product) }}" class="flex-1 py-2 px-3 bg-slate-800 hover:bg-slate-700 text-slate-200 font-bold rounded-xl text-xs text-center transition flex items-center justify-center space-x-1 border border-slate-700"> </div>
<i data-lucide="pencil" class="w-3.5 h-3.5"></i> <div class="flex items-center space-x-2">
<span>Bewerken</span> <a href="{{ route('admin.products.edit', $product) }}" class="flex-1 py-1.5 px-3 bg-white hover:bg-slate-100 text-slate-700 font-medium rounded-lg text-xs text-center transition border border-slate-200/80 shadow-2xs">
</a> Bewerken
<a href="{{ route('product.show', $product->slug) }}" target="_blank" class="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition border border-slate-700" title="Bekijk in Webshop"> </a>
<i data-lucide="external-link" class="w-4 h-4"></i> <a href="{{ route('product.show', $product->slug) }}" target="_blank" class="p-1.5 rounded-lg bg-white hover:bg-slate-100 text-slate-600 border border-slate-200/80 transition" title="Bekijk in Shop">
</a> <i data-lucide="external-link" class="w-3.5 h-3.5"></i>
</a>
</div>
</div>
@else
<!-- Curation / Publish Form -->
<form action="{{ route('admin.makerworld.staging.publish', $product) }}" method="POST" class="p-3 bg-slate-50/50 border-t border-slate-100 space-y-2.5">
@csrf
<div>
<label class="block text-[10px] font-medium uppercase tracking-wider text-slate-500 mb-0.5">Titel</label>
<input
type="text"
name="name"
value="{{ old('name', $product->name) }}"
required
class="w-full px-2.5 py-1.5 bg-white border border-slate-200 rounded-lg text-slate-800 text-xs font-medium focus:outline-none focus:ring-1 focus:ring-slate-400"
>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-[10px] font-medium uppercase tracking-wider text-slate-500 mb-0.5">Categorie</label>
<select name="category_id" required class="w-full px-2 py-1.5 bg-white border border-slate-200 rounded-lg text-slate-800 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400">
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ $product->category_id == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<div>
<label class="block text-[10px] font-medium uppercase tracking-wider text-slate-500 mb-0.5">Prijs (€)</label>
<input
type="number"
step="0.01"
name="base_price"
value="{{ old('base_price', $product->base_price) }}"
required
class="w-full px-2 py-1.5 bg-white border border-slate-200 rounded-lg text-slate-900 text-xs font-semibold focus:outline-none focus:ring-1 focus:ring-slate-400"
>
</div> </div>
</div> </div>
@else
<!-- Curation / Publish Form (Inline) -->
<form action="{{ route('admin.makerworld.staging.publish', $product) }}" method="POST" class="p-4 bg-slate-950 border-t border-slate-800 space-y-3">
@csrf
<!-- Editable Product Name --> <div class="pt-1 flex items-center justify-between gap-2">
<div> <button type="submit" class="flex-1 py-1.5 px-3 bg-slate-900 hover:bg-slate-800 text-white font-medium rounded-lg text-xs transition flex items-center justify-center space-x-1 cursor-pointer">
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Producttitel</label> <i data-lucide="check" class="w-3.5 h-3.5"></i>
<input <span>Publiceer</span>
type="text" </button>
name="name"
value="{{ old('name', $product->name) }}"
required
class="w-full px-3 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-white text-xs font-bold focus:ring-amber-400"
>
</div>
<div class="grid grid-cols-2 gap-2"> <a href="{{ route('admin.products.edit', $product) }}" class="p-1.5 rounded-lg text-slate-500 hover:text-slate-900 hover:bg-white border border-slate-200 transition" title="Foto's en calculator">
<!-- Category selector --> <i data-lucide="sliders" class="w-3.5 h-3.5"></i>
<div> </a>
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Categorie</label>
<select name="category_id" required class="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-white text-xs font-bold focus:ring-amber-400">
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ $product->category_id == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<!-- Retail Price --> <button
<div> type="submit"
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Verkoopprijs (€)</label> form="reject-form-{{ $product->id }}"
<input class="p-1.5 rounded-lg text-slate-400 hover:text-rose-600 hover:bg-rose-50 border border-slate-200 transition cursor-pointer"
type="number" title="Verwijderen"
step="0.01" >
name="base_price" <i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
value="{{ old('base_price', $product->base_price) }}" </button>
required </div>
class="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-amber-300 text-xs font-black focus:ring-amber-400"
>
</div>
</div>
<!-- Action Buttons -->
<div class="pt-2 flex items-center justify-between gap-2">
<button type="submit" class="flex-1 py-2 px-3 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-black rounded-xl text-xs uppercase tracking-wider shadow-md shadow-emerald-500/20 transition flex items-center justify-center space-x-1">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Publiceer naar Webshop</span>
</button>
<button
type="submit"
form="reject-form-{{ $product->id }}"
class="p-2 rounded-xl text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 border border-slate-800 transition"
title="Concept afwijzen & verwijderen"
>
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
</form>
@endif
<!-- Standalone Delete Form for this product -->
<form id="reject-form-{{ $product->id }}" action="{{ route('admin.makerworld.staging.reject', $product) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit concept wilt verwijderen?');" class="hidden">
@csrf
@method('DELETE')
</form> </form>
</div> @endif
@endforeach
</div>
<!-- Pagination --> <form id="reject-form-{{ $product->id }}" action="{{ route('admin.makerworld.staging.reject', $product) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit concept wilt verwijderen?');" class="hidden">
<div class="mt-8"> @csrf
{{ $stagedProducts->links() }} @method('DELETE')
</div> </form>
@endif </div>
@endforeach
</div>
<div class="mt-6">
{{ $stagedProducts->links() }}
</div>
@endif
</div>
</div> </div>
<script> <script>
@@ -402,4 +358,3 @@ function artikelBeheerApp() {
} }
</script> </script>
@endsection @endsection
+35 -36
View File
@@ -6,65 +6,65 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between pb-6 border-b border-slate-800"> <div class="flex items-center justify-between pb-6 border-b border-slate-200/80">
<div> <div>
<div class="inline-flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1"> <div class="inline-flex items-center space-x-1.5 text-xs text-slate-500 font-medium mb-1">
<i data-lucide="shopping-cart" class="w-4 h-4"></i> <i data-lucide="shopping-cart" class="w-3.5 h-3.5"></i>
<span>Verkopen & Facturatie</span> <span>Verkopen & Orders</span>
</div> </div>
<h1 class="font-heading text-2xl sm:text-3xl font-black text-white">Bestellingen ({{ $orders->total() }})</h1> <h1 class="text-2xl font-bold tracking-tight text-slate-900">Bestellingen ({{ $orders->total() }})</h1>
<p class="text-xs sm:text-sm text-slate-400 mt-1">Inzicht in binnengekomen bestellingen, status en klantgegevens.</p> <p class="text-xs text-slate-500 mt-1">Overzicht van binnengekomen bestellingen, orderstatussen en verzending.</p>
</div> </div>
</div> </div>
<!-- Orders Table --> <!-- Orders Table -->
<div class="rounded-3xl bg-slate-900 border border-slate-800 overflow-hidden shadow-xl"> <div class="rounded-2xl bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-left text-xs text-slate-300"> <table class="w-full text-left text-xs text-slate-700">
<thead class="bg-slate-950/80 text-[11px] uppercase tracking-wider text-slate-400 font-bold border-b border-slate-800"> <thead class="bg-slate-50/80 text-[11px] uppercase tracking-wider text-slate-500 font-semibold border-b border-slate-200/70">
<tr> <tr>
<th class="py-3.5 px-4">Order #</th> <th class="py-3 px-4">Order #</th>
<th class="py-3.5 px-4">Datum</th> <th class="py-3 px-4">Datum</th>
<th class="py-3.5 px-4">Klant</th> <th class="py-3 px-4">Klant</th>
<th class="py-3.5 px-4">Items</th> <th class="py-3 px-4">Items</th>
<th class="py-3.5 px-4">Totaal</th> <th class="py-3 px-4">Totaal</th>
<th class="py-3.5 px-4">Status</th> <th class="py-3 px-4">Status</th>
<th class="py-3.5 px-4 text-right">Details</th> <th class="py-3 px-4 text-right">Details</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-800/80 font-medium"> <tbody class="divide-y divide-slate-100 font-medium">
@forelse($orders as $order) @forelse($orders as $order)
<tr class="hover:bg-slate-800/50 transition"> <tr class="hover:bg-slate-50/60 transition">
<td class="py-3.5 px-4 font-mono font-bold text-white"> <td class="py-3.5 px-4 font-mono font-bold text-slate-900">
{{ $order->order_number }} {{ $order->order_number }}
</td> </td>
<td class="py-3.5 px-4 text-slate-400"> <td class="py-3.5 px-4 text-slate-500">
{{ $order->created_at->format('d-m-Y H:i') }} {{ $order->created_at->format('d-m-Y H:i') }}
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3.5 px-4">
<div class="font-bold text-white">{{ $order->customer_name }}</div> <div class="font-semibold text-slate-900">{{ $order->customer_name }}</div>
<div class="text-[11px] text-slate-400">{{ $order->customer_email }}</div> <div class="text-[11px] text-slate-400 font-normal">{{ $order->customer_email }}</div>
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3.5 px-4">
<span class="px-2 py-0.5 rounded-md bg-slate-800 text-slate-300 text-[11px] font-bold"> <span class="px-2 py-0.5 rounded-md bg-slate-100 text-slate-700 text-[11px] font-medium">
{{ $order->items->count() }} items {{ $order->items->count() }} items
</span> </span>
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3.5 px-4">
<span class="font-heading font-black text-amber-400 text-sm">{{ $order->formatted_total }}</span> <span class="font-bold text-slate-900">{{ $order->formatted_total }}</span>
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3.5 px-4">
<span class="px-2.5 py-1 rounded-full text-[10px] font-black uppercase <span class="px-2 py-0.5 rounded-full text-[10px] font-semibold
@if($order->status === 'completed' || $order->status === 'shipped') bg-emerald-500/20 text-emerald-400 @if($order->status === 'completed' || $order->status === 'shipped') bg-emerald-50 text-emerald-700 border border-emerald-200/60
@elseif($order->status === 'printed' || $order->status === 'processing') bg-sky-500/20 text-sky-400 @elseif($order->status === 'printed' || $order->status === 'processing') bg-sky-50 text-sky-700 border border-sky-200/60
@elseif($order->status === 'cancelled') bg-rose-500/20 text-rose-400 @elseif($order->status === 'cancelled') bg-rose-50 text-rose-700 border border-rose-200/60
@else bg-amber-500/20 text-amber-400 @endif @else bg-amber-50 text-amber-700 border border-amber-200/60
"> @endif">
{{ $order->status }} {{ $order->status_label }}
</span> </span>
</td> </td>
<td class="py-3.5 px-4 text-right"> <td class="py-3.5 px-4 text-right">
<a href="{{ route('admin.orders.show', $order) }}" class="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold transition inline-flex items-center space-x-1"> <a href="{{ route('admin.orders.show', $order) }}" class="inline-flex items-center space-x-1 px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-slate-900 text-slate-700 hover:text-white text-xs font-medium transition">
<span>Bekijken</span> <span>Bekijken</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i> <i data-lucide="chevron-right" class="w-3 h-3"></i>
</a> </a>
@@ -72,8 +72,8 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="7" class="p-10 text-center text-slate-500"> <td colspan="7" class="py-12 text-center text-slate-400">
Nog geen bestellingen binnengekomen. Nog geen bestellingen geplaatst.
</td> </td>
</tr> </tr>
@endforelse @endforelse
@@ -82,7 +82,7 @@
</div> </div>
@if($orders->hasPages()) @if($orders->hasPages())
<div class="p-4 border-t border-slate-800 bg-slate-950/60"> <div class="p-4 border-t border-slate-100">
{{ $orders->links() }} {{ $orders->links() }}
</div> </div>
@endif @endif
@@ -90,4 +90,3 @@
</div> </div>
@endsection @endsection
+69 -52
View File
@@ -6,20 +6,20 @@
<div class="space-y-6 max-w-5xl mx-auto"> <div class="space-y-6 max-w-5xl mx-auto">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between pb-6 border-b border-slate-800"> <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-6 border-b border-slate-200/80">
<div> <div>
<a href="{{ route('admin.orders.index') }}" class="text-xs text-amber-400 font-bold hover:underline inline-flex items-center space-x-1 mb-1"> <a href="{{ route('admin.orders.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1 transition">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar Bestellingen</span> <span>Terug naar Bestellingen</span>
</a> </a>
<h1 class="font-heading text-2xl font-black text-white">Bestelling #{{ $order->order_number }}</h1> <h1 class="text-2xl font-bold tracking-tight text-slate-900">Bestelling #{{ $order->order_number }}</h1>
<p class="text-xs text-slate-400">Geplaatst op {{ $order->created_at->format('d-m-Y om H:i') }}</p> <p class="text-xs text-slate-500">Geplaatst op {{ $order->created_at->format('d-m-Y om H:i') }}</p>
</div> </div>
<!-- Status update form --> <!-- Status update form -->
<form action="{{ route('admin.orders.status', $order) }}" method="POST" class="flex items-center space-x-2"> <form action="{{ route('admin.orders.status', $order) }}" method="POST" class="flex items-center space-x-2">
@csrf @csrf
<select name="status" class="bg-slate-900 border border-slate-700 rounded-xl px-3 py-2 text-xs font-bold text-white"> <select name="status" class="bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
<option value="pending" {{ $order->status === 'pending' ? 'selected' : '' }}>In afwachting</option> <option value="pending" {{ $order->status === 'pending' ? 'selected' : '' }}>In afwachting</option>
<option value="processing" {{ $order->status === 'processing' ? 'selected' : '' }}>In verwerking</option> <option value="processing" {{ $order->status === 'processing' ? 'selected' : '' }}>In verwerking</option>
<option value="printed" {{ $order->status === 'printed' ? 'selected' : '' }}>Geprint (Print klaar)</option> <option value="printed" {{ $order->status === 'printed' ? 'selected' : '' }}>Geprint (Print klaar)</option>
@@ -27,35 +27,50 @@
<option value="completed" {{ $order->status === 'completed' ? 'selected' : '' }}>Afgerond</option> <option value="completed" {{ $order->status === 'completed' ? 'selected' : '' }}>Afgerond</option>
<option value="cancelled" {{ $order->status === 'cancelled' ? 'selected' : '' }}>Geannuleerd</option> <option value="cancelled" {{ $order->status === 'cancelled' ? 'selected' : '' }}>Geannuleerd</option>
</select> </select>
<button type="submit" class="px-4 py-2 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition"> <button type="submit" class="px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition shadow-xs cursor-pointer">
Status Bijwerken Status Bijwerken
</button> </button>
</form> </form>
</div> </div>
@if(session('success'))
<div class="p-3.5 rounded-xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-center space-x-2.5">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 shrink-0"></i>
<span class="font-medium">{{ session('success') }}</span>
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Order Items --> <!-- Order Items -->
<div class="md:col-span-2 space-y-4"> <div class="md:col-span-2 space-y-4">
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<h3 class="font-heading font-black text-base text-white">Bestelde 3D Prints</h3> <h3 class="font-bold text-sm text-slate-900">Bestelde 3D Prints</h3>
<div class="divide-y divide-slate-800"> <div class="divide-y divide-slate-100">
@foreach($order->items as $item) @foreach($order->items as $item)
<div class="py-3.5 flex items-start justify-between gap-4"> <div class="py-3.5 flex items-start justify-between gap-4">
<div> <div>
<h4 class="font-bold text-white text-sm">{{ $item->product_name }}</h4> <h4 class="font-semibold text-slate-900 text-xs">{{ $item->product_name }}</h4>
<div class="text-xs text-slate-400 space-y-0.5 mt-1"> <div class="text-[11px] text-slate-500 space-y-0.5 mt-1">
<div><strong>Materiaal:</strong> {{ strtoupper($item->material) }}</div> <div><span class="text-slate-400">Materiaal:</span> {{ strtoupper($item->material) }}</div>
<div><strong>Kleur:</strong> {{ $item->color }}</div> <div><span class="text-slate-400">Kleur:</span> {{ $item->color }}</div>
<div><strong>Schaal:</strong> {{ $item->scale ?? '100%' }}</div> <div><span class="text-slate-400">Schaal:</span> {{ $item->scale ?? '100%' }}</div>
@if($item->custom_file_url)
<div class="pt-1">
<a href="{{ $item->custom_file_url }}" target="_blank" class="text-sky-600 hover:underline flex items-center space-x-1 font-mono">
<i data-lucide="download" class="w-3 h-3"></i>
<span>Download 3D bestand (STL/3MF)</span>
</a>
</div>
@endif
</div> </div>
</div> </div>
<div class="text-right"> <div class="text-right">
<span class="font-heading font-black text-white text-sm"> <span class="font-semibold text-slate-900 text-xs">
{{ $item->quantity }}x &euro; {{ number_format($item->unit_price, 2, ',', '.') }} {{ $item->quantity }}x &euro; {{ number_format($item->unit_price, 2, ',', '.') }}
</span> </span>
<div class="text-xs font-black text-amber-400 mt-1"> <div class="text-xs font-bold text-slate-900 mt-0.5">
&euro; {{ number_format($item->total_price, 2, ',', '.') }} &euro; {{ number_format($item->total_price, 2, ',', '.') }}
</div> </div>
</div> </div>
@@ -63,50 +78,52 @@
@endforeach @endforeach
</div> </div>
<div class="pt-4 border-t border-slate-800 flex items-center justify-between font-heading font-black text-lg text-white"> <div class="pt-4 border-t border-slate-100 flex items-center justify-between text-xs">
<span>Totaalbedrag</span> <span class="font-medium text-slate-500">Totaalbedrag (incl. BTW)</span>
<span class="text-amber-400">{{ $order->formatted_total }}</span> <span class="font-bold text-base text-slate-900">{{ $order->formatted_total }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Klantgegevens & Afleveradres --> <!-- Klant & Verzendinfo -->
<div class="space-y-4"> <div class="space-y-4">
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 space-y-4 text-xs"> <div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3">
<h3 class="font-heading font-black text-base text-white">Klant & Verzendadres</h3> <div class="flex items-center justify-between">
<h3 class="font-bold text-xs text-slate-900 uppercase tracking-wider">Klantgegevens</h3>
<div class="space-y-2 text-slate-300"> <a href="{{ route('admin.customers.show', $order->user_id ?? $order->customer_email) }}" class="text-[11px] font-semibold text-slate-900 underline hover:text-slate-700">
<div> Klantprofiel &rarr;
<span class="text-slate-500 block text-[10px] uppercase font-bold">Naam</span> </a>
<strong class="text-white">{{ $order->customer_name }}</strong>
</div>
<div>
<span class="text-slate-500 block text-[10px] uppercase font-bold">E-mail</span>
<span>{{ $order->customer_email }}</span>
</div>
@if($order->customer_phone)
<div>
<span class="text-slate-500 block text-[10px] uppercase font-bold">Telefoon</span>
<span>{{ $order->customer_phone }}</span>
</div>
@endif
<div class="pt-2 border-t border-slate-800">
<span class="text-slate-500 block text-[10px] uppercase font-bold">Adres</span>
<div>{{ $order->shipping_street }} {{ $order->shipping_number }}</div>
<div>{{ $order->shipping_postal_code }} {{ $order->shipping_city }}</div>
<div>{{ $order->shipping_country ?? 'Nederland' }}</div>
</div>
</div> </div>
@if($order->notes) <div class="text-xs space-y-1.5 text-slate-600">
<div class="pt-3 border-t border-slate-800"> <div>
<span class="text-slate-500 block text-[10px] uppercase font-bold">Klant Opmerking</span> <span class="text-slate-400 block text-[10px]">Naam</span>
<p class="text-slate-300 italic mt-1">{{ $order->notes }}</p> <a href="{{ route('admin.customers.show', $order->user_id ?? $order->customer_email) }}" class="font-semibold text-slate-900 hover:underline">
{{ $order->customer_name }}
</a>
</div> </div>
@endif <div>
<span class="text-slate-400 block text-[10px]">E-mailadres</span>
<a href="mailto:{{ $order->customer_email }}" class="text-slate-900 hover:underline font-mono">{{ $order->customer_email }}</a>
</div>
@if($order->customer_phone)
<div>
<span class="text-slate-400 block text-[10px]">Telefoonnummer</span>
<span class="text-slate-900">{{ $order->customer_phone }}</span>
</div>
@endif
</div>
</div>
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3">
<h3 class="font-bold text-xs text-slate-900 uppercase tracking-wider">Verzendadres</h3>
<div class="text-xs text-slate-700 leading-relaxed">
<div class="font-semibold text-slate-900">{{ $order->customer_name }}</div>
<div>{{ $order->shipping_street }} {{ $order->shipping_number }}</div>
<div>{{ $order->shipping_postal_code }} {{ $order->shipping_city }}</div>
<div class="text-slate-400">Nederland</div>
</div>
</div> </div>
</div> </div>
+426 -34
View File
@@ -3,16 +3,86 @@
@section('title', 'Nieuw Product Toevoegen | Jouwdriedee Beheer') @section('title', 'Nieuw Product Toevoegen | Jouwdriedee Beheer')
@section('content') @section('content')
<div class="space-y-6 max-w-4xl mx-auto"> <div class="space-y-6 max-w-5xl mx-auto"
x-data="{
basePrice: '{{ old('base_price', '14.95') }}',
weightGrams: {{ old('weight_grams', 85) }},
printTimeHours: {{ old('print_time_hours', 4.0) }},
colorCount: 1,
material: 'pla',
markupPercent: {{ setting('calc_default_markup', 65.0) }},
showCalculator: true,
rates: {
filamentPriceKg: {{ setting('calc_filament_price_kg', 20.00) }},
electricityKwh: {{ setting('calc_electricity_kwh', 0.35) }},
machineHourly: {{ setting('calc_machine_hourly_rate', 0.50) }},
handlingFlat: {{ setting('calc_handling_flat', 1.50) }},
failureRate: {{ setting('calc_failure_rate', 5.0) }}
},
colorWasteMults: { 1: 1.0, 2: 1.15, 3: 1.25, 4: 1.35 },
materialMults: { 'pla': 1.0, 'petg': 1.20, 'silk': 1.30, 'tpu': 1.40 },
get effectiveWeight() {
let mult = this.colorWasteMults[this.colorCount] || 1.0;
return (this.weightGrams * mult).toFixed(1);
},
get purgeWasteGrams() {
return Math.max(0, this.effectiveWeight - this.weightGrams).toFixed(1);
},
get materialCost() {
let matMult = this.materialMults[this.material] || 1.0;
return ((this.effectiveWeight / 1000) * (this.rates.filamentPriceKg * matMult)).toFixed(2);
},
get electricityCost() {
return (this.printTimeHours * 0.15 * this.rates.electricityKwh).toFixed(2);
},
get machineCost() {
return (this.printTimeHours * this.rates.machineHourly).toFixed(2);
},
get handlingCost() {
let extraColorFee = Math.max(0, this.colorCount - 1) * 0.50;
return (this.rates.handlingFlat + extraColorFee).toFixed(2);
},
get subtotalCost() {
return (parseFloat(this.materialCost) + parseFloat(this.electricityCost) + parseFloat(this.machineCost) + parseFloat(this.handlingCost)).toFixed(2);
},
get failureCost() {
return (parseFloat(this.subtotalCost) * (this.rates.failureRate / 100)).toFixed(2);
},
get totalProductionCost() {
return (parseFloat(this.subtotalCost) + parseFloat(this.failureCost)).toFixed(2);
},
get priceExclVat() {
return (parseFloat(this.totalProductionCost) * (1 + this.markupPercent / 100)).toFixed(2);
},
get suggestedPrice() {
let rawWithVat = parseFloat(this.priceExclVat) * 1.21;
if (rawWithVat <= 7.0) return '6.95';
let floor = Math.floor(rawWithVat);
let fraction = rawWithVat - floor;
if (fraction <= 0.25) {
return (floor < rawWithVat ? floor + 0.50 : floor - 0.05).toFixed(2);
} else if (fraction <= 0.70) {
return (floor + 0.50).toFixed(2);
} else {
return (floor + 0.95).toFixed(2);
}
},
applySuggestedPrice() {
this.basePrice = this.suggestedPrice;
}
}">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between pb-6 border-b border-slate-800"> <div class="flex items-center justify-between pb-5 border-b border-slate-200/60">
<div> <div>
<a href="{{ route('admin.products.index') }}" class="text-xs text-amber-400 font-bold hover:underline inline-flex items-center space-x-1 mb-1"> <a href="{{ route('admin.products.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar Producten</span> <span>Terug naar Producten</span>
</a> </a>
<h1 class="font-heading text-2xl font-black text-white">Nieuw Product Toevoegen</h1> <h1 class="font-heading text-2xl font-bold text-slate-900 tracking-tight">Nieuw Product Toevoegen</h1>
</div> </div>
</div> </div>
@@ -20,17 +90,18 @@
<form action="{{ route('admin.products.store') }}" method="POST" class="space-y-6"> <form action="{{ route('admin.products.store') }}" method="POST" class="space-y-6">
@csrf @csrf
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 space-y-5"> <div class="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/60 shadow-2xs space-y-6">
<!-- Basis Informatie -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="md:col-span-2 space-y-1.5"> <div class="md:col-span-2 space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Productnaam *</label> <label class="block text-xs font-medium text-slate-700">Productnaam *</label>
<input type="text" name="name" value="{{ old('name') }}" required placeholder="bijv. Articulated Mecha Robot" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white font-bold focus:outline-none focus:ring-2 focus:ring-amber-400"> <input type="text" name="name" value="{{ old('name') }}" required placeholder="bijv. Articulated Mecha Robot" class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900 font-medium focus:outline-none focus:ring-1 focus:ring-slate-400 transition">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Categorie *</label> <label class="block text-xs font-medium text-slate-700">Categorie *</label>
<select name="category_id" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2.5 text-xs text-white font-bold"> <select name="category_id" required class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900 font-medium">
@foreach($categories as $cat) @foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id') == $cat->id ? 'selected' : '' }}> <option value="{{ $cat->id }}" {{ old('category_id') == $cat->id ? 'selected' : '' }}>
{{ $cat->name }} {{ $cat->name }}
@@ -40,55 +111,377 @@
</div> </div>
</div> </div>
<!-- ==================================================== -->
<!-- 3D PRINT KOSTPRIJSCALCULATOR WIDGET (Sleek & Subtle) -->
<!-- ==================================================== -->
<div class="p-4 sm:p-5 rounded-xl bg-slate-50/70 border border-slate-200/70 space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<i data-lucide="calculator" class="w-4 h-4 text-slate-500"></i>
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">3D Print Kostprijscalculator</h3>
<span class="px-2 py-0.5 rounded-full bg-slate-200/70 text-slate-700 font-mono text-[10px]">Formule</span>
</div>
<button type="button" @click="showCalculator = !showCalculator" class="text-xs text-slate-500 hover:text-slate-800 font-medium flex items-center space-x-1 cursor-pointer">
<span x-text="showCalculator ? 'Inklappen' : 'Openen'"></span>
<i data-lucide="chevron-down" class="w-3.5 h-3.5 transition-transform" :class="showCalculator ? 'rotate-180' : ''"></i>
</button>
</div>
<div x-show="showCalculator" x-transition class="space-y-4 pt-1">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">
<!-- Gewicht -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Gewicht</label>
<div class="relative">
<input type="number" x-model.number="weightGrams" min="1" max="5000" class="w-full bg-white border border-slate-200 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-slate-900">
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-400">gram</span>
</div>
</div>
<!-- Printtijd -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Printduur</label>
<div class="relative">
<input type="number" step="0.1" x-model.number="printTimeHours" min="0.1" max="100" class="w-full bg-white border border-slate-200 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-slate-900">
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-400">uur</span>
</div>
</div>
<!-- Aantal Kleuren (AMS Purge) -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Kleuren (AMS)</label>
<div class="grid grid-cols-4 gap-1">
<template x-for="c in [1, 2, 3, 4]" :key="c">
<button
type="button"
@click="colorCount = c"
class="py-1 rounded-md text-center font-medium text-xs transition border cursor-pointer"
:class="colorCount === c ? 'bg-slate-900 border-slate-900 text-white shadow-2xs' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'"
x-text="c"
></button>
</template>
</div>
</div>
<!-- Materiaalsoort -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Materiaalsoort</label>
<select x-model="material" class="w-full bg-white border border-slate-200 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-900">
<option value="pla">Bio-PLA (1.0x)</option>
<option value="petg">PETG Tough (1.2x)</option>
<option value="silk">Silk Zijdeglans (1.3x)</option>
<option value="tpu">TPU Flexibel (1.4x)</option>
</select>
</div>
</div>
<!-- Marge Selector -->
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-200/60 text-xs">
<div class="flex items-center space-x-2">
<span class="text-slate-500 font-medium text-[11px]">Marge:</span>
<div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 text-[11px] font-medium">
<button type="button" @click="markupPercent = 35" :class="markupPercent === 35 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">35%</button>
<button type="button" @click="markupPercent = 50" :class="markupPercent === 50 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">50%</button>
<button type="button" @click="markupPercent = 65" :class="markupPercent === 65 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">65%</button>
<button type="button" @click="markupPercent = 100" :class="markupPercent === 100 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">100%</button>
</div>
</div>
<!-- Live Cost Breakdown -->
<div class="flex items-center space-x-3 text-[11px] text-slate-400 font-normal">
<span>Materiaal: €<span x-text="materialCost"></span></span>
<span>Stroom: €<span x-text="electricityCost"></span></span>
<span>Slijtage: €<span x-text="machineCost"></span></span>
<span class="font-semibold text-slate-700">Kostprijs: €<span x-text="totalProductionCost"></span></span>
</div>
</div>
<!-- Callout Strip -->
<div class="p-3 rounded-lg bg-white border border-slate-200/80 shadow-2xs flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<span class="text-[10px] text-slate-400 uppercase font-medium block">Adviesprijs (incl. 21% btw)</span>
<div class="flex items-baseline space-x-1.5">
<span class="font-heading text-lg font-bold text-slate-900">€ <span x-text="suggestedPrice"></span></span>
<span class="text-[11px] text-slate-400">(op basis van <span x-text="weightGrams"></span>g &bull; <span x-text="printTimeHours"></span>u &bull; <span x-text="colorCount"></span> kleur)</span>
</div>
</div>
<button
type="button"
@click="applySuggestedPrice"
class="px-3.5 py-1.5 bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs rounded-lg transition flex items-center justify-center space-x-1.5 cursor-pointer shadow-2xs"
>
<i data-lucide="check" class="w-3.5 h-3.5"></i>
<span>Neem Adviesprijs Over (€ <span x-text="suggestedPrice"></span>)</span>
</button>
</div>
</div>
</div>
<!-- Prijs & Korte Omschrijving -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Basisprijs (€) *</label> <label class="block text-xs font-medium text-slate-700">Basisprijs (€) *</label>
<input type="number" step="0.01" name="base_price" value="{{ old('base_price', '14.95') }}" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white font-bold"> <div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 font-medium text-slate-400 text-xs">€</span>
<input
type="number"
step="0.01"
name="base_price"
x-model="basePrice"
required
class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg pl-7 pr-3 py-2 text-xs text-slate-900 font-semibold focus:outline-none focus:ring-1 focus:ring-slate-400"
>
</div>
</div> </div>
<div class="md:col-span-2 space-y-1.5"> <div class="md:col-span-2 space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Afbeelding Pad of URL</label> <label class="block text-xs font-medium text-slate-700">Korte Omschrijving (voor catalogus cards)</label>
<input type="text" name="image_url" value="{{ old('image_url', '/images/products/crystal-dragon.jpg') }}" placeholder="/images/products/naam.jpg of https://..." class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white"> <input type="text" name="short_description" value="{{ old('short_description') }}" placeholder="Korte samenvatting in 1 zin..." class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900">
</div> </div>
</div> </div>
<div class="space-y-1.5"> <!-- Afbeeldingen & Galerij Beheer -->
<label class="block text-xs font-bold text-slate-300">Korte Omschrijving (voor overzichtskaarten)</label> <div class="pt-5 border-t border-slate-200/60 space-y-4"
<textarea name="short_description" rows="2" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2 text-xs text-white">{{ old('short_description') }}</textarea> x-data="{
imageUrl: '{{ old('image_url', '') }}',
galleryImages: {{ json_encode(old('gallery_images', []), JSON_UNESCAPED_SLASHES) }},
newImageUrl: '',
addError: '',
isUploading: false,
uploadError: '',
showUrlInput: false,
setAsCover(index) {
let oldCover = this.imageUrl;
let newCover = this.galleryImages[index];
this.imageUrl = newCover;
if (oldCover && oldCover !== newCover) {
this.galleryImages[index] = oldCover;
} else {
this.galleryImages.splice(index, 1);
}
},
removeGalleryImage(index) {
this.galleryImages.splice(index, 1);
},
removeCover() {
if (this.galleryImages.length > 0) {
this.imageUrl = this.galleryImages.shift();
} else {
this.imageUrl = '';
}
},
addImageUrl() {
this.addError = '';
let url = this.newImageUrl.trim();
if (!url) return;
if (url === this.imageUrl || this.galleryImages.includes(url)) {
this.addError = 'Deze afbeelding is al gekoppeld!';
return;
}
if (!this.imageUrl) {
this.imageUrl = url;
} else {
this.galleryImages.push(url);
}
this.newImageUrl = '';
},
uploadFile(event) {
let file = event.target.files[0];
if (!file) return;
this.isUploading = true;
this.uploadError = '';
let formData = new FormData();
formData.append('image', file);
formData.append('_token', '{{ csrf_token() }}');
fetch('{{ route('admin.products.upload-image') }}', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
this.isUploading = false;
if (data.success && data.url) {
if (!this.imageUrl) {
this.imageUrl = data.url;
} else {
this.galleryImages.push(data.url);
}
} else {
this.uploadError = data.message || 'Uploaden is mislukt';
}
})
.catch(err => {
this.isUploading = false;
this.uploadError = 'Fout bij uploaden';
});
event.target.value = '';
}
}">
<input type="hidden" name="image_url" :value="imageUrl">
<template x-for="(img, idx) in galleryImages" :key="idx">
<input type="hidden" name="gallery_images[]" :value="img">
</template>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div>
<div class="flex items-center space-x-2">
<i data-lucide="image" class="w-4 h-4 text-slate-500"></i>
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">Productafbeeldingen & Fotogalerij</h3>
<span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-mono text-[11px]" x-text="(imageUrl ? 1 : 0) + galleryImages.length + ' foto(\'s)'"></span>
</div>
<p class="text-xs text-slate-500 mt-0.5">
Hoofdfoto en extra foto's voor de galerij.
</p>
</div>
<div class="flex items-center space-x-2">
<label class="px-3 py-1.5 rounded-lg bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs cursor-pointer">
<i data-lucide="upload" class="w-3.5 h-3.5 text-slate-400"></i>
<span x-text="isUploading ? 'Uploaden...' : 'Foto Uploaden'"></span>
<input type="file" accept="image/*" class="hidden" @change="uploadFile" :disabled="isUploading">
</label>
<button type="button" @click="showUrlInput = !showUrlInput" class="px-3 py-1.5 rounded-lg bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
<i data-lucide="link" class="w-3.5 h-3.5 text-slate-400"></i>
<span>URL Plakken</span>
</button>
</div>
</div>
<!-- URL Toevoegen Bar -->
<div x-show="showUrlInput" x-transition class="p-3 bg-slate-50 rounded-xl border border-slate-200/60 space-y-2">
<div class="flex items-center space-x-2">
<input
type="text"
x-model="newImageUrl"
@keydown.enter.prevent="addImageUrl"
placeholder="Plak hier een foto-URL..."
class="flex-1 bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400"
>
<button type="button" @click="addImageUrl" class="px-3.5 py-1.5 bg-slate-900 hover:bg-slate-800 text-white rounded-lg text-xs font-medium transition">
Toevoegen
</button>
</div>
<p x-show="addError" x-text="addError" class="text-xs text-rose-600 font-medium"></p>
</div>
<div x-show="uploadError" x-text="uploadError" class="p-2.5 bg-rose-50 border border-rose-200 text-xs text-rose-700 rounded-lg font-medium"></div>
<!-- Visuele Weergave -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-start">
<!-- Cover Box -->
<div class="md:col-span-5 bg-slate-50/70 rounded-xl border border-slate-200/80 p-3 space-y-2 relative overflow-hidden">
<div class="flex items-center justify-between">
<span class="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Hoofdafbeelding</span>
<template x-if="imageUrl">
<button type="button" @click="removeCover" class="text-slate-400 hover:text-rose-600 text-[11px] transition">
Verwijderen
</button>
</template>
</div>
<div class="aspect-4/3 rounded-lg bg-white overflow-hidden flex items-center justify-center relative border border-slate-200/60 shadow-2xs">
<template x-if="imageUrl">
<img :src="imageUrl" alt="Hoofdafbeelding" class="w-full h-full object-cover">
</template>
<template x-if="!imageUrl">
<div class="text-center p-4 text-slate-400 space-y-1">
<i data-lucide="image" class="w-6 h-6 mx-auto text-slate-300"></i>
<p class="text-xs">Nog geen foto geselecteerd</p>
</div>
</template>
</div>
</div>
<!-- Gallerij Grid -->
<div class="md:col-span-7 bg-slate-50/50 rounded-xl border border-slate-200/60 p-3 space-y-2">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-slate-700">Extra Galerijfoto's</span>
<span class="text-[10px] text-slate-400" x-text="galleryImages.length + ' foto(\'s)'"></span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-80 overflow-y-auto pr-1">
<template x-for="(img, index) in galleryImages" :key="index">
<div class="group relative rounded-lg bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
<div class="aspect-square w-full overflow-hidden bg-slate-100">
<img :src="img" alt="Gallerij foto" class="w-full h-full object-cover" loading="lazy">
</div>
<div class="absolute inset-0 bg-slate-900/60 opacity-0 group-hover:opacity-100 transition flex flex-col items-center justify-center gap-1 p-2">
<button
type="button"
@click="setAsCover(index)"
class="w-full py-1 px-2 bg-white text-slate-900 rounded text-[10px] font-semibold transition"
>
Als Cover
</button>
<button
type="button"
@click="removeGalleryImage(index)"
class="w-full py-1 px-2 bg-rose-600 text-white rounded text-[10px] font-medium transition"
>
Verwijder
</button>
</div>
</div>
</template>
</div>
<div x-show="galleryImages.length === 0" class="text-center py-6 px-3 rounded-lg border border-dashed border-slate-200 text-slate-400 text-xs">
Geen extra foto's in de galerij.
</div>
</div>
</div>
</div> </div>
<!-- Beschrijving -->
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Volledige Productbeschrijving *</label> <label class="block text-xs font-medium text-slate-700">Productbeschrijving *</label>
<textarea name="description" rows="5" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white leading-relaxed">{{ old('description') }}</textarea> <textarea name="description" rows="5" required placeholder="Gedetailleerde informatie over dit 3D model..." class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900 leading-relaxed focus:outline-none focus:ring-1 focus:ring-slate-400">{{ old('description') }}</textarea>
</div> </div>
<!-- Print Specs --> <!-- Print Specs -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-3 border-t border-slate-800"> <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-4 border-t border-slate-200/60">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Afmetingen (LxBxH)</label> <label class="block text-xs font-medium text-slate-700">Afmetingen</label>
<input type="text" name="dimensions" value="{{ old('dimensions', '15 x 10 x 8 cm') }}" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="text" name="dimensions" value="{{ old('dimensions') }}" placeholder="bijv. 12 x 8 x 10 cm" class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Gewicht (gram)</label> <label class="block text-xs font-medium text-slate-700">Gewicht (gram)</label>
<input type="number" name="weight_grams" value="{{ old('weight_grams', '85') }}" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="number" name="weight_grams" x-model.number="weightGrams" class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Printtijd (uren)</label> <label class="block text-xs font-medium text-slate-700">Printduur (uren)</label>
<input type="number" step="0.1" name="print_time_hours" value="{{ old('print_time_hours', '4.5') }}" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="number" step="0.1" name="print_time_hours" x-model.number="printTimeHours" class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900">
</div> </div>
</div> </div>
<!-- Toggles --> <!-- Toggles -->
<div class="pt-3 border-t border-slate-800 flex items-center space-x-6"> <div class="pt-4 border-t border-slate-200/60 flex items-center space-x-6 text-xs text-slate-700 font-medium">
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200"> <label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" name="is_active" value="1" checked class="w-4 h-4 rounded text-amber-400 bg-slate-950 border-slate-700"> <input type="checkbox" name="is_active" value="1" {{ old('is_active', 1) ? 'checked' : '' }} class="w-4 h-4 rounded text-slate-900 border-slate-300">
<span>Direct zichtbaar in webshop (Actief)</span> <span>Direct zichtbaar in webshop (Actief)</span>
</label> </label>
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200"> <label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" name="is_featured" value="1" class="w-4 h-4 rounded text-amber-400 bg-slate-950 border-slate-700"> <input type="checkbox" name="is_featured" value="1" {{ old('is_featured') ? 'checked' : '' }} class="w-4 h-4 rounded text-slate-900 border-slate-300">
<span>Uitlichten op homepage</span> <span>Uitlichten op homepage</span>
</label> </label>
</div> </div>
@@ -96,16 +489,15 @@
</div> </div>
<div class="flex items-center justify-end space-x-3"> <div class="flex items-center justify-end space-x-3">
<a href="{{ route('admin.products.index') }}" class="px-4 py-2.5 rounded-xl bg-slate-800 text-slate-300 hover:text-white text-xs font-bold transition"> <a href="{{ route('admin.products.index') }}" class="px-4 py-2 rounded-lg bg-white hover:bg-slate-50 text-slate-600 text-xs font-medium transition border border-slate-200">
Annuleren Annuleren
</a> </a>
<button type="submit" class="px-6 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition flex items-center space-x-1.5"> <button type="submit" class="px-5 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center space-x-1.5 shadow-xs cursor-pointer">
<i data-lucide="plus" class="w-4 h-4"></i> <i data-lucide="plus" class="w-4 h-4"></i>
<span>Product Publiceren</span> <span>Product Opslaan</span>
</button> </button>
</div> </div>
</form> </form>
</div> </div>
@endsection @endsection
+432 -37
View File
@@ -3,22 +3,94 @@
@section('title', 'Product Bewerken: ' . $product->name . ' | Jouwdriedee Beheer') @section('title', 'Product Bewerken: ' . $product->name . ' | Jouwdriedee Beheer')
@section('content') @section('content')
<div class="space-y-6 max-w-4xl mx-auto"> <div class="space-y-6 max-w-5xl mx-auto"
x-data="{
basePrice: '{{ old('base_price', $product->base_price) }}',
weightGrams: {{ old('weight_grams', $product->weight_grams ?: 85) }},
printTimeHours: {{ old('print_time_hours', $product->print_time_hours ?: 4.0) }},
colorCount: 1,
material: 'pla',
markupPercent: {{ setting('calc_default_markup', 65.0) }},
showCalculator: true,
rates: {
filamentPriceKg: {{ setting('calc_filament_price_kg', 20.00) }},
electricityKwh: {{ setting('calc_electricity_kwh', 0.35) }},
machineHourly: {{ setting('calc_machine_hourly_rate', 0.50) }},
handlingFlat: {{ setting('calc_handling_flat', 1.50) }},
failureRate: {{ setting('calc_failure_rate', 5.0) }}
},
colorWasteMults: { 1: 1.0, 2: 1.15, 3: 1.25, 4: 1.35 },
materialMults: { 'pla': 1.0, 'petg': 1.20, 'silk': 1.30, 'tpu': 1.40 },
get effectiveWeight() {
let mult = this.colorWasteMults[this.colorCount] || 1.0;
return (this.weightGrams * mult).toFixed(1);
},
get purgeWasteGrams() {
return Math.max(0, this.effectiveWeight - this.weightGrams).toFixed(1);
},
get materialCost() {
let matMult = this.materialMults[this.material] || 1.0;
return ((this.effectiveWeight / 1000) * (this.rates.filamentPriceKg * matMult)).toFixed(2);
},
get electricityCost() {
return (this.printTimeHours * 0.15 * this.rates.electricityKwh).toFixed(2);
},
get machineCost() {
return (this.printTimeHours * this.rates.machineHourly).toFixed(2);
},
get handlingCost() {
let extraColorFee = Math.max(0, this.colorCount - 1) * 0.50;
return (this.rates.handlingFlat + extraColorFee).toFixed(2);
},
get subtotalCost() {
return (parseFloat(this.materialCost) + parseFloat(this.electricityCost) + parseFloat(this.machineCost) + parseFloat(this.handlingCost)).toFixed(2);
},
get failureCost() {
return (parseFloat(this.subtotalCost) * (this.rates.failureRate / 100)).toFixed(2);
},
get totalProductionCost() {
return (parseFloat(this.subtotalCost) + parseFloat(this.failureCost)).toFixed(2);
},
get priceExclVat() {
return (parseFloat(this.totalProductionCost) * (1 + this.markupPercent / 100)).toFixed(2);
},
get suggestedPrice() {
let rawWithVat = parseFloat(this.priceExclVat) * 1.21;
if (rawWithVat <= 7.0) return '6.95';
let floor = Math.floor(rawWithVat);
let fraction = rawWithVat - floor;
if (fraction <= 0.25) {
return (floor < rawWithVat ? floor + 0.50 : floor - 0.05).toFixed(2);
} else if (fraction <= 0.70) {
return (floor + 0.50).toFixed(2);
} else {
return (floor + 0.95).toFixed(2);
}
},
applySuggestedPrice() {
this.basePrice = this.suggestedPrice;
}
}">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between pb-6 border-b border-slate-800"> <div class="flex items-center justify-between pb-5 border-b border-slate-200/60">
<div> <div>
<a href="{{ route('admin.products.index') }}" class="text-xs text-amber-400 font-bold hover:underline inline-flex items-center space-x-1 mb-1"> <a href="{{ route('admin.products.index') }}" class="text-xs text-slate-500 hover:text-slate-900 font-medium inline-flex items-center space-x-1 mb-1">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar Producten</span> <span>Terug naar Producten</span>
</a> </a>
<h1 class="font-heading text-2xl font-black text-white">Product Bewerken: {{ $product->name }}</h1> <h1 class="font-heading text-2xl font-bold text-slate-900 tracking-tight">Product Bewerken: {{ $product->name }}</h1>
</div> </div>
<a href="{{ route('product.show', $product->slug) }}" target="_blank" class="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white text-xs font-bold transition flex items-center space-x-1.5 border border-slate-700"> <div class="flex items-center space-x-2">
<span>Bekijk Live</span> <a href="{{ route('product.show', $product->slug) }}" target="_blank" class="px-3 py-1.5 rounded-lg bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
<i data-lucide="external-link" class="w-3.5 h-3.5"></i> <span>Bekijk Live</span>
</a> <i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
</a>
</div>
</div> </div>
<!-- Edit Form --> <!-- Edit Form -->
@@ -26,17 +98,18 @@
@csrf @csrf
@method('PUT') @method('PUT')
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 space-y-5"> <div class="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/60 shadow-2xs space-y-6">
<!-- Basis Informatie -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="md:col-span-2 space-y-1.5"> <div class="md:col-span-2 space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Productnaam *</label> <label class="block text-xs font-medium text-slate-700">Productnaam *</label>
<input type="text" name="name" value="{{ old('name', $product->name) }}" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white font-bold focus:outline-none focus:ring-2 focus:ring-amber-400"> <input type="text" name="name" value="{{ old('name', $product->name) }}" required class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900 font-medium focus:outline-none focus:ring-1 focus:ring-slate-400 transition">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Categorie *</label> <label class="block text-xs font-medium text-slate-700">Categorie *</label>
<select name="category_id" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2.5 text-xs text-white font-bold"> <select name="category_id" required class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900 font-medium">
@foreach($categories as $cat) @foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id', $product->category_id) == $cat->id ? 'selected' : '' }}> <option value="{{ $cat->id }}" {{ old('category_id', $product->category_id) == $cat->id ? 'selected' : '' }}>
{{ $cat->name }} {{ $cat->name }}
@@ -46,55 +119,378 @@
</div> </div>
</div> </div>
<!-- ==================================================== -->
<!-- 3D PRINT KOSTPRIJSCALCULATOR WIDGET (Sleek & Subtle) -->
<!-- ==================================================== -->
<div class="p-4 sm:p-5 rounded-xl bg-slate-50/70 border border-slate-200/70 space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<i data-lucide="calculator" class="w-4 h-4 text-slate-500"></i>
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">3D Print Kostprijscalculator</h3>
<span class="px-2 py-0.5 rounded-full bg-slate-200/70 text-slate-700 font-mono text-[10px]">Formule</span>
</div>
<button type="button" @click="showCalculator = !showCalculator" class="text-xs text-slate-500 hover:text-slate-800 font-medium flex items-center space-x-1 cursor-pointer">
<span x-text="showCalculator ? 'Inklappen' : 'Openen'"></span>
<i data-lucide="chevron-down" class="w-3.5 h-3.5 transition-transform" :class="showCalculator ? 'rotate-180' : ''"></i>
</button>
</div>
<div x-show="showCalculator" x-transition class="space-y-4 pt-1">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">
<!-- Gewicht -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Gewicht</label>
<div class="relative">
<input type="number" x-model.number="weightGrams" min="1" max="5000" class="w-full bg-white border border-slate-200 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-slate-900">
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-400">gram</span>
</div>
</div>
<!-- Printtijd -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Printduur</label>
<div class="relative">
<input type="number" step="0.1" x-model.number="printTimeHours" min="0.1" max="100" class="w-full bg-white border border-slate-200 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-slate-900">
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-400">uur</span>
</div>
</div>
<!-- Aantal Kleuren (AMS Purge) -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Kleuren (AMS)</label>
<div class="grid grid-cols-4 gap-1">
<template x-for="c in [1, 2, 3, 4]" :key="c">
<button
type="button"
@click="colorCount = c"
class="py-1 rounded-md text-center font-medium text-xs transition border cursor-pointer"
:class="colorCount === c ? 'bg-slate-900 border-slate-900 text-white shadow-2xs' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'"
x-text="c"
></button>
</template>
</div>
</div>
<!-- Materiaalsoort -->
<div class="space-y-1">
<label class="block text-[11px] font-medium text-slate-600">Materiaalsoort</label>
<select x-model="material" class="w-full bg-white border border-slate-200 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-900">
<option value="pla">Bio-PLA (1.0x)</option>
<option value="petg">PETG Tough (1.2x)</option>
<option value="silk">Silk Zijdeglans (1.3x)</option>
<option value="tpu">TPU Flexibel (1.4x)</option>
</select>
</div>
</div>
<!-- Marge Selector -->
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-200/60 text-xs">
<div class="flex items-center space-x-2">
<span class="text-slate-500 font-medium text-[11px]">Marge:</span>
<div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 text-[11px] font-medium">
<button type="button" @click="markupPercent = 35" :class="markupPercent === 35 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">35%</button>
<button type="button" @click="markupPercent = 50" :class="markupPercent === 50 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">50%</button>
<button type="button" @click="markupPercent = 65" :class="markupPercent === 65 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">65%</button>
<button type="button" @click="markupPercent = 100" :class="markupPercent === 100 ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900'" class="px-2 py-0.5 rounded transition">100%</button>
</div>
</div>
<!-- Live Cost Breakdown -->
<div class="flex items-center space-x-3 text-[11px] text-slate-400 font-normal">
<span>Materiaal: €<span x-text="materialCost"></span></span>
<span>Stroom: €<span x-text="electricityCost"></span></span>
<span>Slijtage: €<span x-text="machineCost"></span></span>
<span class="font-semibold text-slate-700">Kostprijs: €<span x-text="totalProductionCost"></span></span>
</div>
</div>
<!-- Callout Strip -->
<div class="p-3 rounded-lg bg-white border border-slate-200/80 shadow-2xs flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<span class="text-[10px] text-slate-400 uppercase font-medium block">Adviesprijs (incl. 21% btw)</span>
<div class="flex items-baseline space-x-1.5">
<span class="font-heading text-lg font-bold text-slate-900">€ <span x-text="suggestedPrice"></span></span>
<span class="text-[11px] text-slate-400">(op basis van <span x-text="weightGrams"></span>g &bull; <span x-text="printTimeHours"></span>u &bull; <span x-text="colorCount"></span> kleur)</span>
</div>
</div>
<button
type="button"
@click="applySuggestedPrice"
class="px-3.5 py-1.5 bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs rounded-lg transition flex items-center justify-center space-x-1.5 cursor-pointer shadow-2xs"
>
<i data-lucide="check" class="w-3.5 h-3.5"></i>
<span>Neem Adviesprijs Over (€ <span x-text="suggestedPrice"></span>)</span>
</button>
</div>
</div>
</div>
<!-- Prijs & Korte Omschrijving -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Basisprijs (€) *</label> <label class="block text-xs font-medium text-slate-700">Basisprijs (€) *</label>
<input type="number" step="0.01" name="base_price" value="{{ old('base_price', $product->base_price) }}" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white font-bold"> <div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 font-medium text-slate-400 text-xs">€</span>
<input
type="number"
step="0.01"
name="base_price"
x-model="basePrice"
required
class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg pl-7 pr-3 py-2 text-xs text-slate-900 font-semibold focus:outline-none focus:ring-1 focus:ring-slate-400"
>
</div>
</div> </div>
<div class="md:col-span-2 space-y-1.5"> <div class="md:col-span-2 space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Afbeelding Pad / URL</label> <label class="block text-xs font-medium text-slate-700">Korte Omschrijving (voor catalogus cards)</label>
<input type="text" name="image_url" value="{{ old('image_url', $product->image_url) }}" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white"> <input type="text" name="short_description" value="{{ old('short_description', $product->short_description) }}" placeholder="Korte samenvatting in 1 zin..." class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900">
</div> </div>
</div> </div>
<div class="space-y-1.5"> <!-- Visueel Afbeeldingen & Fotogalerij Beheer -->
<label class="block text-xs font-bold text-slate-300">Korte Omschrijving (voor catalogus cards)</label> <div class="pt-5 border-t border-slate-200/60 space-y-4"
<textarea name="short_description" rows="2" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2 text-xs text-white">{{ old('short_description', $product->short_description) }}</textarea> x-data="{
imageUrl: '{{ old('image_url', $product->image_url ?? '') }}',
galleryImages: {{ json_encode(old('gallery_images', $product->gallery_images ?? []), JSON_UNESCAPED_SLASHES) }},
newImageUrl: '',
addError: '',
isUploading: false,
uploadError: '',
showUrlInput: false,
setAsCover(index) {
let oldCover = this.imageUrl;
let newCover = this.galleryImages[index];
this.imageUrl = newCover;
if (oldCover && oldCover !== newCover) {
this.galleryImages[index] = oldCover;
} else {
this.galleryImages.splice(index, 1);
}
},
removeGalleryImage(index) {
this.galleryImages.splice(index, 1);
},
removeCover() {
if (this.galleryImages.length > 0) {
this.imageUrl = this.galleryImages.shift();
} else {
this.imageUrl = '';
}
},
addImageUrl() {
this.addError = '';
let url = this.newImageUrl.trim();
if (!url) return;
if (url === this.imageUrl || this.galleryImages.includes(url)) {
this.addError = 'Deze afbeelding is al gekoppeld aan het product!';
return;
}
if (!this.imageUrl) {
this.imageUrl = url;
} else {
this.galleryImages.push(url);
}
this.newImageUrl = '';
},
uploadFile(event) {
let file = event.target.files[0];
if (!file) return;
this.isUploading = true;
this.uploadError = '';
let formData = new FormData();
formData.append('image', file);
formData.append('_token', '{{ csrf_token() }}');
fetch('{{ route('admin.products.upload-image') }}', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
this.isUploading = false;
if (data.success && data.url) {
if (!this.imageUrl) {
this.imageUrl = data.url;
} else {
this.galleryImages.push(data.url);
}
} else {
this.uploadError = data.message || 'Uploaden is mislukt';
}
})
.catch(err => {
this.isUploading = false;
this.uploadError = 'Fout bij uploaden van afbeelding';
});
event.target.value = '';
}
}">
<!-- Verborgen form fields -->
<input type="hidden" name="image_url" :value="imageUrl">
<template x-for="(img, idx) in galleryImages" :key="idx">
<input type="hidden" name="gallery_images[]" :value="img">
</template>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div>
<div class="flex items-center space-x-2">
<i data-lucide="image" class="w-4 h-4 text-slate-500"></i>
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">Productafbeeldingen & Fotogalerij</h3>
<span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-mono text-[11px]" x-text="(imageUrl ? 1 : 0) + galleryImages.length + ' foto(\'s)'"></span>
</div>
<p class="text-xs text-slate-500 mt-0.5">
Hoofdfoto en extra detailfoto's van MakerWorld of uploads.
</p>
</div>
<div class="flex items-center space-x-2">
<label class="px-3 py-1.5 rounded-lg bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs cursor-pointer">
<i data-lucide="upload" class="w-3.5 h-3.5 text-slate-400"></i>
<span x-text="isUploading ? 'Uploaden...' : 'Foto Uploaden'"></span>
<input type="file" accept="image/*" class="hidden" @change="uploadFile" :disabled="isUploading">
</label>
<button type="button" @click="showUrlInput = !showUrlInput" class="px-3 py-1.5 rounded-lg bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
<i data-lucide="link" class="w-3.5 h-3.5 text-slate-400"></i>
<span>URL Plakken</span>
</button>
</div>
</div>
<!-- URL Toevoegen Bar -->
<div x-show="showUrlInput" x-transition class="p-3 bg-slate-50 rounded-xl border border-slate-200/60 space-y-2">
<div class="flex items-center space-x-2">
<input
type="text"
x-model="newImageUrl"
@keydown.enter.prevent="addImageUrl"
placeholder="Plak hier een foto-URL..."
class="flex-1 bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400"
>
<button type="button" @click="addImageUrl" class="px-3.5 py-1.5 bg-slate-900 hover:bg-slate-800 text-white rounded-lg text-xs font-medium transition">
Toevoegen
</button>
</div>
<p x-show="addError" x-text="addError" class="text-xs text-rose-600 font-medium"></p>
</div>
<div x-show="uploadError" x-text="uploadError" class="p-2.5 bg-rose-50 border border-rose-200 text-xs text-rose-700 rounded-lg font-medium"></div>
<!-- Visuele Weergave -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-start">
<!-- Cover Box -->
<div class="md:col-span-5 bg-slate-50/70 rounded-xl border border-slate-200/80 p-3 space-y-2 relative overflow-hidden">
<div class="flex items-center justify-between">
<span class="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Hoofdafbeelding (Cover)</span>
<template x-if="imageUrl">
<button type="button" @click="removeCover" class="text-slate-400 hover:text-rose-600 text-[11px] transition">
Verwijderen
</button>
</template>
</div>
<div class="aspect-4/3 rounded-lg bg-white overflow-hidden flex items-center justify-center relative border border-slate-200/60 shadow-2xs">
<template x-if="imageUrl">
<img :src="imageUrl" alt="Hoofdafbeelding" class="w-full h-full object-cover">
</template>
<template x-if="!imageUrl">
<div class="text-center p-4 text-slate-400 space-y-1">
<i data-lucide="image" class="w-6 h-6 mx-auto text-slate-300"></i>
<p class="text-xs">Geen cover</p>
</div>
</template>
</div>
</div>
<!-- Gallerij Grid -->
<div class="md:col-span-7 bg-slate-50/50 rounded-xl border border-slate-200/60 p-3 space-y-2">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-slate-700">Extra Galerijfoto's</span>
<span class="text-[10px] text-slate-400" x-text="galleryImages.length + ' foto(\'s)'"></span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-80 overflow-y-auto pr-1">
<template x-for="(img, index) in galleryImages" :key="index">
<div class="group relative rounded-lg bg-white border border-slate-200/80 overflow-hidden shadow-2xs">
<div class="aspect-square w-full overflow-hidden bg-slate-100">
<img :src="img" alt="Gallerij foto" class="w-full h-full object-cover" loading="lazy">
</div>
<div class="absolute inset-0 bg-slate-900/60 opacity-0 group-hover:opacity-100 transition flex flex-col items-center justify-center gap-1 p-2">
<button
type="button"
@click="setAsCover(index)"
class="w-full py-1 px-2 bg-white text-slate-900 rounded text-[10px] font-semibold transition"
>
Als Cover
</button>
<button
type="button"
@click="removeGalleryImage(index)"
class="w-full py-1 px-2 bg-rose-600 text-white rounded text-[10px] font-medium transition"
>
Verwijder
</button>
</div>
</div>
</template>
</div>
<div x-show="galleryImages.length === 0" class="text-center py-6 px-3 rounded-lg border border-dashed border-slate-200 text-slate-400 text-xs">
Geen extra foto's in de galerij.
</div>
</div>
</div>
</div> </div>
<!-- Beschrijving -->
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Volledige Productbeschrijving *</label> <label class="block text-xs font-medium text-slate-700">Productbeschrijving *</label>
<textarea name="description" rows="6" required class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white leading-relaxed">{{ old('description', $product->description) }}</textarea> <textarea name="description" rows="5" required class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-2 text-xs text-slate-900 leading-relaxed focus:outline-none focus:ring-1 focus:ring-slate-400">{{ old('description', $product->description) }}</textarea>
</div> </div>
<!-- Print Specs --> <!-- Print Specs -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-3 border-t border-slate-800"> <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-4 border-t border-slate-200/60">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Afmetingen (LxBxH)</label> <label class="block text-xs font-medium text-slate-700">Afmetingen</label>
<input type="text" name="dimensions" value="{{ old('dimensions', $product->dimensions) }}" placeholder="bijv. 12 x 8 x 10 cm" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="text" name="dimensions" value="{{ old('dimensions', $product->dimensions) }}" placeholder="bijv. 12 x 8 x 10 cm" class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Gewicht (gram)</label> <label class="block text-xs font-medium text-slate-700">Gewicht (gram)</label>
<input type="number" name="weight_grams" value="{{ old('weight_grams', $product->weight_grams) }}" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="number" name="weight_grams" x-model.number="weightGrams" class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="block text-xs font-bold text-slate-300">Printtijd (uren)</label> <label class="block text-xs font-medium text-slate-700">Printduur (uren)</label>
<input type="number" step="0.1" name="print_time_hours" value="{{ old('print_time_hours', $product->print_time_hours) }}" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <input type="number" step="0.1" name="print_time_hours" x-model.number="printTimeHours" class="w-full bg-slate-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900">
</div> </div>
</div> </div>
<!-- Toggles --> <!-- Toggles -->
<div class="pt-3 border-t border-slate-800 flex items-center space-x-6"> <div class="pt-4 border-t border-slate-200/60 flex items-center space-x-6 text-xs text-slate-700 font-medium">
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200"> <label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" name="is_active" value="1" {{ old('is_active', $product->is_active) ? 'checked' : '' }} class="w-4 h-4 rounded text-amber-400 bg-slate-950 border-slate-700"> <input type="checkbox" name="is_active" value="1" {{ old('is_active', $product->is_active) ? 'checked' : '' }} class="w-4 h-4 rounded text-slate-900 border-slate-300">
<span>Zichtbaar in webshop (Actief)</span> <span>Zichtbaar in webshop (Actief)</span>
</label> </label>
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200"> <label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" name="is_featured" value="1" {{ old('is_featured', $product->is_featured) ? 'checked' : '' }} class="w-4 h-4 rounded text-amber-400 bg-slate-950 border-slate-700"> <input type="checkbox" name="is_featured" value="1" {{ old('is_featured', $product->is_featured) ? 'checked' : '' }} class="w-4 h-4 rounded text-slate-900 border-slate-300">
<span>Uitlichten op homepage</span> <span>Uitlichten op homepage</span>
</label> </label>
</div> </div>
@@ -102,10 +498,10 @@
</div> </div>
<div class="flex items-center justify-end space-x-3"> <div class="flex items-center justify-end space-x-3">
<a href="{{ route('admin.products.index') }}" class="px-4 py-2.5 rounded-xl bg-slate-800 text-slate-300 hover:text-white text-xs font-bold transition"> <a href="{{ route('admin.products.index') }}" class="px-4 py-2 rounded-lg bg-white hover:bg-slate-50 text-slate-600 text-xs font-medium transition border border-slate-200">
Annuleren Annuleren
</a> </a>
<button type="submit" class="px-6 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition flex items-center space-x-1.5"> <button type="submit" class="px-5 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center space-x-1.5 shadow-xs cursor-pointer">
<i data-lucide="save" class="w-4 h-4"></i> <i data-lucide="save" class="w-4 h-4"></i>
<span>Wijzigingen Opslaan</span> <span>Wijzigingen Opslaan</span>
</button> </button>
@@ -114,4 +510,3 @@
</form> </form>
</div> </div>
@endsection @endsection
+83 -102
View File
@@ -6,201 +6,183 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-800 gap-4"> <div class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-200/60 gap-4">
<div> <div>
<div class="inline-flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1"> <span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 block mb-1">Catalogus & Assortiment</span>
<i data-lucide="box" class="w-4 h-4"></i> <h1 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight">Producten ({{ $products->total() }})</h1>
<span>Assortiment & Voorraad</span> <p class="text-xs sm:text-sm text-slate-500 mt-1">Beheer prijzen, teksten, kostprijscalculaties en afbeeldingen van alle 3D prints.</p>
</div>
<h1 class="font-heading text-2xl sm:text-3xl font-black text-white">Producten Overzicht ({{ $products->total() }})</h1>
<p class="text-xs sm:text-sm text-slate-400 mt-1">Beheer prijzen, teksten, specificaties en afbeeldingen van alle 3D prints.</p>
</div> </div>
<div class="flex items-center space-x-3 self-start sm:self-auto"> <div class="flex items-center space-x-2.5 self-start sm:self-auto">
<a href="{{ route('admin.makerworld.index') }}" class="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white text-xs font-bold transition flex items-center space-x-1.5 border border-slate-700"> <a href="{{ route('admin.makerworld.index') }}" class="px-3.5 py-2 rounded-lg bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 shadow-2xs">
<i data-lucide="download-cloud" class="w-4 h-4"></i> <i data-lucide="download-cloud" class="w-3.5 h-3.5 text-slate-400"></i>
<span>MakerWorld Importer</span> <span>MakerWorld Sync</span>
</a> </a>
<a href="{{ route('admin.products.create') }}" class="px-4 py-2 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition flex items-center space-x-1.5 shadow-sm"> <a href="{{ route('admin.products.create') }}" class="px-3.5 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center space-x-1.5 shadow-2xs">
<i data-lucide="plus" class="w-4 h-4"></i> <i data-lucide="plus" class="w-3.5 h-3.5"></i>
<span>Nieuw Product</span> <span>Nieuw Product</span>
</a> </a>
</div> </div>
</div> </div>
<!-- Status Filter Tabs --> <!-- Status Filter Tabs (Sleek Segmented Control) -->
<div class="flex items-center space-x-2 border-b border-slate-800 pb-2 overflow-x-auto text-xs font-bold"> <div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl w-fit overflow-x-auto text-xs">
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'all'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'all' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}"> <a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'all'])) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'all' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span>Alle Producten</span> <span>Alle</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'all') === 'all' ? 'bg-slate-950 text-amber-400 font-black' : 'bg-slate-800 text-slate-400' }}"> <span class="text-[11px] text-slate-400">({{ $totalAll ?? $products->total() }})</span>
{{ $totalAll ?? $products->total() }}
</span>
</a> </a>
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'published'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'published' ? 'bg-emerald-500 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}"> <a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'published'])) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'published' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Live in Webshop</span> <span>Live in Webshop</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'all') === 'published' ? 'bg-slate-950 text-emerald-400 font-black' : 'bg-slate-800 text-slate-400' }}"> <span class="text-[11px] text-slate-400">({{ $totalPublished ?? 0 }})</span>
{{ $totalPublished ?? 0 }}
</span>
</a> </a>
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'draft'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'draft' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}"> <a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'draft'])) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'draft' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<i data-lucide="layers" class="w-3.5 h-3.5"></i> <span>Concepten / Staging</span>
<span>Concepten / MakerWorld Staging</span>
@if(($totalDraft ?? 0) > 0) @if(($totalDraft ?? 0) > 0)
<span class="px-1.5 py-0.5 rounded text-[10px] bg-amber-400 text-slate-950 font-black"> <span class="px-1.5 py-0.2 rounded-full text-[10px] font-semibold bg-amber-100 text-amber-800">
{{ $totalDraft }} {{ $totalDraft }}
</span> </span>
@endif @endif
</a> </a>
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'makerworld'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'makerworld' ? 'bg-indigo-600 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}"> <a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'makerworld'])) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'makerworld' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
<i data-lucide="download-cloud" class="w-3.5 h-3.5"></i> <span>MakerWorld</span>
<span>MakerWorld Modellen</span> <span class="text-[11px] text-slate-400">({{ $totalMakerWorld ?? 0 }})</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'all') === 'makerworld' ? 'bg-slate-950 text-indigo-300 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $totalMakerWorld ?? 0 }}
</span>
</a> </a>
@if(($totalDraft ?? 0) > 0)
<div class="ml-auto">
<a href="{{ route('admin.makerworld.staging') }}" class="px-3 py-1.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition flex items-center space-x-1.5 shadow-sm">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>Naar Artikel Beheer ({{ $totalDraft }} nieuw) &rarr;</span>
</a>
</div>
@endif
</div> </div>
<!-- Search & Filters --> <!-- Search & Filters -->
<div class="p-4 rounded-2xl bg-slate-900 border border-slate-800 flex flex-col sm:flex-row items-center justify-between gap-3"> <div class="p-3 rounded-xl bg-white border border-slate-200/60 shadow-2xs flex flex-col sm:flex-row items-center justify-between gap-3">
<form action="{{ route('admin.products.index') }}" method="GET" class="w-full sm:w-auto flex-1 flex items-center space-x-2"> <form action="{{ route('admin.products.index') }}" method="GET" class="w-full sm:w-auto flex-1 flex items-center space-x-2">
<div class="relative flex-1 max-w-md"> <div class="relative flex-1 max-w-sm">
<input <input
type="text" type="text"
name="q" name="q"
value="{{ request('q') }}" value="{{ request('q') }}"
placeholder="Zoek op naam of omschrijving..." placeholder="Zoek op naam..."
class="w-full bg-slate-950 border border-slate-700 rounded-xl pl-9 pr-3.5 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-400" class="w-full bg-slate-50 border border-slate-200/80 rounded-lg pl-8 pr-3 py-1.5 text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400 focus:bg-white transition"
> >
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i> <i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
</div> </div>
<select name="category_id" onchange="this.form.submit()" class="bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white"> <select name="category_id" onchange="this.form.submit()" class="bg-slate-50 border border-slate-200/80 rounded-lg px-2.5 py-1.5 text-xs text-slate-700 focus:outline-none focus:ring-1 focus:ring-slate-400">
<option value="">Alle Categorieën</option> <option value="">Alle Categorieën</option>
@foreach($categories as $cat) @foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>{{ $cat->name }}</option> <option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>{{ $cat->name }}</option>
@endforeach @endforeach
</select> </select>
<button type="submit" class="px-3 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-xl text-xs font-bold transition"> <button type="submit" class="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 text-white rounded-lg text-xs font-medium transition">
Filter Filter
</button> </button>
@if(request()->hasAny(['q', 'category_id'])) @if(request()->hasAny(['q', 'category_id']))
<a href="{{ route('admin.products.index') }}" class="text-xs text-rose-400 hover:underline">Wissen</a> <a href="{{ route('admin.products.index') }}" class="text-xs text-rose-600 hover:underline">Wissen</a>
@endif @endif
</form> </form>
@if(($totalDraft ?? 0) > 0)
<a href="{{ route('admin.makerworld.staging') }}" class="text-xs text-slate-600 hover:text-slate-900 font-medium flex items-center space-x-1">
<span>Naar Artikel Beheer ({{ $totalDraft }} nieuw)</span>
<i data-lucide="arrow-right" class="w-3 h-3"></i>
</a>
@endif
</div> </div>
<!-- Products Table --> <!-- Products Table (Clean & Airy) -->
<div class="rounded-3xl bg-slate-900 border border-slate-800 overflow-hidden shadow-xl"> <div class="rounded-xl bg-white border border-slate-200/60 overflow-hidden shadow-2xs">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-left text-xs text-slate-300"> <table class="w-full text-left text-xs text-slate-600">
<thead class="bg-slate-950/80 text-[11px] uppercase tracking-wider text-slate-400 font-bold border-b border-slate-800"> <thead class="bg-slate-50/70 text-[11px] uppercase tracking-wider text-slate-400 font-semibold border-b border-slate-200/60">
<tr> <tr>
<th class="py-3.5 px-4">Product</th> <th class="py-3 px-4">Product</th>
<th class="py-3.5 px-4">Categorie</th> <th class="py-3 px-4">Categorie</th>
<th class="py-3.5 px-4">Basisprijs</th> <th class="py-3 px-4">Prijs</th>
<th class="py-3.5 px-4">Specificaties</th> <th class="py-3 px-4">Specificaties</th>
<th class="py-3.5 px-4">Status</th> <th class="py-3 px-4">Status</th>
<th class="py-3.5 px-4 text-right">Acties</th> <th class="py-3 px-4 text-right">Acties</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-800/80 font-medium"> <tbody class="divide-y divide-slate-100 font-normal">
@forelse($products as $product) @forelse($products as $product)
<tr class="hover:bg-slate-800/50 transition"> <tr class="hover:bg-slate-50/50 transition">
<td class="py-3.5 px-4"> <td class="py-3 px-4">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="w-12 h-12 rounded-xl bg-slate-950 overflow-hidden border border-slate-800 flex-shrink-0"> <div class="w-10 h-10 rounded-lg bg-slate-100 overflow-hidden border border-slate-200/60 shrink-0 relative group">
<img src="{{ $product->image_url }}" alt="{{ $product->name }}" class="w-full h-full object-cover"> <img src="{{ $product->image_url }}" alt="{{ $product->name }}" class="w-full h-full object-cover">
@if(!empty($product->gallery_images) && count($product->gallery_images) > 0)
<span class="absolute bottom-0 right-0 px-1 py-0.2 rounded-tl-sm bg-slate-900/80 text-white font-mono text-[8px]" title="{{ count($product->gallery_images) + 1 }} foto's in galerij">
+{{ count($product->gallery_images) }}
</span>
@endif
</div> </div>
<div> <div>
<a href="{{ route('admin.products.edit', $product) }}" class="font-bold text-white hover:text-amber-400 transition block text-sm"> <a href="{{ route('admin.products.edit', $product) }}" class="font-semibold text-slate-900 hover:text-sky-600 transition block text-xs">
{{ $product->name }} {{ $product->name }}
</a> </a>
<span class="text-[10px] text-slate-500 font-mono">/product/{{ $product->slug }}</span> <span class="text-[10px] text-slate-400 font-mono">/{{ $product->slug }}</span>
</div> </div>
</div> </div>
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3 px-4">
@if($product->category) @if($product->category)
<span class="px-2 py-0.5 rounded-md bg-slate-800 text-slate-300 text-[11px] font-bold border border-slate-700"> <span class="px-2 py-0.5 rounded-md bg-slate-100 text-slate-600 text-[11px] font-medium">
{{ $product->category->name }} {{ $product->category->name }}
</span> </span>
@else @else
<span class="text-slate-500">-</span> <span class="text-slate-400">-</span>
@endif @endif
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3 px-4">
<span class="font-black text-amber-400 font-heading text-sm">{{ $product->formatted_price }}</span> <span class="font-bold text-slate-900 font-heading text-xs">{{ $product->formatted_price }}</span>
</td> </td>
<td class="py-3.5 px-4 text-[11px] text-slate-400 space-y-0.5"> <td class="py-3 px-4 text-[11px] text-slate-400 space-y-0.5">
<div>{{ $product->dimensions ?? 'Standaard' }}</div> <div>{{ $product->dimensions ?? 'Standaard' }}</div>
<div>~{{ $product->weight_grams ?? 0 }}g &bull; {{ $product->print_time_hours ?? 0 }}u</div> <div>~{{ $product->weight_grams ?? 0 }}g &bull; {{ $product->print_time_hours ?? 0 }}u</div>
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3 px-4">
<div class="flex flex-col gap-1 items-start"> <div class="flex flex-col gap-1 items-start">
@if($product->status === 'draft') @if($product->status === 'draft')
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md bg-amber-400/20 text-amber-300 border border-amber-400/40 text-[10px] font-black uppercase tracking-wider"> <span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md bg-amber-50 text-amber-800 text-[10px] font-semibold border border-amber-200/60">
<i data-lucide="layers" class="w-3 h-3"></i> <span>Concept</span>
<span>Concept Staging</span>
</span> </span>
<a href="{{ route('admin.makerworld.staging') }}" class="text-[10px] text-amber-400 hover:underline font-bold">
Beoordeel in Artikel Beheer &rarr;
</a>
@elseif($product->is_active) @elseif($product->is_active)
<span class="inline-flex items-center space-x-1 text-emerald-400 text-[11px] font-bold"> <span class="inline-flex items-center space-x-1.5 text-emerald-700 text-[11px] font-medium">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Live in Webshop</span> <span>Live</span>
</span> </span>
@else @else
<span class="inline-flex items-center space-x-1 text-slate-500 text-[11px] font-bold"> <span class="inline-flex items-center space-x-1.5 text-slate-400 text-[11px]">
<span class="w-1.5 h-1.5 rounded-full bg-slate-500"></span> <span class="w-1.5 h-1.5 rounded-full bg-slate-300"></span>
<span>Verborgen</span> <span>Verborgen</span>
</span> </span>
@endif @endif
@if($product->makerworld_id) @if($product->makerworld_id)
<span class="text-[10px] text-indigo-400 font-mono"> <span class="text-[10px] text-slate-400 font-mono">
MakerWorld #{{ $product->makerworld_id }} MW #{{ $product->makerworld_id }}
</span>
@endif
@if($product->is_featured)
<span class="inline-flex items-center text-amber-400 text-[10px] font-bold">
★ Uitgelicht
</span> </span>
@endif @endif
</div> </div>
</td> </td>
<td class="py-3.5 px-4 text-right"> <td class="py-3 px-4 text-right">
<div class="flex items-center justify-end space-x-2"> <div class="flex items-center justify-end space-x-1">
<a href="{{ route('product.show', $product->slug) }}" target="_blank" class="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition" title="Bekijk in Shop"> <a href="{{ route('product.show', $product->slug) }}" target="_blank" class="p-1.5 rounded-md text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition" title="Bekijk in Shop">
<i data-lucide="external-link" class="w-3.5 h-3.5"></i> <i data-lucide="external-link" class="w-3.5 h-3.5"></i>
</a> </a>
<a href="{{ route('admin.products.edit', $product) }}" class="p-1.5 rounded-lg bg-slate-800 hover:bg-amber-400 text-slate-300 hover:text-slate-950 transition" title="Bewerken"> <a href="{{ route('admin.products.edit', $product) }}" class="p-1.5 rounded-md text-slate-500 hover:text-slate-900 hover:bg-slate-100 transition" title="Bewerken & Prijscalculator">
<i data-lucide="pencil" class="w-3.5 h-3.5"></i> <i data-lucide="pencil" class="w-3.5 h-3.5"></i>
</a> </a>
<form action="{{ route('admin.products.destroy', $product) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit product wilt verwijderen?')"> <form action="{{ route('admin.products.destroy', $product) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit product wilt verwijderen?')">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="p-1.5 rounded-lg bg-slate-800 hover:bg-rose-500 text-slate-300 hover:text-white transition cursor-pointer" title="Verwijderen"> <button type="submit" class="p-1.5 rounded-md text-slate-400 hover:text-rose-600 hover:bg-rose-50 transition cursor-pointer" title="Verwijderen">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> <i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button> </button>
</form> </form>
@@ -209,7 +191,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="6" class="p-8 text-center text-slate-500"> <td colspan="6" class="p-8 text-center text-slate-400">
Geen producten gevonden. Geen producten gevonden.
</td> </td>
</tr> </tr>
@@ -219,7 +201,7 @@
</div> </div>
@if($products->hasPages()) @if($products->hasPages())
<div class="p-4 border-t border-slate-800 bg-slate-950/60"> <div class="p-3.5 border-t border-slate-100 bg-slate-50/50">
{{ $products->links() }} {{ $products->links() }}
</div> </div>
@endif @endif
@@ -227,4 +209,3 @@
</div> </div>
@endsection @endsection
+17 -21
View File
@@ -3,26 +3,23 @@
@section('title', 'Wachtwoord Vergeten | Jouwdriedee.nl') @section('title', 'Wachtwoord Vergeten | Jouwdriedee.nl')
@section('content') @section('content')
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8"> <div class="min-h-[70vh] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6"> <div class="w-full max-w-sm space-y-6">
<!-- Header --> <!-- Header -->
<div class="text-center space-y-2"> <div class="text-center space-y-1.5">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-indigo-100 text-indigo-900 shadow-2xs mb-1"> <h1 class="text-2xl font-bold text-slate-900 tracking-tight">Wachtwoord vergeten</h1>
<i data-lucide="key-round" class="w-6 h-6 text-indigo-600"></i> <p class="text-xs text-slate-500">Vul je e-mailadres in om een herstellink te ontvangen</p>
</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> </div>
<!-- Form Card --> <!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5"> <div class="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<form action="{{ route('password.email') }}" method="POST" class="space-y-4"> <form action="{{ route('password.email') }}" method="POST" class="space-y-4">
@csrf @csrf
<!-- E-mailadres --> <!-- E-mailadres -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="email" class="block text-xs font-bold text-slate-700">Jouw geregistreerde e-mailadres</label> <label for="email" class="block text-xs font-medium text-slate-700">Jouw e-mailadres</label>
<div class="relative"> <div class="relative">
<input <input
type="email" type="email"
@@ -32,26 +29,26 @@
required required
autofocus autofocus
placeholder="jouw@email.nl" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @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> <i data-lucide="mail" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div> </div>
@error('email') @error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<div class="pt-2"> <div class="pt-1">
<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"> <button type="submit" class="w-full py-2.5 px-4 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<i data-lucide="send" class="w-4 h-4"></i> <i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Herstellink Versturen</span> <span>Herstellink versturen</span>
</button> </button>
</div> </div>
</form> </form>
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600"> <div class="pt-3 border-t border-slate-100 text-center text-xs text-slate-500">
<a href="{{ route('login') }}" class="text-rose-600 hover:text-rose-700 underline inline-flex items-center space-x-1"> <a href="{{ route('login') }}" class="text-slate-900 font-medium hover:underline inline-flex items-center space-x-1">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
<span>Terug naar inloggen</span> <span>Terug naar inloggen</span>
</a> </a>
@@ -61,4 +58,3 @@
</div> </div>
</div> </div>
@endsection @endsection
+23 -27
View File
@@ -3,26 +3,23 @@
@section('title', 'Inloggen | Jouwdriedee.nl') @section('title', 'Inloggen | Jouwdriedee.nl')
@section('content') @section('content')
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8"> <div class="min-h-[70vh] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6"> <div class="w-full max-w-sm space-y-6">
<!-- Header --> <!-- Header -->
<div class="text-center space-y-2"> <div class="text-center space-y-1.5">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-amber-100 text-amber-900 shadow-2xs mb-1"> <h1 class="text-2xl font-bold text-slate-900 tracking-tight">Welkom terug</h1>
<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> <p class="text-xs text-slate-500">Log in met jouw e-mailadres en wachtwoord</p>
</div> </div>
<!-- Form Card --> <!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5"> <div class="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<form action="{{ route('login.submit') }}" method="POST" class="space-y-4"> <form action="{{ route('login.submit') }}" method="POST" class="space-y-4">
@csrf @csrf
<!-- E-mailadres --> <!-- E-mailadres -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres</label> <label for="email" class="block text-xs font-medium text-slate-700">E-mailadres</label>
<div class="relative"> <div class="relative">
<input <input
type="email" type="email"
@@ -32,20 +29,20 @@
required required
autofocus autofocus
placeholder="jouw@email.nl" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @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> <i data-lucide="mail" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div> </div>
@error('email') @error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Wachtwoord --> <!-- Wachtwoord -->
<div class="space-y-1.5"> <div class="space-y-1">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<label for="password" class="block text-xs font-bold text-slate-700">Wachtwoord</label> <label for="password" class="block text-xs font-medium text-slate-700">Wachtwoord</label>
<a href="{{ route('password.request') }}" class="text-[11px] font-bold text-rose-600 hover:text-rose-700 transition"> <a href="{{ route('password.request') }}" class="text-[11px] font-medium text-slate-500 hover:text-slate-900 transition">
Wachtwoord vergeten? Wachtwoord vergeten?
</a> </a>
</div> </div>
@@ -56,35 +53,35 @@
id="password" id="password"
required required
placeholder="••••••••" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @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> <i data-lucide="lock" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div> </div>
@error('password') @error('password')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Onthoud mij --> <!-- Onthoud mij -->
<div class="flex items-center justify-between pt-1"> <div class="flex items-center justify-between pt-1">
<label class="flex items-center space-x-2 cursor-pointer"> <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"> <input type="checkbox" name="remember" value="1" class="w-4 h-4 rounded text-slate-900 focus:ring-slate-900 border-slate-300">
<span class="text-xs font-bold text-slate-600">Onthoud mij op dit apparaat</span> <span class="text-xs text-slate-600">Onthoud mij op dit apparaat</span>
</label> </label>
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<div class="pt-2"> <div class="pt-1">
<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"> <button type="submit" class="w-full py-2.5 px-4 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<span>Inloggen</span> <span>Inloggen</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i> <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</button> </button>
</div> </div>
</form> </form>
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600"> <div class="pt-3 border-t border-slate-100 text-center text-xs text-slate-500">
<span>Nog geen account?</span> <span>Nog geen account?</span>
<a href="{{ route('register') }}" class="text-rose-600 hover:text-rose-700 underline ml-1"> <a href="{{ route('register') }}" class="text-slate-900 font-semibold hover:underline ml-1">
Direct registreren Direct registreren
</a> </a>
</div> </div>
@@ -93,4 +90,3 @@
</div> </div>
</div> </div>
@endsection @endsection
+34 -38
View File
@@ -3,26 +3,23 @@
@section('title', 'Registreren | Jouwdriedee.nl') @section('title', 'Registreren | Jouwdriedee.nl')
@section('content') @section('content')
<div class="min-h-[75vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8"> <div class="min-h-[75vh] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6"> <div class="w-full max-w-sm space-y-6">
<!-- Header --> <!-- Header -->
<div class="text-center space-y-2"> <div class="text-center space-y-1.5">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-rose-100 text-rose-900 shadow-2xs mb-1"> <h1 class="text-2xl font-bold text-slate-900 tracking-tight">Account aanmaken</h1>
<i data-lucide="user-plus" class="w-6 h-6 text-rose-600"></i> <p class="text-xs text-slate-500">Meld je aan om bestellingen te volgen en sneller af te rekenen</p>
</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> </div>
<!-- Form Card --> <!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5"> <div class="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<form action="{{ route('register.submit') }}" method="POST" class="space-y-4"> <form action="{{ route('register.submit') }}" method="POST" class="space-y-3.5">
@csrf @csrf
<!-- Naam --> <!-- Naam -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="name" class="block text-xs font-bold text-slate-700">Voor- en achternaam *</label> <label for="name" class="block text-xs font-medium text-slate-700">Voor- en achternaam *</label>
<div class="relative"> <div class="relative">
<input <input
type="text" type="text"
@@ -31,19 +28,19 @@
value="{{ old('name') }}" value="{{ old('name') }}"
required required
autofocus autofocus
placeholder="bijv. Jan Jansen" placeholder="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" class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @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> <i data-lucide="user" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div> </div>
@error('name') @error('name')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- E-mailadres --> <!-- E-mailadres -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres *</label> <label for="email" class="block text-xs font-medium text-slate-700">E-mailadres *</label>
<div class="relative"> <div class="relative">
<input <input
type="email" type="email"
@@ -52,18 +49,18 @@
value="{{ old('email') }}" value="{{ old('email') }}"
required required
placeholder="jouw@email.nl" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @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> <i data-lucide="mail" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div> </div>
@error('email') @error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Wachtwoord --> <!-- Wachtwoord -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="password" class="block text-xs font-bold text-slate-700">Wachtwoord (minimaal 8 tekens) *</label> <label for="password" class="block text-xs font-medium text-slate-700">Wachtwoord (minimaal 8 tekens) *</label>
<div class="relative"> <div class="relative">
<input <input
type="password" type="password"
@@ -71,18 +68,18 @@
id="password" id="password"
required required
placeholder="••••••••" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @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> <i data-lucide="lock" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div> </div>
@error('password') @error('password')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Bevestig Wachtwoord --> <!-- Bevestig Wachtwoord -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="password_confirmation" class="block text-xs font-bold text-slate-700">Bevestig wachtwoord *</label> <label for="password_confirmation" class="block text-xs font-medium text-slate-700">Bevestig wachtwoord *</label>
<div class="relative"> <div class="relative">
<input <input
type="password" type="password"
@@ -90,25 +87,25 @@
id="password_confirmation" id="password_confirmation"
required required
placeholder="••••••••" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
> >
<i data-lucide="shield-check" class="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i> <i data-lucide="shield-check" class="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>
</div> </div>
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<div class="pt-2"> <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"> <button type="submit" class="w-full py-2.5 px-4 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<span>Account Aanmaken</span> <span>Account registreren</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i> <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</button> </button>
</div> </div>
</form> </form>
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600"> <div class="pt-3 border-t border-slate-100 text-center text-xs text-slate-500">
<span>Heb je al een account?</span> <span>Al een account?</span>
<a href="{{ route('login') }}" class="text-rose-600 hover:text-rose-700 underline ml-1"> <a href="{{ route('login') }}" class="text-slate-900 font-semibold hover:underline ml-1">
Hier inloggen Inloggen
</a> </a>
</div> </div>
</div> </div>
@@ -116,4 +113,3 @@
</div> </div>
</div> </div>
@endsection @endsection
+20 -25
View File
@@ -3,27 +3,24 @@
@section('title', 'Nieuw Wachtwoord Instellen | Jouwdriedee.nl') @section('title', 'Nieuw Wachtwoord Instellen | Jouwdriedee.nl')
@section('content') @section('content')
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8"> <div class="min-h-[70vh] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-6"> <div class="w-full max-w-sm space-y-6">
<!-- Header --> <!-- Header -->
<div class="text-center space-y-2"> <div class="text-center space-y-1.5">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-amber-100 text-amber-900 shadow-2xs mb-1"> <h1 class="text-2xl font-bold text-slate-900 tracking-tight">Nieuw wachtwoord</h1>
<i data-lucide="lock" class="w-6 h-6 text-amber-600"></i> <p class="text-xs text-slate-500">Kies een nieuw en veilig wachtwoord voor je account</p>
</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> </div>
<!-- Form Card --> <!-- Form Card -->
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5"> <div class="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<form action="{{ route('password.update') }}" method="POST" class="space-y-4"> <form action="{{ route('password.update') }}" method="POST" class="space-y-3.5">
@csrf @csrf
<input type="hidden" name="token" value="{{ $token }}"> <input type="hidden" name="token" value="{{ $token }}">
<!-- E-mailadres --> <!-- E-mailadres -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres</label> <label for="email" class="block text-xs font-medium text-slate-700">E-mailadres</label>
<input <input
type="email" type="email"
name="email" name="email"
@@ -31,16 +28,16 @@
value="{{ old('email', $email) }}" value="{{ old('email', $email) }}"
required required
readonly 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" class="w-full bg-slate-100 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-600 font-medium cursor-not-allowed"
> >
@error('email') @error('email')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Nieuw Wachtwoord --> <!-- Nieuw Wachtwoord -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="password" class="block text-xs font-bold text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label> <label for="password" class="block text-xs font-medium text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label>
<input <input
type="password" type="password"
name="password" name="password"
@@ -48,31 +45,30 @@
required required
autofocus autofocus
placeholder="••••••••" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @error('password') border-rose-400 @enderror"
> >
@error('password') @error('password')
<p class="text-[11px] font-bold text-rose-500 mt-1">{{ $message }}</p> <p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Bevestig Nieuw Wachtwoord --> <!-- Bevestig Nieuw Wachtwoord -->
<div class="space-y-1.5"> <div class="space-y-1">
<label for="password_confirmation" class="block text-xs font-bold text-slate-700">Bevestig nieuw wachtwoord</label> <label for="password_confirmation" class="block text-xs font-medium text-slate-700">Bevestig nieuw wachtwoord</label>
<input <input
type="password" type="password"
name="password_confirmation" name="password_confirmation"
id="password_confirmation" id="password_confirmation"
required required
placeholder="••••••••" 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" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
> >
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<div class="pt-2"> <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"> <button type="submit" class="w-full py-2.5 px-4 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs 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</span>
<span>Wachtwoord Opslaan & Inloggen</span>
</button> </button>
</div> </div>
</form> </form>
@@ -81,4 +77,3 @@
</div> </div>
</div> </div>
@endsection @endsection
+57 -57
View File
@@ -1,86 +1,86 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Winkelwagen 🛒 | Jouwdriedee.nl') @section('title', 'Winkelwagen | Jouwdriedee.nl')
@section('content') @section('content')
<div class="bg-gradient-to-b from-amber-50 to-[#FFFDF9] border-b border-slate-200/80 py-5 sm:py-7"> <div class="border-b border-slate-200/80 py-6 sm:py-8 bg-slate-50/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight flex items-center space-x-2"> <h1 class="text-2xl sm:text-3xl font-bold tracking-tight text-slate-900">
<span>🛒</span> Winkelwagen
<span>Jouw Winkelwagen</span>
</h1> </h1>
<p class="text-xs text-slate-500 mt-0.5">Controleer jouw geselecteerde 3D prints, custom bestanden en kleuren.</p> <p class="text-xs text-slate-500 mt-1">Overzicht van jouw geselecteerde 3D prints en configuraties.</p>
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-10"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
@if(empty($cart)) @if(empty($cart))
<div class="p-8 sm:p-12 text-center rounded-3xl bg-white border border-slate-100 shadow-sm space-y-4 max-w-lg mx-auto"> <div class="p-10 sm:p-14 text-center rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3 max-w-md mx-auto">
<div class="w-16 h-16 rounded-2xl bg-amber-50 text-slate-800 flex items-center justify-center mx-auto text-3xl"> <div class="w-12 h-12 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto">
🧸 <i data-lucide="shopping-bag" class="w-6 h-6"></i>
</div> </div>
<h2 class="font-heading text-xl font-black text-slate-900">Je winkelmandje is nog leeg!</h2> <h2 class="text-base font-bold text-slate-900">Je winkelwagen is nog leeg</h2>
<p class="text-xs text-slate-500 max-w-sm mx-auto">Bekijk onze vrolijke collectie fidget toys en Pokémon prints, of bestel direct je eigen 3D model.</p> <p class="text-xs text-slate-500 max-w-xs mx-auto">Bekijk onze studio collectie of upload direct een eigen model.</p>
<div class="pt-2 flex flex-col sm:flex-row items-center justify-center gap-2"> <div class="pt-3 flex flex-col sm:flex-row items-center justify-center gap-2">
<a href="{{ route('catalog') }}" class="playful-btn w-full sm:w-auto px-5 py-3 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs inline-flex items-center justify-center space-x-1.5 shadow-xs"> <a href="{{ route('catalog') }}" class="w-full sm:w-auto px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs inline-flex items-center justify-center space-x-1.5 shadow-xs transition">
<span>Bekijk Alle Prints 🎁</span> <span>Bekijk collectie</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a> </a>
<a href="{{ route('custom.request') }}" class="playful-btn w-full sm:w-auto px-5 py-3 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold text-xs inline-flex items-center justify-center space-x-1.5"> <a href="{{ route('custom.request') }}" class="w-full sm:w-auto px-4 py-2 rounded-xl bg-white hover:bg-slate-50 border border-slate-200 text-slate-700 font-medium text-xs inline-flex items-center justify-center space-x-1.5 transition">
<span>Eigen Model Bestellen 🚀</span> <span>Eigen bestand uploaden</span>
</a> </a>
</div> </div>
</div> </div>
@else @else
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Items Lijst --> <!-- Items Lijst -->
<div class="lg:col-span-8 space-y-3 sm:space-y-4"> <div class="lg:col-span-8 space-y-3">
@foreach($cart as $key => $item) @foreach($cart as $key => $item)
<div class="p-4 sm:p-5 rounded-2xl sm:rounded-3xl bg-white border border-slate-100 shadow-xs flex flex-col sm:flex-row items-center justify-between gap-4"> <div class="p-4 sm:p-5 rounded-2xl bg-white border border-slate-200/80 shadow-2xs flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center space-x-3.5 w-full sm:w-auto"> <div class="flex items-center space-x-3.5 w-full sm:w-auto">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-slate-100 overflow-hidden flex-shrink-0 border border-slate-100"> <div class="w-16 h-16 rounded-xl bg-slate-100 overflow-hidden shrink-0 border border-slate-200/60">
<img src="{{ $item['image_url'] }}" alt="{{ $item['name'] }}" class="w-full h-full object-cover"> <img src="{{ $item['image_url'] }}" alt="{{ $item['name'] }}" class="w-full h-full object-cover">
</div> </div>
<div class="space-y-1"> <div class="space-y-1">
<h3 class="font-heading text-sm sm:text-base font-black text-slate-900">{{ $item['name'] }}</h3> <h3 class="text-sm font-semibold text-slate-900">{{ $item['name'] }}</h3>
<div class="flex flex-wrap items-center gap-1.5 text-[11px] text-slate-500"> <div class="flex flex-wrap items-center gap-1.5 text-[11px] text-slate-500">
<span class="px-2 py-0.2 rounded-md bg-rose-50 text-rose-700 font-bold">{{ $item['material'] }}</span> <span class="px-1.5 py-0.2 rounded bg-slate-100 text-slate-700 font-medium">{{ strtoupper($item['material']) }}</span>
<span>•</span> <span>&bull;</span>
<span class="font-semibold text-slate-700">Kleur: {{ $item['color'] }}</span> <span>Kleur: {{ $item['color'] }}</span>
<span>•</span> <span>&bull;</span>
<span class="font-mono text-slate-600">{{ $item['scale'] }}</span> <span class="font-mono text-slate-600">{{ $item['scale'] }}</span>
</div> </div>
@if(!empty($item['custom_file_name'])) @if(!empty($item['custom_file_name']))
<div class="text-[10px] font-bold text-indigo-600 flex items-center space-x-1 pt-0.5"> <div class="text-[10px] text-slate-500 font-mono flex items-center space-x-1 pt-0.5">
<span>📁</span> <i data-lucide="file-code" class="w-3 h-3 text-slate-400"></i>
<span>Bestand: {{ $item['custom_file_name'] }}</span> <span>{{ $item['custom_file_name'] }}</span>
</div> </div>
@endif @endif
@if(!empty($item['custom_model_url'])) @if(!empty($item['custom_model_url']))
<div class="text-[10px] font-bold text-indigo-600 flex items-center space-x-1 pt-0.5"> <div class="text-[10px] text-slate-500 font-mono flex items-center space-x-1 pt-0.5">
<span>🔗</span> <i data-lucide="link" class="w-3 h-3 text-slate-400"></i>
<span class="truncate max-w-xs">{{ $item['custom_model_url'] }}</span> <span class="truncate max-w-xs">{{ $item['custom_model_url'] }}</span>
</div> </div>
@endif @endif
<div class="text-xs font-black text-slate-900 pt-0.5"> <div class="text-xs font-medium text-slate-600 pt-0.5">
€ {{ number_format($item['price'], 2, ',', '.') }} per stuk € {{ number_format($item['price'], 2, ',', '.') }} per stuk
</div> </div>
</div> </div>
</div> </div>
<!-- Aantal en acties --> <!-- Aantal en acties -->
<div class="flex items-center justify-between sm:justify-end space-x-3 w-full sm:w-auto pt-2 sm:pt-0 border-t sm:border-t-0 border-slate-100"> <div class="flex items-center justify-between sm:justify-end space-x-4 w-full sm:w-auto pt-2 sm:pt-0 border-t sm:border-t-0 border-slate-100">
<!-- Update Formulier --> <!-- Update Formulier -->
<form action="{{ route('cart.update') }}" method="POST" class="flex items-center space-x-1"> <form action="{{ route('cart.update') }}" method="POST" class="flex items-center space-x-1.5">
@csrf @csrf
<input type="hidden" name="key" value="{{ $key }}"> <input type="hidden" name="key" value="{{ $key }}">
<input type="number" name="quantity" min="1" max="50" value="{{ $item['quantity'] }}" class="w-14 bg-slate-50 border border-slate-200 rounded-lg px-2 py-1 text-xs text-center font-heading font-black text-slate-900 focus:outline-none"> <input type="number" name="quantity" min="1" max="50" value="{{ $item['quantity'] }}" class="w-12 bg-slate-50 border border-slate-200 rounded-lg px-2 py-1 text-xs text-center font-medium text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-slate-900">
<button type="submit" class="p-1 rounded-lg text-slate-400 hover:text-slate-800 transition" title="Bijwerken"> <button type="submit" class="p-1 rounded-lg text-slate-400 hover:text-slate-800 transition cursor-pointer" title="Bijwerken">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i> <i data-lucide="refresh-cw" class="w-3 h-3"></i>
</button> </button>
</form> </form>
<span class="font-heading text-base font-black text-slate-900 w-20 text-right"> <span class="font-semibold text-slate-900 text-sm w-20 text-right">
€ {{ number_format($item['price'] * $item['quantity'], 2, ',', '.') }} € {{ number_format($item['price'] * $item['quantity'], 2, ',', '.') }}
</span> </span>
@@ -88,7 +88,7 @@
<form action="{{ route('cart.remove', $key) }}" method="POST"> <form action="{{ route('cart.remove', $key) }}" method="POST">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="p-1.5 text-slate-300 hover:text-rose-500 transition cursor-pointer" title="Verwijderen"> <button type="submit" class="p-1.5 text-slate-400 hover:text-rose-600 transition cursor-pointer" title="Verwijderen">
<i data-lucide="trash-2" class="w-4 h-4"></i> <i data-lucide="trash-2" class="w-4 h-4"></i>
</button> </button>
</form> </form>
@@ -99,45 +99,45 @@
<!-- Samenvatting --> <!-- Samenvatting -->
<div class="lg:col-span-4"> <div class="lg:col-span-4">
<div class="p-6 rounded-3xl bg-white border border-slate-100 shadow-md space-y-4 sticky top-24"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4 sticky top-24">
<h3 class="font-heading text-sm font-black text-slate-900 uppercase tracking-wider pb-2.5 border-b border-slate-100 flex items-center space-x-1.5"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider pb-2 border-b border-slate-100">
<span>🧾</span> Besteloverzicht
<span>Overzicht Bestelling</span>
</h3> </h3>
<div class="space-y-2 text-xs font-bold"> <div class="space-y-2 text-xs">
<div class="flex justify-between text-slate-600"> <div class="flex justify-between text-slate-600">
<span>Subtotaal</span> <span>Subtotaal</span>
<span class="font-heading text-sm font-black text-slate-900">€ {{ number_format($subtotal, 2, ',', '.') }}</span> <span class="font-semibold text-slate-900">€ {{ number_format($subtotal, 2, ',', '.') }}</span>
</div> </div>
<div class="flex justify-between text-slate-600"> <div class="flex justify-between text-slate-600">
<span>Verzendkosten</span> <span>Verzendkosten</span>
@if($shipping == 0) @if($shipping == 0)
<span class="text-emerald-700 font-black bg-emerald-100 px-2 py-0.2 rounded-md">GRATIS 🎉</span> <span class="text-emerald-700 font-semibold bg-emerald-50 px-2 py-0.5 rounded-md">Gratis</span>
@else @else
<span class="font-heading text-sm font-black text-slate-900">€ {{ number_format($shipping, 2, ',', '.') }}</span> <span class="font-semibold text-slate-900">€ {{ number_format($shipping, 2, ',', '.') }}</span>
@endif @endif
</div> </div>
@if($subtotal < 50) @if($subtotal < 50)
<p class="text-[11px] text-amber-900 bg-amber-50 p-2.5 rounded-xl border border-amber-200 font-bold"> <p class="text-[11px] text-slate-600 bg-slate-50 p-2.5 rounded-xl border border-slate-200/60">
💡 Tip: Bestel nog voor <strong>€ {{ number_format(50 - $subtotal, 2, ',', '.') }}</strong> voor gratis verzending! Bestel nog voor <strong>€ {{ number_format(50 - $subtotal, 2, ',', '.') }}</strong> voor gratis verzending.
</p> </p>
@endif @endif
</div> </div>
<div class="pt-3 border-t border-slate-100 flex justify-between items-baseline"> <div class="pt-3 border-t border-slate-100 flex justify-between items-baseline">
<span class="font-heading text-sm font-black text-slate-900">Totaal (incl. btw)</span> <span class="text-xs font-medium text-slate-600">Totaal (incl. btw)</span>
<span class="font-heading text-2xl font-black text-slate-900">€ {{ number_format($total, 2, ',', '.') }}</span> <span class="text-xl font-bold text-slate-900">€ {{ number_format($total, 2, ',', '.') }}</span>
</div> </div>
<a href="{{ route('checkout.index') }}" class="playful-btn w-full py-3.5 rounded-2xl bg-gradient-to-r from-amber-400 via-rose-400 to-rose-500 hover:opacity-95 text-slate-950 font-black text-xs uppercase tracking-wider transition shadow-md shadow-rose-500/20 flex items-center justify-center space-x-2"> <a href="{{ route('checkout.index') }}" class="w-full py-3 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition shadow-xs flex items-center justify-center space-x-2">
<span>Doorgaan Naar Afrekenen 🚀</span> <span>Doorgaan naar afrekenen</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a> </a>
<div class="flex items-center justify-center space-x-2 text-[11px] font-bold text-slate-400 pt-1"> <div class="flex items-center justify-center space-x-2 text-[11px] text-slate-400 pt-1">
<span>🔒 Veilig betalen met iDEAL</span> <span>Veilig betalen via iDEAL</span>
<span>•</span> <span>&bull;</span>
<span>PostNL</span> <span>PostNL bezorging</span>
</div> </div>
</div> </div>
</div> </div>
+69 -70
View File
@@ -3,14 +3,14 @@
@section('title', 'Afrekenen | Jouwdriedee.nl') @section('title', 'Afrekenen | Jouwdriedee.nl')
@section('content') @section('content')
<div class="bg-white border-b border-slate-200 py-6"> <div class="border-b border-slate-200/80 py-6 sm:py-8 bg-slate-50/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Afrekenen & Gegevens</h1> <h1 class="text-2xl sm:text-3xl font-bold tracking-tight text-slate-900">Afrekenen & Gegevens</h1>
<p class="text-xs text-slate-500 mt-0.5">Vul je bezorgadres in om jouw 3D-prints in de printwachtrij te plaatsen.</p> <p class="text-xs text-slate-500 mt-1">Vul je bezorgadres in om jouw 3D prints in de productiewachtrij te plaatsen.</p>
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
<form action="{{ route('checkout.store') }}" method="POST"> <form action="{{ route('checkout.store') }}" method="POST">
@csrf @csrf
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
@@ -19,96 +19,96 @@
<div class="lg:col-span-7 space-y-6"> <div class="lg:col-span-7 space-y-6">
<!-- 1. Contactgegevens --> <!-- 1. Contactgegevens -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2"> <h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="user" class="w-4 h-4 text-indigo-600"></i> <i data-lucide="user" class="w-3.5 h-3.5 text-slate-500"></i>
<span>1. Contactgegevens</span> <span>1. Contactgegevens</span>
</h2> </h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
<div class="sm:col-span-2"> <div class="sm:col-span-2 space-y-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">Volledige Naam *</label> <label class="block text-xs font-medium text-slate-700">Volledige Naam *</label>
<input type="text" name="customer_name" required value="{{ old('customer_name') }}" placeholder="Jan de Vries" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="text" name="customer_name" required value="{{ old('customer_name') }}" placeholder="Jan Jansen" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">E-mailadres *</label> <label class="block text-xs font-medium text-slate-700">E-mailadres *</label>
<input type="email" name="customer_email" required value="{{ old('customer_email') }}" placeholder="jan@voorbeeld.nl" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="email" name="customer_email" required value="{{ old('customer_email') }}" placeholder="jan@voorbeeld.nl" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">Telefoonnummer (voor bezorgupdates)</label> <label class="block text-xs font-medium text-slate-700">Telefoonnummer</label>
<input type="text" name="customer_phone" value="{{ old('customer_phone') }}" placeholder="06-12345678" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="text" name="customer_phone" value="{{ old('customer_phone') }}" placeholder="06-12345678" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
</div> </div>
</div> </div>
<!-- 2. Verzendadres --> <!-- 2. Verzendadres -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2"> <h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="map-pin" class="w-4 h-4 text-indigo-600"></i> <i data-lucide="map-pin" class="w-3.5 h-3.5 text-slate-500"></i>
<span>2. Verzendadres (Nederland)</span> <span>2. Verzendadres (Nederland)</span>
</h2> </h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-3 gap-3.5">
<div class="sm:col-span-2"> <div class="sm:col-span-2 space-y-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">Straatnaam *</label> <label class="block text-xs font-medium text-slate-700">Straatnaam *</label>
<input type="text" name="shipping_street" required value="{{ old('shipping_street') }}" placeholder="Hoofdstraat" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="text" name="shipping_street" required value="{{ old('shipping_street') }}" placeholder="Kalverstraat" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">Huisnummer *</label> <label class="block text-xs font-medium text-slate-700">Huisnummer *</label>
<input type="text" name="shipping_number" required value="{{ old('shipping_number') }}" placeholder="12A" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="text" name="shipping_number" required value="{{ old('shipping_number') }}" placeholder="12A" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">Postcode *</label> <label class="block text-xs font-medium text-slate-700">Postcode *</label>
<input type="text" name="shipping_postal_code" required value="{{ old('shipping_postal_code') }}" placeholder="1234 AB" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="text" name="shipping_postal_code" required value="{{ old('shipping_postal_code') }}" placeholder="1012 AB" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
<div class="sm:col-span-2"> <div class="sm:col-span-2 space-y-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">Woonplaats *</label> <label class="block text-xs font-medium text-slate-700">Woonplaats *</label>
<input type="text" name="shipping_city" required value="{{ old('shipping_city') }}" placeholder="Amsterdam" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"> <input type="text" name="shipping_city" required value="{{ old('shipping_city') }}" placeholder="Amsterdam" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900">
</div> </div>
</div> </div>
</div> </div>
<!-- 3. Betaalmethode --> <!-- 3. Betaalmethode -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
<h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2"> <h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="credit-card" class="w-4 h-4 text-indigo-600"></i> <i data-lucide="credit-card" class="w-3.5 h-3.5 text-slate-500"></i>
<span>3. Betaalmethode</span> <span>3. Betaalmethode</span>
</h2> </h2>
<div class="space-y-2.5"> <div class="space-y-2">
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-indigo-200 bg-indigo-50/40 cursor-pointer"> <label class="flex items-center justify-between p-3.5 rounded-xl border border-slate-900/10 bg-slate-50 cursor-pointer">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-2.5">
<input type="radio" name="payment_method" value="ideal" checked class="text-indigo-600 focus:ring-indigo-500"> <input type="radio" name="payment_method" value="ideal" checked class="text-slate-900 focus:ring-slate-900">
<span class="text-xs font-bold text-slate-900">iDEAL</span> <span class="text-xs font-semibold text-slate-900">iDEAL</span>
</div> </div>
<span class="text-[10px] font-mono px-2 py-0.5 rounded bg-white text-indigo-700 font-bold border border-indigo-200">Aanbevolen</span> <span class="text-[10px] font-medium px-2 py-0.5 rounded bg-white text-slate-700 border border-slate-200">Aanbevolen</span>
</label> </label>
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-slate-200 hover:bg-slate-50 cursor-pointer"> <label class="flex items-center justify-between p-3.5 rounded-xl border border-slate-200 hover:bg-slate-50 cursor-pointer transition">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-2.5">
<input type="radio" name="payment_method" value="bancontact" class="text-indigo-600 focus:ring-indigo-500"> <input type="radio" name="payment_method" value="bancontact" class="text-slate-900 focus:ring-slate-900">
<span class="text-xs font-bold text-slate-900">Bancontact</span> <span class="text-xs font-semibold text-slate-900">Bancontact</span>
</div> </div>
<span class="text-[11px] text-slate-400">België</span> <span class="text-[11px] text-slate-400">België</span>
</label> </label>
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-slate-200 hover:bg-slate-50 cursor-pointer"> <label class="flex items-center justify-between p-3.5 rounded-xl border border-slate-200 hover:bg-slate-50 cursor-pointer transition">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-2.5">
<input type="radio" name="payment_method" value="overboeking" class="text-indigo-600 focus:ring-indigo-500"> <input type="radio" name="payment_method" value="overboeking" class="text-slate-900 focus:ring-slate-900">
<span class="text-xs font-bold text-slate-900">Handmatige Bankoverschrijving</span> <span class="text-xs font-semibold text-slate-900">Handmatige Bankoverschrijving</span>
</div> </div>
<span class="text-[11px] text-slate-400">Achteraf / Factuur</span> <span class="text-[11px] text-slate-400">Achteraf</span>
</label> </label>
</div> </div>
<div> <div class="space-y-1 pt-1">
<label class="block text-xs font-semibold text-slate-700 mb-1">Opmerkingen voor de printer (optioneel)</label> <label class="block text-xs font-medium text-slate-700">Opmerkingen voor de printer (optioneel)</label>
<textarea name="notes" rows="2" placeholder="Bijv. neerzetten bij de achterdeur of speciale printinstructie..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"></textarea> <textarea name="notes" rows="2" placeholder="Bijv. gewenste printoriëntatie of bezorginstructie..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"></textarea>
</div> </div>
</div> </div>
@@ -116,49 +116,49 @@
<!-- Rechterkant: Besteloverzicht Card --> <!-- Rechterkant: Besteloverzicht Card -->
<div class="lg:col-span-5"> <div class="lg:col-span-5">
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-md space-y-5 sticky top-24"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4 sticky top-24">
<h3 class="text-sm font-bold text-slate-900 uppercase tracking-wider pb-3 border-b border-slate-100">Jouw Bestelling</h3> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider pb-2 border-b border-slate-100">Jouw Bestelling</h3>
<div class="space-y-3 max-h-72 overflow-y-auto pr-1"> <div class="space-y-2.5 max-h-72 overflow-y-auto pr-1">
@foreach($cart as $item) @foreach($cart as $item)
<div class="flex items-center justify-between text-xs py-2 border-b border-slate-100"> <div class="flex items-center justify-between text-xs py-1.5 border-b border-slate-100">
<div class="space-y-0.5"> <div class="space-y-0.5">
<div class="font-bold text-slate-900">{{ $item['name'] }} <span class="text-slate-400 font-normal">&times; {{ $item['quantity'] }}</span></div> <div class="font-medium text-slate-900">{{ $item['name'] }} <span class="text-slate-400">&times; {{ $item['quantity'] }}</span></div>
<div class="text-[11px] text-slate-500">{{ $item['material'] }} • {{ $item['color'] }} ({{ $item['scale'] }})</div> <div class="text-[11px] text-slate-500">{{ strtoupper($item['material']) }} &bull; {{ $item['color'] }} ({{ $item['scale'] }})</div>
</div> </div>
<span class="font-mono font-bold text-slate-900"> <span class="font-semibold text-slate-900">
€ {{ number_format($item['price'] * $item['quantity'], 2, ',', '.') }} € {{ number_format($item['price'] * $item['quantity'], 2, ',', '.') }}
</span> </span>
</div> </div>
@endforeach @endforeach
</div> </div>
<div class="space-y-2 text-xs pt-2"> <div class="space-y-1.5 text-xs pt-1">
<div class="flex justify-between text-slate-600"> <div class="flex justify-between text-slate-600">
<span>Subtotaal</span> <span>Subtotaal</span>
<span class="font-mono font-bold text-slate-900">€ {{ number_format($subtotal, 2, ',', '.') }}</span> <span class="font-semibold text-slate-900">€ {{ number_format($subtotal, 2, ',', '.') }}</span>
</div> </div>
<div class="flex justify-between text-slate-600"> <div class="flex justify-between text-slate-600">
<span>Verzendkosten</span> <span>Verzendkosten</span>
@if($shipping == 0) @if($shipping == 0)
<span class="text-emerald-600 font-bold">GRATIS</span> <span class="text-emerald-700 font-semibold bg-emerald-50 px-2 py-0.5 rounded-md">Gratis</span>
@else @else
<span class="font-mono font-bold text-slate-900">€ {{ number_format($shipping, 2, ',', '.') }}</span> <span class="font-semibold text-slate-900">€ {{ number_format($shipping, 2, ',', '.') }}</span>
@endif @endif
</div> </div>
</div> </div>
<div class="pt-4 border-t border-slate-100 flex justify-between items-baseline"> <div class="pt-3 border-t border-slate-100 flex justify-between items-baseline">
<span class="text-sm font-bold text-slate-900">Totaalbedrag</span> <span class="text-xs font-medium text-slate-600">Totaalbedrag</span>
<span class="text-2xl font-black text-indigo-600 font-mono">€ {{ number_format($total, 2, ',', '.') }}</span> <span class="text-xl font-bold text-slate-900">€ {{ number_format($total, 2, ',', '.') }}</span>
</div> </div>
<button type="submit" class="w-full py-4 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs uppercase tracking-wider transition shadow-lg shadow-indigo-600/25 flex items-center justify-center space-x-2 cursor-pointer"> <button type="submit" class="w-full py-3 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="check-circle-2" class="w-4 h-4"></i> <i data-lucide="check" class="w-4 h-4"></i>
<span>Bestelling Definitief Plaatsen</span> <span>Bestelling Plaatsen</span>
</button> </button>
<p class="text-center text-[10px] text-slate-400"> <p class="text-center text-[11px] text-slate-400">
Door te bestellen ga je akkoord met de algemene voorwaarden van Jouwdriedee.nl. Door te bestellen ga je akkoord met de algemene voorwaarden van Jouwdriedee.nl.
</p> </p>
</div> </div>
@@ -168,4 +168,3 @@
</form> </form>
</div> </div>
@endsection @endsection
+40 -47
View File
@@ -1,105 +1,98 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Bestelling Ontvangen! | Jouwdriedee.nl') @section('title', 'Bestelling Ontvangen | Jouwdriedee.nl')
@section('content') @section('content')
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center space-y-8"> <div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center space-y-8">
<!-- Success Badge --> <!-- Success Badge -->
<div class="space-y-4"> <div class="space-y-3">
<div class="w-20 h-20 rounded-3xl bg-emerald-100 text-emerald-600 flex items-center justify-center mx-auto shadow-lg shadow-emerald-500/20"> <div class="w-14 h-14 rounded-2xl bg-emerald-50 text-emerald-600 border border-emerald-200/60 flex items-center justify-center mx-auto">
<i data-lucide="check" class="w-10 h-10 stroke-[2.5]"></i> <i data-lucide="check" class="w-7 h-7 stroke-[2]"></i>
</div> </div>
<h1 class="text-3xl sm:text-4xl font-black text-slate-900 tracking-tight">Bedankt voor je bestelling!</h1> <h1 class="text-2xl sm:text-3xl font-bold tracking-tight text-slate-900">Bedankt voor je bestelling!</h1>
<p class="text-xs sm:text-sm text-slate-500 max-w-md mx-auto"> <p class="text-xs text-slate-500 max-w-sm mx-auto">
We hebben jouw bestelling ontvangen. De printbestanden worden klaargezet voor de 3D-printers. We hebben jouw bestelling ontvangen. De printbestanden worden klaargezet voor productie.
</p> </p>
<div class="inline-block px-4 py-2 rounded-xl bg-slate-100 border border-slate-200 text-xs font-mono font-bold text-slate-800"> <div class="inline-block px-3 py-1.5 rounded-xl bg-slate-100 border border-slate-200 text-xs font-mono font-medium text-slate-700">
Bestelnummer: <span class="text-indigo-600">{{ $order->order_number }}</span> Bestelnummer: <span class="font-bold text-slate-900">{{ $order->order_number }}</span>
</div> </div>
</div> </div>
<!-- Live 3D Print Status Tracker --> <!-- Live 3D Print Status Tracker -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-sm text-left space-y-6"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs text-left space-y-5">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="printer" class="w-4 h-4 text-indigo-600"></i> <i data-lucide="printer" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Live Print & Productie Status</span> <span>Productiestatus</span>
</h3> </h3>
<div class="relative pl-6 space-y-6 border-l-2 border-slate-200"> <div class="relative pl-6 space-y-5 border-l-2 border-slate-200">
<!-- Stap 1: Ontvangen --> <!-- Stap 1: Ontvangen -->
<div class="relative"> <div class="relative">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-emerald-500 ring-4 ring-emerald-100"></span> <span class="absolute -left-[31px] top-0.5 w-3.5 h-3.5 rounded-full bg-emerald-500 ring-4 ring-emerald-50"></span>
<h4 class="text-xs font-bold text-slate-900">1. Bestelling ontvangen & geverifieerd</h4> <h4 class="text-xs font-semibold text-slate-900">1. Bestelling ontvangen & gecontroleerd</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Jouw configuratie en materiaalkeuzes zijn opgeslagen.</p> <p class="text-[11px] text-slate-500 mt-0.5">Jouw configuratie en materiaalkeuzes zijn opgeslagen.</p>
</div> </div>
<!-- Stap 2: Slicing & Printbed --> <!-- Stap 2: Slicing & Printbed -->
<div class="relative"> <div class="relative">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-indigo-600 ring-4 ring-indigo-100 animate-pulse"></span> <span class="absolute -left-[31px] top-0.5 w-3.5 h-3.5 rounded-full bg-slate-900 ring-4 ring-slate-100 animate-pulse"></span>
<h4 class="text-xs font-bold text-indigo-600">2. Slicing & printwachtrij (Huidige fase)</h4> <h4 class="text-xs font-semibold text-slate-900">2. Slicing & printwachtrij (Huidige fase)</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Het 3D-model wordt geoptimaliseerd en klaargezet voor de printer.</p> <p class="text-[11px] text-slate-500 mt-0.5">Het 3D-model wordt geoptimaliseerd en klaargezet voor de printer.</p>
</div> </div>
<!-- Stap 3: 3D Printen --> <!-- Stap 3: 3D Printen -->
<div class="relative opacity-60"> <div class="relative opacity-50">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-slate-300"></span> <span class="absolute -left-[31px] top-0.5 w-3.5 h-3.5 rounded-full bg-slate-300"></span>
<h4 class="text-xs font-bold text-slate-700">3. 3D Printen in de studio</h4> <h4 class="text-xs font-medium text-slate-700">3. 3D Printen in de studio</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Laag voor laag geprint op hoge resolutie.</p> <p class="text-[11px] text-slate-500 mt-0.5">Laag voor laag geprint op hoge resolutie.</p>
</div> </div>
<!-- Stap 4: Kwaliteitscontrole & Verzenden --> <!-- Stap 4: Kwaliteitscontrole & Verzenden -->
<div class="relative opacity-60"> <div class="relative opacity-50">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-slate-300"></span> <span class="absolute -left-[31px] top-0.5 w-3.5 h-3.5 rounded-full bg-slate-300"></span>
<h4 class="text-xs font-bold text-slate-700">4. Kwaliteitscontrole, inpakken & PostNL verzending</h4> <h4 class="text-xs font-medium text-slate-700">4. Kwaliteitscontrole & PostNL verzending</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Je ontvangt per e-mail direct een Track & Trace code.</p> <p class="text-[11px] text-slate-500 mt-0.5">Je ontvangt per e-mail een Track & Trace code.</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Besteloverzicht Card --> <!-- Besteloverzicht Card -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-sm text-left space-y-4"> <div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs text-left space-y-4">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider pb-3 border-b border-slate-100"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider pb-2 border-b border-slate-100">
Overzicht van jouw bestelling Overzicht van jouw bestelling
</h3> </h3>
<div class="space-y-3"> <div class="space-y-2">
@foreach($order->items as $item) @foreach($order->items as $item)
<div class="flex items-center justify-between text-xs py-1"> <div class="flex items-center justify-between text-xs py-1">
<div> <div>
<span class="font-bold text-slate-900">{{ $item->product_name }}</span> <div class="font-medium text-slate-900">{{ $item->product_name }} <span class="text-slate-400">&times; {{ $item->quantity }}</span></div>
<span class="text-slate-400">&times; {{ $item->quantity }}</span> <div class="text-[11px] text-slate-500">{{ strtoupper($item->material) }} &bull; {{ $item->color }} ({{ $item->scale }})</div>
<div class="text-[11px] text-slate-500">{{ $item->material }} • {{ $item->color }} ({{ $item->scale }})</div>
</div> </div>
<span class="font-mono font-bold text-slate-900"> <span class="font-semibold text-slate-900">
€ {{ number_format($item->total_price, 2, ',', '.') }} &euro; {{ number_format($item->total_price, 2, ',', '.') }}
</span> </span>
</div> </div>
@endforeach @endforeach
</div> </div>
<div class="pt-3 border-t border-slate-100 flex justify-between items-baseline text-xs"> <div class="pt-3 border-t border-slate-100 flex justify-between items-baseline text-xs">
<span class="font-bold text-slate-700">Totaal betaald</span> <span class="font-medium text-slate-500">Totaal betaald</span>
<span class="text-lg font-black text-indigo-600 font-mono">{{ $order->formatted_total }}</span> <span class="text-base font-bold text-slate-900">{{ $order->formatted_total }}</span>
</div>
<div class="pt-3 border-t border-slate-100 text-xs text-slate-500">
<span class="font-bold text-slate-700 block mb-1">Bezorgadres:</span>
{{ $order->customer_name }}<br>
{{ $order->shipping_street }} {{ $order->shipping_number }}<br>
{{ $order->shipping_postal_code }} {{ $order->shipping_city }}
</div> </div>
</div> </div>
<div> <div class="flex flex-col sm:flex-row items-center justify-center gap-3">
<a href="{{ route('home') }}" class="px-6 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition inline-flex items-center space-x-2 shadow-md"> <a href="{{ route('home') }}" class="px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition shadow-xs">
<span>Terug Naar De Homepagina</span> Terug naar de homepagina
<i data-lucide="arrow-right" class="w-4 h-4"></i> </a>
<a href="{{ route('account.index') }}" class="px-5 py-2.5 rounded-xl bg-white hover:bg-slate-50 border border-slate-200 text-slate-700 font-medium text-xs transition">
Bestelling bekijken in account
</a> </a>
</div> </div>
</div> </div>
@endsection @endsection
+253
View File
@@ -0,0 +1,253 @@
@extends('layouts.app')
@section('title', 'Vraag stellen & Contact | Jouwdriedee.nl')
@section('content')
<!-- Header Banner -->
<div class="border-b border-slate-200/80 py-8 sm:py-12 bg-slate-50/50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-2.5">
<span class="px-3 py-1 rounded-full bg-white border border-slate-200 text-xs font-medium text-slate-700 inline-flex items-center space-x-1.5 shadow-2xs">
<i data-lucide="message-square" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Klantenservice &amp; Advies</span>
</span>
<h1 class="text-3xl sm:text-4xl font-bold tracking-tight text-slate-900">Stel jouw vraag aan de printstudio</h1>
<p class="text-xs sm:text-sm text-slate-500 max-w-xl mx-auto leading-relaxed">
Heb je een vraag over een 3D-model, een lopende bestelling of advies nodig over filament of kleuren? Stuur ons gerust een bericht. Je ontvangt direct een afschrift per e-mail.
</p>
</div>
</div>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
<!-- Success Alert -->
@if(session('success'))
<div class="mb-8 p-4 rounded-2xl bg-emerald-50 border border-emerald-200/60 text-emerald-800 text-xs flex items-start space-x-3 shadow-xs">
<i data-lucide="check-circle" class="w-5 h-5 text-emerald-600 shrink-0 mt-0.5"></i>
<div>
<strong class="font-semibold block text-sm mb-0.5">Bericht succesvol verzonden!</strong>
<span>{{ session('success') }}</span>
</div>
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
<!-- Contactformulier Card -->
<div class="lg:col-span-7 bg-white p-6 sm:p-8 rounded-2xl border border-slate-200/80 shadow-xs space-y-6">
<div>
<h2 class="text-base font-bold text-slate-900">Stel een vraag</h2>
<p class="text-xs text-slate-500 mt-0.5">Vul onderstaand formulier in, dan reageren wij binnen 24 uur.</p>
</div>
<form action="{{ route('contact.store') }}" method="POST" class="space-y-4">
@csrf
<!-- Naam & E-mail -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1">
<label for="name" class="block text-xs font-medium text-slate-700">Jouw naam *</label>
<input
type="text"
name="name"
id="name"
required
value="{{ old('name', $user->name ?? '') }}"
placeholder="Jan Jansen"
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @error('name') border-rose-400 @enderror"
>
@error('name')
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror
</div>
<div class="space-y-1">
<label for="email" class="block text-xs font-medium text-slate-700">E-mailadres *</label>
<input
type="email"
name="email"
id="email"
required
value="{{ old('email', $user->email ?? '') }}"
placeholder="jouw@email.nl"
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @error('email') border-rose-400 @enderror"
>
@error('email')
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror
</div>
</div>
<!-- Telefoon & Bestelnummer -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1">
<label for="phone" class="block text-xs font-medium text-slate-700">Telefoonnummer (optioneel)</label>
<input
type="tel"
name="phone"
id="phone"
value="{{ old('phone') }}"
placeholder="06-12345678"
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
>
</div>
<div class="space-y-1">
<label for="order_number" class="block text-xs font-medium text-slate-700">Bestelnummer (optioneel)</label>
@if($userOrders->isNotEmpty())
<select
name="order_number"
id="order_number"
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
>
<option value="">-- Geen specifieke bestelling --</option>
@foreach($userOrders as $order)
<option value="{{ $order->order_number }}" {{ old('order_number') === $order->order_number ? 'selected' : '' }}>
Order #{{ $order->order_number }} ({{ $order->created_at->format('d-m-Y') }})
</option>
@endforeach
</select>
@else
<input
type="text"
name="order_number"
id="order_number"
value="{{ old('order_number') }}"
placeholder="bijv. ORD-2026-..."
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900"
>
@endif
</div>
</div>
<!-- Onderwerp -->
<div class="space-y-1">
<label for="subject" class="block text-xs font-medium text-slate-700">Onderwerp van jouw vraag *</label>
<input
type="text"
name="subject"
id="subject"
required
value="{{ old('subject') }}"
placeholder="bijv. Vraag over levertijd of materiaalkeuze..."
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 @error('subject') border-rose-400 @enderror"
>
@error('subject')
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Bericht -->
<div class="space-y-1">
<label for="message" class="block text-xs font-medium text-slate-700">Jouw bericht of vraag *</label>
<textarea
name="message"
id="message"
rows="5"
required
placeholder="Typ hier jouw vraag zo duidelijk mogelijk..."
class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3.5 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 leading-relaxed @error('message') border-rose-400 @enderror"
>{{ old('message') }}</textarea>
@error('message')
<p class="text-[11px] text-rose-600 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Note over afschrift -->
<div class="p-3 rounded-xl bg-slate-50 border border-slate-200/80 text-[11px] text-slate-500 flex items-center space-x-2">
<i data-lucide="shield-check" class="w-4 h-4 text-slate-400 shrink-0"></i>
<span>Je ontvangt na verzending direct een <strong>bevestigingsafschrift</strong> via <code class="text-slate-700 font-mono">noreply@jouwdriedee.nl</code>.</span>
</div>
<!-- Submit Button -->
<div class="pt-1">
<button type="submit" class="w-full py-3 px-5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Vraag Versturen</span>
</button>
</div>
</form>
</div>
<!-- Rechterkolom: Info & FAQ links -->
<div class="lg:col-span-5 space-y-5">
<!-- Quick Studio Info -->
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-xs space-y-4">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Directe Contactgegevens</h3>
<div class="space-y-3 text-xs text-slate-600">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500 shrink-0 mt-0.5">
<i data-lucide="mail" class="w-4 h-4"></i>
</div>
<div>
<span class="text-[10px] text-slate-400 uppercase font-medium block">E-mail Klantenservice</span>
<a href="mailto:info@jouwdriedee.nl" class="font-semibold text-slate-900 hover:underline">info@jouwdriedee.nl</a>
<span class="block text-[11px] text-slate-400">Reactie doorgaans binnen 24 uur</span>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500 shrink-0 mt-0.5">
<i data-lucide="clock" class="w-4 h-4"></i>
</div>
<div>
<span class="text-[10px] text-slate-400 uppercase font-medium block">Openingstijden Studio</span>
<span class="font-semibold text-slate-900">Maandag t/m Vrijdag: 09:00 - 18:00</span>
<span class="block text-[11px] text-slate-400">Printers draaien 24/7 door</span>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500 shrink-0 mt-0.5">
<i data-lucide="truck" class="w-4 h-4"></i>
</div>
<div>
<span class="text-[10px] text-slate-400 uppercase font-medium block">Verzending &amp; Levering</span>
<span class="font-semibold text-slate-900">PostNL Track &amp; Trace</span>
<span class="block text-[11px] text-slate-400">Veilig en stootvast verpakt</span>
</div>
</div>
</div>
</div>
<!-- Snelle Links -->
<div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Veelgestelde Hulp</h3>
<div class="space-y-2 text-xs">
<a href="{{ route('custom.request') }}" class="p-2.5 rounded-xl bg-slate-50 hover:bg-slate-100 transition flex items-center justify-between group">
<div class="flex items-center space-x-2.5">
<i data-lucide="layers" class="w-4 h-4 text-slate-500 group-hover:text-slate-900"></i>
<span class="font-medium text-slate-700 group-hover:text-slate-900">Eigen STL / 3MF bestand uploaden</span>
</div>
<i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-400"></i>
</a>
<a href="{{ route('catalog') }}" class="p-2.5 rounded-xl bg-slate-50 hover:bg-slate-100 transition flex items-center justify-between group">
<div class="flex items-center space-x-2.5">
<i data-lucide="box" class="w-4 h-4 text-slate-500 group-hover:text-slate-900"></i>
<span class="font-medium text-slate-700 group-hover:text-slate-900">Assortiment &amp; Collecties bekijken</span>
</div>
<i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-400"></i>
</a>
@auth
<a href="{{ route('account.index') }}" class="p-2.5 rounded-xl bg-slate-50 hover:bg-slate-100 transition flex items-center justify-between group">
<div class="flex items-center space-x-2.5">
<i data-lucide="user" class="w-4 h-4 text-slate-500 group-hover:text-slate-900"></i>
<span class="font-medium text-slate-700 group-hover:text-slate-900">Jouw bestelstatus &amp; account</span>
</div>
<i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-400"></i>
</a>
@endauth
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,68 @@
@extends('emails.layouts.master')
@section('title', 'Afschrift van jouw vraag aan Jouwdriedee.nl')
@section('content')
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding-bottom: 20px;">
<span style="display: inline-block; background-color: #ECFDF5; color: #059669; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 9999px; text-transform: uppercase; letter-spacing: 0.5px;">
Afschrift Klantvraag Ontvangen
</span>
<h1 style="margin: 12px 0 6px 0; font-size: 22px; font-weight: 800; color: #0F172A; line-height: 1.3;">
Bedankt voor je vraag, {{ $communication->name }}!
</h1>
<p style="margin: 0; font-size: 14px; color: #64748B; line-height: 1.5;">
We hebben jouw bericht in goede orde ontvangen. Onze 3D-printspecialisten bekijken jouw vraag zorgvuldig en nemen doorgaans <strong>binnen 24 uur</strong> (op werkdagen) contact met je op.
</p>
</td>
</tr>
<!-- Samenvatting Afschrift Card -->
<tr>
<td style="padding: 16px; background-color: #F8FAFC; border-radius: 12px; border: 1px solid #E2E8F0;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B; width: 130px;"><strong>Onderwerp:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-weight: 600;">{{ $communication->subject }}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Referentienummer:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-family: monospace; font-weight: 700;">#{{ $communication->id }}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Datum &amp; tijd:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A;">{{ $communication->created_at->format('d-m-Y om H:i') }}</td>
</tr>
@if($communication->order)
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Betreft bestelling:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-family: monospace;">#{{ $communication->order->order_number }}</td>
</tr>
@endif
</table>
</td>
</tr>
<!-- Jouw verstuurde bericht -->
<tr>
<td style="padding-top: 20px;">
<h3 style="margin: 0 0 10px 0; font-size: 13px; font-weight: 700; color: #64748B; text-transform: uppercase; letter-spacing: 0.5px;">
Kopie van jouw bericht:
</h3>
<div style="background-color: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 8px; padding: 16px; font-size: 13px; line-height: 1.6; color: #334155; white-space: pre-wrap;">{{ $communication->message }}</div>
</td>
</tr>
<!-- Info note -->
<tr>
<td style="padding-top: 24px;">
<p style="margin: 0; font-size: 12px; color: #94A3B8; line-height: 1.5; text-align: center;">
Dit is een automatisch verzonden afschrift vanuit <strong>noreply@jouwdriedee.nl</strong> ter bevestiging van je aanvraag.<br>
Heb je aanvullende informatie? Reageer dan gerust zodra je ons persoonlijke antwoord ontvangt.
</p>
</td>
</tr>
</table>
@endsection
@@ -0,0 +1,82 @@
@extends('emails.layouts.master')
@section('title', 'Nieuwe klantvraag #' . $communication->id)
@section('content')
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding-bottom: 20px;">
<span style="display: inline-block; background-color: #EEF2FF; color: #4F46E5; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 9999px; text-transform: uppercase; letter-spacing: 0.5px;">
Nieuwe Klantvraag #{{ $communication->id }}
</span>
<h1 style="margin: 12px 0 6px 0; font-size: 22px; font-weight: 800; color: #0F172A; line-height: 1.3;">
{{ $communication->subject }}
</h1>
<p style="margin: 0; font-size: 13px; color: #64748B;">
Binnengekomen via het contactformulier op {{ $communication->created_at->format('d-m-Y om H:i') }}
</p>
</td>
</tr>
<!-- Klantgegevens Card -->
<tr>
<td style="padding: 16px; background-color: #F8FAFC; border-radius: 12px; border: 1px solid #E2E8F0; margin-bottom: 20px;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B; width: 120px;"><strong>Klantnaam:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-weight: 600;">{{ $communication->name }}</td>
</tr>
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>E-mailadres:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A;"><a href="mailto:{{ $communication->email }}" style="color: #4F46E5; text-decoration: none;">{{ $communication->email }}</a></td>
</tr>
@if($communication->phone)
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Telefoonnummer:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A;">{{ $communication->phone }}</td>
</tr>
@endif
@if($communication->order)
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Bestelnummer:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #0F172A; font-family: monospace; font-weight: 700;">
#{{ $communication->order->order_number }}
</td>
</tr>
@endif
@if($communication->user)
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Account status:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #059669; font-weight: 600;">Geregistreerde klant (ID: #{{ $communication->user->id }})</td>
</tr>
@else
<tr>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;"><strong>Account status:</strong></td>
<td style="padding: 4px 0; font-size: 13px; color: #64748B;">Gastbezoeker</td>
</tr>
@endif
</table>
</td>
</tr>
<!-- Bericht van de klant -->
<tr>
<td style="padding-top: 20px;">
<h3 style="margin: 0 0 10px 0; font-size: 14px; font-weight: 700; color: #0F172A; text-transform: uppercase; letter-spacing: 0.5px;">
Bericht van de klant:
</h3>
<div style="background-color: #FFFFFF; border: 1px solid #CBD5E1; border-left: 4px solid #4F46E5; border-radius: 8px; padding: 18px; font-size: 14px; line-height: 1.6; color: #1E293B; white-space: pre-wrap;">{{ $communication->message }}</div>
</td>
</tr>
<!-- CTA knop naar Admin -->
<tr>
<td align="center" style="padding-top: 28px; padding-bottom: 10px;">
<a href="{{ route('admin.communications.show', $communication) }}" style="display: inline-block; background-color: #0F172A; color: #FFFFFF; text-decoration: none; padding: 12px 24px; border-radius: 10px; font-size: 13px; font-weight: 600;">
Bekijk &amp; Beantwoord in Admin &rarr;
</a>
</td>
</tr>
</table>
@endsection
@@ -0,0 +1,50 @@
@extends('emails.layouts.master')
@section('title', 'Reactie op vraag #' . $communication->id)
@section('content')
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding-bottom: 20px;">
<span style="display: inline-block; background-color: #EEF2FF; color: #4F46E5; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 9999px; text-transform: uppercase; letter-spacing: 0.5px;">
Antwoord van Jouwdriedee.nl
</span>
<h1 style="margin: 12px 0 6px 0; font-size: 22px; font-weight: 800; color: #0F172A; line-height: 1.3;">
Beste {{ $communication->name }},
</h1>
<p style="margin: 0; font-size: 14px; color: #64748B;">
We hebben een reactie voor je klaarstaan op jouw vraag met betrekking tot <strong>{{ $communication->subject }}</strong>.
</p>
</td>
</tr>
<!-- Reactie box -->
<tr>
<td style="padding: 20px; background-color: #F8FAFC; border-radius: 12px; border: 1px solid #E2E8F0; border-left: 4px solid #059669;">
<h3 style="margin: 0 0 10px 0; font-size: 13px; font-weight: 700; color: #059669; text-transform: uppercase; letter-spacing: 0.5px;">
Ons antwoord:
</h3>
<div style="font-size: 14px; line-height: 1.6; color: #0F172A; white-space: pre-wrap;">{{ $reply->message }}</div>
</td>
</tr>
<!-- Oorspronkelijk bericht -->
<tr>
<td style="padding-top: 24px;">
<h4 style="margin: 0 0 8px 0; font-size: 12px; font-weight: 700; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.5px;">
Jouw eerdere vraag (#{{ $communication->id }}):
</h4>
<div style="background-color: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 8px; padding: 14px; font-size: 12px; line-height: 1.5; color: #64748B; white-space: pre-wrap;">{{ $communication->message }}</div>
</td>
</tr>
<tr>
<td style="padding-top: 24px; text-align: center;">
<p style="margin: 0; font-size: 13px; color: #64748B;">
Heb je nog meer vragen? Je kunt direct antwoorden op deze e-mail of contact opnemen via <a href="mailto:info@jouwdriedee.nl" style="color: #4F46E5; text-decoration: none; font-weight: 600;">info@jouwdriedee.nl</a>.
</p>
</td>
</tr>
</table>
@endsection
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="nl" class="h-full bg-slate-950 text-slate-100 antialiased">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $offlineTitle ?? 'Aan deze webshop wordt hard gewerkt!' }} | Jouwdriedee.nl</title>
<link rel="icon" type="image/svg+xml" href="/images/logo-icon.svg">
<!-- Plus Jakarta Sans -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
body { font-family: 'Plus Jakarta Sans', system-ui, sans-serif; }
</style>
</head>
<body class="min-h-full flex flex-col justify-between bg-slate-950 text-slate-100 p-4 sm:p-6 lg:p-8 selection:bg-rose-500 selection:text-white">
<!-- Top Logo Bar -->
<div class="max-w-4xl mx-auto w-full flex items-center justify-between py-3">
<div class="flex items-center space-x-3">
<img src="/images/logo.svg" alt="Jouwdriedee.nl" class="h-8 sm:h-9 w-auto brightness-200 invert">
</div>
<div class="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs font-medium">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
<span>Onderhoudsmodus</span>
</div>
</div>
<!-- Main Large Red Offline Banner / Card -->
<main class="max-w-2xl mx-auto w-full my-auto py-10">
<div class="rounded-3xl bg-slate-900 border border-rose-500/30 p-8 sm:p-12 text-center relative overflow-hidden shadow-2xl">
<!-- Glow effect -->
<div class="absolute -top-24 -left-24 w-64 h-64 bg-rose-600/10 rounded-full blur-3xl pointer-events-none"></div>
<!-- Alert Badge -->
<div class="inline-flex items-center space-x-2 px-3.5 py-1 rounded-full bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs font-semibold mb-6">
<span class="w-2 h-2 rounded-full bg-rose-500 animate-pulse"></span>
<span>Webshop Tijdelijk Offline</span>
</div>
<!-- Big Main Headline -->
<h1 class="text-2xl sm:text-4xl font-bold text-white tracking-tight leading-tight mb-4">
{{ $offlineTitle ?? 'Aan deze webshop wordt hard gewerkt!' }}
</h1>
<!-- Explanatory message -->
<p class="text-slate-400 text-xs sm:text-sm max-w-lg mx-auto leading-relaxed mb-8">
{{ $offlineMessage ?? 'Onze 3D printers worden momenteel opnieuw gekalibreerd en we bouwen aan nieuwe toffe prints en functies. We zijn heel snel weer online voor bestellingen!' }}
</p>
<!-- 3 Info Columns -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-left pt-6 border-t border-slate-800/80">
<div class="p-4 rounded-xl bg-slate-950/50 border border-slate-800">
<h3 class="text-xs font-semibold text-white mb-1">Printstudio Update</h3>
<p class="text-[11px] text-slate-400 leading-normal">Finetunen van laagdiktes en toevoegen van nieuwe designs.</p>
</div>
<div class="p-4 rounded-xl bg-slate-950/50 border border-slate-800">
<h3 class="text-xs font-semibold text-white mb-1">Lopende Orders</h3>
<p class="text-[11px] text-slate-400 leading-normal">Reeds geplaatste bestellingen worden gewoon geprint en bezorgd.</p>
</div>
<div class="p-4 rounded-xl bg-slate-950/50 border border-slate-800">
<h3 class="text-xs font-semibold text-white mb-1">Vragen of Contact</h3>
<p class="text-[11px] text-slate-400 leading-normal">
Bereikbaar via <a href="mailto:info@jouwdriedee.nl" class="text-slate-200 underline font-medium">info@jouwdriedee.nl</a>.
</p>
</div>
</div>
</div>
</main>
<!-- Footer with Admin Login Shortcut -->
<footer class="max-w-4xl mx-auto w-full py-4 text-center text-xs text-slate-500 flex flex-col sm:flex-row items-center justify-between gap-2 border-t border-slate-900">
<div>
&copy; {{ date('Y') }} Jouwdriedee.nl &bull; Tot snel!
</div>
<div>
<a href="{{ route('login') }}" class="text-slate-400 hover:text-white transition text-xs font-medium flex items-center space-x-1">
<span>Beheerder inloggen</span>
<span>&rarr;</span>
</a>
</div>
</footer>
</body>
</html>
+304 -90
View File
@@ -1,88 +1,248 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="nl" class="h-full bg-slate-900 text-slate-100 antialiased"> <html lang="nl" class="h-full bg-slate-50 text-slate-800 antialiased">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'Admin Dashboard | Jouwdriedee.nl')</title> <title>@yield('title', 'Admin Dashboard | Jouwdriedee.nl')</title>
<link rel="icon" type="image/svg+xml" href="/images/logo-icon.svg"> <link rel="icon" type="image/svg+xml" href="/images/logo-icon.svg">
<!-- Google Fonts: Fredoka & Nunito --> <!-- Modern Typography: Plus Jakarta Sans -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600;700&family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js']) @vite(['resources/css/app.css', 'resources/js/app.js'])
<style> <style>
body { font-family: 'Nunito', system-ui, sans-serif; } body {
h1, h2, h3, h4, .font-heading { font-family: 'Fredoka', ui-rounded, 'Nunito', sans-serif; } font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: -0.01em;
}
h1, h2, h3, h4, .font-heading {
font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: -0.025em;
}
</style> </style>
</head> </head>
<body class="min-h-full flex flex-col bg-slate-950 text-slate-100" x-data="{ mobileNav: false }"> <body class="min-h-full flex flex-col bg-slate-50/70 text-slate-800 selection:bg-slate-900 selection:text-white" x-data="{ mobileNav: false, catalogOpen: false, relationsOpen: false }">
<!-- Admin Header --> <!-- Admin Header (Sleek, Minimalist, SaaS Style) -->
<header class="bg-slate-900/90 backdrop-blur-md border-b border-slate-800 sticky top-0 z-30"> <header class="bg-white/85 backdrop-blur-md border-b border-slate-200/70 sticky top-0 z-30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16"> <div class="flex items-center justify-between h-16 gap-4">
<!-- Brand / Logo --> <!-- Brand / Logo (Always strictly shrink-0 so it never overlaps or wraps) -->
<div class="flex items-center space-x-6"> <div class="flex items-center space-x-3 shrink-0">
<a href="{{ route('admin.content.index') }}" class="flex items-center space-x-3 group"> <a href="{{ route('admin.content.index') }}" class="flex items-center space-x-2 shrink-0 group">
<img src="/images/logo.svg" alt="Jouwdriedee.nl" class="h-8 w-auto brightness-200 invert"> <img src="/images/logo.svg" alt="Jouwdriedee.nl" class="h-6 w-auto shrink-0">
<span class="px-2 py-0.5 rounded-md bg-amber-400 text-slate-950 font-black text-[10px] uppercase tracking-wider">Beheer</span> <span class="px-2 py-0.5 rounded-md bg-slate-100 text-slate-600 font-semibold text-[10px] tracking-wider uppercase shrink-0 whitespace-nowrap">Studio CMS</span>
</a>
</div>
<!-- Desktop Compact Nav Links with Grouped Dropdowns -->
@php
$draftBadgeCount = 0;
try {
if (\Illuminate\Support\Facades\Schema::hasTable('products')) {
$draftBadgeCount = \App\Models\Product::where('status', 'draft')->count();
}
} catch (\Throwable $e) {
$draftBadgeCount = 0;
}
$unreadCommCount = 0;
try {
if (\Illuminate\Support\Facades\Schema::hasTable('communications')) {
$unreadCommCount = \App\Models\Communication::where('status', 'new')->count();
}
} catch (\Throwable $e) {
$unreadCommCount = 0;
}
$isCatalogActive = request()->routeIs(['admin.products.*', 'admin.makerworld.*', 'admin.categories.*']);
$isRelationsActive = request()->routeIs(['admin.communications.*', 'admin.customers.*']);
@endphp
<nav class="hidden lg:flex items-center space-x-1.5 text-xs font-medium">
<!-- 1. Layout & Teksten -->
<a href="{{ route('admin.content.index') }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 whitespace-nowrap {{ request()->routeIs('admin.content.*') ? 'bg-slate-900 text-white font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}">
<i data-lucide="palette" class="w-3.5 h-3.5"></i>
<span>Layout & Teksten</span>
</a> </a>
<!-- Desktop Nav Links --> <!-- 2. Assortiment (Grouped Dropdown) -->
<nav class="hidden md:flex items-center space-x-1 text-xs font-bold"> <div class="relative shrink-0" @click.outside="catalogOpen = false">
<a href="{{ route('admin.content.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.content.*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}"> <button
<i data-lucide="palette" class="w-4 h-4"></i> type="button"
<span>Layout & Teksten</span> @click="catalogOpen = !catalogOpen; relationsOpen = false"
</a> class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer whitespace-nowrap {{ $isCatalogActive ? 'bg-slate-900 text-white font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}"
>
<a href="{{ route('admin.products.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.products.*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}"> <i data-lucide="box" class="w-3.5 h-3.5"></i>
<i data-lucide="box" class="w-4 h-4"></i> <span>Assortiment</span>
<span>Producten</span>
</a>
@php
$draftBadgeCount = \App\Models\Product::where('status', 'draft')->count();
@endphp
<a href="{{ route('admin.makerworld.staging') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}">
<i data-lucide="layers" class="w-4 h-4"></i>
<span>Artikel Beheer</span>
@if($draftBadgeCount > 0) @if($draftBadgeCount > 0)
<span class="ml-1 px-1.5 py-0.2 rounded-full text-[10px] font-black {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-slate-950 text-amber-400' : 'bg-amber-400 text-slate-950' }}"> <span class="px-1.5 py-0.2 rounded-full text-[10px] font-semibold {{ $isCatalogActive ? 'bg-white text-slate-900' : 'bg-slate-200 text-slate-800' }}">
{{ $draftBadgeCount }} {{ $draftBadgeCount }}
</span> </span>
@endif @endif
</a> <i data-lucide="chevron-down" class="w-3 h-3 text-slate-400 transition-transform duration-200" :class="catalogOpen ? 'rotate-180' : ''"></i>
</button>
<a href="{{ route('admin.categories.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.categories.*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}"> <!-- Dropdown Menu -->
<i data-lucide="folder-tree" class="w-4 h-4"></i> <div
<span>Categorieën</span> x-show="catalogOpen"
</a> x-cloak
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute left-0 mt-1.5 w-60 rounded-xl bg-white border border-slate-200/80 shadow-lg py-1.5 z-40 text-xs"
>
<a href="{{ route('admin.products.index') }}" class="px-3 py-2 flex items-center justify-between hover:bg-slate-50 transition {{ request()->routeIs('admin.products.*') ? 'bg-slate-50 text-slate-900 font-semibold' : 'text-slate-700' }}">
<div class="flex items-center space-x-2.5">
<i data-lucide="box" class="w-3.5 h-3.5 text-slate-400"></i>
<div>
<div class="font-medium text-slate-900">Producten</div>
<div class="text-[11px] text-slate-500">Actieve winkelartikelen</div>
</div>
</div>
</a>
<a href="{{ route('admin.orders.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.orders.*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}"> <a href="{{ route('admin.makerworld.staging') }}" class="px-3 py-2 flex items-center justify-between hover:bg-slate-50 transition {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-slate-50 text-slate-900 font-semibold' : 'text-slate-700' }}">
<i data-lucide="shopping-cart" class="w-4 h-4"></i> <div class="flex items-center space-x-2.5">
<span>Bestellingen</span> <i data-lucide="layers" class="w-3.5 h-3.5 text-slate-400"></i>
</a> <div>
<div class="font-medium text-slate-900">Artikel Beheer</div>
<div class="text-[11px] text-slate-500">MakerWorld items beoordelen</div>
</div>
</div>
@if($draftBadgeCount > 0)
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-semibold bg-amber-100 text-amber-800">
{{ $draftBadgeCount }}
</span>
@endif
</a>
<a href="{{ route('admin.makerworld.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.makerworld.index*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}"> <div class="my-1 border-t border-slate-100"></div>
<i data-lucide="download-cloud" class="w-4 h-4"></i>
<span>MakerWorld Sync</span>
</a>
</nav>
</div>
<!-- Right Actions --> <a href="{{ route('admin.categories.index') }}" class="px-3 py-2 flex items-center space-x-2.5 hover:bg-slate-50 transition {{ request()->routeIs('admin.categories.*') ? 'bg-slate-50 text-slate-900 font-semibold' : 'text-slate-700' }}">
<div class="flex items-center space-x-3"> <i data-lucide="folder-tree" class="w-3.5 h-3.5 text-slate-400"></i>
<a href="{{ route('home') }}" target="_blank" class="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold transition flex items-center space-x-1.5 border border-slate-700"> <div>
<span>Naar Webshop</span> <div class="font-medium text-slate-900">Categorieën</div>
<i data-lucide="external-link" class="w-3.5 h-3.5"></i> <div class="text-[11px] text-slate-500">Winkelindeling beheren</div>
</div>
</a>
<a href="{{ route('admin.makerworld.index') }}" class="px-3 py-2 flex items-center space-x-2.5 hover:bg-slate-50 transition {{ request()->routeIs('admin.makerworld.index*') ? 'bg-slate-50 text-slate-900 font-semibold' : 'text-slate-700' }}">
<i data-lucide="download-cloud" class="w-3.5 h-3.5 text-slate-400"></i>
<div>
<div class="font-medium text-slate-900">MakerWorld Sync</div>
<div class="text-[11px] text-slate-500">Collecties & automatische check</div>
</div>
</a>
</div>
</div>
<!-- 3. Bestellingen (Direct Link) -->
<a href="{{ route('admin.orders.index') }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 whitespace-nowrap {{ request()->routeIs('admin.orders.*') ? 'bg-slate-900 text-white font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}">
<i data-lucide="shopping-cart" class="w-3.5 h-3.5"></i>
<span>Bestellingen</span>
</a> </a>
<!-- Mobile trigger --> <!-- 4. Klanten & Communicatie (Grouped Dropdown) -->
<button @click="mobileNav = !mobileNav" type="button" class="md:hidden p-2 rounded-xl bg-slate-800 text-slate-200 hover:bg-slate-700"> <div class="relative shrink-0" @click.outside="relationsOpen = false">
<button
type="button"
@click="relationsOpen = !relationsOpen; catalogOpen = false"
class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 cursor-pointer whitespace-nowrap {{ $isRelationsActive ? 'bg-slate-900 text-white font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100' }}"
>
<i data-lucide="users" class="w-3.5 h-3.5"></i>
<span>Klanten & Communicatie</span>
@if($unreadCommCount > 0)
<span class="px-1.5 py-0.2 rounded-full text-[10px] font-bold {{ $isRelationsActive ? 'bg-white text-slate-900' : 'bg-sky-500 text-white animate-pulse' }}">
{{ $unreadCommCount }}
</span>
@endif
<i data-lucide="chevron-down" class="w-3 h-3 text-slate-400 transition-transform duration-200" :class="relationsOpen ? 'rotate-180' : ''"></i>
</button>
<!-- Dropdown Menu -->
<div
x-show="relationsOpen"
x-cloak
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute left-0 mt-1.5 w-64 rounded-xl bg-white border border-slate-200/80 shadow-lg py-1.5 z-40 text-xs"
>
<a href="{{ route('admin.communications.index') }}" class="px-3 py-2 flex items-center justify-between hover:bg-slate-50 transition {{ request()->routeIs('admin.communications.*') ? 'bg-slate-50 text-slate-900 font-semibold' : 'text-slate-700' }}">
<div class="flex items-center space-x-2.5">
<i data-lucide="message-square" class="w-3.5 h-3.5 text-slate-400"></i>
<div>
<div class="font-medium text-slate-900">Communicatie & Vragen</div>
<div class="text-[11px] text-slate-500">Contactberichten & antwoorden</div>
</div>
</div>
@if($unreadCommCount > 0)
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-sky-500 text-white animate-pulse">
{{ $unreadCommCount }}
</span>
@endif
</a>
<div class="my-1 border-t border-slate-100"></div>
<a href="{{ route('admin.customers.index') }}" class="px-3 py-2 flex items-center space-x-2.5 hover:bg-slate-50 transition {{ request()->routeIs('admin.customers.*') ? 'bg-slate-50 text-slate-900 font-semibold' : 'text-slate-700' }}">
<i data-lucide="users" class="w-3.5 h-3.5 text-slate-400"></i>
<div>
<div class="font-medium text-slate-900">Klanten & Profielen</div>
<div class="text-[11px] text-slate-500">Geregistreerden & gastbestellers</div>
</div>
</a>
</div>
</div>
</nav>
<!-- Right Actions -->
<div class="flex items-center space-x-2 shrink-0">
@php
$isShopOffline = false;
try {
if (\Illuminate\Support\Facades\Schema::hasTable('settings')) {
$isShopOffline = \App\Models\Setting::get('shop_offline', '0') === '1';
}
} catch (\Throwable $e) {
$isShopOffline = false;
}
@endphp
<!-- Webshop Offline / Online Status Switcher (Compact) -->
<form action="{{ route('admin.toggle-offline') }}" method="POST" class="inline">
@csrf
@if($isShopOffline)
<button type="submit" class="px-2.5 py-1.5 rounded-lg bg-rose-500 hover:bg-rose-600 text-white text-xs font-semibold transition flex items-center space-x-1.5 shadow-2xs cursor-pointer" title="Klik om de webshop weer direct online te zetten">
<span class="w-1.5 h-1.5 rounded-full bg-white animate-pulse"></span>
<span>Offline</span>
</button>
@else
<button type="submit" class="px-2.5 py-1.5 rounded-lg bg-slate-50 hover:bg-slate-100 text-slate-700 text-xs font-medium transition flex items-center space-x-1.5 border border-slate-200/80 cursor-pointer" title="Klik om de webshop direct offline te zetten voor bezoekers">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span class="hidden sm:inline">Live</span>
</button>
@endif
</form>
<a href="{{ route('home') }}" target="_blank" class="p-1.5 sm:px-2.5 sm:py-1.5 rounded-lg text-slate-600 hover:text-slate-900 hover:bg-slate-100 text-xs font-medium transition flex items-center space-x-1.5 border border-transparent hover:border-slate-200" title="Bekijk Live Webshop">
<span class="hidden sm:inline">Webshop</span>
<i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
</a>
<!-- Mobile trigger (Shown on screens below lg) -->
<button @click="mobileNav = !mobileNav" type="button" class="lg:hidden p-1.5 rounded-lg text-slate-600 hover:bg-slate-100">
<i data-lucide="menu" class="w-5 h-5"></i> <i data-lucide="menu" class="w-5 h-5"></i>
</button> </button>
</div> </div>
@@ -90,54 +250,109 @@
</div> </div>
</div> </div>
<!-- Mobile Drawer --> <!-- Mobile Drawer (Clean, Grouped Sections) -->
<div x-show="mobileNav" x-cloak class="md:hidden border-t border-slate-800 bg-slate-900 px-4 py-3 space-y-1 text-sm font-bold"> <div x-show="mobileNav" x-cloak class="lg:hidden border-t border-slate-200/80 bg-white px-4 py-3 space-y-3 text-xs font-medium shadow-md">
<a href="{{ route('admin.content.index') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.content.*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}"> <div>
🎨 Layout & Teksten <span class="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-3 block mb-1">Content & Layout</span>
</a> <a href="{{ route('admin.content.index') }}" class="block px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.content.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
<a href="{{ route('admin.products.index') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.products.*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}"> Layout & Teksten
📦 Producten </a>
</a> </div>
<a href="{{ route('admin.makerworld.staging') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}">
⚡ Artikel Beheer (Geïmporteerd) <div>
@if(($draftBadgeCount ?? 0) > 0) <span class="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-3 block mb-1">Assortiment</span>
<span class="ml-2 px-1.5 py-0.5 rounded-full text-xs font-black bg-amber-400 text-slate-950"> <div class="space-y-0.5">
{{ $draftBadgeCount }} nieuw <a href="{{ route('admin.products.index') }}" class="block px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.products.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
</span> Producten
@endif </a>
</a> <a href="{{ route('admin.makerworld.staging') }}" class="flex items-center justify-between px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
<a href="{{ route('admin.categories.index') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.categories.*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}"> <span>Artikel Beheer (Geïmporteerd)</span>
📁 Categorieën @if(($draftBadgeCount ?? 0) > 0)
</a> <span class="px-1.5 py-0.5 rounded-full text-[10px] font-semibold bg-slate-200 text-slate-800">
<a href="{{ route('admin.orders.index') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.orders.*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}"> {{ $draftBadgeCount }}
🛒 Bestellingen </span>
</a> @endif
<a href="{{ route('admin.makerworld.index') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.makerworld.*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}"> </a>
🤖 MakerWorld Importer <a href="{{ route('admin.categories.index') }}" class="block px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.categories.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
</a> Categorieën
</a>
<a href="{{ route('admin.makerworld.index') }}" class="block px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.makerworld.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
MakerWorld Sync
</a>
</div>
</div>
<div>
<span class="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-3 block mb-1">Verkoop & Klanten</span>
<div class="space-y-0.5">
<a href="{{ route('admin.orders.index') }}" class="block px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.orders.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
Bestellingen
</a>
<a href="{{ route('admin.communications.index') }}" class="flex items-center justify-between px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.communications.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>Communicatie</span>
@if(($unreadCommCount ?? 0) > 0)
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-sky-500 text-white">
{{ $unreadCommCount }}
</span>
@endif
</a>
<a href="{{ route('admin.customers.index') }}" class="block px-3 py-1.5 rounded-lg {{ request()->routeIs('admin.customers.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
Klanten &amp; Profielen
</a>
</div>
</div>
</div> </div>
</header> </header>
<!-- Offline Alert Banner -->
@if(!empty($isShopOffline))
<div class="bg-rose-500 text-white px-4 py-2.5 text-xs font-medium border-b border-rose-600 shadow-sm flex flex-col sm:flex-row items-center justify-between gap-3 sticky top-16 z-20">
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-white animate-pulse shrink-0"></span>
<span>De webshop staat momenteel <strong>OFFLINE</strong> voor bezoekers (rode onderhoudsbanner actief). Alleen jij als beheerder kunt de website bezoeken.</span>
</div>
<form action="{{ route('admin.toggle-offline') }}" method="POST" class="shrink-0">
@csrf
<button type="submit" class="px-3 py-1 bg-white hover:bg-slate-50 text-rose-700 font-semibold rounded-md text-xs transition shadow-2xs cursor-pointer flex items-center space-x-1.5">
<span>Zet Nu Weer Online</span>
</button>
</form>
</div>
@endif
<!-- Main Admin Container --> <!-- Main Admin Container -->
<main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8"> <main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-10">
<!-- Flash Alerts --> <!-- Flash Alerts -->
@if(session('success')) @if(session('success'))
<div class="mb-6 p-4 rounded-2xl bg-emerald-950/80 border border-emerald-500/40 text-emerald-200 text-xs sm:text-sm font-bold flex items-center justify-between shadow-lg"> <div class="mb-6 p-4 rounded-xl bg-emerald-50/80 border border-emerald-200/60 text-emerald-800 text-xs font-medium flex items-center justify-between shadow-2xs">
<div class="flex items-center space-x-2.5"> <div class="flex items-center space-x-2.5">
<i data-lucide="check-circle" class="w-5 h-5 text-emerald-400"></i> <i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 shrink-0"></i>
<span>{{ session('success') }}</span> <span>{{ session('success') }}</span>
</div> </div>
<a href="{{ route('home') }}" target="_blank" class="px-3 py-1 rounded-xl bg-emerald-500 text-slate-950 font-black text-xs hover:bg-emerald-400 transition flex items-center space-x-1"> <a href="{{ route('home') }}" target="_blank" class="px-2.5 py-1 rounded-md bg-emerald-600 hover:bg-emerald-700 text-white font-medium text-[11px] transition shrink-0">
<span>Bekijk Live</span> Bekijk Live Shop
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a> </a>
</div> </div>
@endif @endif
@if(session('warning'))
<div class="mb-6 p-4 rounded-xl bg-amber-50/80 border border-amber-200/60 text-amber-800 text-xs font-medium flex items-center space-x-2.5 shadow-2xs">
<i data-lucide="alert-triangle" class="w-4 h-4 text-amber-600 shrink-0"></i>
<span>{{ session('warning') }}</span>
</div>
@endif
@if(session('info'))
<div class="mb-6 p-4 rounded-xl bg-blue-50/80 border border-blue-200/60 text-blue-800 text-xs font-medium flex items-center space-x-2.5 shadow-2xs">
<i data-lucide="info" class="w-4 h-4 text-blue-600 shrink-0"></i>
<span>{{ session('info') }}</span>
</div>
@endif
@if(session('error')) @if(session('error'))
<div class="mb-6 p-4 rounded-2xl bg-rose-950/80 border border-rose-500/40 text-rose-200 text-xs sm:text-sm font-bold flex items-center space-x-2.5 shadow-lg"> <div class="mb-6 p-4 rounded-xl bg-rose-50/80 border border-rose-200/60 text-rose-800 text-xs font-medium flex items-center space-x-2.5 shadow-2xs">
<i data-lucide="alert-circle" class="w-5 h-5 text-rose-400"></i> <i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 shrink-0"></i>
<span>{{ session('error') }}</span> <span>{{ session('error') }}</span>
</div> </div>
@endif @endif
@@ -145,10 +360,9 @@
@yield('content') @yield('content')
</main> </main>
<footer class="border-t border-slate-800/80 py-4 text-center text-xs text-slate-500"> <footer class="border-t border-slate-200/60 py-4 text-center text-[11px] text-slate-400 bg-white">
Jouwdriedee.nl Beheerpaneel &bull; Direct gekoppeld aan live webshop Jouwdriedee.nl Beheerpaneel &bull; Direct gekoppeld aan live webshop
</footer> </footer>
</body> </body>
</html> </html>
+142 -112
View File
@@ -7,42 +7,65 @@
<meta name="description" content="@yield('meta_description', setting('meta_description', 'Jouwdriedee.nl - Kleurrijke en gedetailleerde 3D prints, soepele fidget toys, Pokémon planters en toffe collectibles. Met zorg en precisie geprint in Nederland!'))"> <meta name="description" content="@yield('meta_description', setting('meta_description', 'Jouwdriedee.nl - Kleurrijke en gedetailleerde 3D prints, soepele fidget toys, Pokémon planters en toffe collectibles. Met zorg en precisie geprint in Nederland!'))">
<link rel="icon" type="image/svg+xml" href="/images/logo-icon.svg"> <link rel="icon" type="image/svg+xml" href="/images/logo-icon.svg">
<!-- Google Fonts: Fredoka & Nunito --> <!-- Modern Typography: Plus Jakarta Sans -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600;700&family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js']) @vite(['resources/css/app.css', 'resources/js/app.js'])
<style> <style>
body { body {
font-family: 'Nunito', system-ui, sans-serif; font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: -0.01em;
} }
h1, h2, h3, h4, .font-heading { h1, h2, h3, h4, .font-heading {
font-family: 'Fredoka', ui-rounded, 'Nunito', sans-serif; font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: -0.025em;
} }
</style> </style>
</head> </head>
<body class="min-h-full flex flex-col bg-[#FBFBFA] text-slate-800 selection:bg-amber-300 selection:text-slate-900" x-data="{ mobileMenuOpen: false }"> <body class="min-h-full flex flex-col bg-white text-slate-800 selection:bg-sky-500 selection:text-white" x-data="{ mobileMenuOpen: false }">
<!-- Top Announcement Bar (Inschakelbaar en bewerkbaar via Admin) --> <!-- Admin Offline Preview Notice -->
@if(\App\Models\Setting::get('shop_offline', '0') === '1' && auth()->check() && auth()->user()->isAdmin())
<div class="bg-rose-500 text-white px-4 py-2 text-xs font-semibold flex flex-col sm:flex-row items-center justify-between sticky top-0 z-50 shadow-sm gap-2">
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-white animate-pulse"></span>
<span>⚠️ OFFLINE BEHEERDERS-PREVIEW: Webshop staat momenteel OFFLINE voor bezoekers. Alleen jij als beheerder ziet deze pagina.</span>
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('admin.content.index') }}" class="px-2.5 py-1 rounded-md bg-rose-600 hover:bg-rose-700 text-white text-[11px] font-medium transition">
Naar Beheer
</a>
<form action="{{ route('admin.toggle-offline') }}" method="POST" class="inline">
@csrf
<button type="submit" class="px-3 py-1 rounded-md bg-white text-rose-600 hover:bg-rose-50 text-[11px] font-semibold shadow-xs transition cursor-pointer">
Zet Nu Weer Online
</button>
</form>
</div>
</div>
@endif
<!-- Sleek Top Announcement Bar -->
@if(setting('announcement_enabled', '1') == '1') @if(setting('announcement_enabled', '1') == '1')
<div class="bg-slate-900 text-white text-xs py-2 px-3 sm:px-4 font-bold border-b border-slate-800"> <div class="bg-slate-950 text-slate-400 text-[11px] py-2 px-4 font-normal border-b border-slate-900">
<div class="max-w-7xl mx-auto flex items-center justify-between"> <div class="max-w-7xl mx-auto flex items-center justify-between">
<div class="flex items-center space-x-2 mx-auto sm:mx-0 text-center sm:text-left text-slate-300"> <div class="flex items-center space-x-2 mx-auto sm:mx-0">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
<span>{{ setting('announcement_text', 'Nederlandse 3D Printstudio • Haarscherpe afwerking op Bambu Lab printers') }}</span> <span class="text-slate-300 font-medium">{{ setting('announcement_text', 'Nederlandse 3D Printstudio • Haarscherpe afwerking op Bambu Lab printers') }}</span>
</div> </div>
<div class="hidden md:flex items-center space-x-5 text-[11px] text-slate-300"> <div class="hidden md:flex items-center space-x-5 text-slate-400">
@if(setting('announcement_badge_1')) @if(setting('announcement_badge_1'))
<span class="flex items-center space-x-1"> <span class="flex items-center space-x-1.5">
<i data-lucide="truck" class="w-3.5 h-3.5 text-amber-400"></i> <i data-lucide="truck" class="w-3.5 h-3.5 text-sky-400"></i>
<span>{{ setting('announcement_badge_1', 'Gratis verzending vanaf € 50,-') }}</span> <span>{{ setting('announcement_badge_1', 'Gratis verzending vanaf € 50,-') }}</span>
</span> </span>
@endif @endif
@if(setting('announcement_badge_2')) @if(setting('announcement_badge_2'))
<span class="flex items-center space-x-1"> <span class="flex items-center space-x-1.5">
<i data-lucide="clock" class="w-3.5 h-3.5 text-sky-400"></i> <i data-lucide="clock" class="w-3.5 h-3.5 text-slate-400"></i>
<span>{{ setting('announcement_badge_2', 'Verzending binnen 2-3 werkdagen') }}</span> <span>{{ setting('announcement_badge_2', 'Verzending binnen 2-3 werkdagen') }}</span>
</span> </span>
@endif @endif
@@ -51,62 +74,65 @@
</div> </div>
@endif @endif
<!-- Main Navigation Header --> <!-- Modern Glass Navigation Bar -->
<header class="sticky top-0 z-40 bg-white/95 backdrop-blur-md border-b border-slate-200/80 shadow-xs"> <header class="sticky top-0 z-40 bg-white/80 backdrop-blur-md border-b border-slate-200/60 transition-all">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16 sm:h-20"> <div class="flex items-center justify-between h-16">
<!-- Catchy Brand Logo --> <!-- Brand Logo -->
<div class="flex items-center space-x-4 sm:space-x-8"> <div class="flex items-center space-x-8">
<a href="{{ route('home') }}" class="flex items-center group py-1"> <a href="{{ route('home') }}" class="flex items-center group py-1">
<img src="/images/logo.svg" alt="{{ setting('site_name', 'Jouwdriedee.nl') }}" class="h-9 sm:h-11 w-auto transition-transform duration-200 group-hover:scale-105"> <img src="/images/logo.svg" alt="{{ setting('site_name', 'Jouwdriedee.nl') }}" class="h-8 sm:h-9 w-auto transition-transform duration-200 group-hover:opacity-90">
</a> </a>
<!-- Desktop Nav Links --> <!-- Minimalist Navigation Links -->
<nav class="hidden lg:flex items-center space-x-1.5 text-sm font-bold text-slate-700"> <nav class="hidden lg:flex items-center space-x-1 text-xs font-medium text-slate-600">
<a href="{{ route('home') }}" class="px-3.5 py-2 rounded-xl transition {{ request()->routeIs('home') ? 'bg-amber-100 text-amber-950 font-black' : 'hover:bg-slate-100 text-slate-700' }}"> <a href="{{ route('home') }}" class="px-3 py-1.5 rounded-lg transition {{ request()->routeIs('home') ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
Home Home
</a> </a>
<a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="px-3.5 py-2 rounded-xl transition {{ request('category') === 'fidget-toys' ? 'bg-violet-100 text-violet-900 font-black' : 'hover:bg-violet-50 hover:text-violet-700 text-slate-700' }}"> <a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="px-3 py-1.5 rounded-lg transition {{ request('category') === 'fidget-toys' ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
Fidget Toys Fidget Toys
</a> </a>
<a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="px-3.5 py-2 rounded-xl transition {{ request('category') === 'pokemon-3d' ? 'bg-amber-100 text-amber-900 font-black' : 'hover:bg-amber-50 hover:text-amber-700 text-slate-700' }}"> <a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="px-3 py-1.5 rounded-lg transition {{ request('category') === 'pokemon-3d' ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
Pokémon Pokémon
</a> </a>
<a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="px-3.5 py-2 rounded-xl transition {{ request('category') === 'film-popculture' ? 'bg-rose-100 text-rose-900 font-black' : 'hover:bg-rose-50 hover:text-rose-700 text-slate-700' }}"> <a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="px-3 py-1.5 rounded-lg transition {{ request('category') === 'film-popculture' ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
Film & Gaming Film & Gaming
</a> </a>
<a href="{{ route('catalog') }}" class="px-3.5 py-2 rounded-xl transition {{ request()->routeIs('catalog') && !request('category') ? 'bg-slate-200 text-slate-900 font-black' : 'hover:bg-slate-100 text-slate-700' }}"> <a href="{{ route('catalog') }}" class="px-3 py-1.5 rounded-lg transition {{ request()->routeIs('catalog') && !request('category') ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
Alle Prints Alle Prints
</a> </a>
<a href="{{ route('contact.index') }}" class="px-3 py-1.5 rounded-lg transition {{ request()->routeIs('contact.*') ? 'bg-slate-100 text-slate-900 font-semibold' : 'hover:text-slate-900 hover:bg-slate-50' }}">
Vraag stellen
</a>
</nav> </nav>
</div> </div>
<!-- Right Action Bar --> <!-- Right Action Bar -->
<div class="flex items-center space-x-2 sm:space-x-3"> <div class="flex items-center space-x-3">
<!-- Search Bar in Header --> <!-- Clean Search Bar -->
<form action="{{ route('catalog') }}" method="GET" class="hidden md:flex items-center relative"> <form action="{{ route('catalog') }}" method="GET" class="hidden md:flex items-center relative">
<input <input
type="text" type="text"
name="q" name="q"
value="{{ request('q') }}" value="{{ request('q') }}"
placeholder="Zoek print, model..." placeholder="Zoek model..."
class="w-44 lg:w-56 bg-slate-100/90 border border-slate-200/80 rounded-xl pl-8 pr-3 py-2 text-xs font-semibold text-slate-800 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-rose-400 transition" class="w-48 lg:w-60 bg-slate-50/80 border border-slate-200/70 rounded-lg pl-8 pr-3 py-1.5 text-xs text-slate-800 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-slate-900/10 focus:border-slate-400 transition"
> >
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 pointer-events-none"></i> <i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 pointer-events-none"></i>
</form> </form>
<!-- Cart Button with Counter --> <!-- Cart Button with Pill Badge -->
@php @php
$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-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"> <a href="{{ route('cart.index') }}" class="px-3.5 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs flex items-center space-x-1.5 transition shadow-xs cursor-pointer">
<i data-lucide="shopping-bag" class="w-4 h-4 text-slate-900"></i> <i data-lucide="shopping-bag" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">Winkelwagen</span> <span class="hidden sm:inline">Winkelmand</span>
@if($cartCount > 0) @if($cartCount > 0)
<span class="inline-flex items-center justify-center px-1.5 py-0.5 text-[10px] font-black text-white bg-rose-500 rounded-full"> <span class="ml-1 inline-flex items-center justify-center px-1.5 py-0.2 rounded-full text-[10px] font-bold bg-white text-slate-900">
{{ $cartCount }} {{ $cartCount }}
</span> </span>
@endif @endif
@@ -114,34 +140,36 @@
<!-- User Account / Login Button --> <!-- User Account / Login Button -->
@guest @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"> <a href="{{ route('login') }}" class="px-3 py-1.5 rounded-lg text-slate-600 hover:text-slate-900 hover:bg-slate-50 font-medium text-xs flex items-center space-x-1.5 transition">
<i data-lucide="user" class="w-4 h-4 text-slate-700"></i> <i data-lucide="user" class="w-3.5 h-3.5 text-slate-500"></i>
<span class="hidden sm:inline">Inloggen</span> <span class="hidden sm:inline">Inloggen</span>
</a> </a>
@else @else
<div class="relative" x-data="{ userMenu: false }"> <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"> <button @click="userMenu = !userMenu" type="button" class="px-2.5 py-1.5 rounded-lg text-slate-700 hover:bg-slate-100 font-medium 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="w-5 h-5 rounded-full bg-slate-100 text-slate-700 flex items-center justify-center text-[10px] font-bold border border-slate-200">
{{ strtoupper(substr(auth()->user()->name, 0, 1)) }}
</span>
<span class="hidden sm:inline">{{ Str::limit(auth()->user()->name, 12) }}</span> <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> <i data-lucide="chevron-down" class="w-3 h-3 text-slate-400"></i>
</button> </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"> <div x-show="userMenu" @click.away="userMenu = false" x-cloak class="absolute right-0 mt-2 w-48 rounded-xl bg-white border border-slate-200/80 shadow-lg py-1.5 z-50 text-xs font-medium 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"> <a href="{{ route('account.index') }}" class="flex items-center space-x-2 px-3.5 py-2 hover:bg-slate-50 text-slate-700">
<i data-lucide="user-circle" class="w-4 h-4 text-slate-500"></i> <i data-lucide="user" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Mijn Account</span> <span>Mijn Account</span>
</a> </a>
@if(auth()->user()->isAdmin()) @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"> <a href="{{ route('admin.content.index') }}" class="flex items-center space-x-2 px-3.5 py-2 hover:bg-sky-50 text-sky-700 font-semibold">
<i data-lucide="shield" class="w-4 h-4 text-amber-500"></i> <i data-lucide="shield" class="w-3.5 h-3.5 text-sky-600"></i>
<span>Admin Beheer</span> <span>Admin Beheer</span>
</a> </a>
@endif @endif
<div class="border-t border-slate-100 my-1"></div> <div class="border-t border-slate-100 my-1"></div>
<form action="{{ route('logout') }}" method="POST"> <form action="{{ route('logout') }}" method="POST">
@csrf @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"> <button type="submit" class="w-full text-left flex items-center space-x-2 px-3.5 py-2 hover:bg-rose-50 text-rose-600 cursor-pointer">
<i data-lucide="log-out" class="w-4 h-4"></i> <i data-lucide="log-out" class="w-3.5 h-3.5"></i>
<span>Uitloggen</span> <span>Uitloggen</span>
</button> </button>
</form> </form>
@@ -150,7 +178,7 @@
@endguest @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-1.5 rounded-lg text-slate-600 hover:bg-slate-100 transition">
<i data-lucide="menu" class="w-5 h-5"></i> <i data-lucide="menu" class="w-5 h-5"></i>
</button> </button>
</div> </div>
@@ -158,65 +186,61 @@
</div> </div>
<!-- Mobile Menu Drawer --> <!-- Mobile Menu Drawer -->
<div x-show="mobileMenuOpen" x-cloak class="lg:hidden border-t border-slate-200 bg-white px-4 pt-3 pb-6 space-y-3 shadow-lg"> <div x-show="mobileMenuOpen" x-cloak class="lg:hidden border-t border-slate-200/80 bg-white px-4 pt-3 pb-5 space-y-3 shadow-lg">
<form action="{{ route('catalog') }}" method="GET" class="flex items-center relative mb-2"> <form action="{{ route('catalog') }}" method="GET" class="flex items-center relative mb-2">
<input <input
type="text" type="text"
name="q" name="q"
value="{{ request('q') }}" value="{{ request('q') }}"
placeholder="Zoek print, model..." placeholder="Zoek model..."
class="w-full bg-slate-100 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs font-semibold placeholder-slate-400" class="w-full bg-slate-50 border border-slate-200 rounded-lg pl-9 pr-3.5 py-2 text-xs font-normal placeholder-slate-400"
> >
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3"></i> <i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3"></i>
</form> </form>
<div class="flex flex-col space-y-1.5 font-bold text-sm"> <div class="flex flex-col space-y-1 font-medium text-xs">
<a href="{{ route('home') }}" class="px-3.5 py-2.5 rounded-xl hover:bg-slate-50 flex items-center justify-between {{ request()->routeIs('home') ? 'bg-amber-50 text-amber-950 font-black' : '' }}"> <a href="{{ route('home') }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between {{ request()->routeIs('home') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600' }}">
<span>Home</span> <span>Home</span>
</a> </a>
<a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="px-3.5 py-2.5 rounded-xl hover:bg-violet-50 flex items-center justify-between text-violet-700"> <a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-600">
<span>Fidget Toys & Sensory</span> <span>Fidget Toys & Sensory</span>
<span class="text-xs bg-violet-100 px-2 py-0.5 rounded-md font-bold">Bestsellers</span>
</a> </a>
<a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="px-3.5 py-2.5 rounded-xl hover:bg-amber-50 flex items-center justify-between text-amber-700"> <a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-600">
<span>Pokémon 3D Prints</span> <span>Pokémon 3D Prints</span>
<span class="text-xs bg-amber-100 px-2 py-0.5 rounded-md font-bold">Populair</span>
</a> </a>
<a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="px-3.5 py-2.5 rounded-xl hover:bg-rose-50 flex items-center justify-between text-rose-700"> <a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-600">
<span>Film, Gaming & Collectibles</span> <span>Film, Gaming & Collectibles</span>
</a> </a>
<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 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-600">
<span>Alle Prints Bekijken</span> <span>Alle Prints Bekijken</span>
</a> </a>
<a href="{{ route('contact.index') }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-900 font-medium">
<span>Vraag stellen / Contact</span>
</a>
<div class="border-t border-slate-100 pt-2 mt-1"> <div class="border-t border-slate-100 pt-3 mt-2">
@guest @guest
<div class="grid grid-cols-2 gap-2"> <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"> <a href="{{ route('login') }}" class="py-2 px-3 rounded-lg bg-slate-100 text-slate-800 text-center font-medium 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> <span>Inloggen</span>
</a> </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"> <a href="{{ route('register') }}" class="py-2 px-3 rounded-lg bg-slate-900 text-white text-center font-medium 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> <span>Registreren</span>
</a> </a>
</div> </div>
@else @else
<div class="space-y-1"> <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"> <a href="{{ route('account.index') }}" class="px-3 py-2 rounded-lg hover:bg-slate-50 flex items-center justify-between text-slate-800">
<span>Mijn Account ({{ auth()->user()->name }})</span> <span>Mijn Account ({{ auth()->user()->name }})</span>
<i data-lucide="user" class="w-4 h-4 text-amber-500"></i>
</a> </a>
@if(auth()->user()->isAdmin()) @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"> <a href="{{ route('admin.content.index') }}" class="px-3 py-2 rounded-lg bg-sky-50 text-sky-800 font-semibold flex items-center justify-between">
<span>Admin Beheer</span> <span>Admin Beheer</span>
<i data-lucide="shield" class="w-4 h-4 text-amber-600"></i>
</a> </a>
@endif @endif
<form action="{{ route('logout') }}" method="POST"> <form action="{{ route('logout') }}" method="POST">
@csrf @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"> <button type="submit" class="w-full text-left px-3 py-2 rounded-lg hover:bg-rose-50 text-rose-600 font-medium flex items-center justify-between">
<span>Uitloggen</span> <span>Uitloggen</span>
<i data-lucide="log-out" class="w-4 h-4"></i>
</button> </button>
</form> </form>
</div> </div>
@@ -229,19 +253,19 @@
<!-- Global Flash Alerts --> <!-- Global Flash Alerts -->
@if(session('success')) @if(session('success'))
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<div class="p-3.5 sm:p-4 rounded-2xl bg-emerald-50 border border-emerald-200 text-emerald-900 text-xs sm:text-sm font-bold flex items-center justify-between shadow-xs"> <div class="p-3.5 rounded-xl bg-emerald-50/80 border border-emerald-200/60 text-emerald-800 text-xs font-medium flex items-center justify-between shadow-xs">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600"></i> <i data-lucide="check-circle" class="w-4 h-4 text-emerald-600"></i>
<span>{{ session('success') }}</span> <span>{{ session('success') }}</span>
</div> </div>
<a href="{{ route('cart.index') }}" class="px-3 py-1 bg-white border border-emerald-300 rounded-xl text-xs font-black text-emerald-800 hover:bg-emerald-100 transition">Winkelwagen &rarr;</a> <a href="{{ route('cart.index') }}" class="text-xs font-semibold text-emerald-700 hover:underline">Winkelwagen &rarr;</a>
</div> </div>
</div> </div>
@endif @endif
@if(session('error')) @if(session('error'))
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<div class="p-3.5 sm:p-4 rounded-2xl bg-rose-50 border border-rose-200 text-rose-900 text-xs sm:text-sm font-bold flex items-center space-x-2 shadow-xs"> <div class="p-3.5 rounded-xl bg-rose-50/80 border border-rose-200/60 text-rose-800 text-xs font-medium flex items-center space-x-2 shadow-xs">
<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600"></i> <i data-lucide="alert-circle" class="w-4 h-4 text-rose-600"></i>
<span>{{ session('error') }}</span> <span>{{ session('error') }}</span>
</div> </div>
@@ -253,81 +277,87 @@
@yield('content') @yield('content')
</main> </main>
<!-- Footer (Licht, Helder, Aanpasbaar via Admin) --> <!-- Fresh, Minimalist, Modern Footer -->
<footer class="bg-slate-900 text-white border-t border-slate-800 pt-12 pb-10 mt-16"> <footer class="border-t border-slate-200/60 bg-slate-50/50 pt-14 pb-12 mt-20 text-slate-600 text-xs">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 pb-10 border-b border-slate-800"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-10 pb-12 border-b border-slate-200/60">
<!-- Col 1: Brand & Info --> <!-- Col 1: Brand & Info -->
<div class="space-y-3 sm:col-span-1"> <div class="space-y-3 sm:col-span-1">
<div class="flex items-center space-x-2.5"> <a href="{{ route('home') }}" class="inline-block">
<img src="/images/logo.svg" alt="{{ setting('site_name', 'Jouwdriedee.nl') }}" class="h-8 w-auto brightness-200 invert"> <img src="/images/logo.svg" alt="{{ setting('site_name', 'Jouwdriedee.nl') }}" class="h-7 w-auto">
</div> </a>
<p class="text-xs text-slate-300 leading-relaxed font-medium"> <p class="text-xs text-slate-500 leading-relaxed font-normal">
{{ setting('footer_description', 'Gespecialiseerd in het 3D printen van gedetailleerde modellen, soepel bewegende fidget toys en iconische popculture figuren. Gemaakt met hoogwaardige filamenten op hypermoderne FDM-printers.') }} {{ setting('footer_description', 'Gespecialiseerd in het 3D printen van gedetailleerde modellen, soepel bewegende fidget toys en iconische popculture figuren. Gemaakt met hoogwaardige filamenten op hypermoderne FDM-printers.') }}
</p> </p>
<div class="inline-flex items-center space-x-1.5 text-[11px] font-bold text-emerald-400 bg-emerald-950/60 px-2.5 py-1 rounded-full border border-emerald-500/30"> <div class="inline-flex items-center space-x-1.5 text-[11px] font-medium text-emerald-700 bg-emerald-50 px-2.5 py-1 rounded-full border border-emerald-200/50">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>{{ setting('footer_status_text', 'Printers gekalibreerd & actief') }}</span> <span>{{ setting('footer_status_text', 'Printers gekalibreerd & actief') }}</span>
</div> </div>
</div> </div>
<!-- Col 2: Categorieën --> <!-- Col 2: Categorieën -->
<div class="space-y-2"> <div class="space-y-2.5">
<h3 class="font-heading text-xs font-black uppercase tracking-wider text-amber-300">Collectie</h3> <h3 class="text-xs font-semibold text-slate-900 tracking-wider uppercase text-[11px]">Collecties</h3>
<ul class="space-y-1.5 text-xs text-slate-300 font-semibold"> <ul class="space-y-2 text-xs text-slate-500 font-normal">
<li><a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="hover:text-white transition">Fidget Toys & Slugs</a></li> <li><a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="hover:text-slate-900 transition">Fidget Toys & Slugs</a></li>
<li><a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="hover:text-white transition">Pokémon 3D Prints</a></li> <li><a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="hover:text-slate-900 transition">Pokémon 3D Prints</a></li>
<li><a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="hover:text-white transition">Film, Gaming & Collectibles</a></li> <li><a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="hover:text-slate-900 transition">Film, Gaming & Props</a></li>
<li><a href="{{ route('catalog') }}" class="hover:text-white transition">Alle Prints Bekijken</a></li> <li><a href="{{ route('catalog') }}" class="hover:text-slate-900 transition">Alle Prints Bekijken</a></li>
</ul> </ul>
</div> </div>
<!-- Col 3: Materialen & Kwaliteit --> <!-- Col 3: Materialen & Kwaliteit -->
<div class="space-y-2"> <div class="space-y-2.5">
<h3 class="font-heading text-xs font-black uppercase tracking-wider text-sky-300">Materialen & Kwaliteit</h3> <h3 class="text-xs font-semibold text-slate-900 tracking-wider uppercase text-[11px]">Precisie & Materialen</h3>
<ul class="space-y-1.5 text-xs text-slate-300 font-medium"> <ul class="space-y-2 text-xs text-slate-500 font-normal">
<li class="flex items-center space-x-1.5"> <li class="flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-400"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
<span><strong>Bio-PLA+</strong>: Biologisch & geurloos</span> <span><strong>Bio-PLA+</strong>: Biologisch & reukloos</span>
</li> </li>
<li class="flex items-center space-x-1.5"> <li class="flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-sky-400"></span> <span class="w-1.5 h-1.5 rounded-full bg-sky-400"></span>
<span><strong>PETG Tough</strong>: Slagvast & stevig</span> <span><strong>PETG Tough</strong>: Slagvast & hittebestendig</span>
</li> </li>
<li class="flex items-center space-x-1.5"> <li class="flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-amber-400"></span> <span class="w-1.5 h-1.5 rounded-full bg-indigo-400"></span>
<span><strong>TPU 95A</strong>: Flexibel rubber</span> <span><strong>TPU 95A</strong>: Flexibel rubber</span>
</li> </li>
<li class="flex items-center space-x-1.5"> <li class="flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-purple-400"></span> <span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
<span><strong>Silk Glans</strong>: Prachtige glans</span> <span><strong>Silk Glans</strong>: Luxe zijdeglans</span>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Col 4: Contact --> <!-- Col 4: Contact -->
<div class="space-y-2"> <div class="space-y-2.5">
<h3 class="font-heading text-xs font-black uppercase tracking-wider text-rose-300">Bestelling & Verzending</h3> <h3 class="text-xs font-semibold text-slate-900 tracking-wider uppercase text-[11px]">Service & Contact</h3>
<p class="text-xs text-slate-300 font-medium">Heb je een vraag over een print of levertijd van jouw bestelling?</p> <p class="text-xs text-slate-500 leading-relaxed font-normal">Vragen over custom printopdrachten of bestellingen?</p>
<div class="p-3 rounded-2xl bg-slate-800/80 border border-slate-700 text-xs space-y-1 font-bold"> <div class="p-3 rounded-xl bg-white border border-slate-200/60 text-xs space-y-1.5 font-medium shadow-2xs">
<div class="flex items-center space-x-1.5 text-slate-200"> <div class="flex items-center space-x-2 text-slate-700">
<i data-lucide="mail" class="w-3.5 h-3.5 text-amber-400"></i> <i data-lucide="mail" class="w-3.5 h-3.5 text-slate-400"></i>
<span>{{ setting('footer_email', 'info@jouwdriedee.nl') }}</span> <span>{{ setting('footer_email', 'info@jouwdriedee.nl') }}</span>
</div> </div>
<div class="flex items-center space-x-1.5 text-slate-200"> <div class="flex items-center space-x-2 text-slate-700">
<i data-lucide="truck" class="w-3.5 h-3.5 text-amber-400"></i> <i data-lucide="truck" class="w-3.5 h-3.5 text-slate-400"></i>
<span>{{ setting('footer_shipping_text', 'Snelle verzending via PostNL') }}</span> <span>{{ setting('footer_shipping_text', 'Snelle verzending via PostNL') }}</span>
</div> </div>
<div class="pt-1">
<a href="{{ route('contact.index') }}" class="inline-flex items-center space-x-1 text-slate-900 font-semibold hover:underline">
<span>Direct vraag stellen</span>
<span>&rarr;</span>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="pt-6 flex flex-col sm:flex-row items-center justify-between text-xs text-slate-400 font-bold gap-3 text-center sm:text-left"> <div class="pt-8 flex flex-col sm:flex-row items-center justify-between text-xs text-slate-400 font-normal gap-3 text-center sm:text-left">
<p>&copy; {{ date('Y') }} {{ setting('site_name', 'Jouwdriedee.nl') }}. {{ setting('footer_copyright', 'Alle 3D prints worden op bestelling vervaardigd.') }}</p> <p>&copy; {{ date('Y') }} {{ setting('site_name', 'Jouwdriedee.nl') }}. {{ setting('footer_copyright', 'Alle 3D prints worden op bestelling vervaardigd.') }}</p>
<div class="flex items-center space-x-3"> <div class="text-slate-400 text-[11px]">
<span>{{ setting('footer_payment_text', 'iDEAL & Bancontact • PostNL Track & Trace') }}</span> {{ setting('footer_payment_text', 'iDEAL & Bancontact • PostNL Track & Trace') }}
</div> </div>
</div> </div>
</div> </div>
+61 -60
View File
@@ -3,46 +3,45 @@
@section('title', '3D Print Catalogus | Jouwdriedee.nl') @section('title', '3D Print Catalogus | Jouwdriedee.nl')
@section('content') @section('content')
<!-- Page Header --> <!-- Page Header (Fris & Minimalistisch) -->
<div class="bg-slate-50 border-b border-slate-200/80 py-6 sm:py-8"> <div class="border-b border-slate-100 bg-slate-50/50 py-8 sm:py-10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<div class="inline-flex items-center space-x-1.5 px-3 py-1 rounded-full bg-white border border-slate-200 text-xs font-bold text-slate-700 shadow-2xs mb-2"> <span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Assortiment</span>
<span>Overzicht 3D Modellen</span> <h1 class="font-heading text-2xl sm:text-4xl font-extrabold text-slate-900 tracking-tight mt-0.5">3D Print Collectie</h1>
</div> <p class="text-xs sm:text-sm text-slate-500 mt-1">Fidget toys, Pokémon figuren en popculture modellen. Geprint op aanvraag in jouw gewenste kleur.</p>
<h1 class="font-heading text-2xl sm:text-4xl font-black text-slate-900 tracking-tight">Onze 3D Print Collectie</h1>
<p class="text-xs text-slate-500 mt-1">Fidget toys, Pokémon figuren en popculture modellen. Geprint op aanvraag in jouw gewenste kleur.</p>
</div> </div>
@if(request()->hasAny(['category', 'material', 'q', 'sort'])) @if(request()->hasAny(['category', 'material', 'q', 'sort']))
<a href="{{ route('catalog') }}" class="playful-btn px-3 py-1.5 rounded-xl bg-white border border-slate-200 text-xs font-bold text-rose-600 hover:bg-rose-50 flex items-center space-x-1 self-start shadow-2xs"> <a href="{{ route('catalog') }}" class="px-3 py-1.5 rounded-lg bg-white border border-slate-200 text-xs font-medium text-rose-600 hover:bg-rose-50 flex items-center space-x-1.5 self-start shadow-2xs transition">
<span>✕ Filters Wissen</span> <i data-lucide="x" class="w-3.5 h-3.5"></i>
<span>Filters wissen</span>
</a> </a>
@endif @endif
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-10" x-data="{ mobileFiltersOpen: false }"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12" x-data="{ mobileFiltersOpen: false }">
<!-- Mobile Filter Toggle Bar (Zodat mobiele gebruikers niet 600px omlaag hoeven te scrollen!) --> <!-- Mobile Filter Toggle Bar -->
<div class="lg:hidden mb-4 flex items-center justify-between gap-2"> <div class="lg:hidden mb-6 flex items-center justify-between gap-2">
<button <button
type="button" type="button"
@click="mobileFiltersOpen = !mobileFiltersOpen" @click="mobileFiltersOpen = !mobileFiltersOpen"
class="flex-1 py-2.5 px-4 rounded-xl bg-white border border-slate-200 text-slate-800 text-xs font-bold flex items-center justify-center space-x-2 shadow-2xs" class="flex-1 py-2 px-3.5 rounded-lg bg-white border border-slate-200 text-slate-800 text-xs font-medium flex items-center justify-center space-x-2 shadow-2xs"
> >
<i data-lucide="sliders-horizontal" class="w-4 h-4 text-rose-500"></i> <i data-lucide="sliders-horizontal" class="w-3.5 h-3.5 text-slate-500"></i>
<span x-text="mobileFiltersOpen ? 'Verberg Filters' : 'Filters & Categorieën'"></span> <span x-text="mobileFiltersOpen ? 'Verberg filters' : 'Filters & Categorieën'"></span>
@if(request()->hasAny(['category', 'material', 'q'])) @if(request()->hasAny(['category', 'material', 'q']))
<span class="w-2 h-2 rounded-full bg-rose-500"></span> <span class="w-1.5 h-1.5 rounded-full bg-sky-500"></span>
@endif @endif
</button> </button>
<!-- Snelle Sortering op Mobiel --> <!-- Quick Sort on Mobile -->
<form action="{{ route('catalog') }}" method="GET" class="w-40"> <form action="{{ route('catalog') }}" method="GET" class="w-36">
@if(request('category')) <input type="hidden" name="category" value="{{ request('category') }}"> @endif @if(request('category')) <input type="hidden" name="category" value="{{ request('category') }}"> @endif
@if(request('material')) <input type="hidden" name="material" value="{{ request('material') }}"> @endif @if(request('material')) <input type="hidden" name="material" value="{{ request('material') }}"> @endif
@if(request('q')) <input type="hidden" name="q" value="{{ request('q') }}"> @endif @if(request('q')) <input type="hidden" name="q" value="{{ request('q') }}"> @endif
<select name="sort" onchange="this.form.submit()" class="w-full bg-white border border-slate-200 rounded-xl px-2.5 py-2.5 text-xs font-bold text-slate-700 shadow-2xs"> <select name="sort" onchange="this.form.submit()" class="w-full bg-white border border-slate-200 rounded-lg px-2.5 py-2 text-xs font-medium text-slate-700 shadow-2xs">
<option value="newest" {{ request('sort') === 'newest' ? 'selected' : '' }}>Nieuwste</option> <option value="newest" {{ request('sort') === 'newest' ? 'selected' : '' }}>Nieuwste</option>
<option value="price_asc" {{ request('sort') === 'price_asc' ? 'selected' : '' }}>Prijs &uarr;</option> <option value="price_asc" {{ request('sort') === 'price_asc' ? 'selected' : '' }}>Prijs &uarr;</option>
<option value="price_desc" {{ request('sort') === 'price_desc' ? 'selected' : '' }}>Prijs &darr;</option> <option value="price_desc" {{ request('sort') === 'price_desc' ? 'selected' : '' }}>Prijs &darr;</option>
@@ -50,35 +49,36 @@
</form> </form>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6 lg:gap-8"> <div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Filters Sidebar -->
<aside :class="mobileFiltersOpen ? 'block' : 'hidden lg:block'" class="space-y-6">
<div class="p-5 rounded-2xl bg-white border border-slate-200/60 shadow-2xs space-y-6">
<!-- Filters Sidebar (Op mobiel inklapbaar via Alpine.js) -->
<aside :class="mobileFiltersOpen ? 'block' : 'hidden lg:block'" class="space-y-4">
<div class="p-5 sm:p-6 rounded-3xl bg-white border border-slate-100 shadow-sm space-y-5">
<div class="flex items-center justify-between pb-3 border-b border-slate-100"> <div class="flex items-center justify-between pb-3 border-b border-slate-100">
<span class="font-heading text-xs font-black text-slate-900 uppercase tracking-wider flex items-center space-x-1.5"> <span class="text-xs font-semibold uppercase tracking-wider text-slate-900 flex items-center space-x-1.5">
<i data-lucide="filter" class="w-3.5 h-3.5 text-rose-500"></i> <i data-lucide="filter" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Filters & Zoeken</span> <span>Filters</span>
</span> </span>
@if(request()->hasAny(['category', 'material', 'q', 'sort'])) @if(request()->hasAny(['category', 'material', 'q', 'sort']))
<a href="{{ route('catalog') }}" class="text-[11px] font-bold text-rose-500 hover:underline">Wissen</a> <a href="{{ route('catalog') }}" class="text-[11px] text-rose-600 hover:underline">Wissen</a>
@endif @endif
</div> </div>
<!-- Zoekveld --> <!-- Zoekveld -->
<form action="{{ route('catalog') }}" method="GET" class="space-y-4"> <form action="{{ route('catalog') }}" method="GET" class="space-y-5">
@if(request('category')) <input type="hidden" name="category" value="{{ request('category') }}"> @endif @if(request('category')) <input type="hidden" name="category" value="{{ request('category') }}"> @endif
@if(request('material')) <input type="hidden" name="material" value="{{ request('material') }}"> @endif @if(request('material')) <input type="hidden" name="material" value="{{ request('material') }}"> @endif
<div> <div>
<label class="block text-xs font-bold text-slate-700 mb-1.5">Zoeken</label> <label class="block text-xs font-medium text-slate-700 mb-1.5">Zoeken</label>
<div class="relative"> <div class="relative">
<input <input
type="text" type="text"
name="q" name="q"
value="{{ request('q') }}" value="{{ request('q') }}"
placeholder="Draak, Pikachu, Grogu..." placeholder="Draak, Pikachu, Grogu..."
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-8 pr-3 py-2 text-xs font-bold text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-rose-400" class="w-full bg-slate-50 border border-slate-200 rounded-lg pl-8 pr-3 py-1.5 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-slate-400"
> >
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i> <i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
</div> </div>
@@ -86,13 +86,13 @@
<!-- Categorie filter --> <!-- Categorie filter -->
<div> <div>
<label class="block text-xs font-bold text-slate-700 mb-2">Categorieën</label> <label class="block text-xs font-medium text-slate-700 mb-2">Categorieën</label>
<div class="space-y-1"> <div class="space-y-0.5">
<a href="{{ route('catalog', array_merge(request()->except('category', 'page'), [])) }}" class="flex items-center justify-between px-3 py-2 rounded-xl text-xs font-bold transition {{ !request('category') ? 'bg-amber-100 text-amber-950 font-black' : 'text-slate-600 hover:bg-slate-50' }}"> <a href="{{ route('catalog', array_merge(request()->except('category', 'page'), [])) }}" class="flex items-center justify-between px-2.5 py-1.5 rounded-lg text-xs transition {{ !request('category') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>Alle categorieën</span> <span>Alle categorieën</span>
</a> </a>
@foreach($categories as $cat) @foreach($categories as $cat)
<a href="{{ route('catalog', array_merge(request()->except('category', 'page'), ['category' => $cat->slug])) }}" class="flex items-center justify-between px-3 py-2 rounded-xl text-xs font-bold transition {{ request('category') === $cat->slug ? 'bg-amber-100 text-amber-950 font-black' : 'text-slate-600 hover:bg-slate-50' }}"> <a href="{{ route('catalog', array_merge(request()->except('category', 'page'), ['category' => $cat->slug])) }}" class="flex items-center justify-between px-2.5 py-1.5 rounded-lg text-xs transition {{ request('category') === $cat->slug ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>{{ $cat->name }}</span> <span>{{ $cat->name }}</span>
</a> </a>
@endforeach @endforeach
@@ -101,13 +101,13 @@
<!-- Materiaal filter --> <!-- Materiaal filter -->
<div> <div>
<label class="block text-xs font-bold text-slate-700 mb-2">Filament Materiaal</label> <label class="block text-xs font-medium text-slate-700 mb-2">Filament Materiaal</label>
<div class="space-y-1"> <div class="space-y-0.5">
<a href="{{ route('catalog', array_merge(request()->except('material', 'page'), [])) }}" class="flex items-center justify-between px-3 py-1.5 rounded-xl text-xs font-bold transition {{ !request('material') ? 'bg-sky-100 text-sky-950 font-black' : 'text-slate-600 hover:bg-slate-50' }}"> <a href="{{ route('catalog', array_merge(request()->except('material', 'page'), [])) }}" class="flex items-center justify-between px-2.5 py-1.5 rounded-lg text-xs transition {{ !request('material') ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>Alle materialen</span> <span>Alle materialen</span>
</a> </a>
@foreach($materials as $mat) @foreach($materials as $mat)
<a href="{{ route('catalog', array_merge(request()->except('material', 'page'), ['material' => $mat->code])) }}" class="flex items-center justify-between px-3 py-1.5 rounded-xl text-xs font-bold transition {{ request('material') === $mat->code ? 'bg-sky-100 text-sky-950 font-black' : 'text-slate-600 hover:bg-slate-50' }}"> <a href="{{ route('catalog', array_merge(request()->except('material', 'page'), ['material' => $mat->code])) }}" class="flex items-center justify-between px-2.5 py-1.5 rounded-lg text-xs transition {{ request('material') === $mat->code ? 'bg-slate-100 text-slate-900 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>{{ $mat->name }}</span> <span>{{ $mat->name }}</span>
</a> </a>
@endforeach @endforeach
@@ -116,8 +116,8 @@
<!-- Sortering Desktop --> <!-- Sortering Desktop -->
<div class="hidden lg:block"> <div class="hidden lg:block">
<label class="block text-xs font-bold text-slate-700 mb-1.5">Sorteren</label> <label class="block text-xs font-medium text-slate-700 mb-1.5">Sorteren</label>
<select name="sort" onchange="this.form.submit()" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold text-slate-700 focus:bg-white focus:outline-none"> <select name="sort" onchange="this.form.submit()" class="w-full bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-700 focus:bg-white focus:outline-none">
<option value="newest" {{ request('sort') === 'newest' ? 'selected' : '' }}>Nieuwste toevoegingen</option> <option value="newest" {{ request('sort') === 'newest' ? 'selected' : '' }}>Nieuwste toevoegingen</option>
<option value="price_asc" {{ request('sort') === 'price_asc' ? 'selected' : '' }}>Prijs: Laag naar hoog</option> <option value="price_asc" {{ request('sort') === 'price_asc' ? 'selected' : '' }}>Prijs: Laag naar hoog</option>
<option value="price_desc" {{ request('sort') === 'price_desc' ? 'selected' : '' }}>Prijs: Hoog naar laag</option> <option value="price_desc" {{ request('sort') === 'price_desc' ? 'selected' : '' }}>Prijs: Hoog naar laag</option>
@@ -127,54 +127,55 @@
</div> </div>
</aside> </aside>
<!-- Producten Overzicht (2 kolommen op mobiel, 3 op desktop) --> <!-- Producten Overzicht -->
<div class="lg:col-span-3 space-y-6"> <div class="lg:col-span-3 space-y-6">
@if($products->isEmpty()) @if($products->isEmpty())
<div class="p-10 text-center rounded-3xl bg-white border border-slate-100 shadow-sm space-y-3"> <div class="p-12 text-center rounded-2xl bg-white border border-slate-200/60 shadow-2xs space-y-3">
<div class="w-14 h-14 rounded-2xl bg-amber-50 text-slate-800 flex items-center justify-center mx-auto text-3xl"> <div class="w-12 h-12 rounded-xl bg-slate-100 text-slate-600 flex items-center justify-center mx-auto text-xl">
🙈 <i data-lucide="box" class="w-6 h-6 text-slate-400"></i>
</div> </div>
<h3 class="font-heading text-lg font-black text-slate-900">Geen artikelen gevonden</h3> <h3 class="font-heading text-base font-semibold text-slate-900">Geen artikelen gevonden</h3>
<p class="text-xs text-slate-500 max-w-sm mx-auto">Er zijn geen prints gevonden die voldoen aan je huidige filters. Wis je filters of bekijk alle categorieën!</p> <p class="text-xs text-slate-500 max-w-sm mx-auto">Er zijn geen prints gevonden die voldoen aan je huidige filters. Wis je filters of bekijk alle categorieën!</p>
<a href="{{ route('catalog') }}" class="playful-btn inline-flex px-5 py-2.5 rounded-xl bg-amber-400 text-slate-950 text-xs font-black shadow-xs">Alle prints bekijken 🎁</a> <a href="{{ route('catalog') }}" class="inline-flex px-4 py-2 rounded-lg bg-slate-900 text-white text-xs font-medium hover:bg-slate-800 transition">Alle prints bekijken</a>
</div> </div>
@else @else
<div class="grid grid-cols-2 md:grid-cols-3 gap-3 sm:gap-6"> <div class="grid grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6">
@foreach($products as $product) @foreach($products as $product)
<div class="playful-card rounded-2xl sm:rounded-3xl bg-white border border-slate-100 shadow-xs hover:shadow-lg overflow-hidden flex flex-col justify-between group"> <div class="group flex flex-col justify-between bg-white rounded-2xl p-3 border border-slate-200/60 card-hover">
<div> <div>
<div class="relative h-40 sm:h-56 bg-slate-100 overflow-hidden"> <div class="relative aspect-square rounded-xl overflow-hidden bg-slate-50">
<img <img
src="{{ $product->image_url }}" src="{{ $product->image_url }}"
alt="{{ $product->name }}" alt="{{ $product->name }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" class="w-full h-full object-cover group-hover:scale-103 transition-transform duration-500 ease-out"
loading="lazy" loading="lazy"
> >
@if($product->category) @if($product->category)
<span class="absolute top-2 left-2 sm:top-3 sm:left-3 px-2 py-0.5 rounded-md bg-white/90 backdrop-blur-xs text-[9px] sm:text-[10px] font-black text-slate-800 shadow-2xs"> <span class="absolute top-2 left-2 px-2 py-0.5 rounded-md bg-white/90 backdrop-blur-xs text-[10px] font-medium text-slate-700 border border-slate-200/50 shadow-2xs">
{{ $product->category->name }} {{ $product->category->name }}
</span> </span>
@endif @endif
</div> </div>
<div class="p-3 sm:p-4"> <div class="pt-3 pb-1">
<h3 class="font-heading text-xs sm:text-sm font-black text-slate-900 group-hover:text-rose-600 transition line-clamp-2"> <h3 class="font-heading text-xs sm:text-sm font-semibold text-slate-900 group-hover:text-sky-600 transition line-clamp-1">
<a href="{{ route('product.show', $product->slug) }}"> <a href="{{ route('product.show', $product->slug) }}">
{{ $product->name }} {{ $product->name }}
</a> </a>
</h3> </h3>
<p class="hidden sm:block text-xs text-slate-500 mt-1 line-clamp-2 leading-relaxed"> <p class="text-[11px] text-slate-400 mt-0.5 line-clamp-1 font-normal">
{{ $product->short_description }} {{ $product->short_description }}
</p> </p>
</div> </div>
</div> </div>
<div class="p-3 sm:p-4 pt-0 border-t border-slate-100/80 mt-1 flex items-center justify-between"> <div class="pt-2 border-t border-slate-100 flex items-center justify-between mt-2">
<div> <div>
<span class="text-[9px] sm:text-[10px] text-slate-400 block font-bold uppercase">Vanaf</span> <span class="text-[10px] text-slate-400 block font-normal leading-none">Vanaf</span>
<span class="font-heading text-sm sm:text-base font-black text-slate-900">{{ $product->formatted_price }}</span> <span class="font-heading text-sm font-bold text-slate-900">{{ $product->formatted_price }}</span>
</div> </div>
<a href="{{ route('product.show', $product->slug) }}" class="playful-btn px-2.5 sm:px-3 py-1 sm:py-1.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 text-[10px] sm:text-xs font-black transition flex items-center space-x-1 shadow-2xs"> <a href="{{ route('product.show', $product->slug) }}" class="px-2.5 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white text-[11px] font-medium transition flex items-center space-x-1 cursor-pointer">
<span>Kleur 🎨</span> <span>Kies kleur</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
</a> </a>
</div> </div>
</div> </div>
+107 -111
View File
@@ -1,30 +1,30 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Eigen 3D Model Printen (STL / 3MF) | Jouwdriedee.nl') @section('title', 'Eigen 3D Model Printen | Jouwdriedee.nl')
@section('content') @section('content')
<!-- Header Banner --> <!-- Header Banner -->
<div class="bg-slate-50 border-b border-slate-200/80 py-8 sm:py-12"> <div class="border-b border-slate-200/80 py-8 sm:py-12 bg-slate-50/50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-3"> <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-2.5">
<span class="px-3.5 py-1 rounded-full bg-white border border-slate-200 text-xs font-bold text-slate-700 inline-flex items-center space-x-1.5 shadow-2xs"> <span class="px-3 py-1 rounded-full bg-white border border-slate-200 text-xs font-medium text-slate-700 inline-flex items-center space-x-1.5 shadow-2xs">
<i data-lucide="layers" class="w-3.5 h-3.5 text-rose-500"></i> <i data-lucide="layers" class="w-3.5 h-3.5 text-slate-500"></i>
<span>3D Print op Aanvraag & Maatwerk</span> <span>3D Print op Aanvraag</span>
</span> </span>
<h1 class="font-heading text-3xl sm:text-5xl font-black text-slate-900 tracking-tight">Eigen 3D Model Printen</h1> <h1 class="text-3xl sm:text-4xl font-bold tracking-tight text-slate-900">Eigen 3D Model Printen</h1>
<p class="text-xs sm:text-sm text-slate-600 max-w-xl mx-auto leading-relaxed font-normal"> <p class="text-xs sm:text-sm text-slate-500 max-w-xl mx-auto leading-relaxed">
Heb je een 3D-bestand van MakerWorld, Printables of Thingiverse, of een eigen CAD-ontwerp? Upload jouw bestand of plak de link, kies je materiaal en bestel direct online. Upload jouw eigen STL, 3MF of STEP bestand, of plak de link van een MakerWorld of Printables model. Kies je materiaal en configureer direct online.
</p> </p>
</div> </div>
</div> </div>
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12" <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12"
x-data="{ x-data="{
activeTab: 'direct', // 'direct' of 'offerte' activeTab: 'direct',
selectedSize: 'medium', selectedSize: 'medium',
sizePrices: { sizePrices: {
'klein': 9.95, // tot 8 cm 'klein': 9.95,
'medium': 16.95, // tot 15 cm 'medium': 16.95,
'groot': 24.95 // tot 22 cm 'groot': 24.95
}, },
selectedMaterial: 'pla', selectedMaterial: 'pla',
materialMultipliers: { materialMultipliers: {
@@ -32,7 +32,7 @@
'{{ $mat->code }}': {{ $mat->price_multiplier }}, '{{ $mat->code }}': {{ $mat->price_multiplier }},
@endforeach @endforeach
}, },
selectedColor: '{{ $colors[0]->name ?? 'Zon Geel (Pikachu)' }}', selectedColor: '{{ $colors[0]->name ?? 'Zwart' }}',
quantity: 1, quantity: 1,
get calculatedTotal() { get calculatedTotal() {
@@ -48,75 +48,75 @@
}"> }">
<!-- Tab Selector --> <!-- Tab Selector -->
<div class="flex items-center justify-center p-1.5 bg-slate-100 rounded-2xl max-w-md mx-auto mb-8"> <div class="flex items-center justify-center p-1 bg-slate-100 rounded-xl max-w-xs mx-auto mb-8">
<button <button
type="button" type="button"
@click="activeTab = 'direct'" @click="activeTab = 'direct'"
:class="activeTab === 'direct' ? 'bg-white text-slate-900 shadow-sm font-black' : 'text-slate-600 hover:text-slate-900 font-bold'" :class="activeTab === 'direct' ? 'bg-white text-slate-900 shadow-2xs font-semibold' : 'text-slate-600 hover:text-slate-900 font-medium'"
class="flex-1 py-2.5 px-4 rounded-xl text-xs transition flex items-center justify-center space-x-1.5" class="flex-1 py-1.5 px-3 rounded-lg text-xs transition flex items-center justify-center space-x-1.5 cursor-pointer"
> >
<i data-lucide="shopping-bag" class="w-3.5 h-3.5"></i> <i data-lucide="shopping-bag" class="w-3.5 h-3.5"></i>
<span>Direct online bestellen</span> <span>Direct bestellen</span>
</button> </button>
<button <button
type="button" type="button"
@click="activeTab = 'offerte'" @click="activeTab = 'offerte'"
:class="activeTab === 'offerte' ? 'bg-white text-slate-900 shadow-sm font-black' : 'text-slate-600 hover:text-slate-900 font-bold'" :class="activeTab === 'offerte' ? 'bg-white text-slate-900 shadow-2xs font-semibold' : 'text-slate-600 hover:text-slate-900 font-medium'"
class="flex-1 py-2.5 px-4 rounded-xl text-xs transition flex items-center justify-center space-x-1.5" class="flex-1 py-1.5 px-3 rounded-lg text-xs transition flex items-center justify-center space-x-1.5 cursor-pointer"
> >
<i data-lucide="file-text" class="w-3.5 h-3.5"></i> <i data-lucide="file-text" class="w-3.5 h-3.5"></i>
<span>Maatwerk offerte</span> <span>Maatwerk offerte</span>
</button> </button>
</div> </div>
<!-- TAB 1: DIRECT BESTELLEN EN IN WINKELWAGEN --> <!-- TAB 1: DIRECT BESTELLEN -->
<div x-show="activeTab === 'direct'" class="p-6 sm:p-10 rounded-3xl bg-white border border-slate-200/80 shadow-lg shadow-slate-200/40 space-y-8"> <div x-show="activeTab === 'direct'" class="p-6 sm:p-8 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-6">
<form action="{{ route('custom.cart.add') }}" method="POST" enctype="multipart/form-data" class="space-y-6"> <form action="{{ route('custom.cart.add') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
@csrf @csrf
<!-- 1. Model bron: Bestand of Link --> <!-- 1. Model bron: Bestand of Link -->
<div class="space-y-4"> <div class="space-y-3">
<div class="border-b border-slate-100 pb-3"> <div class="border-b border-slate-100 pb-2">
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">
<span>1. 3D-bestand uploaden of link invoeren</span> 1. 3D-bestand uploaden of link invoeren
</h3> </h3>
<p class="text-xs text-slate-500 mt-0.5">Ondersteunt STL, 3MF, STEP en OBJ, of een directe link naar het model op MakerWorld of Printables.</p> <p class="text-xs text-slate-500 mt-0.5">Ondersteunt STL, 3MF, STEP en OBJ, of een directe link naar een model.</p>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
<!-- Bestand upload --> <!-- Bestand upload -->
<div class="p-4 rounded-2xl bg-slate-50 border border-slate-200/80 hover:border-slate-300 transition"> <div class="p-4 rounded-xl bg-slate-50 border border-slate-200 hover:border-slate-300 transition">
<label class="block text-xs font-bold text-slate-800 mb-1 flex items-center space-x-1.5"> <label class="block text-xs font-semibold text-slate-800 mb-1 flex items-center space-x-1.5">
<i data-lucide="file" class="w-3.5 h-3.5 text-rose-500"></i> <i data-lucide="file-up" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Bestand uploaden (.stl, .3mf, .step, .obj)</span> <span>Bestand uploaden (.stl, .3mf, .step)</span>
</label> </label>
<input type="file" name="model_file" accept=".stl,.step,.stp,.obj,.3mf,.zip" class="w-full text-xs text-slate-600 file:mr-3 file:py-1.5 file:px-3 file:rounded-xl file:border-0 file:text-xs file:font-bold file:bg-slate-900 file:text-white hover:file:bg-slate-800 cursor-pointer"> <input type="file" name="model_file" accept=".stl,.step,.stp,.obj,.3mf,.zip" class="w-full text-xs text-slate-600 file:mr-3 file:py-1 file:px-2.5 file:rounded-lg file:border-0 file:text-xs file:font-medium file:bg-slate-900 file:text-white hover:file:bg-slate-800 cursor-pointer">
<span class="text-[10px] text-slate-400 block mt-1">Maximale bestandsgrootte: 50MB</span> <span class="text-[10px] text-slate-400 block mt-1">Tot 50 MB per bestand</span>
</div> </div>
<!-- Of URL --> <!-- Of URL -->
<div class="p-4 rounded-2xl bg-slate-50 border border-slate-200/80 hover:border-slate-300 transition"> <div class="p-4 rounded-xl bg-slate-50 border border-slate-200 hover:border-slate-300 transition">
<label class="block text-xs font-bold text-slate-800 mb-1 flex items-center space-x-1.5"> <label class="block text-xs font-semibold text-slate-800 mb-1 flex items-center space-x-1.5">
<i data-lucide="link" class="w-3.5 h-3.5 text-rose-500"></i> <i data-lucide="link" class="w-3.5 h-3.5 text-slate-500"></i>
<span>Of plak een MakerWorld / Printables link</span> <span>Of plak een MakerWorld / Printables link</span>
</label> </label>
<input type="url" name="model_url" placeholder="https://makerworld.com/nl/models/..." class="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-medium placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-rose-400"> <input type="url" name="model_url" placeholder="https://makerworld.com/nl/models/..." class="w-full bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-900">
<span class="text-[10px] text-slate-400 block mt-1">Wij downloaden en inspecteren het bestand</span> <span class="text-[10px] text-slate-400 block mt-1">Wij halen de specificaties automatisch op</span>
</div> </div>
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 mb-1">Omschrijving of modelnaam (optioneel)</label> <label class="block text-xs font-medium text-slate-700">Omschrijving of modelnaam (optioneel)</label>
<input type="text" name="model_name" placeholder="Bijv. Pikachu sleutelhanger of Fidget Toy" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs font-medium text-slate-800 focus:bg-white focus:outline-none"> <input type="text" name="model_name" placeholder="Bijv. Bureau organizer of vervangend tandwiel" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-slate-900">
</div> </div>
</div> </div>
<!-- 2. Formaat & Afmeting --> <!-- 2. Formaat & Afmeting -->
<div class="space-y-3"> <div class="space-y-3">
<div class="border-b border-slate-100 pb-2"> <div class="border-b border-slate-100 pb-2">
<h3 class="font-heading text-base font-black text-slate-900"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">
<span>2. Formaat en schaal selecteren</span> 2. Formaat en schaal selecteren
</h3> </h3>
</div> </div>
@@ -125,57 +125,57 @@
<button <button
type="button" type="button"
@click="selectedSize = 'klein'" @click="selectedSize = 'klein'"
:class="selectedSize === 'klein' ? 'border-amber-400 bg-amber-50 ring-2 ring-amber-400 font-bold' : 'border-slate-200 hover:bg-slate-50 font-semibold'" :class="selectedSize === 'klein' ? 'border-slate-900 bg-slate-50 ring-1 ring-slate-900 font-semibold' : 'border-slate-200 hover:bg-slate-50 font-normal'"
class="p-4 rounded-2xl border text-left transition cursor-pointer flex flex-col justify-between" class="p-4 rounded-xl border text-left transition cursor-pointer flex flex-col justify-between"
> >
<div> <div>
<span class="text-xs font-bold text-slate-900 block">Klein (tot 8 cm)</span> <span class="text-xs font-bold text-slate-900 block">Klein (tot 8 cm)</span>
<span class="text-[11px] text-slate-500 block mt-0.5">Sleutelhangers, miniaturen, kleine clips</span> <span class="text-[11px] text-slate-500 block mt-0.5">Sleutelhangers, clips, miniaturen</span>
</div> </div>
<span class="font-heading text-base font-black text-slate-900 mt-2 block">€ 9,95</span> <span class="text-sm font-bold text-slate-900 mt-2 block">&euro; 9,95</span>
</button> </button>
<button <button
type="button" type="button"
@click="selectedSize = 'medium'" @click="selectedSize = 'medium'"
:class="selectedSize === 'medium' ? 'border-amber-400 bg-amber-50 ring-2 ring-amber-400 font-bold' : 'border-slate-200 hover:bg-slate-50 font-semibold'" :class="selectedSize === 'medium' ? 'border-slate-900 bg-slate-50 ring-1 ring-slate-900 font-semibold' : 'border-slate-200 hover:bg-slate-50 font-normal'"
class="p-4 rounded-2xl border text-left transition cursor-pointer flex flex-col justify-between" class="p-4 rounded-xl border text-left transition cursor-pointer flex flex-col justify-between"
> >
<div> <div>
<span class="text-xs font-bold text-slate-900 block">Medium (tot 15 cm)</span> <span class="text-xs font-bold text-slate-900 block">Medium (tot 15 cm)</span>
<span class="text-[11px] text-slate-500 block mt-0.5">Figuren, plantenbakjes, bureau-organizers</span> <span class="text-[11px] text-slate-500 block mt-0.5">Figuren, bureau-organizers</span>
</div> </div>
<span class="font-heading text-base font-black text-slate-900 mt-2 block">€ 16,95</span> <span class="text-sm font-bold text-slate-900 mt-2 block">&euro; 16,95</span>
</button> </button>
<button <button
type="button" type="button"
@click="selectedSize = 'groot'" @click="selectedSize = 'groot'"
:class="selectedSize === 'groot' ? 'border-amber-400 bg-amber-50 ring-2 ring-amber-400 font-bold' : 'border-slate-200 hover:bg-slate-50 font-semibold'" :class="selectedSize === 'groot' ? 'border-slate-900 bg-slate-50 ring-1 ring-slate-900 font-semibold' : 'border-slate-200 hover:bg-slate-50 font-normal'"
class="p-4 rounded-2xl border text-left transition cursor-pointer flex flex-col justify-between" class="p-4 rounded-xl border text-left transition cursor-pointer flex flex-col justify-between"
> >
<div> <div>
<span class="text-xs font-bold text-slate-900 block">Groot (tot 22 cm)</span> <span class="text-xs font-bold text-slate-900 block">Groot (tot 22 cm)</span>
<span class="text-[11px] text-slate-500 block mt-0.5">Grote displays, cosplay props, vazen</span> <span class="text-[11px] text-slate-500 block mt-0.5">Cosplay props, grote displays</span>
</div> </div>
<span class="font-heading text-base font-black text-slate-900 mt-2 block">€ 24,95</span> <span class="text-sm font-bold text-slate-900 mt-2 block">&euro; 24,95</span>
</button> </button>
</div> </div>
</div> </div>
<!-- 3. Materiaal & Kleur --> <!-- 3. Materiaal & Kleur -->
<div class="space-y-4"> <div class="space-y-3">
<div class="border-b border-slate-100 pb-2"> <div class="border-b border-slate-100 pb-2">
<h3 class="font-heading text-base font-black text-slate-900"> <h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">
<span>3. Materiaal en printkleur</span> 3. Materiaal en printkleur
</h3> </h3>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Materiaal --> <!-- Materiaal -->
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 mb-1.5">Filament type</label> <label class="block text-xs font-medium text-slate-700">Filament type</label>
<select name="material" x-model="selectedMaterial" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-xs font-bold text-slate-800 focus:bg-white focus:outline-none"> <select name="material" x-model="selectedMaterial" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs font-medium text-slate-900 focus:bg-white focus:outline-none">
@foreach($materials as $mat) @foreach($materials as $mat)
<option value="{{ $mat->code }}">{{ $mat->name }}</option> <option value="{{ $mat->code }}">{{ $mat->name }}</option>
@endforeach @endforeach
@@ -183,57 +183,54 @@
</div> </div>
<!-- Kleur --> <!-- Kleur -->
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 mb-1.5">Printkleur: <span class="text-rose-500 font-bold" x-text="selectedColor"></span></label> <label class="block text-xs font-medium text-slate-700">Kleur: <span class="font-semibold text-slate-900" x-text="selectedColor"></span></label>
<input type="hidden" name="color" x-model="selectedColor"> <input type="hidden" name="color" x-model="selectedColor">
<div class="flex flex-wrap gap-2 pt-1"> <div class="flex flex-wrap gap-1.5 pt-1">
@foreach($colors as $c) @foreach($colors as $c)
<button <button
type="button" type="button"
@click="selectedColor = '{{ $c->name }}'" @click="selectedColor = '{{ $c->name }}'"
:class="selectedColor === '{{ $c->name }}' ? 'ring-3 ring-rose-500 ring-offset-2 scale-110' : 'hover:scale-105 border-slate-300'" :class="selectedColor === '{{ $c->name }}' ? 'ring-2 ring-slate-900 ring-offset-1' : 'hover:scale-105 border-slate-200'"
class="w-7 h-7 rounded-xl border shadow-2xs transition flex items-center justify-center cursor-pointer relative" class="w-6 h-6 rounded-full border shadow-2xs transition flex items-center justify-center cursor-pointer"
style="background-color: {{ $c->hex }};" style="background-color: {{ $c->hex }};"
title="{{ $c->name }}" title="{{ $c->name }}"
> >
<template x-if="selectedColor === '{{ $c->name }}'">
<i data-lucide="check" class="w-3.5 h-3.5 {{ in_array($c->name, ['Sneeuwwit', 'Zon Geel (Pikachu)']) ? 'text-slate-950' : 'text-white' }} stroke-[3]"></i>
</template>
</button> </button>
@endforeach @endforeach
</div> </div>
</div> </div>
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 mb-1">Bijzondere instructies (optioneel)</label> <label class="block text-xs font-medium text-slate-700">Bijzondere instructies (optioneel)</label>
<textarea name="notes" rows="2" placeholder="Bijv. gewenste oriëntatie, specifieke wanddikte of infill..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-xs text-slate-800 focus:bg-white focus:outline-none"></textarea> <textarea name="notes" rows="2" placeholder="Bijv. gewenste printoriëntatie of gewenste wanddikte..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none"></textarea>
</div> </div>
</div> </div>
<!-- 4. Prijs & Bestelknop --> <!-- 4. Prijs & Bestelknop -->
<div class="p-5 rounded-2xl bg-slate-50 border border-slate-200/80 flex flex-col sm:flex-row items-center justify-between gap-4"> <div class="p-4 rounded-xl bg-slate-50 border border-slate-200 flex flex-col sm:flex-row items-center justify-between gap-4">
<div> <div>
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider block">Berekende Stukprijs</span> <span class="text-[10px] font-semibold text-slate-400 uppercase tracking-wider block">Berekende stukprijs</span>
<div class="flex items-baseline space-x-2"> <div class="flex items-baseline space-x-1.5">
<span class="font-heading text-3xl font-black text-slate-900">€ <span x-text="unitPrice"></span></span> <span class="text-2xl font-bold text-slate-900">&euro; <span x-text="unitPrice"></span></span>
<span class="text-xs text-slate-500">incl. btw & materiaal</span> <span class="text-xs text-slate-500">incl. btw</span>
</div> </div>
</div> </div>
<div class="flex items-center space-x-3 w-full sm:w-auto"> <div class="flex items-center space-x-3 w-full sm:w-auto">
<!-- Aantal --> <!-- Aantal -->
<div class="flex items-center border border-slate-200 rounded-xl bg-white overflow-hidden shadow-2xs"> <div class="flex items-center border border-slate-200 rounded-lg bg-white overflow-hidden">
<button type="button" @click="if (quantity > 1) quantity--" class="px-3 py-2 text-slate-700 font-bold cursor-pointer text-sm hover:bg-slate-100">-</button> <button type="button" @click="if (quantity > 1) quantity--" class="px-2.5 py-1 text-slate-600 font-bold cursor-pointer hover:bg-slate-50">-</button>
<span class="px-3 py-2 font-heading font-black text-xs text-slate-900" x-text="quantity"></span> <span class="px-2.5 py-1 text-xs font-semibold text-slate-900" x-text="quantity"></span>
<input type="hidden" name="quantity" x-model="quantity"> <input type="hidden" name="quantity" x-model="quantity">
<button type="button" @click="if (quantity < 20) quantity++" class="px-3 py-2 text-slate-700 font-bold cursor-pointer text-sm hover:bg-slate-100">+</button> <button type="button" @click="if (quantity < 20) quantity++" class="px-2.5 py-1 text-slate-600 font-bold cursor-pointer hover:bg-slate-50">+</button>
</div> </div>
<!-- Bestelknop --> <!-- Bestelknop -->
<button type="submit" class="playful-btn flex-1 sm:flex-none px-6 py-3.5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm shadow-xs flex items-center justify-center space-x-2 cursor-pointer"> <button type="submit" class="flex-1 sm:flex-none px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs shadow-xs flex items-center justify-center space-x-1.5 transition cursor-pointer">
<i data-lucide="shopping-bag" class="w-4 h-4"></i> <i data-lucide="shopping-bag" class="w-3.5 h-3.5"></i>
<span>In Winkelwagen • € <span x-text="calculatedTotal"></span></span> <span>In winkelwagen &bull; &euro; <span x-text="calculatedTotal"></span></span>
</button> </button>
</div> </div>
</div> </div>
@@ -242,48 +239,48 @@
</div> </div>
<!-- TAB 2: VRIJBLIJVENDE OFFERTE (VOOR MAATWERK & ADVIES) --> <!-- TAB 2: VRIJBLIJVENDE OFFERTE -->
<div x-show="activeTab === 'offerte'" class="p-6 sm:p-10 rounded-3xl bg-white border border-slate-200/80 shadow-lg shadow-slate-200/40 space-y-6"> <div x-show="activeTab === 'offerte'" class="p-6 sm:p-8 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-5">
<div class="border-b border-slate-100 pb-3"> <div class="border-b border-slate-100 pb-3">
<h3 class="font-heading text-lg font-black text-slate-900">Vrijblijvende Maatwerk Offerte</h3> <h3 class="text-base font-bold text-slate-900">Vrijblijvende Maatwerk Offerte</h3>
<p class="text-xs text-slate-500 mt-1">Voor grotere series, meerkleurige assemblages of speciale technische vereisten. We reageren binnen 24 uur met een passend voorstel.</p> <p class="text-xs text-slate-500 mt-0.5">Voor grotere series, multi-color assemblages of specifieke technische kunststoffen. Binnen 24 uur reactie.</p>
</div> </div>
<form action="{{ route('custom.store') }}" method="POST" enctype="multipart/form-data" class="space-y-5"> <form action="{{ route('custom.store') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
@csrf @csrf
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">Naam *</label> <label class="block text-xs font-medium text-slate-700">Naam *</label>
<input type="text" name="name" required value="{{ old('name') }}" placeholder="Bijv. Jan de Vries" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none"> <input type="text" name="name" required value="{{ old('name') }}" placeholder="Jan Jansen" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none">
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">E-mailadres *</label> <label class="block text-xs font-medium text-slate-700">E-mailadres *</label>
<input type="email" name="email" required value="{{ old('email') }}" placeholder="jouw@email.nl" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none"> <input type="email" name="email" required value="{{ old('email') }}" placeholder="jouw@email.nl" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none">
</div> </div>
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">Telefoonnummer (optioneel)</label> <label class="block text-xs font-medium text-slate-700">Telefoonnummer (optioneel)</label>
<input type="text" name="phone" value="{{ old('phone') }}" placeholder="06-12345678" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none"> <input type="text" name="phone" value="{{ old('phone') }}" placeholder="06-12345678" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none">
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">3D Model Bestand (.stl, .step, .obj, .zip)</label> <label class="block text-xs font-medium text-slate-700">3D Model Bestand (.stl, .step, .obj, .zip)</label>
<div class="p-6 border-2 border-dashed border-slate-200 hover:border-slate-300 rounded-2xl bg-slate-50/60 text-center transition"> <div class="p-5 border border-dashed border-slate-200 hover:border-slate-300 rounded-xl bg-slate-50/50 text-center transition">
<input type="file" name="model_file" accept=".stl,.step,.stp,.obj,.3mf,.zip" class="w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-xs file:font-bold file:bg-slate-900 file:text-white hover:file:bg-slate-800 cursor-pointer"> <input type="file" name="model_file" accept=".stl,.step,.stp,.obj,.3mf,.zip" class="w-full text-xs text-slate-500 file:mr-3 file:py-1.5 file:px-3 file:rounded-lg file:border-0 file:text-xs file:font-medium file:bg-slate-900 file:text-white hover:file:bg-slate-800 cursor-pointer">
<p class="text-[11px] text-slate-400 mt-2">Ondersteunde formaten: STL, STEP, OBJ, 3MF of ZIP (max 50 MB)</p> <p class="text-[10px] text-slate-400 mt-1.5">STL, STEP, OBJ, 3MF of ZIP (max 50 MB)</p>
</div> </div>
</div> </div>
<div> <div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">Toelichting, Afmetingen & Aantal *</label> <label class="block text-xs font-medium text-slate-700">Toelichting, Afmetingen & Aantal *</label>
<textarea name="description" rows="4" required placeholder="Vermeld eventuele gewenste afmetingen, aantal en toepassing..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3.5 text-xs text-slate-900 focus:bg-white focus:outline-none"></textarea> <textarea name="description" rows="3" required placeholder="Vermeld eventuele gewenste afmetingen, aantal stuks en gewenste mechanische sterkte..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-xs text-slate-900 focus:bg-white focus:outline-none"></textarea>
</div> </div>
<button type="submit" class="playful-btn w-full py-4 rounded-2xl bg-slate-900 hover:bg-slate-800 text-white font-black text-xs uppercase tracking-wider transition shadow-sm flex items-center justify-center space-x-2 cursor-pointer"> <button type="submit" class="w-full py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition shadow-xs flex items-center justify-center space-x-1.5 cursor-pointer">
<i data-lucide="send" class="w-4 h-4"></i> <i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Aanvraag Verzenden</span> <span>Aanvraag Verzenden</span>
</button> </button>
</form> </form>
@@ -291,4 +288,3 @@
</div> </div>
@endsection @endsection
+156 -180
View File
@@ -3,62 +3,62 @@
@section('title', setting('site_name', 'Jouwdriedee.nl') . ' | ' . setting('site_tagline', 'Kleurrijke 3D Prints, Fidget Toys & Collectibles')) @section('title', setting('site_name', 'Jouwdriedee.nl') . ' | ' . setting('site_tagline', 'Kleurrijke 3D Prints, Fidget Toys & Collectibles'))
@section('content') @section('content')
<!-- Hero Section (Helder, Kleurrijk, Aanpasbaar via Admin) --> <!-- Hero Section (Fris, Subtiel, Modern & Ruimtelijk) -->
<section class="relative overflow-hidden bg-gradient-to-b from-amber-50/50 via-white to-[#FBFBFA] pt-8 pb-12 sm:pt-14 sm:pb-18 border-b border-slate-200/70"> <section class="relative overflow-hidden pt-10 pb-16 sm:pt-16 sm:pb-24 border-b border-slate-100 bg-gradient-to-b from-slate-50/60 to-white">
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 items-center"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center">
<!-- Left Hero Content --> <!-- Left Hero Content -->
<div class="lg:col-span-7 space-y-4 sm:space-y-6 text-center lg:text-left"> <div class="lg:col-span-7 space-y-6 text-center lg:text-left">
<div class="inline-flex items-center space-x-2 px-3.5 py-1.5 rounded-full bg-white border border-slate-200/80 shadow-2xs text-slate-800 text-xs font-bold"> <div class="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-white border border-slate-200/80 text-slate-700 text-xs font-medium shadow-2xs">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>{{ setting('hero_badge', 'Nederlandse 3D Printstudio • Bambu Lab Precisie') }}</span> <span>{{ setting('hero_badge', 'Nederlandse 3D Printstudio • Bambu Lab Precisie') }}</span>
</div> </div>
<h1 class="font-heading text-3xl sm:text-5xl lg:text-6xl font-black tracking-tight text-slate-900 leading-[1.15]"> <h1 class="font-heading text-4xl sm:text-6xl font-extrabold tracking-tight text-slate-900 leading-[1.08]">
{!! nl2br(e(setting('hero_title', 'Kleurrijke 3D prints, fidget toys & collectibles.'))) !!} {!! nl2br(e(setting('hero_title', 'Kleurrijke 3D prints, fidget toys & collectibles.'))) !!}
</h1> </h1>
<p class="text-sm sm:text-base text-slate-600 max-w-xl mx-auto lg:mx-0 font-normal leading-relaxed"> <p class="text-base sm:text-lg text-slate-500 max-w-xl mx-auto lg:mx-0 font-normal leading-relaxed">
{{ setting('hero_subtitle', 'Van soepel bewegende kristaldraken en geometrische Pokémon planters tot filmprops op schaal. Haarscherp geprint in hoogwaardig Bio-PLA+, stevig PETG of flexibel TPU in jouw favoriete kleur!') }} {{ setting('hero_subtitle', 'Van soepel bewegende kristaldraken en geometrische Pokémon planters tot filmprops op schaal. Haarscherp geprint in hoogwaardig Bio-PLA+, stevig PETG of flexibel TPU in jouw favoriete kleur!') }}
</p> </p>
<!-- Action CTA's --> <!-- Clean Minimal CTAs -->
<div class="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-3 pt-2"> <div class="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-3 pt-1">
<a href="{{ setting('hero_primary_btn_url', route('catalog')) }}" class="w-full sm:w-auto px-7 py-3.5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-sm flex items-center justify-center space-x-2 transition shadow-xs"> <a href="{{ setting('hero_primary_btn_url', route('catalog')) }}" class="w-full sm:w-auto px-6 py-3 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-sm flex items-center justify-center space-x-2 transition shadow-xs">
<span>{{ setting('hero_primary_btn_text', 'Bekijk de Collectie') }}</span> <span>{{ setting('hero_primary_btn_text', 'Bekijk de Collectie') }}</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i> <i data-lucide="arrow-right" class="w-4 h-4"></i>
</a> </a>
<a href="{{ setting('hero_secondary_btn_url', route('catalog', ['category' => 'fidget-toys'])) }}" class="w-full sm:w-auto px-6 py-3.5 rounded-2xl bg-violet-100 hover:bg-violet-200 text-violet-900 font-bold text-sm flex items-center justify-center space-x-2 transition"> <a href="{{ setting('hero_secondary_btn_url', route('catalog', ['category' => 'fidget-toys'])) }}" class="w-full sm:w-auto px-5 py-3 rounded-xl text-slate-700 hover:text-slate-900 hover:bg-slate-100 font-medium text-sm flex items-center justify-center space-x-2 transition">
<span>{{ setting('hero_secondary_btn_text', 'Fidget Toys & Slugs') }}</span> <span>{{ setting('hero_secondary_btn_text', 'Fidget Toys & Slugs') }}</span>
<i data-lucide="sparkles" class="w-4 h-4 text-violet-600"></i> <i data-lucide="sparkles" class="w-4 h-4 text-slate-400"></i>
</a> </a>
</div> </div>
<!-- Technische specificaties / Kwaliteitsbadges --> <!-- Sleek Micro Spec Badges -->
<div class="pt-4 grid grid-cols-3 gap-2 sm:gap-4 max-w-md mx-auto lg:mx-0"> <div class="pt-6 border-t border-slate-200/60 grid grid-cols-3 gap-4 max-w-md mx-auto lg:mx-0 text-left">
<div class="p-3 bg-white border border-slate-200/80 rounded-2xl shadow-2xs text-center"> <div>
<div class="text-base sm:text-xl font-black text-slate-900 font-heading"> <div class="text-base sm:text-lg font-bold text-slate-900">
{{ setting('hero_stat_1_val', '0.12 mm') }} {{ setting('hero_stat_1_val', '0.12 mm') }}
</div> </div>
<div class="text-[10px] sm:text-[11px] font-semibold text-slate-500 mt-0.5"> <div class="text-[11px] text-slate-400 font-normal mt-0.5">
{{ setting('hero_stat_1_lbl', 'Minimale laaghoogte') }} {{ setting('hero_stat_1_lbl', 'Minimale laaghoogte') }}
</div> </div>
</div> </div>
<div class="p-3 bg-white border border-slate-200/80 rounded-2xl shadow-2xs text-center"> <div>
<div class="text-base sm:text-xl font-black text-slate-900 font-heading"> <div class="text-base sm:text-lg font-bold text-slate-900">
{{ setting('hero_stat_2_val', 'Bambu Lab') }} {{ setting('hero_stat_2_val', 'Bambu Lab') }}
</div> </div>
<div class="text-[10px] sm:text-[11px] font-semibold text-slate-500 mt-0.5"> <div class="text-[11px] text-slate-400 font-normal mt-0.5">
{{ setting('hero_stat_2_lbl', 'X1-Carbon Precisie') }} {{ setting('hero_stat_2_lbl', 'X1-Carbon Precisie') }}
</div> </div>
</div> </div>
<div class="p-3 bg-white border border-slate-200/80 rounded-2xl shadow-2xs text-center"> <div>
<div class="text-base sm:text-xl font-black text-slate-900 font-heading"> <div class="text-base sm:text-lg font-bold text-slate-900">
{{ setting('hero_stat_3_val', '2-3 Dagen') }} {{ setting('hero_stat_3_val', '2-3 Dagen') }}
</div> </div>
<div class="text-[10px] sm:text-[11px] font-semibold text-slate-500 mt-0.5"> <div class="text-[11px] text-slate-400 font-normal mt-0.5">
{{ setting('hero_stat_3_lbl', 'Verzending via PostNL') }} {{ setting('hero_stat_3_lbl', 'Verzending via PostNL') }}
</div> </div>
</div> </div>
@@ -66,41 +66,29 @@
</div> </div>
<!-- Right Hero Card / Werkelijke 3D Print Productfoto --> <!-- Right Hero Card: Frameless Editorial Showcase -->
<div class="lg:col-span-5 relative"> <div class="lg:col-span-5 relative">
<div class="relative rounded-3xl bg-white p-3 sm:p-4 border border-slate-200/90 shadow-lg"> <div class="relative rounded-3xl overflow-hidden bg-slate-100 shadow-xl shadow-slate-900/5 aspect-4/3 sm:aspect-square group border border-slate-200/60">
<img
src="/images/products/crystal-dragon.jpg"
alt="Articulated Kristaldraak 3D Print"
class="w-full h-full object-cover object-center group-hover:scale-102 transition-transform duration-700 ease-out"
>
<div class="absolute top-6 right-6 bg-slate-900/90 backdrop-blur-xs text-white font-bold text-[10px] sm:text-xs px-3 py-1 rounded-full shadow-sm z-10"> <!-- Minimalist Floating Pill -->
{{ setting('hero_card_badge', 'Meest gekozen print') }} <div class="absolute bottom-4 left-4 right-4 p-3 rounded-2xl bg-white/90 backdrop-blur-md border border-white/40 shadow-sm flex items-center justify-between">
</div> <div>
<span class="text-[10px] font-semibold uppercase tracking-wider text-slate-400 block">Uitgelicht</span>
<div class="relative h-64 sm:h-84 rounded-2xl overflow-hidden bg-slate-100"> <h3 class="font-heading font-bold text-slate-900 text-xs sm:text-sm">Articulated Kristaldraak</h3>
<img </div>
src="/images/products/crystal-dragon.jpg" <div class="text-right">
alt="Articulated Kristaldraak 3D Print" <span class="text-[10px] text-slate-400 block">Vanaf</span>
class="w-full h-full object-cover object-center hover:scale-105 transition-transform duration-500" <span class="font-heading font-bold text-slate-900 text-xs sm:text-sm">€ 17,95</span>
>
<div class="absolute bottom-2.5 left-2.5 right-2.5 p-3 rounded-xl bg-white/95 backdrop-blur-xs border border-slate-100 shadow-sm flex items-center justify-between">
<div>
<span class="text-[10px] font-bold uppercase tracking-wider text-rose-500 block">Fidget & Sensory</span>
<h3 class="font-heading font-black text-slate-900 text-xs sm:text-sm">Articulated Kristaldraak</h3>
</div>
<div class="text-right">
<span class="text-[10px] text-slate-400 block font-semibold">Vanaf</span>
<span class="font-heading font-black text-slate-900 text-xs sm:text-sm">€ 17,95</span>
</div>
</div> </div>
</div> </div>
<div class="mt-3 flex items-center justify-between text-[11px] text-slate-500 font-semibold px-1"> <div class="absolute top-4 right-4 bg-slate-900/80 backdrop-blur-md text-white text-[11px] font-medium px-3 py-1 rounded-full shadow-xs">
<span class="flex items-center space-x-1"> {{ setting('hero_card_badge', 'Meest gekozen print') }}
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Print-in-place scharnieren</span>
</span>
<span class="flex items-center space-x-1">
<span>Kleur naar keuze selecteerbaar</span>
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -109,156 +97,144 @@
</div> </div>
</section> </section>
<!-- 3 Categorieën Grid (Plat, Helder & Aanpasbaar) --> <!-- 3 Categorieën Strip (Schoon, Modern & Subtiel) -->
<section class="py-12 sm:py-16 bg-white border-b border-slate-200/70"> <section class="py-16 sm:py-20 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-8 gap-2">
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-10 gap-3">
<div> <div>
<span class="text-xs font-bold uppercase tracking-wider text-rose-500"> <span class="text-xs font-semibold uppercase tracking-wider text-slate-400">
{{ setting('categories_tag', 'Collecties') }} {{ setting('categories_tag', 'Collecties') }}
</span> </span>
<h2 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight"> <h2 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight mt-1">
{{ setting('categories_title', 'Ontdek per categorie') }} {{ setting('categories_title', 'Ontdek per categorie') }}
</h2> </h2>
<p class="text-xs text-slate-500 mt-1"> <p class="text-xs sm:text-sm text-slate-500 mt-1 max-w-lg">
{{ setting('categories_subtitle', 'Selecteer een categorie voor alle beschikbare modellen, formaten en kleurstalen.') }} {{ setting('categories_subtitle', 'Selecteer een categorie voor alle beschikbare modellen, formaten en kleurstalen.') }}
</p> </p>
</div> </div>
<a href="{{ route('catalog') }}" class="text-xs font-bold text-rose-600 hover:text-rose-700 flex items-center space-x-1 self-start sm:self-auto"> <a href="{{ route('catalog') }}" class="text-xs font-semibold text-slate-700 hover:text-slate-900 flex items-center space-x-1.5 self-start sm:self-auto transition">
<span>Alle prints bekijken</span> <span>Alles bekijken</span>
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a> </a>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 sm:gap-8">
<!-- 1. Fidget Toys --> <!-- 1. Fidget Toys -->
<a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="rounded-3xl bg-[#FAF7FD] border border-violet-100 hover:border-violet-200 transition-all duration-300 overflow-hidden group shadow-2xs hover:shadow-md flex flex-col justify-between"> <a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="group block">
<div class="h-48 overflow-hidden bg-violet-50"> <div class="aspect-4/3 rounded-2xl overflow-hidden bg-slate-100 border border-slate-200/60 transition-all duration-300 group-hover:border-slate-300 group-hover:shadow-md">
<img src="/images/categories/fidget-toys.jpg" alt="Fidget Toys 3D Prints" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"> <img src="/images/categories/fidget-toys.jpg" alt="Fidget Toys 3D Prints" class="w-full h-full object-cover group-hover:scale-103 transition-transform duration-500">
</div> </div>
<div class="p-5"> <div class="pt-3.5 space-y-1">
<span class="text-[10px] font-bold text-violet-600 uppercase tracking-wider block">Beweegbare Scharnieren</span> <div class="flex items-center justify-between">
<h3 class="font-heading text-lg font-black text-slate-900 group-hover:text-violet-700 transition">Fidget Toys & Sensory</h3> <h3 class="font-heading font-semibold text-base text-slate-900 group-hover:text-sky-600 transition">Fidget Toys & Sensory</h3>
<p class="text-xs text-slate-500 mt-1.5 line-clamp-2 leading-relaxed">Kristaldraken, wriemel-slakken en compacte folding cubes met soepel klikgeluid.</p> <i data-lucide="arrow-up-right" class="w-4 h-4 text-slate-400 group-hover:text-slate-900 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition"></i>
<div class="mt-4 flex items-center text-xs font-black text-violet-600 group-hover:translate-x-1 transition-transform">
<span>Bekijk collectie</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5 ml-1"></i>
</div> </div>
<p class="text-xs text-slate-500 line-clamp-1 font-normal">Kristaldraken, wriemel-slakken en compacte cubes met soepele scharnieren.</p>
</div> </div>
</a> </a>
<!-- 2. Pokémon --> <!-- 2. Pokémon -->
<a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="rounded-3xl bg-[#FFFBF0] border border-amber-100 hover:border-amber-200 transition-all duration-300 overflow-hidden group shadow-2xs hover:shadow-md flex flex-col justify-between"> <a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="group block">
<div class="h-48 overflow-hidden bg-amber-50"> <div class="aspect-4/3 rounded-2xl overflow-hidden bg-slate-100 border border-slate-200/60 transition-all duration-300 group-hover:border-slate-300 group-hover:shadow-md">
<img src="/images/categories/pokemon-3d.jpg" alt="Pokémon 3D Prints" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"> <img src="/images/categories/pokemon-3d.jpg" alt="Pokémon 3D Prints" class="w-full h-full object-cover group-hover:scale-103 transition-transform duration-500">
</div> </div>
<div class="p-5"> <div class="pt-3.5 space-y-1">
<span class="text-[10px] font-bold text-amber-600 uppercase tracking-wider block">Gedetailleerde Figuren</span> <div class="flex items-center justify-between">
<h3 class="font-heading text-lg font-black text-slate-900 group-hover:text-amber-700 transition">Pokémon 3D Prints</h3> <h3 class="font-heading font-semibold text-base text-slate-900 group-hover:text-sky-600 transition">Pokémon 3D Prints</h3>
<p class="text-xs text-slate-500 mt-1.5 line-clamp-2 leading-relaxed">Low-poly Pikachu planters, beweegbare Charizard, sluitende Pokéballs en Gengar.</p> <i data-lucide="arrow-up-right" class="w-4 h-4 text-slate-400 group-hover:text-slate-900 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition"></i>
<div class="mt-4 flex items-center text-xs font-black text-amber-700 group-hover:translate-x-1 transition-transform">
<span>Bekijk collectie</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5 ml-1"></i>
</div> </div>
<p class="text-xs text-slate-500 line-clamp-1 font-normal">Pikachu planters, beweegbare Charizard, Pokéballs en Gengar figuren.</p>
</div> </div>
</a> </a>
<!-- 3. Film & Gaming --> <!-- 3. Film & Gaming -->
<a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="rounded-3xl bg-[#FFF5F5] border border-rose-100 hover:border-rose-200 transition-all duration-300 overflow-hidden group shadow-2xs hover:shadow-md flex flex-col justify-between"> <a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="group block">
<div class="h-48 overflow-hidden bg-rose-50"> <div class="aspect-4/3 rounded-2xl overflow-hidden bg-slate-100 border border-slate-200/60 transition-all duration-300 group-hover:border-slate-300 group-hover:shadow-md">
<img src="/images/categories/film-popculture.jpg" alt="Film & Gaming 3D Prints" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"> <img src="/images/categories/film-popculture.jpg" alt="Film & Gaming 3D Prints" class="w-full h-full object-cover group-hover:scale-103 transition-transform duration-500">
</div> </div>
<div class="p-5"> <div class="pt-3.5 space-y-1">
<span class="text-[10px] font-bold text-rose-500 uppercase tracking-wider block">Collectibles & Display</span> <div class="flex items-center justify-between">
<h3 class="font-heading text-lg font-black text-slate-900 group-hover:text-rose-600 transition">Film, Gaming & Props</h3> <h3 class="font-heading font-semibold text-base text-slate-900 group-hover:text-sky-600 transition">Film, Gaming & Props</h3>
<p class="text-xs text-slate-500 mt-1.5 line-clamp-2 leading-relaxed">Lightsaber standaarden, Thor Mjolnir bureau-organizers en gedetailleerde Grogu figuren.</p> <i data-lucide="arrow-up-right" class="w-4 h-4 text-slate-400 group-hover:text-slate-900 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition"></i>
<div class="mt-4 flex items-center text-xs font-black text-rose-600 group-hover:translate-x-1 transition-transform">
<span>Bekijk collectie</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5 ml-1"></i>
</div> </div>
<p class="text-xs text-slate-500 line-clamp-1 font-normal">Lightsaber displays, Mjolnir desk organizers en iconische filmfiguren.</p>
</div> </div>
</a> </a>
</div> </div>
</div> </div>
</section> </section>
<!-- Uitgelichte Prints Showcase --> <!-- Uitgelichte Prints Showcase (Crisp & Modern Grid) -->
<section class="py-12 sm:py-16 bg-[#FBFBFA]"> <section class="py-16 sm:py-20 bg-slate-50/50 border-t border-slate-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-8 gap-3">
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-10 gap-3">
<div> <div>
<span class="text-xs font-bold uppercase tracking-wider text-rose-500"> <span class="text-xs font-semibold uppercase tracking-wider text-slate-400">
{{ setting('featured_tag', 'Selectie') }} {{ setting('featured_tag', 'Selectie') }}
</span> </span>
<h2 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight"> <h2 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight mt-1">
{{ setting('featured_title', 'Populaire modellen') }} {{ setting('featured_title', 'Populaire modellen') }}
</h2> </h2>
<p class="text-xs text-slate-500 mt-1"> <p class="text-xs sm:text-sm text-slate-500 mt-1">
{{ setting('featured_subtitle', 'Elk item wordt op bestelling geprint in jouw gewenste kleur en met de hand nagekeken.') }} {{ setting('featured_subtitle', 'Elk item wordt op bestelling geprint in jouw gewenste kleur en met de hand nagekeken.') }}
</p> </p>
</div> </div>
<a href="{{ route('catalog') }}" class="px-4 py-2 rounded-xl bg-white border border-slate-200 text-slate-800 text-xs font-bold hover:bg-slate-50 transition flex items-center space-x-1.5 self-start sm:self-auto shadow-2xs"> <a href="{{ route('catalog') }}" class="px-4 py-2 rounded-lg bg-white border border-slate-200 text-slate-700 text-xs font-medium hover:bg-slate-50 transition flex items-center space-x-1.5 self-start sm:self-auto shadow-2xs">
<span>Volledige catalogus</span> <span>Volledige catalogus</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i> <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a> </a>
</div> </div>
<!-- 2 Kolommen mobiel, 3 op desktop --> <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 sm:gap-6">
<div class="grid grid-cols-2 md:grid-cols-3 gap-3 sm:gap-6">
@foreach($featuredProducts as $product) @foreach($featuredProducts as $product)
<div class="rounded-2xl sm:rounded-3xl bg-white border border-slate-200/80 shadow-2xs hover:shadow-md transition-all duration-200 overflow-hidden flex flex-col justify-between group"> <div class="group flex flex-col justify-between bg-white rounded-2xl p-3 border border-slate-200/60 card-hover">
<!-- Afbeelding & Categorie Badge --> <div>
<div class="relative h-44 sm:h-60 overflow-hidden bg-slate-100"> <!-- Product Photo Frame -->
<img <div class="relative aspect-square rounded-xl overflow-hidden bg-slate-50">
src="{{ $product->image_url }}" <img
alt="{{ $product->name }}" src="{{ $product->image_url }}"
class="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-500" alt="{{ $product->name }}"
loading="lazy" class="w-full h-full object-cover object-center group-hover:scale-103 transition-transform duration-500 ease-out"
> loading="lazy"
@if($product->category) >
<div class="absolute top-2 left-2 sm:top-3 sm:left-3"> @if($product->category)
<span class="px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-md bg-white/90 backdrop-blur-xs text-slate-900 text-[9px] sm:text-[10px] font-black shadow-2xs"> <div class="absolute top-2 left-2">
{{ $product->category->name }} <span class="px-2 py-0.5 rounded-md bg-white/90 backdrop-blur-xs text-slate-700 text-[10px] font-medium border border-slate-200/50 shadow-2xs">
</span> {{ $product->category->name }}
</div> </span>
@endif </div>
@endif
</div>
@if($product->print_time_hours) <!-- Product Info -->
<div class="hidden sm:flex absolute bottom-2.5 left-2.5 px-2 py-0.5 rounded-md bg-slate-900/80 backdrop-blur-xs text-white text-[10px] font-medium items-center space-x-1"> <div class="pt-3 pb-1">
<i data-lucide="clock" class="w-3 h-3 text-sky-400"></i> <h3 class="font-heading text-xs sm:text-sm font-semibold text-slate-900 group-hover:text-sky-600 transition line-clamp-1">
<span>{{ $product->print_time_hours }}u printtijd</span>
</div>
@endif
</div>
<!-- Inhoud -->
<div class="p-3.5 sm:p-5 flex-1 flex flex-col justify-between space-y-3">
<div>
<h3 class="font-heading text-xs sm:text-base font-black text-slate-900 group-hover:text-rose-600 transition tracking-tight line-clamp-2">
<a href="{{ route('product.show', $product->slug) }}"> <a href="{{ route('product.show', $product->slug) }}">
{{ $product->name }} {{ $product->name }}
</a> </a>
</h3> </h3>
<p class="hidden sm:block text-xs text-slate-500 mt-1 line-clamp-2 leading-relaxed"> <p class="text-[11px] text-slate-400 mt-0.5 line-clamp-1 font-normal">
{{ $product->short_description }} {{ $product->short_description }}
</p> </p>
</div> </div>
</div>
<!-- Footer met prijs & actieknop --> <!-- Footer: Price & Direct Link -->
<div class="pt-2 sm:pt-3 border-t border-slate-100 flex items-center justify-between"> <div class="pt-2 border-t border-slate-100 flex items-center justify-between mt-2">
<div> <div>
<span class="text-[9px] sm:text-[10px] text-slate-400 font-bold uppercase block">Vanaf</span> <span class="text-[10px] text-slate-400 block font-normal leading-none">Vanaf</span>
<span class="font-heading text-sm sm:text-lg font-black text-slate-900"> <span class="font-heading text-sm font-bold text-slate-900">
{{ $product->formatted_price }} {{ $product->formatted_price }}
</span> </span>
</div>
<a href="{{ route('product.show', $product->slug) }}" class="px-3 sm:px-3.5 py-1.5 sm:py-2 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 text-[10px] sm:text-xs font-bold transition flex items-center space-x-1">
<span>Kies Kleur</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
</a>
</div> </div>
<a href="{{ route('product.show', $product->slug) }}" class="px-2.5 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white text-[11px] font-medium transition flex items-center space-x-1 cursor-pointer">
<span>Kies kleur</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
</a>
</div> </div>
</div> </div>
@@ -267,80 +243,80 @@
</div> </div>
</section> </section>
<!-- Heldere Kwaliteits & Voordelen Banner (Aanpasbaar via Admin) --> <!-- Subtiele USP's (Puur, Wit & Ruimtelijk) -->
<section class="py-12 sm:py-16 bg-white border-t border-slate-200/80"> <section class="py-16 sm:py-20 bg-white border-t border-slate-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center max-w-2xl mx-auto mb-10"> <div class="text-center max-w-xl mx-auto mb-14">
<span class="text-xs font-bold uppercase tracking-wider text-rose-500"> <span class="text-xs font-semibold uppercase tracking-wider text-slate-400">
{{ setting('usp_tag', 'Waarom Jouwdriedee.nl?') }} {{ setting('usp_tag', 'Waarom Jouwdriedee.nl?') }}
</span> </span>
<h2 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight mt-1"> <h2 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight mt-1">
{{ setting('usp_title', 'Kwaliteit en passie in elke printlaag') }} {{ setting('usp_title', 'Kwaliteit en passie in elke printlaag') }}
</h2> </h2>
<p class="text-xs sm:text-sm text-slate-500 mt-2 leading-relaxed"> <p class="text-xs sm:text-sm text-slate-500 mt-2 font-normal leading-relaxed">
{{ setting('usp_subtitle', 'Geen vage levertijden of ruwe prints. Elk model wordt in eigen werkplaats gekalibreerd, geprint en gecontroleerd voordat het veilig verpakt op de post gaat.') }} {{ setting('usp_subtitle', 'Geen vage levertijden of ruwe prints. Elk model wordt in eigen werkplaats gekalibreerd, geprint en gecontroleerd voordat het veilig verpakt op de post gaat.') }}
</p> </p>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<!-- Feature 1 --> <!-- USP 1 -->
<div class="p-5 rounded-3xl bg-[#FAF7FD] border border-violet-100 text-left space-y-2"> <div class="space-y-2">
<div class="w-10 h-10 rounded-2xl bg-violet-200/60 text-violet-700 flex items-center justify-center font-black"> <div class="w-9 h-9 rounded-lg bg-slate-100 text-slate-800 flex items-center justify-center font-medium">
<i data-lucide="printer" class="w-5 h-5"></i> <i data-lucide="printer" class="w-4 h-4"></i>
</div> </div>
<h3 class="font-heading font-black text-slate-900 text-sm"> <h3 class="font-heading font-semibold text-slate-900 text-sm">
{{ setting('usp_1_title', 'Bambu Lab Precisie') }} {{ setting('usp_1_title', 'Bambu Lab Precisie') }}
</h3> </h3>
<p class="text-xs text-slate-500 leading-relaxed font-medium"> <p class="text-xs text-slate-500 leading-relaxed font-normal">
{{ setting('usp_1_desc', 'Haarscherpe lagen vanaf 0.12mm. Vaste toleranties waardoor scharnieren moeiteloos bewegen.') }} {{ setting('usp_1_desc', 'Haarscherpe lagen vanaf 0.12mm. Vaste toleranties waardoor scharnieren moeiteloos bewegen.') }}
</p> </p>
</div> </div>
<!-- Feature 2 --> <!-- USP 2 -->
<div class="p-5 rounded-3xl bg-[#FFFBF0] border border-amber-100 text-left space-y-2"> <div class="space-y-2">
<div class="w-10 h-10 rounded-2xl bg-amber-200/60 text-amber-800 flex items-center justify-center font-black"> <div class="w-9 h-9 rounded-lg bg-slate-100 text-slate-800 flex items-center justify-center font-medium">
<i data-lucide="palette" class="w-5 h-5"></i> <i data-lucide="palette" class="w-4 h-4"></i>
</div> </div>
<h3 class="font-heading font-black text-slate-900 text-sm"> <h3 class="font-heading font-semibold text-slate-900 text-sm">
{{ setting('usp_2_title', '16+ Kleurvariaties') }} {{ setting('usp_2_title', '16+ Kleurvariaties') }}
</h3> </h3>
<p class="text-xs text-slate-500 leading-relaxed font-medium"> <p class="text-xs text-slate-500 leading-relaxed font-normal">
{{ setting('usp_2_desc', 'Van glanzend Silk Goud en Galaxy Zwart tot mat pastel en glow-in-the-dark filament.') }} {{ setting('usp_2_desc', 'Van glanzend Silk Goud en Galaxy Zwart tot mat pastel en glow-in-the-dark filament.') }}
</p> </p>
</div> </div>
<!-- Feature 3 --> <!-- USP 3 -->
<div class="p-5 rounded-3xl bg-[#F0FDF4] border border-emerald-100 text-left space-y-2"> <div class="space-y-2">
<div class="w-10 h-10 rounded-2xl bg-emerald-200/60 text-emerald-800 flex items-center justify-center font-black"> <div class="w-9 h-9 rounded-lg bg-slate-100 text-slate-800 flex items-center justify-center font-medium">
<i data-lucide="shield-check" class="w-5 h-5"></i> <i data-lucide="shield-check" class="w-4 h-4"></i>
</div> </div>
<h3 class="font-heading font-black text-slate-900 text-sm"> <h3 class="font-heading font-semibold text-slate-900 text-sm">
{{ setting('usp_3_title', 'Veilig & Duurzaam PLA+') }} {{ setting('usp_3_title', 'Veilig & Duurzaam PLA+') }}
</h3> </h3>
<p class="text-xs text-slate-500 leading-relaxed font-medium"> <p class="text-xs text-slate-500 leading-relaxed font-normal">
{{ setting('usp_3_desc', 'Gemaakt van hernieuwbare plantaardige zetmelen. Geurloos, gifvrij en stevig in gebruik.') }} {{ setting('usp_3_desc', 'Gemaakt van hernieuwbare plantaardige zetmelen. Geurloos, gifvrij en stevig in gebruik.') }}
</p> </p>
</div> </div>
<!-- Feature 4 --> <!-- USP 4 -->
<div class="p-5 rounded-3xl bg-[#EFF6FF] border border-sky-100 text-left space-y-2"> <div class="space-y-2">
<div class="w-10 h-10 rounded-2xl bg-sky-200/60 text-sky-800 flex items-center justify-center font-black"> <div class="w-9 h-9 rounded-lg bg-slate-100 text-slate-800 flex items-center justify-center font-medium">
<i data-lucide="truck" class="w-5 h-5"></i> <i data-lucide="truck" class="w-4 h-4"></i>
</div> </div>
<h3 class="font-heading font-black text-slate-900 text-sm"> <h3 class="font-heading font-semibold text-slate-900 text-sm">
{{ setting('usp_4_title', 'Snelle Verzending') }} {{ setting('usp_4_title', 'Snelle Verzending') }}
</h3> </h3>
<p class="text-xs text-slate-500 leading-relaxed font-medium"> <p class="text-xs text-slate-500 leading-relaxed font-normal">
{{ setting('usp_4_desc', 'Met zorg verpakt in beschermend materiaal. Binnen 2-3 werkdagen verzonden via PostNL.') }} {{ setting('usp_4_desc', 'Met zorg verpakt in beschermend materiaal. Binnen 2-3 werkdagen verzonden via PostNL.') }}
</p> </p>
</div> </div>
</div> </div>
<div class="mt-8 text-center"> <div class="mt-12 text-center">
<a href="{{ route('catalog') }}" class="inline-flex items-center space-x-2 px-6 py-3 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition shadow-xs"> <a href="{{ route('catalog') }}" class="inline-flex items-center space-x-2 px-5 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-900 font-medium text-xs transition">
<span>{{ setting('usp_btn_text', 'Bekijk alle 3D prints in de winkel') }}</span> <span>{{ setting('usp_btn_text', 'Bekijk alle 3D prints in de winkel') }}</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i> <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a> </a>
</div> </div>
+182 -107
View File
@@ -1,24 +1,24 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', $product->name . ' 🎨 | Jouwdriedee.nl') @section('title', $product->name . ' | Jouwdriedee.nl')
@section('content') @section('content')
<!-- Breadcrumbs --> <!-- Breadcrumbs (Subtiel) -->
<div class="bg-amber-50/50 border-b border-slate-200/80 py-2.5 sm:py-3"> <div class="border-b border-slate-100 bg-white py-3">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-xs font-bold text-slate-500 flex items-center space-x-1.5 overflow-x-auto whitespace-nowrap"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-xs font-normal text-slate-500 flex items-center space-x-2 overflow-x-auto whitespace-nowrap">
<a href="{{ route('home') }}" class="hover:text-rose-600">Home 🏠</a> <a href="{{ route('home') }}" class="hover:text-slate-900 transition">Home</a>
<span>/</span> <span class="text-slate-300">/</span>
<a href="{{ route('catalog') }}" class="hover:text-rose-600">Catalogus 🎁</a> <a href="{{ route('catalog') }}" class="hover:text-slate-900 transition">Catalogus</a>
@if($product->category) @if($product->category)
<span>/</span> <span class="text-slate-300">/</span>
<a href="{{ route('catalog', ['category' => $product->category->slug]) }}" class="hover:text-rose-600">{{ $product->category->name }}</a> <a href="{{ route('catalog', ['category' => $product->category->slug]) }}" class="hover:text-slate-900 transition">{{ $product->category->name }}</a>
@endif @endif
<span>/</span> <span class="text-slate-300">/</span>
<span class="font-black text-slate-900 truncate">{{ $product->name }}</span> <span class="text-slate-900 font-medium truncate">{{ $product->name }}</span>
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-10" <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-14"
x-data="{ x-data="{
basePrice: {{ $product->base_price }}, basePrice: {{ $product->base_price }},
selectedMaterial: '{{ $product->available_materials[0] ?? 'pla' }}', selectedMaterial: '{{ $product->available_materials[0] ?? 'pla' }}',
@@ -51,108 +51,183 @@
} }
}"> }">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-14">
<!-- Linkerkant: Productgalerij & Specificaties --> <!-- Linkerkant: Productgalerij & Details -->
<div class="lg:col-span-6 space-y-4 sm:space-y-6"> <div class="lg:col-span-7 space-y-6">
<div class="rounded-3xl bg-white border border-slate-100 overflow-hidden shadow-sm"> @php
<div class="relative h-72 sm:h-96 md:h-112 bg-slate-100 flex items-center justify-center overflow-hidden"> $gallery = [];
if (!empty($product->image_url)) {
$gallery[] = $product->image_url;
}
if (!empty($product->gallery_images) && is_array($product->gallery_images)) {
foreach ($product->gallery_images as $gImg) {
if (!empty($gImg) && !in_array($gImg, $gallery, true)) {
$gallery[] = $gImg;
}
}
}
if (empty($gallery)) {
$gallery[] = '/images/products/crystal-dragon.jpg';
}
@endphp
<!-- Fotokiezer -->
<div class="space-y-3"
x-data="{
activeImage: '{{ $gallery[0] }}',
images: {{ json_encode($gallery, JSON_UNESCAPED_SLASHES) }},
currentIndex: 0,
nextImage() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.activeImage = this.images[this.currentIndex];
},
prevImage() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
this.activeImage = this.images[this.currentIndex];
},
selectImage(idx) {
this.currentIndex = idx;
this.activeImage = this.images[idx];
}
}">
<div class="rounded-2xl bg-slate-50 overflow-hidden relative group border border-slate-200/60 aspect-4/3 sm:aspect-square">
<img <img
src="{{ $product->image_url }}" :src="activeImage"
alt="{{ $product->name }}" alt="{{ $product->name }}"
class="w-full h-full object-cover object-center" class="w-full h-full object-cover object-center transition-all duration-300"
> >
<div class="absolute top-3 left-3 flex flex-col gap-1.5">
@if($product->is_featured) @if($product->is_featured)
<span class="px-3 py-1 rounded-full bg-gradient-to-r from-rose-500 to-amber-500 text-white font-heading text-xs font-black shadow-sm flex items-center space-x-1"> <div class="absolute top-3 left-3">
<span>🔥</span> <span class="px-2.5 py-1 rounded-md bg-white/95 backdrop-blur-xs text-slate-800 text-[11px] font-medium border border-slate-200/60 shadow-2xs">
<span>Populair</span> Uitgelicht
</span> </span>
@endif </div>
</div> @endif
<!-- Foto Counter Badge -->
@if(count($gallery) > 1)
<div class="absolute bottom-3 right-3">
<span class="px-2.5 py-1 rounded-md bg-white/90 backdrop-blur-xs text-slate-600 border border-slate-200/50 font-mono text-[11px] shadow-2xs">
<span x-text="currentIndex + 1"></span> / {{ count($gallery) }}
</span>
</div>
<!-- Vorige / Volgende Pijlen -->
<button
type="button"
@click="prevImage"
class="absolute left-3 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-white/90 hover:bg-white text-slate-700 shadow-sm flex items-center justify-center transition opacity-0 group-hover:opacity-100 cursor-pointer"
aria-label="Vorige foto"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
</button>
<button
type="button"
@click="nextImage"
class="absolute right-3 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-white/90 hover:bg-white text-slate-700 shadow-sm flex items-center justify-center transition opacity-0 group-hover:opacity-100 cursor-pointer"
aria-label="Volgende foto"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</button>
@endif
</div> </div>
<!-- Thumbnails Strip -->
@if(count($gallery) > 1)
<div class="flex items-center space-x-2 overflow-x-auto pb-1">
<template x-for="(img, idx) in images" :key="idx">
<button
type="button"
@click="selectImage(idx)"
class="relative w-16 h-16 rounded-xl overflow-hidden shrink-0 border transition-all cursor-pointer"
:class="currentIndex === idx ? 'border-slate-900 ring-1 ring-slate-900 opacity-100' : 'border-slate-200 opacity-60 hover:opacity-100'"
>
<img :src="img" alt="Thumbnail" class="w-full h-full object-cover">
</button>
</template>
</div>
@endif
</div> </div>
<!-- Print Specificaties Card (Minder lomp, verfijnd) --> <!-- Print Specificaties Strip (Subtiel) -->
<div class="p-4 sm:p-6 rounded-3xl bg-white border border-slate-100 shadow-xs space-y-3"> <div class="p-5 rounded-2xl bg-slate-50/70 border border-slate-200/60 space-y-4">
<h3 class="font-heading text-xs font-black text-slate-900 uppercase tracking-wider flex items-center space-x-1.5"> <h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">
<span>🖨️</span> Print Specificaties
<span>Print Details & Formaat</span>
</h3> </h3>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 text-xs"> <div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">
<div class="p-3 rounded-2xl bg-slate-50 text-center"> <div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
<span class="text-slate-400 block text-[10px] uppercase font-bold">📏 Formaat</span> <span class="text-slate-400 block text-[10px] uppercase font-medium">Formaat</span>
<span class="font-black text-slate-800 mt-0.5 block font-heading">{{ $product->dimensions ?? 'Standaard' }}</span> <span class="font-semibold text-slate-800 mt-0.5 block">{{ $product->dimensions ?? 'Standaard' }}</span>
</div> </div>
<div class="p-3 rounded-2xl bg-slate-50 text-center"> <div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
<span class="text-slate-400 block text-[10px] uppercase font-bold">⚖️ Gewicht</span> <span class="text-slate-400 block text-[10px] uppercase font-medium">Gewicht</span>
<span class="font-black text-slate-800 mt-0.5 block font-heading">~{{ $product->weight_grams ?? '80' }}g</span> <span class="font-semibold text-slate-800 mt-0.5 block">~{{ $product->weight_grams ?? '80' }}g</span>
</div> </div>
<div class="p-3 rounded-2xl bg-slate-50 text-center"> <div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
<span class="text-slate-400 block text-[10px] uppercase font-bold">⏱️ Printtijd</span> <span class="text-slate-400 block text-[10px] uppercase font-medium">Printduur</span>
<span class="font-black text-slate-800 mt-0.5 block font-heading">{{ $product->print_time_hours ?? '4.0' }} uur</span> <span class="font-semibold text-slate-800 mt-0.5 block">{{ $product->print_time_hours ?? '4.0' }} uur</span>
</div> </div>
<div class="p-3 rounded-2xl bg-slate-50 text-center"> <div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
<span class="text-slate-400 block text-[10px] uppercase font-bold">🔍 Resolutie</span> <span class="text-slate-400 block text-[10px] uppercase font-medium">Resolutie</span>
<span class="font-black text-slate-800 mt-0.5 block font-heading">0.12 - 0.16 mm</span> <span class="font-semibold text-slate-800 mt-0.5 block">0.12 - 0.16 mm</span>
</div> </div>
</div> </div>
<div class="pt-2 flex flex-col sm:flex-row items-center justify-between text-[11px] text-slate-500 font-bold border-t border-slate-100 gap-1.5"> <div class="pt-2 flex flex-col sm:flex-row items-center justify-between text-[11px] text-slate-500 font-normal border-t border-slate-200/50 gap-2">
<span class="flex items-center space-x-1"> <span class="flex items-center space-x-1.5">
<span>✨</span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Geprint op precisie Bambu Lab printers</span> <span>Geprint op precisie Bambu Lab X1/P1 printers</span>
</span>
<span class="flex items-center space-x-1">
<span>🌱</span>
<span>Veilig & biologisch afbreekbaar PLA</span>
</span> </span>
<span>100% biologisch afbreekbaar Bio-PLA+</span>
</div> </div>
</div> </div>
<!-- Productomschrijving --> <!-- Productomschrijving -->
<div class="p-4 sm:p-6 rounded-3xl bg-white border border-slate-100 shadow-xs space-y-2"> <div class="p-5 rounded-2xl bg-white border border-slate-200/60 space-y-3">
<h3 class="font-heading text-sm font-black text-slate-900 flex items-center space-x-1.5"> <h3 class="text-sm font-semibold text-slate-900">
<span>📝</span> Omschrijving
<span>Over deze 3D print</span>
</h3> </h3>
<div class="text-xs text-slate-600 leading-relaxed space-y-1.5 font-medium"> <div class="text-xs text-slate-600 leading-relaxed space-y-2 font-normal">
{!! nl2br($product->description) !!} {!! nl2br(strip_tags($product->description, '<p><br><strong><b><em><i><u><ul><ol><li><h3><h4>')) !!}
</div> </div>
</div> </div>
</div> </div>
<!-- Rechterkant: Interactive Configurator & Bestelmodule --> <!-- Rechterkant: Clean Configurator & Bestelmodule -->
<div class="lg:col-span-6 space-y-6"> <div class="lg:col-span-5 space-y-6">
<div class="p-5 sm:p-8 rounded-3xl bg-white border border-slate-100 shadow-lg shadow-slate-200/40 space-y-5 sm:space-y-6"> <div class="p-6 sm:p-7 rounded-2xl bg-white border border-slate-200/70 shadow-xs space-y-6">
<div> <div class="space-y-1.5">
@if($product->category) @if($product->category)
<span class="inline-block px-2.5 py-0.5 rounded-full bg-amber-100 text-[10px] sm:text-xs font-black text-amber-900 mb-1.5"> <span class="inline-block px-2.5 py-0.5 rounded-md bg-slate-100 text-[10px] font-medium text-slate-700">
{{ $product->category->name }} {{ $product->category->name }}
</span> </span>
@endif @endif
<h1 class="font-heading text-xl sm:text-3xl font-black text-slate-900 tracking-tight">{{ $product->name }}</h1> <h1 class="font-heading text-xl sm:text-2xl font-bold text-slate-900 tracking-tight">{{ $product->name }}</h1>
<p class="text-xs text-slate-500 mt-1 leading-relaxed">{{ $product->short_description }}</p> <p class="text-xs text-slate-500 font-normal leading-relaxed">{{ $product->short_description }}</p>
</div> </div>
<!-- Prijs Display --> <!-- Prijs Display -->
<div class="p-3.5 sm:p-4 rounded-2xl bg-gradient-to-r from-amber-50 to-rose-50 border border-amber-200/60 flex items-center justify-between"> <div class="p-4 rounded-xl bg-slate-50 border border-slate-200/60 flex items-center justify-between">
<div> <div>
<span class="text-[10px] text-slate-500 block font-bold uppercase">Stukprijs</span> <span class="text-[10px] text-slate-400 block uppercase font-medium">Stukprijs</span>
<div class="flex items-baseline space-x-1.5"> <div class="flex items-baseline space-x-1.5">
<span class="font-heading text-2xl sm:text-3xl font-black text-slate-900">€ <span x-text="unitPriceFormatted"></span></span> <span class="font-heading text-2xl font-extrabold text-slate-900">€ <span x-text="unitPriceFormatted"></span></span>
<span class="text-xs text-slate-500 font-normal">incl. btw</span> <span class="text-[11px] text-slate-500">incl. btw</span>
</div> </div>
</div> </div>
<div class="text-right"> <div class="text-right">
<span class="px-2.5 py-1 rounded-full bg-emerald-100 text-emerald-800 text-[11px] font-black inline-flex items-center space-x-1"> <span class="px-2.5 py-1 rounded-full bg-emerald-50 text-emerald-700 text-[11px] font-medium inline-flex items-center space-x-1.5 border border-emerald-200/60">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Print-klaar</span> <span>Op bestelling geprint</span>
</span> </span>
</div> </div>
</div> </div>
@@ -169,9 +244,9 @@
<!-- 1. Materiaal Keuze --> <!-- 1. Materiaal Keuze -->
<div> <div>
<div class="flex items-center justify-between mb-1.5"> <div class="flex items-center justify-between mb-2">
<label class="text-xs font-black text-slate-800 uppercase tracking-wider">1. Kies Materiaal</label> <label class="text-xs font-semibold text-slate-900">1. Materiaal</label>
<span class="text-xs font-black text-rose-500" x-text="selectedMaterial.toUpperCase()"></span> <span class="text-xs font-medium text-slate-500" x-text="selectedMaterial.toUpperCase()"></span>
</div> </div>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
@foreach($product->available_materials ?? ['pla'] as $matCode) @foreach($product->available_materials ?? ['pla'] as $matCode)
@@ -180,40 +255,40 @@
<button <button
type="button" type="button"
@click="selectedMaterial = '{{ $mat->code }}'" @click="selectedMaterial = '{{ $mat->code }}'"
:class="selectedMaterial === '{{ $mat->code }}' ? 'border-amber-400 bg-amber-50 ring-2 ring-amber-400 font-black text-slate-950' : 'border-slate-200 bg-white hover:bg-slate-50 text-slate-700'" :class="selectedMaterial === '{{ $mat->code }}' ? 'border-slate-900 bg-slate-900 text-white shadow-xs' : 'border-slate-200 bg-white hover:bg-slate-50 text-slate-700'"
class="p-2.5 rounded-xl border text-left transition flex flex-col justify-between cursor-pointer" class="p-2.5 rounded-xl border text-left transition flex flex-col justify-between cursor-pointer"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between w-full">
<span class="text-xs font-bold">{{ $mat->name }}</span> <span class="text-xs font-medium">{{ $mat->name }}</span>
@if($mat->price_multiplier > 1.0) @if($mat->price_multiplier > 1.0)
<span class="text-[9px] px-1 py-0.2 rounded bg-slate-100 font-mono text-slate-600">+{{ round(($mat->price_multiplier - 1) * 100) }}%</span> <span class="text-[9px] px-1 rounded font-mono" :class="selectedMaterial === '{{ $mat->code }}' ? 'bg-slate-800 text-slate-300' : 'bg-slate-100 text-slate-600'">+{{ round(($mat->price_multiplier - 1) * 100) }}%</span>
@endif @endif
</div> </div>
<p class="text-[10px] text-slate-500 mt-0.5 line-clamp-1">{{ $mat->description }}</p> <p class="text-[10px] mt-0.5 line-clamp-1" :class="selectedMaterial === '{{ $mat->code }}' ? 'text-slate-300' : 'text-slate-400'">{{ $mat->description }}</p>
</button> </button>
@endif @endif
@endforeach @endforeach
</div> </div>
</div> </div>
<!-- 2. Kleur Selectie (Swatches met mooie afwerking) --> <!-- 2. Kleur Selectie -->
<div> <div>
<div class="flex items-center justify-between mb-1.5"> <div class="flex items-center justify-between mb-2">
<label class="text-xs font-black text-slate-800 uppercase tracking-wider">2. Kies Jouw Kleur</label> <label class="text-xs font-semibold text-slate-900">2. Kleur</label>
<span class="text-xs font-bold text-rose-600" x-text="selectedColor"></span> <span class="text-xs font-medium text-slate-600" x-text="selectedColor"></span>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@foreach($allColors as $c) @foreach($allColors as $c)
<button <button
type="button" type="button"
@click="selectedColor = '{{ $c->name }}'" @click="selectedColor = '{{ $c->name }}'"
:class="selectedColor === '{{ $c->name }}' ? 'ring-3 ring-rose-500 ring-offset-2 scale-110' : 'hover:scale-105 border-slate-300'" :class="selectedColor === '{{ $c->name }}' ? 'ring-2 ring-slate-900 ring-offset-2 scale-105' : 'hover:scale-105 border-slate-200 opacity-90 hover:opacity-100'"
class="w-8 h-8 rounded-xl border shadow-xs transition flex items-center justify-center cursor-pointer relative" class="w-7 h-7 rounded-full border shadow-2xs transition flex items-center justify-center cursor-pointer relative"
style="background-color: {{ $c->hex }};" style="background-color: {{ $c->hex }};"
title="{{ $c->name }}" title="{{ $c->name }}"
> >
<template x-if="selectedColor === '{{ $c->name }}'"> <template x-if="selectedColor === '{{ $c->name }}'">
<i data-lucide="check" class="w-4 h-4 {{ in_array($c->name, ['Sneeuwwit', 'Zon Geel (Pikachu)']) ? 'text-slate-950' : 'text-white' }} stroke-[3]"></i> <i data-lucide="check" class="w-3.5 h-3.5 {{ in_array($c->name, ['Sneeuwwit', 'Zon Geel (Pikachu)']) ? 'text-slate-950' : 'text-white' }} stroke-[3]"></i>
</template> </template>
</button> </button>
@endforeach @endforeach
@@ -222,59 +297,59 @@
<!-- 3. Formaat & Schaal --> <!-- 3. Formaat & Schaal -->
<div> <div>
<div class="flex items-center justify-between mb-1.5"> <div class="flex items-center justify-between mb-2">
<label class="text-xs font-black text-slate-800 uppercase tracking-wider">3. Formaat / Schaal</label> <label class="text-xs font-semibold text-slate-900">3. Formaat</label>
<span class="text-xs font-bold text-slate-700" x-text="selectedScale"></span> <span class="text-xs font-medium text-slate-600" x-text="selectedScale"></span>
</div> </div>
<div class="grid grid-cols-3 gap-2 text-xs"> <div class="grid grid-cols-3 gap-2 text-xs">
<button <button
type="button" type="button"
@click="selectedScale = '100%'" @click="selectedScale = '100%'"
:class="selectedScale === '100%' ? 'border-rose-400 bg-rose-50 ring-2 ring-rose-300 font-black text-slate-900' : 'border-slate-200 font-bold hover:bg-slate-50'" :class="selectedScale === '100%' ? 'border-slate-900 bg-slate-900 text-white font-medium shadow-xs' : 'border-slate-200 text-slate-700 hover:bg-slate-50'"
class="p-2.5 rounded-xl border text-center transition cursor-pointer" class="p-2 rounded-lg border text-center transition cursor-pointer"
> >
<span>100% (Normaal)</span> <span>100% (Normaal)</span>
</button> </button>
<button <button
type="button" type="button"
@click="selectedScale = '150%'" @click="selectedScale = '150%'"
:class="selectedScale === '150%' ? 'border-rose-400 bg-rose-50 ring-2 ring-rose-300 font-black text-slate-900' : 'border-slate-200 font-bold hover:bg-slate-50'" :class="selectedScale === '150%' ? 'border-slate-900 bg-slate-900 text-white font-medium shadow-xs' : 'border-slate-200 text-slate-700 hover:bg-slate-50'"
class="p-2.5 rounded-xl border text-center transition cursor-pointer" class="p-2 rounded-lg border text-center transition cursor-pointer"
> >
<span>150% (+45%)</span> <span>150% (+45%)</span>
</button> </button>
<button <button
type="button" type="button"
@click="selectedScale = '200%'" @click="selectedScale = '200%'"
:class="selectedScale === '200%' ? 'border-rose-400 bg-rose-50 ring-2 ring-rose-300 font-black text-slate-900' : 'border-slate-200 font-bold hover:bg-slate-50'" :class="selectedScale === '200%' ? 'border-slate-900 bg-slate-900 text-white font-medium shadow-xs' : 'border-slate-200 text-slate-700 hover:bg-slate-50'"
class="p-2.5 rounded-xl border text-center transition cursor-pointer" class="p-2 rounded-lg border text-center transition cursor-pointer"
> >
<span>200% (+95%)</span> <span>200% (+95%)</span>
</button> </button>
</div> </div>
</div> </div>
<!-- Aantal en Toevoegen Knop --> <!-- Aantal en Bestelknop -->
<div class="pt-3 border-t border-slate-100 space-y-3"> <div class="pt-2 border-t border-slate-100 space-y-3">
<div class="flex items-center space-x-2.5"> <div class="flex items-center space-x-2.5">
<!-- Stepper --> <!-- Stepper -->
<div class="flex items-center border border-slate-200 rounded-xl bg-slate-50 overflow-hidden"> <div class="flex items-center border border-slate-200 rounded-xl bg-slate-50 overflow-hidden">
<button type="button" @click="if (quantity > 1) quantity--" class="px-3 py-2.5 hover:bg-slate-200 transition text-slate-700 font-black cursor-pointer text-sm">-</button> <button type="button" @click="if (quantity > 1) quantity--" class="px-3 py-2 hover:bg-slate-200 transition text-slate-700 font-semibold cursor-pointer text-xs">-</button>
<span class="px-3.5 py-2.5 font-heading font-black text-xs text-slate-900" x-text="quantity"></span> <span class="px-3.5 py-2 font-mono text-xs font-semibold text-slate-900" x-text="quantity"></span>
<button type="button" @click="if (quantity < 50) quantity++" class="px-3 py-2.5 hover:bg-slate-200 transition text-slate-700 font-black cursor-pointer text-sm">+</button> <button type="button" @click="if (quantity < 50) quantity++" class="px-3 py-2 hover:bg-slate-200 transition text-slate-700 font-semibold cursor-pointer text-xs">+</button>
</div> </div>
<!-- Knop Desktop --> <!-- Bestelknop -->
<button type="submit" class="playful-btn flex-1 py-3 px-5 rounded-2xl bg-gradient-to-r from-amber-400 to-rose-400 hover:from-amber-500 hover:to-rose-500 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-1.5 shadow-md shadow-rose-500/20 cursor-pointer"> <button type="submit" class="flex-1 py-2.5 px-4 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
<span>🛒</span> <i data-lucide="shopping-bag" class="w-4 h-4"></i>
<span>In Winkelmandje • € <span x-text="calculatedPrice"></span></span> <span>In winkelwagen • € <span x-text="calculatedPrice"></span></span>
</button> </button>
</div> </div>
<div class="flex items-center justify-center space-x-3 text-[11px] text-slate-500 font-bold pt-1"> <div class="flex items-center justify-center space-x-3 text-[11px] text-slate-400 font-normal pt-1">
<span>🚚 Gratis verzending vanaf €50</span> <span>Gratis verzending vanaf €50</span>
<span>•</span> <span>•</span>
<span>📦 PostNL Track & Trace</span> <span>PostNL Track & Trace</span>
</div> </div>
</div> </div>
+2 -2
View File
@@ -8,8 +8,8 @@ Artisan::command('inspire', function () {
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote'); })->purpose('Display an inspiring quote');
// Automatische MakerWorld Collecties Synchronisatie iedere 10 minuten // Automatische MakerWorld Collecties Synchronisatie iedere 5 minuten
Schedule::command('app:sync-makerworld-collections') Schedule::command('app:sync-makerworld-collections')
->everyTenMinutes() ->everyFiveMinutes()
->withoutOverlapping() ->withoutOverlapping()
->runInBackground(); ->runInBackground();
+31 -6
View File
@@ -2,7 +2,9 @@
use App\Http\Controllers\AccountController; 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\CommunicationController as AdminCommunicationController;
use App\Http\Controllers\Admin\ContentController; use App\Http\Controllers\Admin\ContentController;
use App\Http\Controllers\Admin\CustomerController as AdminCustomerController;
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;
@@ -11,6 +13,7 @@ use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController; 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\ContactController;
use App\Http\Controllers\ShopController; use App\Http\Controllers\ShopController;
use App\Models\Category; use App\Models\Category;
use App\Models\Product; use App\Models\Product;
@@ -20,8 +23,12 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
// Health & Diagnostics // Health & Diagnostics (Alleen toegankelijk in debug-modus of voor ingelogde admins)
Route::get('/health-debug', function () { Route::get('/health-debug', function () {
if (! config('app.debug') && ! (auth()->check() && auth()->user()->isAdmin())) {
abort(404);
}
$dbPath = config('database.connections.sqlite.database'); $dbPath = config('database.connections.sqlite.database');
$diagnostics = [ $diagnostics = [
@@ -76,25 +83,29 @@ Route::post('/winkelwagen/bijwerken', [CartController::class, 'update'])->name('
Route::delete('/winkelwagen/verwijderen/{key}', [CartController::class, 'remove'])->name('cart.remove'); Route::delete('/winkelwagen/verwijderen/{key}', [CartController::class, 'remove'])->name('cart.remove');
Route::get('/winkelwagen/aantal', [CartController::class, 'count'])->name('cart.count'); Route::get('/winkelwagen/aantal', [CartController::class, 'count'])->name('cart.count');
// Contact & Klantvraag stellen
Route::get('/contact', [ContactController::class, 'index'])->name('contact.index');
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store')->middleware('throttle:10,1');
// Afrekenen & Bevestiging // Afrekenen & Bevestiging
Route::get('/afrekenen', [CheckoutController::class, 'index'])->name('checkout.index'); Route::get('/afrekenen', [CheckoutController::class, 'index'])->name('checkout.index');
Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store'); Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store')->middleware('throttle:10,1');
Route::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success'); Route::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success');
// Authenticatie & Klantenportaal // Authenticatie & Klantenportaal
Route::middleware('guest')->group(function () { Route::middleware('guest')->group(function () {
Route::get('/inloggen', [LoginController::class, 'showLoginForm'])->name('login'); Route::get('/inloggen', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit'); Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit')->middleware('throttle:5,1');
Route::redirect('/login', '/inloggen'); Route::redirect('/login', '/inloggen');
Route::get('/registreren', [RegisterController::class, 'showRegistrationForm'])->name('register'); Route::get('/registreren', [RegisterController::class, 'showRegistrationForm'])->name('register');
Route::post('/registreren', [RegisterController::class, 'register'])->name('register.submit'); Route::post('/registreren', [RegisterController::class, 'register'])->name('register.submit')->middleware('throttle:6,1');
Route::redirect('/register', '/registreren'); Route::redirect('/register', '/registreren');
Route::get('/wachtwoord-vergeten', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request'); Route::get('/wachtwoord-vergeten', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/wachtwoord-vergeten', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email'); Route::post('/wachtwoord-vergeten', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email')->middleware('throttle:3,1');
Route::get('/wachtwoord-resetten/{token}', [ForgotPasswordController::class, 'showResetForm'])->name('password.reset'); Route::get('/wachtwoord-resetten/{token}', [ForgotPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/wachtwoord-resetten', [ForgotPasswordController::class, 'reset'])->name('password.update'); Route::post('/wachtwoord-resetten', [ForgotPasswordController::class, 'reset'])->name('password.update')->middleware('throttle:5,1');
}); });
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
@@ -116,8 +127,11 @@ Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(fun
Route::post('/content', [ContentController::class, 'update'])->name('content.update'); Route::post('/content', [ContentController::class, 'update'])->name('content.update');
Route::post('/content/reset', [ContentController::class, 'reset'])->name('content.reset'); Route::post('/content/reset', [ContentController::class, 'reset'])->name('content.reset');
Route::post('/content/test-mail', [ContentController::class, 'sendTestMail'])->name('content.test-mail'); Route::post('/content/test-mail', [ContentController::class, 'sendTestMail'])->name('content.test-mail');
Route::post('/toggle-offline', [ContentController::class, 'toggleOffline'])->name('toggle-offline');
// Producten Beheer // Producten Beheer
Route::post('/products/upload-image', [AdminProductController::class, 'uploadImage'])->name('products.upload-image');
Route::post('/products/calculate-price', [AdminProductController::class, 'calculatePrice'])->name('products.calculate-price');
Route::resource('products', AdminProductController::class)->except(['show']); Route::resource('products', AdminProductController::class)->except(['show']);
// Categorieën Beheer // Categorieën Beheer
@@ -128,6 +142,17 @@ Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(fun
Route::get('/orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show'); Route::get('/orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
Route::post('/orders/{order}/status', [AdminOrderController::class, 'updateStatus'])->name('orders.status'); Route::post('/orders/{order}/status', [AdminOrderController::class, 'updateStatus'])->name('orders.status');
// Communicatie Beheer (Vragen van klanten & antwoorden)
Route::get('/communicatie', [AdminCommunicationController::class, 'index'])->name('communications.index');
Route::get('/communicatie/{communication}', [AdminCommunicationController::class, 'show'])->name('communications.show');
Route::post('/communicatie/{communication}/status', [AdminCommunicationController::class, 'updateStatus'])->name('communications.status');
Route::post('/communicatie/{communication}/reply', [AdminCommunicationController::class, 'sendReply'])->name('communications.reply');
Route::delete('/communicatie/{communication}', [AdminCommunicationController::class, 'destroy'])->name('communications.destroy');
// Klanten & Klantprofielen Beheer
Route::get('/klanten', [AdminCustomerController::class, 'index'])->name('customers.index');
Route::get('/klanten/{customer}', [AdminCustomerController::class, 'show'])->name('customers.show');
// MakerWorld Importer & Collecties Auto-Sync // MakerWorld Importer & Collecties Auto-Sync
Route::get('/makerworld', [MakerWorldController::class, 'index'])->name('makerworld.index'); Route::get('/makerworld', [MakerWorldController::class, 'index'])->name('makerworld.index');
Route::post('/makerworld/login', [MakerWorldController::class, 'login'])->name('makerworld.login'); Route::post('/makerworld/login', [MakerWorldController::class, 'login'])->name('makerworld.login');
+257
View File
@@ -7,7 +7,11 @@ 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 App\Models\User;
use App\Services\PriceCalculatorService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase; use Tests\TestCase;
class AdminContentTest extends TestCase class AdminContentTest extends TestCase
@@ -174,4 +178,257 @@ class AdminContentTest extends TestCase
$showResponse->assertStatus(200); $showResponse->assertStatus(200);
$showResponse->assertSee('ORD-TEST-999'); $showResponse->assertSee('ORD-TEST-999');
} }
public function test_admin_can_update_product_images_and_gallery(): void
{
$category = Category::create([
'name' => 'Fidget Gadgets',
'slug' => 'fidget-gadgets-gallery-test',
]);
$product = Product::create([
'category_id' => $category->id,
'name' => 'Multi Foto Draak',
'slug' => 'multi-foto-draak',
'short_description' => 'Test met meerdere fotos',
'description' => 'Test beschrijving met galerij',
'base_price' => 19.95,
'image_url' => 'https://cdn.example.com/cover.jpg',
'gallery_images' => ['https://cdn.example.com/photo1.jpg', 'https://cdn.example.com/photo2.jpg'],
'is_active' => true,
]);
// Edit page loads with visual image gallery
$editResponse = $this->actingAs($this->admin)->get(route('admin.products.edit', $product));
$editResponse->assertStatus(200);
$editResponse->assertSee('Productafbeeldingen & Fotogalerij', false);
$editResponse->assertSee('Hoofdafbeelding (Cover)');
// Update product with new cover and extra gallery images
$updateResponse = $this->actingAs($this->admin)->put(route('admin.products.update', $product), [
'name' => 'Multi Foto Draak Bijgewerkt',
'category_id' => $category->id,
'base_price' => 21.50,
'short_description' => 'Geüpdatet',
'description' => 'Nieuwe omschrijving',
'image_url' => 'https://cdn.example.com/new-cover.jpg',
'gallery_images' => [
'https://cdn.example.com/photo1.jpg',
'https://cdn.example.com/photo2.jpg',
'https://cdn.example.com/photo3.jpg',
'https://cdn.example.com/new-cover.jpg', // Should be excluded because it matches cover
],
'is_active' => 1,
]);
$updateResponse->assertRedirect(route('admin.products.index'));
$product->refresh();
$this->assertEquals('https://cdn.example.com/new-cover.jpg', $product->image_url);
$this->assertCount(3, $product->gallery_images);
$this->assertContains('https://cdn.example.com/photo3.jpg', $product->gallery_images);
$this->assertNotContains('https://cdn.example.com/new-cover.jpg', $product->gallery_images);
// Storefront product details page shows the multi-image gallery
$shopResponse = $this->get(route('product.show', $product->slug));
$shopResponse->assertStatus(200);
$shopResponse->assertSee('https://cdn.example.com/new-cover.jpg');
$shopResponse->assertSee('https://cdn.example.com/photo1.jpg');
$shopResponse->assertSee('https://cdn.example.com/photo3.jpg');
}
public function test_admin_can_upload_product_image(): void
{
Storage::fake('public');
$onePixelJpeg = base64_decode('/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA=');
$file = UploadedFile::fake()->createWithContent('test-print-photo.jpg', $onePixelJpeg);
$response = $this->actingAs($this->admin)->postJson(route('admin.products.upload-image'), [
'image' => $file,
]);
$response->assertStatus(200);
$response->assertJson([
'success' => true,
]);
$url = $response->json('url');
$this->assertStringStartsWith('/storage/products/', $url);
$storagePath = str_replace('/storage/', '', $url);
Storage::disk('public')->assertExists($storagePath);
}
public function test_admin_can_toggle_webshop_offline_mode(): void
{
$this->assertEquals('0', Setting::get('shop_offline', '0'));
// Toggle offline via admin button
$response = $this->actingAs($this->admin)->post(route('admin.toggle-offline'));
$response->assertRedirect();
Setting::flushCache();
$this->assertEquals('1', Setting::get('shop_offline'));
// Logout admin so subsequent requests are unauthenticated guest requests
auth()->logout();
$this->flushSession();
// Guest visitor receives 503 with big red offline banner
$guestResponse = $this->get(route('home'));
$guestResponse->assertStatus(503);
$guestResponse->assertSee('Aan deze webshop wordt hard gewerkt', false);
$guestResponse->assertSee('Webshop Tijdelijk Offline');
// Other customer routes are blocked
$catalogResponse = $this->get(route('catalog'));
$catalogResponse->assertStatus(503);
// Login page remains accessible
$loginResponse = $this->get(route('login'));
$loginResponse->assertStatus(200);
// Admin can still view storefront normally with preview banner
$adminShopResponse = $this->actingAs($this->admin)->get(route('home'));
$adminShopResponse->assertStatus(200);
$adminShopResponse->assertSee('OFFLINE BEHEERDERS-PREVIEW');
// Toggle back online
$this->actingAs($this->admin)->post(route('admin.toggle-offline'));
Setting::flushCache();
$this->assertEquals('0', Setting::get('shop_offline'));
// Logout admin and verify guest can visit normally again
auth()->logout();
$this->flushSession();
$guestOnlineResponse = $this->get(route('home'));
$guestOnlineResponse->assertStatus(200);
}
public function test_site_sends_modern_security_headers(): void
{
$response = $this->get('/');
$response->assertHeader('X-Frame-Options', 'SAMEORIGIN');
$response->assertHeader('X-Content-Type-Options', 'nosniff');
$response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
public function test_3d_print_price_calculator_service_calculates_correctly(): void
{
$calculator = app(PriceCalculatorService::class);
// 100 grams, 4 hours, 1 color, PLA, 40% margin
$result = $calculator->calculate(
weightGrams: 100,
printTimeHours: 4.0,
colorCount: 1,
material: 'pla',
customMarkupPercent: 40.0
);
$this->assertArrayHasKey('suggested_price', $result);
$this->assertArrayHasKey('suggested_price_formatted', $result);
$this->assertArrayHasKey('breakdown', $result);
$this->assertArrayHasKey('material_cost', $result['breakdown']);
$this->assertArrayHasKey('electricity_cost', $result['breakdown']);
$this->assertArrayHasKey('machine_cost', $result['breakdown']);
$this->assertArrayHasKey('total_cost_price', $result['breakdown']);
$this->assertGreaterThan(5.0, $result['suggested_price']);
// Multi-color test: 4 colors should cost more than 1 color due to purge waste & setup
$multiColorResult = $calculator->calculate(
weightGrams: 100,
printTimeHours: 4.0,
colorCount: 4,
material: 'pla',
customMarkupPercent: 40.0
);
$this->assertGreaterThan($result['suggested_price'], $multiColorResult['suggested_price']);
$this->assertGreaterThan($result['breakdown']['material_cost'], $multiColorResult['breakdown']['material_cost']);
}
public function test_admin_can_use_calculate_price_api_endpoint(): void
{
$response = $this->actingAs($this->admin)->postJson(route('admin.products.calculate-price'), [
'weight_grams' => 120,
'print_time_hours' => 5.5,
'color_count' => 2,
'material' => 'petg',
'markup_percent' => 50,
]);
$response->assertStatus(200);
$response->assertJsonStructure([
'success',
'data' => [
'inputs',
'rates',
'breakdown' => [
'effective_weight_grams',
'material_cost',
'electricity_cost',
'machine_cost',
'handling_cost',
'failure_cost',
'total_cost_price',
'profit_amount',
'price_excl_vat',
'vat_amount',
'price_incl_vat_exact',
],
'suggested_price',
'suggested_price_formatted',
],
]);
}
public function test_admin_can_save_calculator_tariffs_in_content_settings(): void
{
$response = $this->actingAs($this->admin)->post(route('admin.content.update'), [
'calc_filament_price_kg' => '22.50',
'calc_electricity_kwh' => '0.40',
'calc_printer_wattage' => '180',
'calc_machine_hourly_rate' => '0.65',
'calc_handling_flat' => '2.00',
'calc_default_markup' => '45',
'calc_failure_rate' => '8',
]);
$response->assertRedirect(route('admin.content.index'));
Setting::flushCache();
$this->assertEquals(22.50, (float) Setting::get('calc_filament_price_kg'));
$this->assertEquals(0.40, (float) Setting::get('calc_electricity_kwh'));
$this->assertEquals(180, (int) Setting::get('calc_printer_wattage'));
$this->assertEquals(0.65, (float) Setting::get('calc_machine_hourly_rate'));
$this->assertEquals(2.00, (float) Setting::get('calc_handling_flat'));
$this->assertEquals(45, (int) Setting::get('calc_default_markup'));
$this->assertEquals(8, (int) Setting::get('calc_failure_rate'));
}
public function test_sending_test_mail_redirects_and_stays_on_email_tab(): void
{
Mail::fake();
$response = $this->actingAs($this->admin)->post(route('admin.content.test-mail'), [
'test_email' => 'niek.rabelink@gmail.com',
]);
$response->assertRedirect(route('admin.content.index', ['tab' => 'email']));
$response->assertSessionHas('success');
}
public function test_updating_content_with_active_tab_preserves_tab(): void
{
$response = $this->actingAs($this->admin)->post(route('admin.content.update'), [
'active_tab' => 'calculator',
'site_name' => 'Jouwdriedee 3D Print Studio',
]);
$response->assertRedirect(route('admin.content.index', ['tab' => 'calculator']));
$response->assertSessionHas('success');
}
} }
+245
View File
@@ -0,0 +1,245 @@
<?php
namespace Tests\Feature;
use App\Mail\ContactInquiryConfirmationMail;
use App\Mail\ContactInquiryReceivedMail;
use App\Mail\ContactInquiryReplyMail;
use App\Models\Communication;
use App\Models\Order;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class CustomerCommunicationTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private User $customer;
protected function setUp(): void
{
parent::setUp();
$this->admin = User::factory()->create([
'name' => 'Admin Beheerder',
'email' => 'admin@jouwdriedee.nl',
'is_admin' => true,
]);
$this->customer = User::factory()->create([
'name' => 'Kees de Vries',
'email' => 'kees@example.com',
'is_admin' => false,
]);
}
public function test_contact_page_loads_successfully(): void
{
$response = $this->get(route('contact.index'));
$response->assertStatus(200);
$response->assertSee('Stel een vraag');
$response->assertSee('Directe Contactgegevens');
}
public function test_customer_can_submit_question_and_both_emails_are_dispatched(): void
{
Mail::fake();
$response = $this->post(route('contact.store'), [
'name' => 'Lisa Jansen',
'email' => 'lisa@example.com',
'phone' => '06-87654321',
'subject' => 'Vraag over levertijd van Pokémon model',
'message' => 'Goedemiddag, hoe lang duurt het printen van het Charizard model in PETG?',
]);
$response->assertRedirect(route('contact.index'));
$response->assertSessionHas('success');
// Check database
$this->assertDatabaseHas('communications', [
'name' => 'Lisa Jansen',
'email' => 'lisa@example.com',
'subject' => 'Vraag over levertijd van Pokémon model',
'status' => 'new',
]);
$communication = Communication::where('email', 'lisa@example.com')->first();
$this->assertNotNull($communication);
// 1. Mail to webshop management (info@jouwdrieedee.nl / info@jouwdriedee.nl)
Mail::assertSent(ContactInquiryReceivedMail::class, function ($mail) use ($communication) {
return $mail->communication->id === $communication->id;
});
// 2. Afschrift to customer from (noreply@jouwdriedee.nl)
Mail::assertSent(ContactInquiryConfirmationMail::class, function ($mail) use ($communication) {
$hasCorrectRecipient = $mail->hasTo('lisa@example.com');
$hasCorrectFrom = $mail->hasFrom('noreply@jouwdriedee.nl');
return $hasCorrectRecipient && $hasCorrectFrom && $mail->communication->id === $communication->id;
});
}
public function test_logged_in_customer_inquiry_is_linked_and_visible_in_account(): void
{
Mail::fake();
$response = $this->actingAs($this->customer)->post(route('contact.store'), [
'name' => $this->customer->name,
'email' => $this->customer->email,
'subject' => 'Speciale kleur vraag',
'message' => 'Kan het model ook in glow-in-the-dark filament worden geprint?',
]);
$response->assertRedirect(route('contact.index'));
$this->assertDatabaseHas('communications', [
'user_id' => $this->customer->id,
'email' => 'kees@example.com',
'subject' => 'Speciale kleur vraag',
]);
// Customer dashboard in /mijn-account shows their question
$accountResponse = $this->actingAs($this->customer)->get(route('account.index'));
$accountResponse->assertStatus(200);
$accountResponse->assertSee('Speciale kleur vraag');
$accountResponse->assertSee('Mijn Vragen');
}
public function test_admin_can_view_communications_in_dedicated_admin_section(): void
{
$comm = Communication::create([
'name' => 'Peter Bakker',
'email' => 'peter@example.com',
'subject' => 'Garantie op 3D prints',
'message' => 'Wat voor garantie zit er op technische 3D prints?',
'status' => 'new',
]);
// Admin list
$indexResponse = $this->actingAs($this->admin)->get(route('admin.communications.index'));
$indexResponse->assertStatus(200);
$indexResponse->assertSee('Communicatie');
$indexResponse->assertSee('Garantie op 3D prints');
$indexResponse->assertSee('Peter Bakker');
// Admin detail
$showResponse = $this->actingAs($this->admin)->get(route('admin.communications.show', $comm));
$showResponse->assertStatus(200);
$showResponse->assertSee('Garantie op 3D prints');
$showResponse->assertSee('Wat voor garantie zit er op technische 3D prints?');
// Status is automatically transitioned from new to in_progress upon opening
$this->assertEquals('in_progress', $comm->fresh()->status);
}
public function test_admin_can_reply_to_customer_from_admin_section(): void
{
Mail::fake();
$comm = Communication::create([
'name' => 'Sophie Visser',
'email' => 'sophie@example.com',
'subject' => 'Custom formaat mogelijk?',
'message' => 'Kan de plantenbak ook 30cm hoog geprint worden?',
'status' => 'in_progress',
]);
$replyResponse = $this->actingAs($this->admin)->post(route('admin.communications.reply', $comm), [
'message' => 'Beste Sophie, ja zeker! We kunnen dit model opschalen naar 30cm. De levertijd is 3 dagen.',
]);
$replyResponse->assertRedirect();
$replyResponse->assertSessionHas('success');
// Verify status and reply in database
$this->assertEquals('resolved', $comm->fresh()->status);
$this->assertNotNull($comm->fresh()->last_replied_at);
$this->assertDatabaseHas('communication_replies', [
'communication_id' => $comm->id,
'is_admin_reply' => true,
]);
// Verify email was sent to customer from info@jouwdriedee.nl
Mail::assertSent(ContactInquiryReplyMail::class, function ($mail) {
return $mail->hasTo('sophie@example.com') && $mail->hasFrom('info@jouwdriedee.nl');
});
}
public function test_admin_can_view_communication_inside_customer_profile(): void
{
// Link inquiry to customer
$comm = Communication::create([
'user_id' => $this->customer->id,
'name' => $this->customer->name,
'email' => $this->customer->email,
'subject' => 'Vraag binnen profiel',
'message' => 'Dit is een vraag die in het klantprofiel zichtbaar moet zijn.',
'status' => 'new',
]);
// Also create an order for this customer
Order::create([
'user_id' => $this->customer->id,
'order_number' => 'ORD-TEST-999',
'customer_name' => $this->customer->name,
'customer_email' => $this->customer->email,
'shipping_street' => 'Dorpsstraat',
'shipping_number' => '1',
'shipping_postal_code' => '1234 AB',
'shipping_city' => 'Utrecht',
'subtotal' => 45.00,
'shipping_cost' => 0.00,
'total' => 45.00,
'status' => 'pending',
'payment_status' => 'paid',
]);
// 1. Customers overview in admin
$indexResponse = $this->actingAs($this->admin)->get(route('admin.customers.index'));
$indexResponse->assertStatus(200);
$indexResponse->assertSee($this->customer->name);
$indexResponse->assertSee($this->customer->email);
// 2. Customer profile in admin (by user ID)
$profileResponse = $this->actingAs($this->admin)->get(route('admin.customers.show', $this->customer->id));
$profileResponse->assertStatus(200);
$profileResponse->assertSee('Klantprofiel: '.$this->customer->name);
$profileResponse->assertSee('Communicatie &amp; Vragen', false);
$profileResponse->assertSee('Vraag binnen profiel');
$profileResponse->assertSee('ORD-TEST-999');
// 3. Customer profile in admin by email
$profileByEmail = $this->actingAs($this->admin)->get(route('admin.customers.show', $this->customer->email));
$profileByEmail->assertStatus(200);
$profileByEmail->assertSee('Vraag binnen profiel');
}
public function test_guest_and_non_admin_cannot_access_admin_communications_or_customers(): void
{
// Guests redirected to login
$this->get(route('admin.communications.index'))->assertRedirect(route('login'));
$this->get(route('admin.customers.index'))->assertRedirect(route('login'));
// Normal customer is redirected to home with error flash
$this->actingAs($this->customer)->get(route('admin.communications.index'))->assertRedirect(route('home'));
$this->actingAs($this->customer)->get(route('admin.customers.index'))->assertRedirect(route('home'));
}
public function test_admin_layout_does_not_crash_even_if_communications_table_is_initially_missing(): void
{
Schema::dropIfExists('communication_replies');
Schema::dropIfExists('communications');
// Layout renders with HTTP 200 without 500 server error even when table was absent
$response = $this->actingAs($this->admin)->get(route('admin.content.index'));
$response->assertStatus(200);
}
}
+6 -1
View File
@@ -2,9 +2,14 @@
namespace Tests; namespace Tests;
use App\Models\Setting;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
// protected function setUp(): void
{
parent::setUp();
Setting::flushCache();
}
} }